Remove last of NativePatchBukkit commands

This commit is contained in:
Bjorn Beishline 2026-02-03 22:50:39 -07:00
parent 4829d59c1e
commit 8e4b13d42b
17 changed files with 274 additions and 704 deletions

View File

@ -1,374 +0,0 @@
package org.patchbukkit.bridge;
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.VarHandle;
import java.util.UUID;
import org.bukkit.event.Event;
import org.bukkit.event.Listener;
import org.bukkit.plugin.Plugin;
import org.patchbukkit.events.PatchBukkitEventSerializer;
public class NativePatchBukkit {
private static final Linker LINKER = Linker.nativeLinker();
private static MethodHandle getWorldNative;
private static MethodHandle freeStringNative;
private static MethodHandle getRegistryDataNative;
private static MethodHandle playerEntityPlaySoundNative;
private static MethodHandle playerPlaySoundNative;
// Struct layout matching Rust's #[repr(C)] AbilitiesFFI
private static final StructLayout ABILITIES_LAYOUT =
MemoryLayout.structLayout(
ValueLayout.JAVA_BOOLEAN.withName("invulnerable"),
ValueLayout.JAVA_BOOLEAN.withName("flying"),
ValueLayout.JAVA_BOOLEAN.withName("allow_flying"),
ValueLayout.JAVA_BOOLEAN.withName("creative"),
ValueLayout.JAVA_BOOLEAN.withName("allow_modify_world"),
MemoryLayout.paddingLayout(3), // Padding to align f32 to 4-byte boundary
ValueLayout.JAVA_FLOAT.withName("fly_speed"),
ValueLayout.JAVA_FLOAT.withName("walk_speed")
);
// VarHandles for accessing struct fields
private static final VarHandle INVULNERABLE = ABILITIES_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("invulnerable")
);
private static final VarHandle FLYING = ABILITIES_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("flying")
);
private static final VarHandle ALLOW_FLYING = ABILITIES_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("allow_flying")
);
private static final VarHandle CREATIVE = ABILITIES_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("creative")
);
private static final VarHandle ALLOW_MODIFY_WORLD =
ABILITIES_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("allow_modify_world")
);
private static final VarHandle FLY_SPEED = ABILITIES_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("fly_speed")
);
private static final VarHandle WALK_SPEED = ABILITIES_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("walk_speed")
);
// Struct layout for Vec3
private static final StructLayout VEC3_LAYOUT = MemoryLayout.structLayout(
ValueLayout.JAVA_DOUBLE.withName("x"),
ValueLayout.JAVA_DOUBLE.withName("y"),
ValueLayout.JAVA_DOUBLE.withName("z")
);
// VarHandles for Vec3 fields
private static final VarHandle VEC3_X = VEC3_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("x")
);
private static final VarHandle VEC3_Y = VEC3_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("y")
);
private static final VarHandle VEC3_Z = VEC3_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("z")
);
public record Vec3(double x, double y, double z) {}
/**
* Java record to hold player abilities.
*/
public record Abilities(
boolean invulnerable,
boolean flying,
boolean allowFlying,
boolean creative,
boolean allowModifyWorld,
float flySpeed,
float walkSpeed
) {
// "Wither" methods - return a copy with one field changed
public Abilities withInvulnerable(boolean value) {
return new Abilities(
value,
flying,
allowFlying,
creative,
allowModifyWorld,
flySpeed,
walkSpeed
);
}
public Abilities withFlying(boolean value) {
return new Abilities(
invulnerable,
value,
allowFlying,
creative,
allowModifyWorld,
flySpeed,
walkSpeed
);
}
public Abilities withAllowFlying(boolean value) {
return new Abilities(
invulnerable,
flying,
value,
creative,
allowModifyWorld,
flySpeed,
walkSpeed
);
}
public Abilities withCreative(boolean value) {
return new Abilities(
invulnerable,
flying,
allowFlying,
value,
allowModifyWorld,
flySpeed,
walkSpeed
);
}
public Abilities withAllowModifyWorld(boolean value) {
return new Abilities(
invulnerable,
flying,
allowFlying,
creative,
value,
flySpeed,
walkSpeed
);
}
public Abilities withFlySpeed(float value) {
return new Abilities(
invulnerable,
flying,
allowFlying,
creative,
allowModifyWorld,
value,
walkSpeed
);
}
public Abilities withWalkSpeed(float value) {
return new Abilities(
invulnerable,
flying,
allowFlying,
creative,
allowModifyWorld,
flySpeed,
value
);
}
}
/**
* Called from Rust during initialization to register native function pointers.
*/
public static void initCallbacks(
long freeStringAddr,
long getWorldAddr,
long getRegistryDataAddr,
long playerEntityPlaySoundAddr,
long playerPlaySoundAddr
) {
// const char* rust_get_world(const char* uuid)
getWorldNative = LINKER.downcallHandle(
MemorySegment.ofAddress(getWorldAddr),
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)
);
// void rust_free_string(const char* str)
freeStringNative = LINKER.downcallHandle(
MemorySegment.ofAddress(freeStringAddr),
FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)
);
// const char* rust_get_registry_entries(const char* registry_name)
getRegistryDataNative = LINKER.downcallHandle(
MemorySegment.ofAddress(getRegistryDataAddr),
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)
);
// void rust_player_entity_play_sound(const char* player_uuid, const char* sound_name,
// const char* sound_category, const char* entity_uuid, float volume, float pitch)
playerEntityPlaySoundNative = LINKER.downcallHandle(
MemorySegment.ofAddress(playerEntityPlaySoundAddr),
FunctionDescriptor.ofVoid(
ValueLayout.ADDRESS, // player_uuid
ValueLayout.ADDRESS, // sound_name
ValueLayout.ADDRESS, // sound_category
ValueLayout.ADDRESS, // entity_uuid
ValueLayout.JAVA_FLOAT, // volume
ValueLayout.JAVA_FLOAT // pitch
)
);
// void rust_player_play_sound(const char* player_uuid, const char* sound_name,
// const char* sound_category, double x, double y, double z, float volume, float pitch)
playerPlaySoundNative = LINKER.downcallHandle(
MemorySegment.ofAddress(playerPlaySoundAddr),
FunctionDescriptor.ofVoid(
ValueLayout.ADDRESS, // player_uuid
ValueLayout.ADDRESS, // sound_name
ValueLayout.ADDRESS, // sound_category
ValueLayout.JAVA_DOUBLE, // x
ValueLayout.JAVA_DOUBLE, // y
ValueLayout.JAVA_DOUBLE, // z
ValueLayout.JAVA_FLOAT, // volume
ValueLayout.JAVA_FLOAT // pitch
)
);
}
/**
* Free a string allocated by the Rust side.
*/
private static void freeRustString(MemorySegment ptr) {
try {
freeStringNative.invokeExact(ptr);
} catch (Throwable t) {
throw new RuntimeException("Failed to free Rust string", t);
}
}
/**
* Get the world UUID for an entity.
*/
public static String getWorld(UUID entityUuid) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment uuidStr = arena.allocateFrom(entityUuid.toString());
MemorySegment resultPtr = (MemorySegment) getWorldNative.invokeExact(uuidStr);
if (resultPtr.equals(MemorySegment.NULL)) {
return null;
}
// Read the string, then free the Rust-allocated memory
try {
String result = resultPtr.reinterpret(Long.MAX_VALUE).getString(0);
return result;
} finally {
freeRustString(resultPtr);
}
} catch (Throwable t) {
throw new RuntimeException("Failed to call native getWorld", t);
}
}
/**
* Get all data for a registry (entries + tags) as JSON.
*
* @param registryName The registry identifier (e.g. "sound_event", "block")
* @return JSON string with "entries" and "tags" fields, or null if registry unknown
*/
public static String getRegistryData(String registryName) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment nameStr = arena.allocateFrom(registryName);
MemorySegment resultPtr = (MemorySegment) getRegistryDataNative.invokeExact(nameStr);
if (resultPtr.equals(MemorySegment.NULL)) return null;
try {
return resultPtr.reinterpret(Long.MAX_VALUE).getString(0);
} finally {
freeRustString(resultPtr);
}
} catch (Throwable t) {
throw new RuntimeException("Failed to get registry data: " + registryName, t);
}
}
/**
* Play a sound effect attached to an entity, heard by a specific player.
*
* @param playerUuid The player who will hear the sound
* @param soundName Sound identifier (e.g. "block.note_block.chime")
* @param soundCategory Category name (e.g. "master", "music", "blocks")
* @param entityUuid The entity the sound is attached to
* @param volume Volume (1.0 = normal)
* @param pitch Pitch (1.0 = normal)
*/
public static void playerEntityPlaySound(
UUID playerUuid,
String soundName,
String soundCategory,
UUID entityUuid,
float volume,
float pitch
) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment playerUuidStr = arena.allocateFrom(playerUuid.toString());
MemorySegment soundNameStr = arena.allocateFrom(soundName);
MemorySegment soundCategoryStr = arena.allocateFrom(soundCategory);
MemorySegment entityUuidStr = arena.allocateFrom(entityUuid.toString());
playerEntityPlaySoundNative.invokeExact(
playerUuidStr,
soundNameStr,
soundCategoryStr,
entityUuidStr,
volume,
pitch
);
} catch (Throwable t) {
throw new RuntimeException("Failed to call native entityPlaySound", t);
}
}
/**
* Play a sound effect at a location, heard by a specific player.
*
* @param playerUuid The player who will hear the sound
* @param soundName Sound identifier (e.g. "block.note_block.chime")
* @param soundCategory Category name (e.g. "master", "music", "blocks")
* @param x X coordinate
* @param y Y coordinate
* @param z Z coordinate
* @param volume Volume (1.0 = normal)
* @param pitch Pitch (1.0 = normal)
*/
public static void playerPlaySound(
UUID playerUuid,
String soundName,
String soundCategory,
double x,
double y,
double z,
float volume,
float pitch
) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment playerUuidStr = arena.allocateFrom(playerUuid.toString());
MemorySegment soundNameStr = arena.allocateFrom(soundName);
MemorySegment soundCategoryStr = arena.allocateFrom(soundCategory);
playerPlaySoundNative.invokeExact(
playerUuidStr,
soundNameStr,
soundCategoryStr,
x,
y,
z,
volume,
pitch
);
} catch (Throwable t) {
throw new RuntimeException("Failed to call native playerPlaySound", t);
}
}
}

View File

@ -38,7 +38,6 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.patchbukkit.bridge.BridgeUtils; import org.patchbukkit.bridge.BridgeUtils;
import org.patchbukkit.bridge.NativePatchBukkit;
import org.patchbukkit.world.PatchBukkitWorld; import org.patchbukkit.world.PatchBukkitWorld;
import io.papermc.paper.datacomponent.DataComponentType; import io.papermc.paper.datacomponent.DataComponentType;
@ -325,11 +324,8 @@ public class PatchBukkitEntity implements Entity {
@Override @Override
public @NotNull World getWorld() { public @NotNull World getWorld() {
var world_uuid_str = NativePatchBukkit.getWorld(this.uuid); var location = NativeBridgeFfi.getLocation(BridgeUtils.convertUuid(this.uuid));
UUID world_uuid = UUID.fromString(world_uuid_str); return PatchBukkitWorld.getOrCreate(BridgeUtils.convertUuid(location.getWorld().getUuid()));
var world = PatchBukkitWorld.getOrCreate(world_uuid);
return world;
} }
@Override @Override

View File

@ -5,15 +5,7 @@ import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Collection; import java.util.*;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import org.bukkit.BanEntry; import org.bukkit.BanEntry;
@ -63,7 +55,6 @@ import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable; import org.jetbrains.annotations.Unmodifiable;
import org.jetbrains.annotations.UnmodifiableView; import org.jetbrains.annotations.UnmodifiableView;
import org.patchbukkit.bridge.BridgeUtils; import org.patchbukkit.bridge.BridgeUtils;
import org.patchbukkit.bridge.NativePatchBukkit;
import org.patchbukkit.registry.PatchBukkitSound; import org.patchbukkit.registry.PatchBukkitSound;
import com.destroystokyo.paper.ClientOption; import com.destroystokyo.paper.ClientOption;
@ -82,6 +73,8 @@ import net.md_5.bungee.api.chat.BaseComponent;
import patchbukkit.bridge.NativeBridgeFfi; import patchbukkit.bridge.NativeBridgeFfi;
import patchbukkit.message.SendMessageRequest; import patchbukkit.message.SendMessageRequest;
import patchbukkit.abilities.SetAbilitiesRequest; import patchbukkit.abilities.SetAbilitiesRequest;
import patchbukkit.sound.PlayerEntityPlaySoundRequest;
import patchbukkit.sound.PlayerPlaySoundRequest;
@SuppressWarnings({ "deprecation", "removal" }) @SuppressWarnings({ "deprecation", "removal" })
public class PatchBukkitPlayer public class PatchBukkitPlayer
@ -694,49 +687,87 @@ public class PatchBukkitPlayer
@Override @Override
public void playSound(Location location, Sound sound, SoundCategory category, float volume, float pitch) { public void playSound(Location location, Sound sound, SoundCategory category, float volume, float pitch) {
var patchBukkitSound = (PatchBukkitSound) sound; var patchBukkitSound = (PatchBukkitSound) sound;
NativePatchBukkit.playerPlaySound(this.uuid, patchBukkitSound.getOriginalName(), category.name(), location.x(), location.y(), location.z(), volume, pitch); this.playSound0(location, patchBukkitSound.getOriginalName(), category, volume, pitch, OptionalLong.empty());
} }
@Override @Override
public void playSound(Location location, String sound, SoundCategory category, float volume, float pitch) { public void playSound(Location location, String sound, SoundCategory category, float volume, float pitch) {
NativePatchBukkit.playerPlaySound(this.uuid, sound, category.name(), location.x(), location.y(), location.z(), volume, pitch); this.playSound0(location, sound, category, volume, pitch, OptionalLong.empty());
} }
@Override @Override
public void playSound(Location location, Sound sound, SoundCategory category, float volume, float pitch, public void playSound(Location location, Sound sound, SoundCategory category, float volume, float pitch,
long seed) { long seed) {
// TODO Auto-generated method stub var patchBukkitSound = (PatchBukkitSound) sound;
throw new UnsupportedOperationException("Unimplemented method 'playSound'"); this.playSound0(location, patchBukkitSound.getOriginalName(), category, volume, pitch, OptionalLong.of(seed));
} }
@Override @Override
public void playSound(Location location, String sound, SoundCategory category, float volume, float pitch, public void playSound(Location location, String sound, SoundCategory category, float volume, float pitch,
long seed) { long seed) {
// TODO Auto-generated method stub this.playSound0(location, sound, category, volume, pitch, OptionalLong.of(seed));
throw new UnsupportedOperationException("Unimplemented method 'playSound'"); }
private void playSound0(Location location, String sound, SoundCategory category, float volume, float pitch, OptionalLong seed) {
var request = PlayerPlaySoundRequest.newBuilder()
.setPlayerUuid(BridgeUtils.convertUuid(this.uuid))
.setLocation(
patchbukkit.common.Location.newBuilder()
.setPosition(
patchbukkit.common.Vec3.newBuilder()
.setX(location.x())
.setY(location.y())
.setZ(location.z())
).setWorld(
patchbukkit.common.World.newBuilder().setUuid(BridgeUtils.convertUuid(location.getWorld().getUID()))
).setPitch(location.getPitch())
.setYaw(location.getYaw())
).setSound(
patchbukkit.sound.Sound.newBuilder()
.setCategory(category.name())
.setName(sound)
).setVolume(volume)
.setPitch(pitch);
if (seed.isPresent()) request.setSeed(seed.getAsLong());
NativeBridgeFfi.playerPlaySound(request.build());
} }
@Override @Override
public void playSound(Entity entity, Sound sound, SoundCategory category, float volume, float pitch) { public void playSound(Entity entity, Sound sound, SoundCategory category, float volume, float pitch) {
var patchBukkitSound = (PatchBukkitSound) sound; var patchBukkitSound = (PatchBukkitSound) sound;
NativePatchBukkit.playerEntityPlaySound(this.uuid, patchBukkitSound.getOriginalName(), category.name(), entity.getUniqueId(), volume, pitch); this.playSound0(entity, patchBukkitSound.getOriginalName(), category, volume, pitch, OptionalLong.empty());
} }
@Override @Override
public void playSound(Entity entity, String sound, SoundCategory category, float volume, float pitch) { public void playSound(Entity entity, String sound, SoundCategory category, float volume, float pitch) {
NativePatchBukkit.playerEntityPlaySound(this.uuid, sound, category.name(), entity.getUniqueId(), volume, pitch); this.playSound0(entity, sound, category, volume, pitch, OptionalLong.empty());
} }
@Override @Override
public void playSound(Entity entity, Sound sound, SoundCategory category, float volume, float pitch, long seed) { public void playSound(Entity entity, Sound sound, SoundCategory category, float volume, float pitch, long seed) {
// TODO Auto-generated method stub var patchBukkitSound = (PatchBukkitSound) sound;
throw new UnsupportedOperationException("Unimplemented method 'playSound'"); this.playSound0(entity, patchBukkitSound.getOriginalName(), category, volume, pitch, OptionalLong.of(seed));
} }
@Override @Override
public void playSound(Entity entity, String sound, SoundCategory category, float volume, float pitch, long seed) { public void playSound(Entity entity, String sound, SoundCategory category, float volume, float pitch, long seed) {
// TODO Auto-generated method stub this.playSound0(entity, sound, category, volume, pitch, OptionalLong.of(seed));
throw new UnsupportedOperationException("Unimplemented method 'playSound'"); }
private void playSound0(Entity entity, String sound, SoundCategory category, float volume, float pitch, OptionalLong seed) {
var request = PlayerEntityPlaySoundRequest.newBuilder()
.setPlayerUuid(BridgeUtils.convertUuid(this.uuid))
.setEntityUuid(BridgeUtils.convertUuid(entity.getUniqueId()))
.setSound(
patchbukkit.sound.Sound.newBuilder()
.setCategory(category.name())
.setName(sound)
).setVolume(volume)
.setPitch(pitch);
if (seed.isPresent()) request.setSeed(seed.getAsLong());
NativeBridgeFfi.playerEntityPlaySound(request.build());
} }
@Override @Override

View File

@ -13,7 +13,6 @@ import org.bukkit.plugin.IllegalPluginAccessException;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.RegisteredListener; import org.bukkit.plugin.RegisteredListener;
import org.patchbukkit.bridge.BridgeUtils; import org.patchbukkit.bridge.BridgeUtils;
import org.patchbukkit.bridge.NativePatchBukkit;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;

View File

@ -12,57 +12,44 @@ import org.bukkit.NamespacedKey;
import org.bukkit.Registry; import org.bukkit.Registry;
import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.patchbukkit.bridge.NativePatchBukkit; import patchbukkit.bridge.NativeBridgeFfi;
import patchbukkit.registry.GetRegistryDataRequest;
import patchbukkit.registry.GetRegistryDataResponse;
import patchbukkit.registry.RegistryType;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Stream; import java.util.stream.Stream;
public class PatchBukkitRegistry<B extends Keyed> implements Registry<B> { public class PatchBukkitRegistry<P, B extends Keyed> implements Registry<B> {
private final Map<NamespacedKey, B> entries = new LinkedHashMap<>(); private final Map<NamespacedKey, B> entries = new LinkedHashMap<>();
private final Map<String, PatchBukkitTag<B>> tags = new LinkedHashMap<>(); private final Map<String, PatchBukkitTag<B>> tags = new LinkedHashMap<>();
/**
* Build a registry by fetching JSON from the native side and parsing
* entries and tags from the combined response.
*
* Expected JSON shape:
* {
* "entries": [{"name": "...", "id": ...}, ...],
* "tags": {"minecraft:logs": ["minecraft:oak_log", ...], ...}
* }
*
* @param registryName The native registry name (e.g. "sound_event")
* @param registryKey The Paper RegistryKey for this registry
* @param factory Converts a single JsonObject into a B instance
*/
public PatchBukkitRegistry( public PatchBukkitRegistry(
String registryName, RegistryType registryType,
RegistryKey<B> registryKey, Function<GetRegistryDataResponse, List<P>> extractor,
Function<JsonObject, B> factory Function<P, B> factory
) { ) {
String json = NativePatchBukkit.getRegistryData(registryName); if (registryType == null) return;
if (json == null) return;
JsonObject root = JsonParser.parseString(json).getAsJsonObject(); GetRegistryDataRequest request = GetRegistryDataRequest.newBuilder()
.setRegistry(registryType)
.build();
JsonArray entriesArray = root.getAsJsonArray("entries"); GetRegistryDataResponse response = NativeBridgeFfi.getRegistryData(request);
if (entriesArray != null) { if (response == null) return;
for (JsonElement element : entriesArray) {
B value = factory.apply(element.getAsJsonObject()); List<P> protoEntries = extractor.apply(response);
if (value != null) { for (P protoEntry : protoEntries) {
entries.put(value.getKey(), value); B value = factory.apply(protoEntry);
} if (value != null) {
entries.put(value.getKey(), value);
} }
} }
JsonElement tagsElement = root.get("tags");
if (tagsElement != null && tagsElement.isJsonObject()) {
loadTags(tagsElement.getAsJsonObject(), registryKey);
}
} }
private void loadTags(JsonObject tagMap, RegistryKey<B> registryKey) { private void loadTags(JsonObject tagMap, RegistryKey<B> registryKey) {
for (Map.Entry<String, JsonElement> entry : tagMap.entrySet()) { for (Map.Entry<String, JsonElement> entry : tagMap.entrySet()) {
String tagName = entry.getKey(); String tagName = entry.getKey();
@ -116,6 +103,14 @@ public class PatchBukkitRegistry<B extends Keyed> implements Registry<B> {
return tag; return tag;
} }
public static <B extends Keyed> PatchBukkitRegistry<Object, B> empty(RegistryKey<B> registryKey) {
return new PatchBukkitRegistry<>(
null,
response -> Collections.emptyList(),
obj -> null
);
}
@Override @Override
public @NonNull Collection<Tag<B>> getTags() { public @NonNull Collection<Tag<B>> getTags() {
return Collections.unmodifiableCollection(tags.values()); return Collections.unmodifiableCollection(tags.values());

View File

@ -6,11 +6,13 @@ import org.bukkit.Keyed;
import org.bukkit.Registry; import org.bukkit.Registry;
import org.bukkit.Sound; import org.bukkit.Sound;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.patchbukkit.bridge.NativePatchBukkit;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import patchbukkit.registry.GetRegistryDataResponse;
import patchbukkit.registry.RegistryType;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function; import java.util.function.Function;
@ -22,21 +24,21 @@ public class PatchBukkitRegistryAccess implements RegistryAccess {
* Maps a RegistryKey to (native registry name, factory function). * Maps a RegistryKey to (native registry name, factory function).
* Add an entry here for each registry you support. * Add an entry here for each registry you support.
*/ */
private static final Map<RegistryKey<?>, RegistryFactory<?>> FACTORIES = Map.of( private static final Map<RegistryKey<?>, RegistryFactory<?, ?>> FACTORIES = Map.of(
RegistryKey.SOUND_EVENT, new RegistryFactory<>( RegistryKey.SOUND_EVENT, new RegistryFactory<>(
"sound_event", RegistryType.SOUND_EVENT,
RegistryKey.SOUND_EVENT, response -> response.getSoundEvent().getSoundEventsList(),
json -> new PatchBukkitSound( data -> new PatchBukkitSound(
json.get("name").getAsString(), data.getName(),
json.get("id").getAsInt() data.getId()
) )
) )
); );
private record RegistryFactory<B extends Keyed>( private record RegistryFactory<P, B extends Keyed>(
String nativeRegistryName, RegistryType registryType,
RegistryKey<B> registryKey, Function<GetRegistryDataResponse, List<P>> extractor,
Function<JsonObject, B> factory Function<P, B> factory
) {} ) {}
@Override @Override
@ -51,25 +53,21 @@ public class PatchBukkitRegistryAccess implements RegistryAccess {
if (registryKey == null) return null; if (registryKey == null) return null;
return (Registry<T>) instances.computeIfAbsent(registryKey, key -> { return (Registry<T>) instances.computeIfAbsent(registryKey, key -> {
RegistryFactory<?> factoryEntry = FACTORIES.get(key); RegistryFactory<?, ?> factoryEntry = FACTORIES.get(key);
if (factoryEntry != null) { if (factoryEntry != null) {
return buildRegistry(factoryEntry); return buildRegistry(factoryEntry);
} }
// Return an empty registry for unsupported types
// We return an empty registry for unsupported types, since if we don't the server will crash immediately when the registry class is first loaded return PatchBukkitRegistry.empty((RegistryKey) key);
return new PatchBukkitRegistry<>(
key.key().value(),
(RegistryKey) key,
json -> null
);
}); });
} }
private static <B extends Keyed> PatchBukkitRegistry<B> buildRegistry(RegistryFactory<B> factory) { @SuppressWarnings("unchecked")
private static <P, B extends Keyed> PatchBukkitRegistry<P, B> buildRegistry(RegistryFactory<P, B> factory) {
return new PatchBukkitRegistry<>( return new PatchBukkitRegistry<>(
factory.nativeRegistryName(), factory.registryType(),
factory.registryKey(), factory.extractor(),
factory.factory() factory.factory()
); );
} }

View File

@ -1589,10 +1589,7 @@ public class PatchBukkitWorld
@Override @Override
public @NotNull UUID getUID() { public @NotNull UUID getUID() {
// TODO Auto-generated method stub return this.uuid;
throw new UnsupportedOperationException(
"Unimplemented method 'getUID'"
);
} }
@Override @Override

View File

@ -7,6 +7,8 @@ import "patchbukkit/abilities.proto";
import "patchbukkit/common/types.proto"; import "patchbukkit/common/types.proto";
import "patchbukkit/events/event.proto"; import "patchbukkit/events/event.proto";
import "patchbukkit/message.proto"; import "patchbukkit/message.proto";
import "patchbukkit/registry.proto";
import "patchbukkit/sound.proto";
service NativeBridge { service NativeBridge {
rpc GetAbilities(patchbukkit.common.UUID) returns (patchbukkit.abilities.Abilities); rpc GetAbilities(patchbukkit.common.UUID) returns (patchbukkit.abilities.Abilities);
@ -18,4 +20,9 @@ service NativeBridge {
rpc SendMessage(patchbukkit.message.SendMessageRequest) returns (google.protobuf.Empty); rpc SendMessage(patchbukkit.message.SendMessageRequest) returns (google.protobuf.Empty);
rpc GetLocation(patchbukkit.common.UUID) returns (patchbukkit.common.Location); rpc GetLocation(patchbukkit.common.UUID) returns (patchbukkit.common.Location);
rpc GetRegistryData(patchbukkit.registry.GetRegistryDataRequest) returns (patchbukkit.registry.GetRegistryDataResponse);
rpc PlayerEntityPlaySound(patchbukkit.sound.PlayerEntityPlaySoundRequest) returns (google.protobuf.Empty);
rpc PlayerPlaySound(patchbukkit.sound.PlayerPlaySoundRequest) returns (google.protobuf.Empty);
} }

View File

@ -0,0 +1,31 @@
syntax = "proto3";
package patchbukkit.registry;
import "patchbukkit/common/types.proto";
option java_multiple_files = true;
option java_package = "patchbukkit.registry";
enum RegistryType {
SOUND_EVENT = 0;
}
message GetRegistryDataRequest {
RegistryType registry = 1;
}
message GetRegistryDataResponse {
oneof registry {
SoundEventRegistryData sound_event = 1;
}
}
message SoundEvent {
uint32 id = 1;
string name = 2;
}
message SoundEventRegistryData {
repeated SoundEvent sound_events = 1;
}

View File

@ -0,0 +1,31 @@
syntax = "proto3";
package patchbukkit.sound;
import "patchbukkit/common/types.proto";
option java_multiple_files = true;
option java_package = "patchbukkit.sound";
message Sound {
string name = 1;
string category = 2;
}
message PlayerEntityPlaySoundRequest {
patchbukkit.common.UUID player_uuid = 1;
Sound sound = 2;
patchbukkit.common.UUID entity_uuid = 3;
float volume = 4;
float pitch = 5;
optional int64 seed = 6;
}
message PlayerPlaySoundRequest {
patchbukkit.common.UUID player_uuid = 1;
Sound sound = 2;
patchbukkit.common.Location location = 3;
float volume = 4;
float pitch = 5;
optional int64 seed = 6;
}

View File

@ -1,9 +1,7 @@
use std::ffi::c_char;
use pumpkin::command::args::entities::{EntitySelectorType, TargetSelector}; use pumpkin::command::args::entities::{EntitySelectorType, TargetSelector};
use crate::{ use crate::{
java::native_callbacks::{CALLBACK_CONTEXT, utils::get_string}, java::native_callbacks::CALLBACK_CONTEXT,
proto::patchbukkit::common::{Location, Uuid, Vec3, World}, proto::patchbukkit::common::{Location, Uuid, Vec3, World},
}; };

View File

@ -1,9 +0,0 @@
use std::ffi::{CString, c_char};
pub extern "C" fn rust_free_string(ptr: *mut c_char) {
if !ptr.is_null() {
unsafe {
drop(CString::from_raw(ptr));
}
}
}

View File

@ -1,7 +1,7 @@
use std::sync::{Arc, OnceLock}; use std::sync::{Arc, OnceLock};
use anyhow::Result; use anyhow::Result;
use j4rs::{InvocationArg, Jvm}; use j4rs::Jvm;
use pumpkin::plugin::Context; use pumpkin::plugin::Context;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@ -16,9 +16,6 @@ pub use events::*;
pub mod location; pub mod location;
pub use location::*; pub use location::*;
pub mod memory;
pub use memory::*;
pub mod message; pub mod message;
pub use message::*; pub use message::*;
@ -28,9 +25,6 @@ pub use registry::*;
pub mod sound; pub mod sound;
pub use sound::*; pub use sound::*;
pub mod world;
pub use world::*;
pub mod utils; pub mod utils;
static CALLBACK_CONTEXT: OnceLock<CallbackContext> = OnceLock::new(); static CALLBACK_CONTEXT: OnceLock<CallbackContext> = OnceLock::new();
@ -58,47 +52,6 @@ pub fn init_callback_context(
Ok(()) Ok(())
} }
pub fn encode_with_length(data: Vec<u8>) -> *mut u8 {
let len = data.len() as u32;
let mut result = Vec::with_capacity(4 + data.len());
result.extend_from_slice(&len.to_le_bytes());
result.extend(data);
let ptr = result.as_mut_ptr();
std::mem::forget(result);
ptr
}
pub extern "C" fn rust_free_bytes(ptr: *mut u8, len: u32) {
if !ptr.is_null() {
unsafe {
let total_len = 4 + len as usize;
drop(Vec::from_raw_parts(ptr, total_len, total_len));
}
}
}
pub fn initialize_callbacks(jvm: &Jvm) -> Result<()> { pub fn initialize_callbacks(jvm: &Jvm) -> Result<()> {
let free_string_addr = memory::rust_free_string as *const () as i64; initialize_ffi_callbacks(jvm)
let get_world_addr = world::rust_get_world as *const () as i64;
let rust_get_registry_data_addr = registry::rust_get_registry_data as *const () as i64;
let rust_player_entity_play_sound_addr =
sound::rust_player_entity_play_sound as *const () as i64;
let rust_player_play_sound_addr = sound::rust_player_play_sound as *const () as i64;
jvm.invoke_static(
"org.patchbukkit.bridge.NativePatchBukkit",
"initCallbacks",
&[
InvocationArg::try_from(free_string_addr)?.into_primitive()?,
InvocationArg::try_from(get_world_addr)?.into_primitive()?,
InvocationArg::try_from(rust_get_registry_data_addr)?.into_primitive()?,
InvocationArg::try_from(rust_player_entity_play_sound_addr)?.into_primitive()?,
InvocationArg::try_from(rust_player_play_sound_addr)?.into_primitive()?,
],
)?;
initialize_ffi_callbacks(jvm)?;
Ok(())
} }

View File

@ -1,60 +1,30 @@
use std::{collections::HashMap, ffi::CString, os::raw::c_char}; use crate::proto::patchbukkit::registry::{
GetRegistryDataRequest, GetRegistryDataResponse, RegistryType, SoundEvent,
SoundEventRegistryData, get_registry_data_response::Registry,
};
use serde::Serialize; pub fn ffi_native_bridge_get_registry_data_impl(
use serde_json::Value; request: GetRegistryDataRequest,
) -> Option<GetRegistryDataResponse> {
use crate::java::native_callbacks::utils::get_string; let registry = match request.registry {
val if val == RegistryType::SoundEvent as i32 => {
#[derive(Serialize, Debug, Clone)] let sounds = pumpkin_data::sound::Sound::slice()
struct Registry {
pub entries: Vec<Value>,
pub tags: HashMap<String, Vec<String>>,
}
impl Registry {
pub const fn new(entries: Vec<Value>, tags: HashMap<String, Vec<String>>) -> Self {
Self { entries, tags }
}
}
#[must_use]
pub extern "C" fn rust_get_registry_data(registry_name: *const c_char) -> *const c_char {
let name = get_string(registry_name);
let registry: Option<Registry> = match name.as_ref() {
"sound_event" => {
let entries: Vec<serde_json::Value> = pumpkin_data::sound::Sound::slice()
.iter() .iter()
.map(|s| { .map(|s| {
serde_json::json!({ let mut sound_event = SoundEvent::default();
"name": s.to_name(), sound_event.id = *s as u32;
"id": *s as u16 sound_event.name = s.to_name().to_string();
}) sound_event
}) })
.collect(); .collect::<Vec<_>>();
Some(Registry::new(entries, HashMap::new())) let mut data = SoundEventRegistryData::default();
data.sound_events = sounds;
Registry::SoundEvent(data)
} }
_ => None, _ => unreachable!(),
}; };
let registry = match registry { let mut response = GetRegistryDataResponse::default();
Some(registry) => registry, response.registry = Some(registry);
None => return std::ptr::null(), Some(response)
};
let json_str = match serde_json::to_string(&registry) {
Ok(json_str) => json_str,
Err(err) => {
log::error!("Failed to serialize registry: {err}");
return std::ptr::null();
}
};
match CString::new(json_str) {
Ok(cstring) => cstring.into_raw(),
Err(err) => {
log::error!("Failed to convert registry string to CString: {err}");
std::ptr::null_mut()
}
}
} }

View File

@ -1,111 +1,89 @@
use std::ffi::{c_char, c_double, c_float};
use pumpkin::command::args::entities::{EntitySelectorType, TargetSelector}; use pumpkin::command::args::entities::{EntitySelectorType, TargetSelector};
use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_protocol::{IdOr, java::client::play::CEntitySoundEffect}; use pumpkin_protocol::{IdOr, java::client::play::CEntitySoundEffect};
use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::vector3::Vector3;
use rand::{RngExt, rng}; use rand::{RngExt, rng};
use crate::java::native_callbacks::{CALLBACK_CONTEXT, utils::get_string}; use crate::{
java::native_callbacks::CALLBACK_CONTEXT,
proto::patchbukkit::sound::{PlayerEntityPlaySoundRequest, PlayerPlaySoundRequest},
};
pub extern "C" fn rust_player_entity_play_sound( pub fn ffi_native_bridge_player_entity_play_sound_impl(
player_uuid_ptr: *const c_char, request: PlayerEntityPlaySoundRequest,
sound_name_ptr: *const c_char, ) -> Option<()> {
sound_category_ptr: *const c_char, log::error!("PlayerEntityPlaySoundRequest sent");
entity_uuid_ptr: *const c_char, let ctx = CALLBACK_CONTEXT.get()?;
volume: c_float, let player_uuid = uuid::Uuid::parse_str(&request.player_uuid?.value).ok()?;
pitch: c_float, let entity_uuid = uuid::Uuid::parse_str(&request.entity_uuid?.value).ok()?;
) {
let player_uuid_str = get_string(player_uuid_ptr);
let sound_name = get_string(sound_name_ptr);
let sound_category = get_string(sound_category_ptr);
let entity_uuid_str = get_string(entity_uuid_ptr);
if let Some(ctx) = CALLBACK_CONTEXT.get() { let player = ctx.plugin_context.server.get_player_by_uuid(player_uuid)?;
let player_uuid = uuid::Uuid::parse_str(&player_uuid_str).unwrap();
let entity_uuid = uuid::Uuid::parse_str(&entity_uuid_str).unwrap();
ctx.runtime.spawn(async move { let sound = request.sound?;
let player = ctx.plugin_context.server.get_player_by_uuid(player_uuid); let pumpkin_sound = Sound::from_name(&sound.name)?;
let category = SoundCategory::from_name(&sound.category.to_lowercase())?;
let entity = ctx.plugin_context.server.select_entities( let seed = rng().random::<i64>();
&TargetSelector::new(EntitySelectorType::Uuid(entity_uuid)),
None,
);
if entity.len() != 1 {
return;
}
let entity = entity.first().unwrap().get_entity(); ctx.runtime.spawn(async move {
let entity = ctx.plugin_context.server.select_entities(
&TargetSelector::new(EntitySelectorType::Uuid(entity_uuid)),
None,
);
if let Some(player) = player { if entity.len() != 1 {
let sound = match Sound::from_name(&sound_name) { return None;
Some(sound) => sound, }
None => return,
};
let category = match SoundCategory::from_name(&sound_category) { let entity = entity.first()?.get_entity();
Some(category) => category,
None => return,
};
let seed = rng().random::<i64>(); player
.client
.enqueue_packet(&CEntitySoundEffect::new(
IdOr::Id(pumpkin_sound as u16),
category,
entity.entity_id.into(),
request.volume,
request.pitch,
seed,
))
.await;
player Some(())
.client });
.enqueue_packet(&CEntitySoundEffect::new(
IdOr::Id(sound as u16), Some(())
category,
entity.entity_id.into(),
volume,
pitch,
seed,
))
.await;
}
});
}
} }
pub extern "C" fn rust_player_play_sound( pub fn ffi_native_bridge_player_play_sound_impl(request: PlayerPlaySoundRequest) -> Option<()> {
player_uuid_ptr: *const c_char, let ctx = CALLBACK_CONTEXT.get()?;
sound_name_ptr: *const c_char, let player_uuid = uuid::Uuid::parse_str(&request.player_uuid?.value).ok()?;
sound_category_ptr: *const c_char, let player = ctx.plugin_context.server.get_player_by_uuid(player_uuid)?;
x: c_double,
y: c_double,
z: c_double,
volume: c_float,
pitch: c_float,
) {
let player_uuid_str = get_string(player_uuid_ptr);
let sound_name = get_string(sound_name_ptr);
let sound_category = get_string(sound_category_ptr);
if let Some(ctx) = CALLBACK_CONTEXT.get() { let sound = request.sound?;
let player_uuid = uuid::Uuid::parse_str(&player_uuid_str).unwrap(); let pumpkin_sound = Sound::from_name(&sound.name)?;
let category = SoundCategory::from_name(&sound.category.to_lowercase())?;
ctx.runtime.spawn(async move { let seed = match request.seed {
let player = ctx.plugin_context.server.get_player_by_uuid(player_uuid); Some(seed) => seed as f64,
None => rng().random::<f64>(),
};
if let Some(player) = player { let position = request.location?.position?;
let sound = match Sound::from_name(&sound_name) { let position: Vector3<f64> = Vector3::new(position.x, position.y, position.z);
Some(sound) => sound,
None => return,
};
let category = match SoundCategory::from_name(&sound_category.to_lowercase()) { ctx.runtime.spawn(async move {
Some(category) => category, player
None => return, .play_sound(
}; pumpkin_sound as u16,
category,
&position,
request.volume,
request.pitch,
seed,
)
.await;
});
let seed = rng().random::<f64>(); Some(())
let position: Vector3<f64> = Vector3::new(x, y, z);
player
.play_sound(sound as u16, category, &position, volume, pitch, seed)
.await;
}
});
}
} }

View File

@ -1,39 +0,0 @@
use std::ffi::{CString, c_char};
use pumpkin::command::args::entities::{EntitySelectorType, TargetSelector};
use crate::java::native_callbacks::{CALLBACK_CONTEXT, utils::get_string};
pub extern "C" fn rust_get_world(entity_uuid_ptr: *const c_char) -> *mut c_char {
if entity_uuid_ptr.is_null() {
return std::ptr::null_mut();
}
let entity_uuid = get_string(entity_uuid_ptr);
let Some(ctx) = CALLBACK_CONTEXT.get() else {
return std::ptr::null_mut();
};
let Ok(uuid) = uuid::Uuid::parse_str(&entity_uuid) else {
return std::ptr::null_mut();
};
let entities = ctx
.plugin_context
.server
.select_entities(&TargetSelector::new(EntitySelectorType::Uuid(uuid)), None);
if entities.len() != 1 {
return std::ptr::null_mut();
}
let entity = entities.first().unwrap().get_entity();
let world = entity.world.load();
let world_uuid = world.uuid.to_string();
match CString::new(world_uuid) {
Ok(cstring) => cstring.into_raw(), // Transfers ownership to caller
Err(_) => std::ptr::null_mut(),
}
}

View File

@ -18,6 +18,14 @@ pub mod patchbukkit {
pub mod message { pub mod message {
include!(concat!(env!("OUT_DIR"), "/patchbukkit.message.rs")); include!(concat!(env!("OUT_DIR"), "/patchbukkit.message.rs"));
} }
pub mod registry {
include!(concat!(env!("OUT_DIR"), "/patchbukkit.registry.rs"));
}
pub mod sound {
include!(concat!(env!("OUT_DIR"), "/patchbukkit.sound.rs"));
}
} }
include!(concat!(env!("OUT_DIR"), "/ffi_init.rs")); include!(concat!(env!("OUT_DIR"), "/ffi_init.rs"));