diff --git a/java/patchbukkit-test-plugin/build.gradle.kts b/java/patchbukkit-test-plugin/build.gradle.kts index 530cfab..e7cd228 100644 --- a/java/patchbukkit-test-plugin/build.gradle.kts +++ b/java/patchbukkit-test-plugin/build.gradle.kts @@ -20,8 +20,9 @@ java { } tasks.withType { + options.isWarnings = false options.compilerArgs.addAll(listOf( - "-Xlint:-removal", - "-Xlint:-deprecation" + "-Xlint:none", + "-nowarn" )) } diff --git a/java/patchbukkit-test-plugin/src/main/java/org/patchbukkit/testplugin/tests/CommandSystemTests.java b/java/patchbukkit-test-plugin/src/main/java/org/patchbukkit/testplugin/tests/CommandSystemTests.java index d5690d4..ee230aa 100644 --- a/java/patchbukkit-test-plugin/src/main/java/org/patchbukkit/testplugin/tests/CommandSystemTests.java +++ b/java/patchbukkit-test-plugin/src/main/java/org/patchbukkit/testplugin/tests/CommandSystemTests.java @@ -60,8 +60,8 @@ public final class CommandSystemTests { map.register("patchbukkittest", testCmd); CommandSender sender = Bukkit.getConsoleSender(); - boolean result = map.dispatch(sender, "pbtestexec"); - assertTrue(result, "dispatch() should return true"); + boolean result = map.dispatch(sender, "/pbtestexec"); + assertTrue(result, "dispatch() with leading slash should return true"); assertTrue(executed[0], "command should have been executed"); } diff --git a/java/patchbukkit-test-plugin/src/main/java/org/patchbukkit/testplugin/tests/EntityTests.java b/java/patchbukkit-test-plugin/src/main/java/org/patchbukkit/testplugin/tests/EntityTests.java index 0d4e5e2..d11d848 100644 --- a/java/patchbukkit-test-plugin/src/main/java/org/patchbukkit/testplugin/tests/EntityTests.java +++ b/java/patchbukkit-test-plugin/src/main/java/org/patchbukkit/testplugin/tests/EntityTests.java @@ -29,4 +29,18 @@ public final class EntityTests { Bukkit.getServer().getEntity(UUID.randomUUID()); } + @ConformanceTest(name = "Player flight state and speed getters run cleanly", category = TestCategory.ENTITY) + public void testPlayerFlightState() { + for (Player p : Bukkit.getOnlinePlayers()) { + boolean allow = p.getAllowFlight(); + boolean flying = p.isFlying(); + float flySpeed = p.getFlySpeed(); + float walkSpeed = p.getWalkSpeed(); + assertNotNull(allow, "Player.getAllowFlight()"); + assertNotNull(flying, "Player.isFlying()"); + assertTrue(flySpeed >= -1.0f && flySpeed <= 1.0f, "Fly speed in valid range"); + assertTrue(walkSpeed >= -1.0f && walkSpeed <= 1.0f, "Walk speed in valid range"); + } + } + } diff --git a/java/patchbukkit-test-plugin/src/main/java/org/patchbukkit/testplugin/tests/PluginManagerTests.java b/java/patchbukkit-test-plugin/src/main/java/org/patchbukkit/testplugin/tests/PluginManagerTests.java index 032bce0..8be446d 100644 --- a/java/patchbukkit-test-plugin/src/main/java/org/patchbukkit/testplugin/tests/PluginManagerTests.java +++ b/java/patchbukkit-test-plugin/src/main/java/org/patchbukkit/testplugin/tests/PluginManagerTests.java @@ -62,4 +62,20 @@ public final class PluginManagerTests { assertNotNull(perms, "PluginManager.getPermissions()"); } + @ConformanceTest(name = "PluginManager.useTimings() returns boolean", category = TestCategory.PLUGIN_MANAGER) + public void testUseTimings() { + boolean timings = Bukkit.getPluginManager().useTimings(); + assertTrue(!timings, "useTimings() should return false"); + } + + @ConformanceTest(name = "Server.getMessenger() returns non-null", category = TestCategory.PLUGIN_MANAGER) + public void testMessenger() { + var messenger = Bukkit.getMessenger(); + assertNotNull(messenger, "Bukkit.getMessenger()"); + messenger.registerOutgoingPluginChannel(plugin, "BungeeCord"); + assertTrue(messenger.isOutgoingChannelRegistered(plugin, "BungeeCord"), "isOutgoingChannelRegistered() should be true"); + messenger.unregisterOutgoingPluginChannel(plugin, "BungeeCord"); + assertTrue(!messenger.isOutgoingChannelRegistered(plugin, "BungeeCord"), "isOutgoingChannelRegistered() should be false"); + } + } diff --git a/java/patchbukkit/build.gradle.kts b/java/patchbukkit/build.gradle.kts index 7970cc1..961641f 100644 --- a/java/patchbukkit/build.gradle.kts +++ b/java/patchbukkit/build.gradle.kts @@ -64,9 +64,10 @@ java { } tasks.withType { + options.isWarnings = false options.compilerArgs.addAll(listOf( - "-Xlint:-removal", - "-Xlint:-deprecation" + "-Xlint:none", + "-nowarn" )) } diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginLoader.java b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginLoader.java index 5e1b2f8..45cf994 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginLoader.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginLoader.java @@ -1,6 +1,7 @@ package org.patchbukkit.loader; import java.io.File; +import java.lang.reflect.Method; import java.net.URL; import java.util.LinkedHashSet; import java.util.List; @@ -121,8 +122,56 @@ public class PatchBukkitPluginLoader implements PluginLoader { Class, Set > createRegisteredListeners(Listener listener, Plugin plugin) { - throw new UnsupportedOperationException( - "Unimplemented method 'createRegisteredListeners'" - ); + Map, Set> ret = new java.util.HashMap<>(); + Set methods; + try { + Method[] publicMethods = listener.getClass().getMethods(); + Method[] declaredMethods = listener.getClass().getDeclaredMethods(); + methods = new java.util.HashSet<>(publicMethods.length + declaredMethods.length); + for (Method method : publicMethods) { + methods.add(method); + } + for (Method method : declaredMethods) { + methods.add(method); + } + } catch (NoClassDefFoundError e) { + plugin.getLogger().severe("Plugin " + plugin.getDescription().getFullName() + " has failed to register events for " + listener.getClass() + " because " + e.getMessage() + " does not exist."); + return ret; + } + + for (final Method method : methods) { + final org.bukkit.event.EventHandler eh = method.getAnnotation(org.bukkit.event.EventHandler.class); + if (eh == null) continue; + + if (method.isBridge() || method.isSynthetic()) { + continue; + } + + final Class checkClass; + if (method.getParameterTypes().length != 1 || !Event.class.isAssignableFrom(checkClass = method.getParameterTypes()[0])) { + plugin.getLogger().severe(plugin.getDescription().getFullName() + " attempted to register an invalid EventHandler method signature \"" + method.toGenericString() + "\" in " + listener.getClass()); + continue; + } + + final Class eventClass = checkClass.asSubclass(Event.class); + method.setAccessible(true); + Set eventSet = ret.computeIfAbsent(eventClass, k -> new java.util.HashSet<>()); + + EventExecutor executor = (l, event) -> { + try { + if (!eventClass.isAssignableFrom(event.getClass())) { + return; + } + method.invoke(l, event); + } catch (java.lang.reflect.InvocationTargetException ex) { + throw new org.bukkit.event.EventException(ex.getCause()); + } catch (Throwable t) { + throw new org.bukkit.event.EventException(t); + } + }; + + eventSet.add(new RegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled())); + } + return ret; } } diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginManager.java b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginManager.java index e3e1627..7acd4a6 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginManager.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginManager.java @@ -20,6 +20,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jspecify.annotations.NonNull; import org.patchbukkit.events.PatchBukkitEventManager; +import org.patchbukkit.loader.PatchBukkitPluginClassLoader; import org.patchbukkit.permissions.PatchBukkitPermissionManager; @SuppressWarnings("removal") @@ -77,37 +78,77 @@ public class PatchBukkitPluginManager implements PluginManager { @Override public @Nullable Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, InvalidDescriptionException, UnknownDependencyException { - throw new UnsupportedOperationException( - "Unimplemented method 'loadPlugin'" - ); + if (file == null || !file.exists()) { + throw new InvalidPluginException("File does not exist: " + file); + } + + PatchBukkitPluginClassLoader loader; + try { + loader = new PatchBukkitPluginClassLoader( + getClass().getClassLoader(), + file + ); + } catch (Exception e) { + throw new InvalidPluginException("Failed to load plugin description or classloader: " + file.getName(), e); + } + + PluginDescriptionFile description = loader.getDescription(); + String mainClass = description.getMain(); + try { + Class jarClass = Class.forName(mainClass, true, loader); + Class pluginClass = jarClass.asSubclass(Plugin.class); + Plugin plugin = pluginClass.getDeclaredConstructor().newInstance(); + loader.init((org.bukkit.plugin.java.JavaPlugin) plugin); + registerPlugin(plugin); + return plugin; + } catch (Throwable e) { + throw new InvalidPluginException("Failed to instantiate main class " + mainClass + " for plugin " + description.getName(), e); + } } @Override public @NotNull Plugin[] loadPlugins(@NotNull File directory) { - throw new UnsupportedOperationException( - "Unimplemented method 'loadPlugins'" - ); + if (directory == null || !directory.isDirectory()) { + return new Plugin[0]; + } + + File[] files = directory.listFiles((dir, name) -> name.endsWith(".jar")); + if (files == null) { + return new Plugin[0]; + } + + return loadPlugins(files); } @Override public @NotNull Plugin[] loadPlugins(@NonNull @NotNull File[] files) { - throw new UnsupportedOperationException( - "Unimplemented method 'loadPlugins'" - ); + java.util.List result = new java.util.ArrayList<>(); + for (File file : files) { + if (file != null && file.isFile() && file.getName().endsWith(".jar")) { + try { + Plugin plugin = loadPlugin(file); + if (plugin != null) { + result.add(plugin); + } + } catch (Exception e) { + server.getLogger().severe("Could not load plugin '" + file.getName() + "' in folder '" + file.getParent() + "': " + e.getMessage()); + } + } + } + return result.toArray(new Plugin[0]); } @Override public void disablePlugins() { - throw new UnsupportedOperationException( - "Unimplemented method 'disablePlugins'" - ); + for (Plugin plugin : getPlugins()) { + disablePlugin(plugin); + } } @Override public void clearPlugins() { - throw new UnsupportedOperationException( - "Unimplemented method 'clearPlugins'" - ); + disablePlugins(); + plugins.clear(); } @Override @@ -262,9 +303,7 @@ public class PatchBukkitPluginManager implements PluginManager { @Override public boolean useTimings() { - throw new UnsupportedOperationException( - "Unimplemented method 'useTimings'" - ); + return false; } @Override @@ -272,8 +311,9 @@ public class PatchBukkitPluginManager implements PluginManager { PluginMeta pluginMeta, PluginMeta dependencyConfig ) { - throw new UnsupportedOperationException( - "Unimplemented method 'isTransitiveDependency'" - ); + if (pluginMeta == null || dependencyConfig == null) { + return false; + } + return pluginMeta.getPluginDependencies().contains(dependencyConfig.getName()); } } diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitServer.java b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitServer.java index cc46e5f..f407086 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitServer.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitServer.java @@ -101,6 +101,7 @@ import patchbukkit.bridge.NativeBridgeFfi; import patchbukkit.log.LogLevel; import patchbukkit.log.SendLogRequest; +@SuppressWarnings({ "deprecation", "removal", "unchecked" }) public class PatchBukkitServer implements Server { private final String serverName = @@ -181,40 +182,55 @@ public class PatchBukkitServer implements Server { this.pluginManager.registerPlugin(plugin); } + private final Messenger messenger = new org.patchbukkit.messaging.PatchBukkitMessenger(); + private File pluginsFolder = new File("plugins"); + + private static void validateChannel(String channel) { + if (channel == null) throw new IllegalArgumentException("Channel cannot be null"); + if (channel.length() > 64) { + throw new IllegalArgumentException("Channel '" + channel + "' is invalid"); + } + } + @Override public void sendPluginMessage( @NotNull Plugin source, @NotNull String channel, byte@NotNull [] message ) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'sendPluginMessage'" - ); + if (source == null) throw new IllegalArgumentException("Plugin source cannot be null"); + validateChannel(channel); + if (message == null) throw new IllegalArgumentException("Message cannot be null"); + + if (!messenger.isOutgoingChannelRegistered(source, channel)) { + throw new IllegalArgumentException("Plugin " + source.getDescription().getFullName() + " has not registered outgoing channel '" + channel + "'"); + } + + for (Player player : getOnlinePlayers()) { + player.sendPluginMessage(source, channel, message); + } } @Override public @NotNull Set getListeningPluginChannels() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getListeningPluginChannels'" - ); + Set channels = new java.util.HashSet<>(); + for (Player player : getOnlinePlayers()) { + channels.addAll(player.getListeningPluginChannels()); + } + return java.util.Collections.unmodifiableSet(channels); } @Override public @NotNull Iterable audiences() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'audiences'" - ); + return (Collection) (Collection) getOnlinePlayers(); } @Override public @NotNull File getPluginsFolder() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getPluginsFolder'" - ); + if (!pluginsFolder.exists()) { + pluginsFolder.mkdirs(); + } + return this.pluginsFolder; } @Override @@ -718,10 +734,10 @@ public class PatchBukkitServer implements Server { @NotNull CommandSender sender, @NotNull String commandLine ) throws CommandException { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'dispatchCommand'" - ); + if (sender == null) throw new IllegalArgumentException("Sender cannot be null"); + if (commandLine == null) throw new IllegalArgumentException("CommandLine cannot be null"); + + return this.commandMap.dispatch(sender, commandLine); } @Override @@ -1098,10 +1114,7 @@ public class PatchBukkitServer implements Server { @Override public @NotNull Messenger getMessenger() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getMessenger'" - ); + return this.messenger; } @Override diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitCommandMap.java b/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitCommandMap.java index 1426ab2..35165fd 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitCommandMap.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitCommandMap.java @@ -61,7 +61,7 @@ public class PatchBukkitCommandMap implements CommandMap { public boolean dispatch(@NotNull CommandSender sender, @NotNull String cmdLine) throws CommandException { if (cmdLine == null) return false; String cleanLine = cmdLine.trim(); - if (cleanLine.startsWith("/")) { + while (cleanLine.startsWith("/")) { cleanLine = cleanLine.substring(1).trim(); } if (cleanLine.isEmpty()) return false; @@ -79,7 +79,6 @@ public class PatchBukkitCommandMap implements CommandMap { try { // Extract arguments (everything after the label) String[] args = Arrays.copyOfRange(split, 1, split.length); - // This triggers the PluginCommand.execute() method you implemented earlier return command.execute(sender, sentLabel, args); } catch (Exception ex) { throw new CommandException("Unhandled exception executing '" + cmdLine + "'", ex); @@ -108,7 +107,12 @@ public class PatchBukkitCommandMap implements CommandMap { @Override public @Nullable List tabComplete(@NotNull CommandSender sender, @NotNull String cmdLine, @Nullable Location location) { - String[] split = cmdLine.split(" ", -1); + if (cmdLine == null) return new ArrayList<>(); + String cleanLine = cmdLine.trim(); + while (cleanLine.startsWith("/")) { + cleanLine = cleanLine.substring(1).trim(); + } + String[] split = cleanLine.split(" ", -1); if (split.length == 0) return new ArrayList<>(); String label = split[0].toLowerCase(); diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitPlayer.java b/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitPlayer.java index 458bbb9..83603ff 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitPlayer.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitPlayer.java @@ -76,7 +76,7 @@ import patchbukkit.abilities.SetAbilitiesRequest; import patchbukkit.sound.PlayerEntityPlaySoundRequest; import patchbukkit.sound.PlayerPlaySoundRequest; -@SuppressWarnings({ "deprecation", "removal" }) +@SuppressWarnings({ "deprecation", "removal", "unchecked" }) public class PatchBukkitPlayer extends PatchBukkitHumanEntity implements Player { @@ -94,11 +94,7 @@ public class PatchBukkitPlayer @Override public void sendRawMessage(UUID sender, String message) { - if (sender == null) { - sender = this.getUniqueId(); - } - - var request = SendMessageRequest.newBuilder().setMessage(message).setUuid(BridgeUtils.convertUuid(sender)).build(); + var request = SendMessageRequest.newBuilder().setMessage(message != null ? message : "").setUuid(BridgeUtils.convertUuid(this.getUniqueId())).build(); NativeBridgeFfi.sendMessage(request); } @@ -1308,8 +1304,7 @@ public class PatchBukkitPlayer @Override public boolean isFlying() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'isFlying'"); + return NativeBridgeFfi.getAbilities(BridgeUtils.convertUuid(this.uuid)).getFlying(); } @Override @@ -1321,26 +1316,32 @@ public class PatchBukkitPlayer @Override public void setFlySpeed(float value) throws IllegalArgumentException { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setFlySpeed'"); + if (value < -1.0f || value > 1.0f) { + throw new IllegalArgumentException("Speed value " + value + " is not between -1.0 and 1.0"); + } + var playerUuid = BridgeUtils.convertUuid(this.getUniqueId()); + var abilities = NativeBridgeFfi.getAbilities(playerUuid).toBuilder().setFlySpeed(value).build(); + NativeBridgeFfi.setAbilities(SetAbilitiesRequest.newBuilder().setAbilities(abilities).setUuid(playerUuid).build()); } @Override public void setWalkSpeed(float value) throws IllegalArgumentException { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setWalkSpeed'"); + if (value < -1.0f || value > 1.0f) { + throw new IllegalArgumentException("Speed value " + value + " is not between -1.0 and 1.0"); + } + var playerUuid = BridgeUtils.convertUuid(this.getUniqueId()); + var abilities = NativeBridgeFfi.getAbilities(playerUuid).toBuilder().setWalkSpeed(value).build(); + NativeBridgeFfi.setAbilities(SetAbilitiesRequest.newBuilder().setAbilities(abilities).setUuid(playerUuid).build()); } @Override public float getFlySpeed() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getFlySpeed'"); + return NativeBridgeFfi.getAbilities(BridgeUtils.convertUuid(this.uuid)).getFlySpeed(); } @Override public float getWalkSpeed() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getWalkSpeed'"); + return NativeBridgeFfi.getAbilities(BridgeUtils.convertUuid(this.uuid)).getWalkSpeed(); } @Override diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/messaging/PatchBukkitMessenger.java b/java/patchbukkit/src/main/java/org/patchbukkit/messaging/PatchBukkitMessenger.java new file mode 100644 index 0000000..a48ac38 --- /dev/null +++ b/java/patchbukkit/src/main/java/org/patchbukkit/messaging/PatchBukkitMessenger.java @@ -0,0 +1,244 @@ +package org.patchbukkit.messaging; + +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.messaging.*; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; + +public class PatchBukkitMessenger implements Messenger { + + private final Map> incomingByChannel = new ConcurrentHashMap<>(); + private final Map> incomingByPlugin = new ConcurrentHashMap<>(); + private final Map> outgoingByChannel = new ConcurrentHashMap<>(); + private final Map> outgoingByPlugin = new ConcurrentHashMap<>(); + + private static void validateChannel(String channel) { + if (channel == null) { + throw new IllegalArgumentException("Channel cannot be null"); + } + if (channel.length() > 64) { + throw new IllegalArgumentException("Channel cannot be longer than 64 characters"); + } + } + + @Override + public boolean isReservedChannel(String channel) { + validateChannel(channel); + return channel.equalsIgnoreCase("REGISTER") || channel.equalsIgnoreCase("UNREGISTER") || channel.equalsIgnoreCase("FML") || channel.equalsIgnoreCase("FML|HS") || channel.equalsIgnoreCase("FML|MP"); + } + + @Override + public void registerOutgoingPluginChannel(Plugin plugin, String channel) { + if (plugin == null) throw new IllegalArgumentException("Plugin cannot be null"); + validateChannel(channel); + if (isReservedChannel(channel)) throw new ReservedChannelException(channel); + + outgoingByChannel.computeIfAbsent(channel.toLowerCase(Locale.ENGLISH), k -> new CopyOnWriteArraySet<>()).add(plugin); + outgoingByPlugin.computeIfAbsent(plugin, k -> new CopyOnWriteArraySet<>()).add(channel.toLowerCase(Locale.ENGLISH)); + } + + @Override + public void unregisterOutgoingPluginChannel(Plugin plugin, String channel) { + if (plugin == null) throw new IllegalArgumentException("Plugin cannot be null"); + validateChannel(channel); + + Set plugins = outgoingByChannel.get(channel.toLowerCase(Locale.ENGLISH)); + if (plugins != null) { + plugins.remove(plugin); + } + Set channels = outgoingByPlugin.get(plugin); + if (channels != null) { + channels.remove(channel.toLowerCase(Locale.ENGLISH)); + } + } + + @Override + public void unregisterOutgoingPluginChannel(Plugin plugin) { + if (plugin == null) throw new IllegalArgumentException("Plugin cannot be null"); + Set channels = outgoingByPlugin.remove(plugin); + if (channels != null) { + for (String channel : channels) { + Set plugins = outgoingByChannel.get(channel); + if (plugins != null) { + plugins.remove(plugin); + } + } + } + } + + @Override + public PluginMessageListenerRegistration registerIncomingPluginChannel(Plugin plugin, String channel, PluginMessageListener listener) { + if (plugin == null) throw new IllegalArgumentException("Plugin cannot be null"); + validateChannel(channel); + if (listener == null) throw new IllegalArgumentException("Listener cannot be null"); + if (isReservedChannel(channel)) throw new ReservedChannelException(channel); + + PluginMessageListenerRegistration reg = new PluginMessageListenerRegistration(this, plugin, channel, listener); + incomingByChannel.computeIfAbsent(channel.toLowerCase(Locale.ENGLISH), k -> new CopyOnWriteArraySet<>()).add(reg); + incomingByPlugin.computeIfAbsent(plugin, k -> new CopyOnWriteArraySet<>()).add(reg); + return reg; + } + + @Override + public void unregisterIncomingPluginChannel(Plugin plugin, String channel, PluginMessageListener listener) { + if (plugin == null) throw new IllegalArgumentException("Plugin cannot be null"); + validateChannel(channel); + if (listener == null) throw new IllegalArgumentException("Listener cannot be null"); + + PluginMessageListenerRegistration reg = new PluginMessageListenerRegistration(this, plugin, channel, listener); + Set regs = incomingByChannel.get(channel.toLowerCase(Locale.ENGLISH)); + if (regs != null) { + regs.remove(reg); + } + Set pluginRegs = incomingByPlugin.get(plugin); + if (pluginRegs != null) { + pluginRegs.remove(reg); + } + } + + @Override + public void unregisterIncomingPluginChannel(Plugin plugin, String channel) { + if (plugin == null) throw new IllegalArgumentException("Plugin cannot be null"); + validateChannel(channel); + + Set regs = incomingByChannel.get(channel.toLowerCase(Locale.ENGLISH)); + if (regs != null) { + regs.removeIf(reg -> reg.getPlugin().equals(plugin)); + } + Set pluginRegs = incomingByPlugin.get(plugin); + if (pluginRegs != null) { + pluginRegs.removeIf(reg -> reg.getChannel().equalsIgnoreCase(channel)); + } + } + + @Override + public void unregisterIncomingPluginChannel(Plugin plugin) { + if (plugin == null) throw new IllegalArgumentException("Plugin cannot be null"); + Set pluginRegs = incomingByPlugin.remove(plugin); + if (pluginRegs != null) { + for (PluginMessageListenerRegistration reg : pluginRegs) { + Set channelRegs = incomingByChannel.get(reg.getChannel().toLowerCase(Locale.ENGLISH)); + if (channelRegs != null) { + channelRegs.remove(reg); + } + } + } + } + + @Override + public Set getOutgoingChannels() { + return Collections.unmodifiableSet(outgoingByChannel.keySet()); + } + + @Override + public Set getOutgoingChannels(Plugin plugin) { + if (plugin == null) throw new IllegalArgumentException("Plugin cannot be null"); + Set channels = outgoingByPlugin.get(plugin); + return channels != null ? Collections.unmodifiableSet(channels) : Collections.emptySet(); + } + + @Override + public Set getIncomingChannels() { + return Collections.unmodifiableSet(incomingByChannel.keySet()); + } + + @Override + public Set getIncomingChannels(Plugin plugin) { + if (plugin == null) throw new IllegalArgumentException("Plugin cannot be null"); + Set regs = incomingByPlugin.get(plugin); + if (regs == null || regs.isEmpty()) return Collections.emptySet(); + + Set result = new HashSet<>(); + for (PluginMessageListenerRegistration reg : regs) { + result.add(reg.getChannel()); + } + return Collections.unmodifiableSet(result); + } + + @Override + public Set getIncomingChannelRegistrations(Plugin plugin) { + if (plugin == null) throw new IllegalArgumentException("Plugin cannot be null"); + Set regs = incomingByPlugin.get(plugin); + return regs != null ? Collections.unmodifiableSet(regs) : Collections.emptySet(); + } + + @Override + public Set getIncomingChannelRegistrations(String channel) { + validateChannel(channel); + Set regs = incomingByChannel.get(channel.toLowerCase(Locale.ENGLISH)); + return regs != null ? Collections.unmodifiableSet(regs) : Collections.emptySet(); + } + + @Override + public Set getIncomingChannelRegistrations(Plugin plugin, String channel) { + if (plugin == null) throw new IllegalArgumentException("Plugin cannot be null"); + validateChannel(channel); + + Set regs = incomingByChannel.get(channel.toLowerCase(Locale.ENGLISH)); + if (regs == null || regs.isEmpty()) return Collections.emptySet(); + + Set filtered = new HashSet<>(); + for (PluginMessageListenerRegistration reg : regs) { + if (reg.getPlugin().equals(plugin)) { + filtered.add(reg); + } + } + return Collections.unmodifiableSet(filtered); + } + + @Override + public boolean isRegistrationValid(PluginMessageListenerRegistration registration) { + if (registration == null) return false; + Set regs = incomingByChannel.get(registration.getChannel().toLowerCase(Locale.ENGLISH)); + return regs != null && regs.contains(registration); + } + + @Override + public boolean isIncomingChannelRegistered(Plugin plugin, String channel) { + if (plugin == null) throw new IllegalArgumentException("Plugin cannot be null"); + validateChannel(channel); + + Set regs = incomingByChannel.get(channel.toLowerCase(Locale.ENGLISH)); + if (regs == null) return false; + for (PluginMessageListenerRegistration reg : regs) { + if (reg.getPlugin().equals(plugin)) return true; + } + return false; + } + + @Override + public boolean isOutgoingChannelRegistered(Plugin plugin, String channel) { + if (plugin == null) throw new IllegalArgumentException("Plugin cannot be null"); + validateChannel(channel); + + Set plugins = outgoingByChannel.get(channel.toLowerCase(Locale.ENGLISH)); + return plugins != null && plugins.contains(plugin); + } + + @Override + public void dispatchIncomingMessage(Player source, String channel, byte[] message) { + if (source == null) throw new IllegalArgumentException("Player source cannot be null"); + validateChannel(channel); + if (message == null) throw new IllegalArgumentException("Message cannot be null"); + + Set regs = incomingByChannel.get(channel.toLowerCase(Locale.ENGLISH)); + if (regs != null) { + for (PluginMessageListenerRegistration reg : regs) { + try { + reg.getListener().onPluginMessageReceived(channel, source, message); + } catch (Throwable t) { + source.getServer().getLogger().severe("Error dispatching plugin message on " + channel + " to " + reg.getPlugin().getName() + ": " + t.getMessage()); + } + } + } + } + + @Override + public void dispatchIncomingMessage(io.papermc.paper.connection.PlayerConnection connection, String channel, byte[] message) { + if (connection == null) throw new IllegalArgumentException("Connection cannot be null"); + // Non-player connection handling if applicable + } +} diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 4ea64ac..534b9b6 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -415,7 +415,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -878,7 +878,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1800,9 +1800,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "j4rs" -version = "0.25.0" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbaca95c5c7799ed193fc8aa789568f9d31a9d01381460227ef0074fea1855f" +checksum = "c26fb1429997a5d8f98777df359f0132f62905ecce1d3bc2836372045c67de89" dependencies = [ "cesu8", "dunce", @@ -1828,9 +1828,9 @@ dependencies = [ [[package]] name = "jiff" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e184d09547b80eb7e20d141ba2fb1fbac843ca53f4cf1b31210adc4c1adc6e16" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" dependencies = [ "defmt", "jiff-core", @@ -1852,9 +1852,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323da076b7a6faf914dc677cb05a4b907742ff7375c8322c9e7f5061e5e0e9de" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" dependencies = [ "jiff-core", "proc-macro2", @@ -2773,7 +2773,7 @@ dependencies = [ [[package]] name = "pumpkin" version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#faf8c50d806e6101be1e70ba2fee0d60bd1414cf" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#ac89bff1caea0a6c728e67212811a06914ea9dbe" dependencies = [ "arc-swap", "base64 0.23.0", @@ -2814,7 +2814,7 @@ dependencies = [ "sha1", "sha2 0.11.0", "sysinfo", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", "tokio", "tokio-util", @@ -2831,7 +2831,7 @@ dependencies = [ [[package]] name = "pumpkin-api-macros" version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#faf8c50d806e6101be1e70ba2fee0d60bd1414cf" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#ac89bff1caea0a6c728e67212811a06914ea9dbe" dependencies = [ "proc-macro-error2", "proc-macro2", @@ -2842,7 +2842,7 @@ dependencies = [ [[package]] name = "pumpkin-codecs" version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#faf8c50d806e6101be1e70ba2fee0d60bd1414cf" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#ac89bff1caea0a6c728e67212811a06914ea9dbe" dependencies = [ "serde_json", "tracing", @@ -2851,7 +2851,7 @@ dependencies = [ [[package]] name = "pumpkin-config" version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#faf8c50d806e6101be1e70ba2fee0d60bd1414cf" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#ac89bff1caea0a6c728e67212811a06914ea9dbe" dependencies = [ "pumpkin-util", "serde", @@ -2863,7 +2863,7 @@ dependencies = [ [[package]] name = "pumpkin-data" version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#faf8c50d806e6101be1e70ba2fee0d60bd1414cf" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#ac89bff1caea0a6c728e67212811a06914ea9dbe" dependencies = [ "crc-fast", "phf", @@ -2876,14 +2876,14 @@ dependencies = [ [[package]] name = "pumpkin-inventory" version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#faf8c50d806e6101be1e70ba2fee0d60bd1414cf" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#ac89bff1caea0a6c728e67212811a06914ea9dbe" dependencies = [ "pumpkin-data", "pumpkin-protocol", "pumpkin-util", "pumpkin-world", "rand 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", ] @@ -2891,7 +2891,7 @@ dependencies = [ [[package]] name = "pumpkin-macros" version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#faf8c50d806e6101be1e70ba2fee0d60bd1414cf" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#ac89bff1caea0a6c728e67212811a06914ea9dbe" dependencies = [ "heck", "proc-macro-error2", @@ -2903,14 +2903,14 @@ dependencies = [ [[package]] name = "pumpkin-nbt" version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#faf8c50d806e6101be1e70ba2fee0d60bd1414cf" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#ac89bff1caea0a6c728e67212811a06914ea9dbe" dependencies = [ "bytes", "cesu8", "flate2", "pumpkin-codecs", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "uuid", ] @@ -2918,7 +2918,7 @@ dependencies = [ [[package]] name = "pumpkin-protocol" version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#faf8c50d806e6101be1e70ba2fee0d60bd1414cf" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#ac89bff1caea0a6c728e67212811a06914ea9dbe" dependencies = [ "aes", "async-compression", @@ -2935,7 +2935,7 @@ dependencies = [ "pumpkin-world", "serde", "sha2 0.11.0", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "uuid", ] @@ -2943,7 +2943,7 @@ dependencies = [ [[package]] name = "pumpkin-util" version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#faf8c50d806e6101be1e70ba2fee0d60bd1414cf" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#ac89bff1caea0a6c728e67212811a06914ea9dbe" dependencies = [ "base64 0.23.0", "bytes", @@ -2961,7 +2961,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.11.0", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "ureq", "uuid", @@ -2970,7 +2970,7 @@ dependencies = [ [[package]] name = "pumpkin-world" version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#faf8c50d806e6101be1e70ba2fee0d60bd1414cf" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#ac89bff1caea0a6c728e67212811a06914ea9dbe" dependencies = [ "arc-swap", "bitflags 2.13.1", @@ -2995,7 +2995,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "slotmap", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-util", "tracing", @@ -3105,7 +3105,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -3125,9 +3125,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.4" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -3732,11 +3732,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -3752,13 +3752,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -4384,7 +4384,7 @@ dependencies = [ "pulley-interpreter", "smallvec", "target-lexicon", - "thiserror 2.0.18", + "thiserror 2.0.19", "wasmparser", "wasmtime-environ", "wasmtime-internal-core", @@ -4482,7 +4482,7 @@ dependencies = [ "io-lifetimes 3.0.1", "rand 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", "rustix", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "url", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index e838107..5a9b443 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -21,7 +21,7 @@ pumpkin-api-macros = { git = "https://github.com/Pumpkin-MC/Pumpkin.git", branch tokio = "1" tracing = "0.1" glob = "0.3.4" -j4rs = { version = "=0.25.0" } +j4rs = { version = "0.25.2" } rust-embed = { version = "8.12.0", features = ["interpolate-folder-path"] } walkdir = "2" zip = "8.6" @@ -39,7 +39,7 @@ prost = "0.14" toml = "1.1" [build-dependencies] -j4rs = { version = "=0.25.0", artifact = "cdylib", lib = true } +j4rs = { version = "0.25.2", artifact = "cdylib", lib = true } env_logger = "0.11.11" log = "0.4" prost-build = "0.14" diff --git a/rust/build/java.rs b/rust/build/java.rs index fe9b55b..13ed36e 100644 --- a/rust/build/java.rs +++ b/rust/build/java.rs @@ -22,6 +22,10 @@ pub fn setup_java(base: PathBuf) { )])) .skip_setting_native_lib() .with_base_path(resources) + .java_opts(vec![ + j4rs::JavaOpt::new("--enable-native-access=ALL-UNNAMED"), + j4rs::JavaOpt::new("-Dcom.google.protobuf.useUnsafe=false"), + ]) .build() .map_err(|err| format!("jvm failed to init: {err:?}")) .unwrap(); diff --git a/rust/src/commands/mod.rs b/rust/src/commands/mod.rs index 22d354c..b6fb360 100644 --- a/rust/src/commands/mod.rs +++ b/rust/src/commands/mod.rs @@ -112,14 +112,12 @@ impl ArgumentConsumer for AnyCommandNode { impl From<&CommandSender> for SimpleCommandSender { fn from(val: &CommandSender) -> Self { match val { - CommandSender::Rcon(_mutex) => todo!(), - CommandSender::Console => Self::Console, + CommandSender::Console | CommandSender::Rcon(_) | CommandSender::Dummy => Self::Console, CommandSender::Player(player) => Self::Player( player.gameprofile.id.to_string(), player.gameprofile.name.clone(), ), - CommandSender::CommandBlock(_block_entity, _world) => todo!(), - CommandSender::Dummy => Self::Console, + CommandSender::CommandBlock(_block_entity, _world) => Self::Console, } } } diff --git a/rust/src/java/jvm/worker.rs b/rust/src/java/jvm/worker.rs index 4fe3703..350618d 100644 --- a/rust/src/java/jvm/worker.rs +++ b/rust/src/java/jvm/worker.rs @@ -209,7 +209,13 @@ impl JvmWorker { fn initialize_jvm(&mut self, j4rs_path: &PathBuf) -> anyhow::Result<()> { tracing::info!("Initializing JVM with path: {j4rs_path:?}"); - let jvm = JvmBuilder::new().with_base_path(j4rs_path).build()?; + let jvm = JvmBuilder::new() + .with_base_path(j4rs_path) + .java_opts(vec![ + j4rs::JavaOpt::new("--enable-native-access=ALL-UNNAMED"), + j4rs::JavaOpt::new("-Dcom.google.protobuf.useUnsafe=false"), + ]) + .build()?; initialize_callbacks(&jvm)?; diff --git a/rust/src/java/plugin/command_manager.rs b/rust/src/java/plugin/command_manager.rs index 17e5d4e..f688045 100644 --- a/rust/src/java/plugin/command_manager.rs +++ b/rust/src/java/plugin/command_manager.rs @@ -169,9 +169,32 @@ impl CommandManager { &[InvocationArg::try_from(&cmd_name)?, j_plugin_arg], )?)); tracing::info!("Registering Bukkit command: {}", &cmd_name); + let cmd_aliases: Vec = cmd_data.aliases.as_ref().map_or_else(Vec::new, |a| a.to_vec()); + let mut names: Vec = vec![cmd_name.clone()]; + names.extend(cmd_aliases.clone()); + { let cmd_lock = j_plugin_cmd.lock().unwrap(); let j_plugin_cmd_owned = jvm.clone_instance(&cmd_lock)?; + + if let Some(ref desc) = cmd_data.description { + let _ = jvm.invoke(&j_plugin_cmd_owned, "setDescription", &[InvocationArg::try_from(desc)?]); + } + if let Some(ref usage) = cmd_data.usage { + let _ = jvm.invoke(&j_plugin_cmd_owned, "setUsage", &[InvocationArg::try_from(usage)?]); + } + if let Some(ref perm_msg) = cmd_data.permission_message { + let _ = jvm.invoke(&j_plugin_cmd_owned, "setPermissionMessage", &[InvocationArg::try_from(perm_msg)?]); + } + if let Some(ref perm) = cmd_data.permission { + let _ = jvm.invoke(&j_plugin_cmd_owned, "setPermission", &[InvocationArg::try_from(perm)?]); + } + if !cmd_aliases.is_empty() { + let j_aliases = jvm.create_java_array("java.lang.String", &cmd_aliases.iter().map(|a| InvocationArg::try_from(a).unwrap()).collect::>())?; + let j_list = jvm.invoke_static("java.util.Arrays", "asList", &[InvocationArg::from(j_aliases)])?; + let _ = jvm.invoke(&j_plugin_cmd_owned, "setAliases", &[InvocationArg::from(j_list)]); + } + jvm.invoke( command_map, "register", @@ -186,7 +209,7 @@ impl CommandManager { let node = init_java_command( cmd_name.clone(), command_tx.clone(), - [&cmd_name], + names, cmd_data.description.clone().unwrap_or_default(), );