From afb0e827133f672cc2ceccedbec5156423bd2bc3 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sat, 8 Aug 2026 20:39:14 +0200 Subject: [PATCH] many fixes --- java/gradle/wrapper/gradle-wrapper.jar | Bin 46175 -> 47505 bytes java/gradle/wrapper/gradle-wrapper.properties | 4 +- java/gradlew | 6 +- java/gradlew.bat | 35 +- java/patchbukkit-test-plugin/build.gradle.kts | 3 +- .../testplugin/tests/CommandSystemTests.java | 39 + .../testplugin/tests/EntityTests.java | 26 + .../testplugin/tests/StubTests.java | 7 + java/patchbukkit/build.gradle.kts | 18 +- java/patchbukkit/logs/latest.log | 0 .../com/destroystokyo/paper/PaperConfig.java | 7 + .../main/java/io/papermc/lib/PaperLib.java | 52 + .../paper/configuration/Configuration.java | 7 + .../org/bukkit/command/SimpleCommandMap.java | 139 ++ .../java/org/patchbukkit/LibraryResolver.java | 43 +- .../org/patchbukkit/PatchBukkitBlockData.java | 104 +- .../PatchBukkitInternalAPIBridge.java | 46 +- .../PatchBukkitPluginClassLoader.java | 210 +- .../patchbukkit/PatchBukkitPluginLoader.java | 79 +- .../patchbukkit/PatchBukkitPluginManager.java | 26 +- .../org/patchbukkit/PatchBukkitServer.java | 266 ++- .../patchbukkit/PatchBukkitUnsafeValues.java | 98 +- .../org/patchbukkit/bridge/BridgeUtils.java | 19 +- .../patchbukkit/command/CommandFactory.java | 27 + .../PatchBukkitBlockCommandSender.java | 60 +- .../command/PatchBukkitCommandMap.java | 133 +- .../patchbukkit/entity/PatchBukkitEntity.java | 48 +- .../entity/PatchBukkitHumanEntity.java | 168 +- .../entity/PatchBukkitLivingEntity.java | 20 +- .../entity/PatchBukkitOfflinePlayer.java | 214 ++ .../patchbukkit/entity/PatchBukkitPlayer.java | 78 +- .../events/PatchBukkitEventFactory.java | 352 +++- .../events/PatchBukkitEventManager.java | 271 +++ .../patchbukkit/help/PatchBukkitHelpMap.java | 54 + .../inventory/PatchBukkitEntityEquipment.java | 194 ++ .../inventory/PatchBukkitItemFactory.java | 100 + .../inventory/PatchBukkitItemStack.java | 92 + .../inventory/PatchBukkitPlayerInventory.java | 539 +++++ .../loader/PatchBukkitBootstrap.java | 269 +++ .../PatchBukkitPersistentDataContainer.java | 84 + .../registry/LegacyRegistryIdentifiers.java | 12 +- .../registry/PatchBukkitBlockType.java | 108 + .../registry/PatchBukkitItemType.java | 69 + .../registry/PatchBukkitRegistry.java | 205 +- .../registry/PatchBukkitRegistryAccess.java | 50 +- .../registry/PatchBukkitSound.java | 14 +- .../org/patchbukkit/tag/PatchBukkitTag.java | 37 + .../patchbukkit/world/PatchBukkitBlock.java | 22 +- .../patchbukkit/world/PatchBukkitChunk.java | 191 ++ .../patchbukkit/world/PatchBukkitWorld.java | 89 +- .../services/io.papermc.paper.ServerBuildInfo | 1 - .../loader/LibraryResolverTest.java | 63 + .../PatchBukkitRegistryAccessTest.java | 143 ++ .../patchbukkit/protocgen/FfiGenerator.java | 22 +- java/settings.gradle.kts | 11 +- proto/patchbukkit/bridge.proto | 16 + proto/patchbukkit/command.proto | 13 + proto/patchbukkit/entity.proto | 13 + proto/patchbukkit/events/block_events.proto | 72 + proto/patchbukkit/events/event.proto | 16 + proto/patchbukkit/events/player_events.proto | 35 + proto/patchbukkit/events/server_events.proto | 5 + proto/patchbukkit/itemstack.proto | 28 + proto/patchbukkit/world.proto | 11 + rust/Cargo.lock | 1817 ++++++++++++++--- rust/Cargo.toml | 2 +- rust/build/java.rs | 19 +- rust/build/protobufs.rs | 12 +- rust/rust-toolchain.toml | 2 +- rust/src/commands/mod.rs | 16 +- rust/src/config/paper.rs | 8 + rust/src/directories.rs | 14 + rust/src/events/handler.rs | 535 ++++- rust/src/java/jar.rs | 132 +- rust/src/java/jvm/commands.rs | 1 + rust/src/java/jvm/worker.rs | 15 +- rust/src/java/native_callbacks/command.rs | 65 + rust/src/java/native_callbacks/entity.rs | 61 + rust/src/java/native_callbacks/events.rs | 728 ++++++- rust/src/java/native_callbacks/itemstack.rs | 192 +- rust/src/java/native_callbacks/mod.rs | 4 + rust/src/java/native_callbacks/world.rs | 52 +- rust/src/java/plugin/command_manager.rs | 84 +- rust/src/java/plugin/event_manager.rs | 17 +- rust/src/java/plugin/loader.rs | 76 + rust/src/java/plugin/manager.rs | 236 +-- rust/src/java/plugin/mod.rs | 1 + rust/src/lib.rs | 93 +- rust/src/proto/mod.rs | 4 + 89 files changed, 8208 insertions(+), 1161 deletions(-) create mode 100644 java/patchbukkit/logs/latest.log create mode 100644 java/patchbukkit/src/main/java/com/destroystokyo/paper/PaperConfig.java create mode 100644 java/patchbukkit/src/main/java/io/papermc/lib/PaperLib.java create mode 100644 java/patchbukkit/src/main/java/io/papermc/paper/configuration/Configuration.java create mode 100644 java/patchbukkit/src/main/java/org/bukkit/command/SimpleCommandMap.java create mode 100644 java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitOfflinePlayer.java create mode 100644 java/patchbukkit/src/main/java/org/patchbukkit/help/PatchBukkitHelpMap.java create mode 100644 java/patchbukkit/src/main/java/org/patchbukkit/inventory/PatchBukkitEntityEquipment.java create mode 100644 java/patchbukkit/src/main/java/org/patchbukkit/inventory/PatchBukkitItemFactory.java create mode 100644 java/patchbukkit/src/main/java/org/patchbukkit/inventory/PatchBukkitItemStack.java create mode 100644 java/patchbukkit/src/main/java/org/patchbukkit/inventory/PatchBukkitPlayerInventory.java create mode 100644 java/patchbukkit/src/main/java/org/patchbukkit/loader/PatchBukkitBootstrap.java create mode 100644 java/patchbukkit/src/main/java/org/patchbukkit/persistence/PatchBukkitPersistentDataContainer.java create mode 100644 java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitBlockType.java create mode 100644 java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitItemType.java create mode 100644 java/patchbukkit/src/main/java/org/patchbukkit/tag/PatchBukkitTag.java create mode 100644 java/patchbukkit/src/main/java/org/patchbukkit/world/PatchBukkitChunk.java delete mode 100644 java/patchbukkit/src/main/resources/META-INF/services/io.papermc.paper.ServerBuildInfo create mode 100644 java/patchbukkit/src/test/java/org/patchbukkit/loader/LibraryResolverTest.java create mode 100644 java/patchbukkit/src/test/java/org/patchbukkit/registry/PatchBukkitRegistryAccessTest.java create mode 100644 proto/patchbukkit/command.proto create mode 100644 proto/patchbukkit/events/block_events.proto create mode 100644 rust/src/java/native_callbacks/command.rs create mode 100644 rust/src/java/plugin/loader.rs diff --git a/java/gradle/wrapper/gradle-wrapper.jar b/java/gradle/wrapper/gradle-wrapper.jar index 61285a659d17295f1de7c53e24fdf13ad755c379..eddabd2eef8d94a5437d6168ff9c87a78ff725b3 100644 GIT binary patch delta 39228 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 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,29 +65,18 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/java/patchbukkit-test-plugin/build.gradle.kts b/java/patchbukkit-test-plugin/build.gradle.kts index ec9b4cc..836d737 100644 --- a/java/patchbukkit-test-plugin/build.gradle.kts +++ b/java/patchbukkit-test-plugin/build.gradle.kts @@ -10,7 +10,8 @@ repositories { } dependencies { - compileOnly("io.papermc.paper:paper-api:26.2.build.87-stable") + compileOnly(project(":patchbukkit")) + compileOnly("io.papermc.paper:paper-api:26.2.build.111-stable") } java { 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 ee230aa..354f120 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 @@ -90,4 +90,43 @@ public final class CommandSystemTests { assertNotNull(found, "CommandMap.getCommand(\"myprefix:pbtestalias\")"); } + @ConformanceTest(name = "CommandMap supports WorldEdit double-slash commands", category = TestCategory.COMMAND_SYSTEM) + public void testDoubleSlashCommands() { + CommandMap map = Bukkit.getServer().getCommandMap(); + final String[] executedLabel = {null}; + Command testCmd = new BukkitCommand("//wand", "wand", "//wand", List.of("//set", "set")) { + @Override + public boolean execute(CommandSender sender, String label, String[] args) { + executedLabel[0] = label; + return true; + } + }; + map.register("worldedit", testCmd); + + CommandSender sender = Bukkit.getConsoleSender(); + + // 1. Dispatch with double slash + boolean resultDouble = map.dispatch(sender, "//wand"); + assertTrue(resultDouble, "dispatch() with double slash should return true"); + assertTrue("//wand".equals(executedLabel[0]), "executed label for //wand"); + + // 2. Dispatch with single slash + boolean resultSingle = map.dispatch(sender, "/wand"); + assertTrue(resultSingle, "dispatch() with single slash should return true"); + + // 3. Dispatch without slash + boolean resultNoSlash = map.dispatch(sender, "wand"); + assertTrue(resultNoSlash, "dispatch() without slash should return true"); + + // 4. Dispatch alias with double slash + boolean resultAlias = map.dispatch(sender, "//set"); + assertTrue(resultAlias, "dispatch() with double slash alias should return true"); + + // 5. getCommand lookups + assertNotNull(map.getCommand("//wand"), "getCommand(\"//wand\")"); + assertNotNull(map.getCommand("/wand"), "getCommand(\"/wand\")"); + assertNotNull(map.getCommand("wand"), "getCommand(\"wand\")"); + assertNotNull(map.getCommand("//set"), "getCommand(\"//set\")"); + } + } 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 d11d848..83695f5 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 @@ -43,4 +43,30 @@ public final class EntityTests { } } + @ConformanceTest(name = "Player ground state and velocity getters run cleanly", category = TestCategory.ENTITY) + public void testPlayerGroundStateAndVelocity() { + for (Player p : Bukkit.getOnlinePlayers()) { + boolean onGround = p.isOnGround(); + org.bukkit.util.Vector vel = p.getVelocity(); + assertNotNull(onGround, "Player.isOnGround()"); + assertNotNull(vel, "Player.getVelocity()"); + } + } + + @ConformanceTest(name = "Player inventory and equipment getters and setters run cleanly", category = TestCategory.ENTITY) + public void testPlayerInventoryAndEquipment() { + for (Player p : Bukkit.getOnlinePlayers()) { + org.bukkit.inventory.PlayerInventory inv = p.getInventory(); + org.bukkit.inventory.EntityEquipment eq = p.getEquipment(); + assertNotNull(inv, "Player.getInventory()"); + assertNotNull(eq, "Player.getEquipment()"); + assertNotNull(inv.getItemInMainHand(), "PlayerInventory.getItemInMainHand()"); + assertNotNull(inv.getItemInOffHand(), "PlayerInventory.getItemInOffHand()"); + assertNotNull(inv.getArmorContents(), "PlayerInventory.getArmorContents()"); + assertTrue(inv.getArmorContents().length == 4, "PlayerInventory.getArmorContents().length == 4"); + assertNotNull(inv.getContents(), "PlayerInventory.getContents()"); + assertTrue(inv.getContents().length == 41, "PlayerInventory.getContents().length == 41"); + assertTrue(inv.getHeldItemSlot() >= 0 && inv.getHeldItemSlot() < 9, "PlayerInventory.getHeldItemSlot() valid slot"); + } + } } diff --git a/java/patchbukkit-test-plugin/src/main/java/org/patchbukkit/testplugin/tests/StubTests.java b/java/patchbukkit-test-plugin/src/main/java/org/patchbukkit/testplugin/tests/StubTests.java index b48a02c..00856bf 100644 --- a/java/patchbukkit-test-plugin/src/main/java/org/patchbukkit/testplugin/tests/StubTests.java +++ b/java/patchbukkit-test-plugin/src/main/java/org/patchbukkit/testplugin/tests/StubTests.java @@ -119,4 +119,11 @@ public final class StubTests { Bukkit.getServer().getAllowNether(); } + @ConformanceTest(name = "PaperLib detection classes and PaperLib.isPaper() are available", category = TestCategory.STUBS) + public void testPaperLibDetection() throws ClassNotFoundException { + Class.forName("com.destroystokyo.paper.PaperConfig"); + Class.forName("io.papermc.paper.configuration.Configuration"); + org.patchbukkit.testplugin.TestAssertions.assertTrue(io.papermc.lib.PaperLib.isPaper(), "PaperLib.isPaper() must be true"); + } + } diff --git a/java/patchbukkit/build.gradle.kts b/java/patchbukkit/build.gradle.kts index 518479c..bc55001 100644 --- a/java/patchbukkit/build.gradle.kts +++ b/java/patchbukkit/build.gradle.kts @@ -1,6 +1,7 @@ plugins { `java-library` id("com.google.protobuf") version "0.10.0" + id("io.papermc.paperweight.userdev") version "2.0.0-SNAPSHOT" } val protobufVersion = "4.35.1" @@ -38,6 +39,7 @@ tasks.named("generateProto") { } repositories { + mavenCentral() maven { name = "papermc" url = uri("https://repo.papermc.io/repository/maven-public/") @@ -45,7 +47,7 @@ repositories { } dependencies { - implementation("io.papermc.paper:paper-api:26.2.build.87-stable") + paperweight.paperDevBundle("26.2.build.111-stable") implementation("net.sf.jopt-simple:jopt-simple:6.0-alpha-3") implementation("io.github.astonbitecode:j4rs:0.25.2") implementation("org.apache.maven:maven-resolver-provider:3.9.6") @@ -55,6 +57,12 @@ dependencies { implementation("org.apache.maven.resolver:maven-resolver-util:1.9.18") implementation("com.google.protobuf:protobuf-java:$protobufVersion") implementation("org.slf4j:slf4j-jdk14:2.0.16") + testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.named("test") { + useJUnitPlatform() } java { @@ -73,12 +81,12 @@ tasks.withType { tasks.named("jar") { duplicatesStrategy = DuplicatesStrategy.EXCLUDE - dependsOn(configurations.runtimeClasspath) + dependsOn(configurations.compileClasspath) from({ - configurations.runtimeClasspath.get() - .filter { it.name.endsWith(".jar") } - .map { zipTree(it) } + configurations.compileClasspath.get().map { file -> + if (file.isDirectory) file else zipTree(file) + } }) exclude( diff --git a/java/patchbukkit/logs/latest.log b/java/patchbukkit/logs/latest.log new file mode 100644 index 0000000..e69de29 diff --git a/java/patchbukkit/src/main/java/com/destroystokyo/paper/PaperConfig.java b/java/patchbukkit/src/main/java/com/destroystokyo/paper/PaperConfig.java new file mode 100644 index 0000000..650de3a --- /dev/null +++ b/java/patchbukkit/src/main/java/com/destroystokyo/paper/PaperConfig.java @@ -0,0 +1,7 @@ +package com.destroystokyo.paper; + +/** + * Stub class for PaperLib and Paper plugin detection (PaperLib.isPaper()). + */ +public class PaperConfig { +} diff --git a/java/patchbukkit/src/main/java/io/papermc/lib/PaperLib.java b/java/patchbukkit/src/main/java/io/papermc/lib/PaperLib.java new file mode 100644 index 0000000..6c7603d --- /dev/null +++ b/java/patchbukkit/src/main/java/io/papermc/lib/PaperLib.java @@ -0,0 +1,52 @@ +package io.papermc.lib; + +import java.util.logging.Level; +import org.bukkit.plugin.Plugin; + +/** + * Stub / implementation of PaperLib for Paper plugin compatibility. + */ +public class PaperLib { + + private PaperLib() { + } + + public static boolean isPaper() { + return true; + } + + public static boolean isSpigot() { + return true; + } + + public static boolean isVersion(int version) { + return true; + } + + public static boolean isVersion(int version, int patch) { + return true; + } + + public static int getMinecraftVersion() { + return 26; + } + + public static int getMinecraftPatchVersion() { + return 2; + } + + public static void suggestPaper(Plugin plugin) { + } + + public static void suggestPaper(Plugin plugin, String reason) { + } + + public static void suggestPaper(Plugin plugin, Level level, String reason) { + } + + public static void suggestPaper(Plugin plugin, String reason, Object... args) { + } + + public static void suggestPaper(Plugin plugin, Level level, String reason, Object... args) { + } +} diff --git a/java/patchbukkit/src/main/java/io/papermc/paper/configuration/Configuration.java b/java/patchbukkit/src/main/java/io/papermc/paper/configuration/Configuration.java new file mode 100644 index 0000000..03cc8f1 --- /dev/null +++ b/java/patchbukkit/src/main/java/io/papermc/paper/configuration/Configuration.java @@ -0,0 +1,7 @@ +package io.papermc.paper.configuration; + +/** + * Stub class for PaperLib and Paper plugin detection (PaperLib.isPaper()). + */ +public class Configuration { +} diff --git a/java/patchbukkit/src/main/java/org/bukkit/command/SimpleCommandMap.java b/java/patchbukkit/src/main/java/org/bukkit/command/SimpleCommandMap.java new file mode 100644 index 0000000..60b54bb --- /dev/null +++ b/java/patchbukkit/src/main/java/org/bukkit/command/SimpleCommandMap.java @@ -0,0 +1,139 @@ +package org.bukkit.command; + +import org.bukkit.Location; +import org.bukkit.Server; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class SimpleCommandMap implements CommandMap { + protected final Map knownCommands; + protected final Server server; + + public SimpleCommandMap(@NotNull Server server, @NotNull Map knownCommands) { + this.server = server; + this.knownCommands = knownCommands != null ? knownCommands : new HashMap<>(); + } + + public SimpleCommandMap(@NotNull Server server) { + this(server, new HashMap<>()); + } + + @Override + public void registerAll(@NotNull String fallbackPrefix, @NotNull List commands) { + if (commands != null) { + for (Command c : commands) { + register(fallbackPrefix, c); + } + } + } + + @Override + public boolean register(@NotNull String label, @NotNull String fallbackPrefix, @NotNull Command command) { + if (label == null || fallbackPrefix == null || command == null) { + return false; + } + label = label.toLowerCase().trim(); + fallbackPrefix = fallbackPrefix.toLowerCase().trim(); + + boolean registeredDirectly = false; + if (!knownCommands.containsKey(label)) { + knownCommands.put(label, command); + registeredDirectly = true; + } + knownCommands.put(fallbackPrefix + ":" + label, command); + + if (command.getAliases() != null) { + for (String alias : command.getAliases()) { + if (alias == null) continue; + alias = alias.toLowerCase().trim(); + if (!knownCommands.containsKey(alias)) { + knownCommands.put(alias, command); + } + knownCommands.put(fallbackPrefix + ":" + alias, command); + } + } + return registeredDirectly; + } + + @Override + public boolean register(@NotNull String fallbackPrefix, @NotNull Command command) { + if (command == null) return false; + return register(command.getName(), fallbackPrefix, command); + } + + @Override + public boolean dispatch(@NotNull CommandSender sender, @NotNull String commandLine) throws CommandException { + if (commandLine == null) return false; + String cleanLine = commandLine.trim(); + while (cleanLine.startsWith("/")) { + cleanLine = cleanLine.substring(1).trim(); + } + if (cleanLine.isEmpty()) return false; + + String[] split = cleanLine.split("\\s+"); + if (split.length == 0) return false; + + String sentLabel = split[0].toLowerCase(); + Command command = knownCommands.get(sentLabel); + if (command == null) return false; + + try { + String[] args = Arrays.copyOfRange(split, 1, split.length); + return command.execute(sender, sentLabel, args); + } catch (Exception ex) { + throw new CommandException("Unhandled exception executing '" + commandLine + "'", ex); + } + } + + @Override + public void clearCommands() { + knownCommands.clear(); + } + + @Override + public @Nullable Command getCommand(@NotNull String name) { + if (name == null) return null; + return knownCommands.get(name.toLowerCase()); + } + + @Override + public @NotNull Map getKnownCommands() { + return Collections.unmodifiableMap(knownCommands); + } + + @Override + public @Nullable List tabComplete(@NotNull CommandSender sender, @NotNull String cmdLine) { + return tabComplete(sender, cmdLine, null); + } + + @Override + public @Nullable List tabComplete(@NotNull CommandSender sender, @NotNull String cmdLine, @Nullable Location location) { + 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(); + Command command = knownCommands.get(label); + + if (split.length == 1) { + List completions = new ArrayList<>(); + for (String key : knownCommands.keySet()) { + if (key.startsWith(label)) completions.add(key); + } + return completions; + } + + if (command != null) { + String[] args = Arrays.copyOfRange(split, 1, split.length); + return command.tabComplete(sender, label, args, location); + } + + return null; + } +} diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/LibraryResolver.java b/java/patchbukkit/src/main/java/org/patchbukkit/LibraryResolver.java index 881a637..62f8427 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/LibraryResolver.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/LibraryResolver.java @@ -50,21 +50,38 @@ public final class LibraryResolver { RepositorySystem system = newRepositorySystem(); RepositorySystemSession session = newSession(system, baseDir); - List repos = List.of( - new RemoteRepository.Builder( - "papermc", - "default", - "https://repo.papermc.io/repository/maven-public/" - ).build(), - new RemoteRepository.Builder( - "central", - "default", - "https://repo1.maven.org/maven2/" - ).build() - ); + List repos = new ArrayList<>(List.of( + new RemoteRepository.Builder("papermc", "default", "https://repo.papermc.io/repository/maven-public/").build(), + new RemoteRepository.Builder("central", "default", "https://repo1.maven.org/maven2/").build(), + new RemoteRepository.Builder("jitpack", "default", "https://jitpack.io/").build(), + new RemoteRepository.Builder("codemc", "default", "https://repo.codemc.io/repository/maven-public/").build(), + new RemoteRepository.Builder("enginehub", "default", "https://maven.enginehub.org/repo/").build(), + new RemoteRepository.Builder("sonatype-snapshots", "default", "https://oss.sonatype.org/content/repositories/snapshots/").build(), + new RemoteRepository.Builder("sonatype-releases", "default", "https://oss.sonatype.org/content/repositories/releases/").build(), + new RemoteRepository.Builder("sponge", "default", "https://repo.spongepowered.org/repository/maven-public/").build(), + new RemoteRepository.Builder("xenondevs", "default", "https://repo.xenondevs.xyz/releases").build(), + new RemoteRepository.Builder("spigotmc", "default", "https://hub.spigotmc.org/nexus/content/repositories/snapshots/").build(), + new RemoteRepository.Builder("miraculixx-snapshots", "default", "https://repo.miraculixx.de/snapshots").build(), + new RemoteRepository.Builder("miraculixx-releases", "default", "https://repo.miraculixx.de/releases").build() + )); + + Set coordsToResolve = new LinkedHashSet<>(); + int customRepoIdx = 0; + for (String item : unique) { + String trimmed = item; + if (trimmed.startsWith("repo:")) { + trimmed = trimmed.substring(5).trim(); + } + if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) { + customRepoIdx++; + repos.add(new RemoteRepository.Builder("custom-repo-" + customRepoIdx, "default", trimmed).build()); + } else { + coordsToResolve.add(trimmed); + } + } Set files = new LinkedHashSet<>(); - for (String coord : unique) { + for (String coord : coordsToResolve) { try { Dependency dependency = new Dependency( new DefaultArtifact(coord), diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitBlockData.java b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitBlockData.java index 6bb0dc5..f288348 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitBlockData.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitBlockData.java @@ -1,13 +1,107 @@ package org.patchbukkit; +import org.bukkit.Material; import org.bukkit.block.BlockType; import org.bukkit.block.data.BlockData; -class PatchBukkitBlockData { - static BlockData newData(BlockType type, String data) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'newData'" +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; + +public final class PatchBukkitBlockData { + + private PatchBukkitBlockData() {} + + public static BlockData newData(Material material, BlockType type, String data) { + final Material mat = material != null ? material : (type != null ? type.asMaterial() : null); + if (mat == null || mat.isLegacy()) { + throw new IllegalArgumentException("Invalid block material: " + (material != null ? material : data)); + } + + try { + Class sharedConstants = Class.forName("net.minecraft.SharedConstants"); + sharedConstants.getMethod("tryDetectVersion").invoke(null); + Class bootstrap = Class.forName("net.minecraft.server.Bootstrap"); + bootstrap.getMethod("bootStrap").invoke(null); + } catch (Throwable ignored) {} + + try { + if (data != null && !data.isEmpty()) { + Class craftBlockData = Class.forName("org.bukkit.craftbukkit.block.data.CraftBlockData"); + java.lang.reflect.Method fromString = craftBlockData.getMethod("fromString", BlockType.class, String.class); + BlockData craftData = (BlockData) fromString.invoke(null, type, data); + if (craftData != null) { + return craftData; + } + } + } catch (Throwable ignored) {} + + final String stateData; + if (data != null && !data.isEmpty()) { + if (data.startsWith("minecraft:") || data.contains(":")) { + stateData = data; + } else if (data.startsWith("[")) { + stateData = mat.getKey().toString() + data; + } else if (!data.contains("[")) { + stateData = mat.getKey().toString(); + } else { + int bracketIdx = data.indexOf('['); + stateData = mat.getKey().toString() + data.substring(bracketIdx); + } + } else { + stateData = mat.getKey().toString(); + } + + return (BlockData) Proxy.newProxyInstance( + PatchBukkitBlockData.class.getClassLoader(), + new Class[]{BlockData.class}, + (proxy, method, args) -> { + String name = method.getName(); + if ("getMaterial".equals(name)) { + return mat; + } + if ("getAsString".equals(name)) { + return stateData; + } + if ("clone".equals(name) || "copy".equals(name)) { + return newData(mat, type, stateData); + } + if ("matches".equals(name) && args != null && args.length == 1) { + if (args[0] instanceof BlockData other) { + return mat.equals(other.getMaterial()); + } + return false; + } + if ("merge".equals(name)) { + return proxy; + } + if ("isSupported".equals(name)) { + return true; + } + if ("isFacingAllowed".equals(name)) { + return true; + } + if ("equals".equals(name) && args != null && args.length == 1) { + if (args[0] instanceof BlockData other) { + return stateData.equals(other.getAsString()); + } + return false; + } + if ("hashCode".equals(name)) { + return stateData.hashCode(); + } + if ("toString".equals(name)) { + return stateData; + } + if (method.isDefault()) { + return InvocationHandler.invokeDefault(proxy, method, args); + } + + Class returnType = method.getReturnType(); + if (returnType == boolean.class) return false; + if (returnType == int.class || returnType == short.class || returnType == long.class || returnType == byte.class) return 0; + if (returnType == float.class || returnType == double.class) return 0.0f; + return null; + } ); } } diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitInternalAPIBridge.java b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitInternalAPIBridge.java index c802a13..7d371cb 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitInternalAPIBridge.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitInternalAPIBridge.java @@ -36,12 +36,10 @@ import org.bukkit.plugin.java.JavaPlugin; import org.jspecify.annotations.Nullable; public class PatchBukkitInternalAPIBridge implements InternalAPIBridge { - @Override public ComponentFlattener componentFlattener() { return ComponentFlattener.basic(); } - @Override public Component resolveWithContext( Component component, @Nullable CommandSender context, @@ -51,12 +49,25 @@ public class PatchBukkitInternalAPIBridge implements InternalAPIBridge { return component; } - @Override - public ItemStack createEmptyStack() { - return new ItemStack(Material.AIR); + private static final java.lang.reflect.Constructor ITEM_STACK_NO_ARG_CTOR; + + static { + try { + ITEM_STACK_NO_ARG_CTOR = ItemStack.class.getDeclaredConstructor(); + ITEM_STACK_NO_ARG_CTOR.setAccessible(true); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + public ItemStack createEmptyStack() { + try { + return ITEM_STACK_NO_ARG_CTOR.newInstance(); + } catch (Exception e) { + throw new RuntimeException("Failed to create empty ItemStack", e); + } } - @Override public LifecycleEventManager createPluginLifecycleEventManager( JavaPlugin plugin, BooleanSupplier registrationCheck @@ -64,52 +75,46 @@ public class PatchBukkitInternalAPIBridge implements InternalAPIBridge { return null; } - @Override public String getStatisticCriteriaKey(Statistic statistic) { return statistic.name().toLowerCase(); } - @Override public @Nullable Attributable getDefaultEntityAttributes(NamespacedKey key) { return null; } - @Override public boolean hasDefaultEntityAttributes(NamespacedKey key) { return false; } - @Override public ItemStack deserializeItem(byte[] bytes) { - return new ItemStack(Material.AIR); + return createEmptyStack(); } - @Override public String getTranslationKey(EntityType entityType) { return entityType.translationKey(); } - @Override + public org.bukkit.entity.SpawnCategory getSpawnCategory(EntityType entityType) { + return org.bukkit.entity.SpawnCategory.MISC; + } + public @Nullable DamageEffect getDamageEffect(String key) { return null; } - @Override public DamageSource.Builder createDamageSourceBuilder(DamageType damageType) { return null; } - @Override public PoiType.Occupancy createOccupancy(String type) { return null; } - @Override public Set validMannequinPoses() { return Set.of(); } - @Override public GameRule legacyGameRuleBridge( GameRule modernRule, Function toModern, @@ -119,27 +124,22 @@ public class PatchBukkitInternalAPIBridge implements InternalAPIBridge { return null; } - @Override public Component defaultMannequinDescription() { return Component.empty(); } - @Override public SkinParts.Mutable allSkinParts() { return null; } - @Override public ResolvableProfile defaultMannequinProfile() { return null; } - @Override public Predicate restricted(Predicate predicate) { return predicate; } - @Override public CombatEntry createCombatEntry( DamageSource damageSource, float damage, @@ -149,7 +149,6 @@ public class PatchBukkitInternalAPIBridge implements InternalAPIBridge { return null; } - @Override public CombatEntry createCombatEntry( LivingEntity entity, DamageSource damageSource, @@ -158,7 +157,6 @@ public class PatchBukkitInternalAPIBridge implements InternalAPIBridge { return null; } - @Override public Biome constructLegacyCustomBiome() { return null; } diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginClassLoader.java b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginClassLoader.java index 21009c4..9ad5b30 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginClassLoader.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginClassLoader.java @@ -11,6 +11,7 @@ import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarFile; import org.bukkit.plugin.InvalidDescriptionException; @@ -32,6 +33,9 @@ public class PatchBukkitPluginClassLoader ClassLoader.registerAsParallelCapable(); } + public static final java.util.Set ALL_LOADERS = + new java.util.concurrent.CopyOnWriteArraySet<>(); + public PatchBukkitPluginClassLoader(ClassLoader parent, File file) throws MalformedURLException, InvalidDescriptionException { this(parent, file, new URL[0]); @@ -46,6 +50,194 @@ public class PatchBukkitPluginClassLoader this.file = file; this.description = loadDescription(file); this.dataFolder = new File(file.getParentFile(), description.getName()); + ALL_LOADERS.add(this); + + File libsDir = new File(file.getParentFile(), "patchbukkit-libs"); + if (!libsDir.exists()) { + libsDir.mkdirs(); + } + + // Extract and load nested JARs inside plugin JAR + for (File nestedJar : extractNestedJars(file, libsDir)) { + try { + addURL(nestedJar.toURI().toURL()); + } catch (MalformedURLException ignored) {} + } + + List libsToResolve = new ArrayList<>(); + if (this.description.getLibraries() != null) { + libsToResolve.addAll(this.description.getLibraries()); + } + for (String lib : extractLibraries(file)) { + if (!libsToResolve.contains(lib)) { + libsToResolve.add(lib); + } + } + + if (!libsToResolve.isEmpty()) { + List resolved = LibraryResolver.resolveLibraries( + String.join("\n", libsToResolve), + libsDir + ); + for (File lib : resolved) { + if (lib != null && lib.exists()) { + try { + addURL(lib.toURI().toURL()); + } catch (MalformedURLException ignored) {} + } + } + } + } + + private static List extractNestedJars(File file, File libsDir) { + List extractedFiles = new ArrayList<>(); + try (JarFile jar = new JarFile(file)) { + java.util.Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String name = entry.getName(); + if (!entry.isDirectory() && name.endsWith(".jar")) { + if (name.startsWith("META-INF/jars/") || + name.startsWith("paper-libraries/") || + name.startsWith("BOOT-INF/lib/") || + name.startsWith("lib/") || + name.startsWith("libraries/")) { + + File targetFile = new File(libsDir, new File(name).getName()); + if (!targetFile.exists()) { + try (InputStream is = jar.getInputStream(entry); + java.io.FileOutputStream fos = new java.io.FileOutputStream(targetFile)) { + is.transferTo(fos); + } + } + if (targetFile.exists()) { + extractedFiles.add(targetFile); + } + } + } + } + } catch (Throwable ignored) {} + return extractedFiles; + } + + @SuppressWarnings("unchecked") + private static List extractLibraries(File file) { + List libraries = new ArrayList<>(); + try (JarFile jar = new JarFile(file)) { + // 1. Try paper-libraries.json + JarEntry entryJson = jar.getJarEntry("paper-libraries.json"); + if (entryJson != null) { + try (InputStream is = jar.getInputStream(entryJson)) { + org.yaml.snakeyaml.Yaml yaml = new org.yaml.snakeyaml.Yaml(); + Object obj = yaml.load(is); + if (obj instanceof Map data) { + Object repos = data.get("repositories"); + if (repos instanceof Map repoMap) { + for (Object v : repoMap.values()) { + if (v != null && !v.toString().isBlank()) { + String r = v.toString().trim(); + if (!libraries.contains("repo:" + r)) { + libraries.add("repo:" + r); + } + } + } + } else if (repos instanceof List repoList) { + for (Object o : repoList) { + if (o != null) { + String r = o.toString().trim(); + if (o instanceof Map m && m.containsKey("url")) { + r = m.get("url").toString().trim(); + } + if (!libraries.contains("repo:" + r)) { + libraries.add("repo:" + r); + } + } + } + } + Object deps = data.get("dependencies"); + if (deps instanceof List list) { + for (Object o : list) { + if (o != null && !libraries.contains(o.toString())) { + libraries.add(o.toString()); + } + } + } else if (deps instanceof Map map) { + for (Map.Entry e : map.entrySet()) { + if (e.getKey() != null && e.getValue() != null) { + String coord = e.getKey().toString() + ":" + e.getValue().toString(); + if (!libraries.contains(coord)) { + libraries.add(coord); + } + } + } + } + } + } catch (Throwable ignored) {} + } + + // 2. Try paper-libraries.list + JarEntry entryList = jar.getJarEntry("paper-libraries.list"); + if (entryList != null) { + try (InputStream is = jar.getInputStream(entryList); + java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(is, java.nio.charset.StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty() && !line.startsWith("#")) { + if (!libraries.contains(line)) { + libraries.add(line); + } + } + } + } catch (Throwable ignored) {} + } + + // 3. Try paper-plugin.yml or plugin.yml + JarEntry entryYaml = jar.getJarEntry("paper-plugin.yml"); + if (entryYaml == null) { + entryYaml = jar.getJarEntry("plugin.yml"); + } + if (entryYaml != null) { + try (InputStream is = jar.getInputStream(entryYaml)) { + org.yaml.snakeyaml.Yaml yaml = new org.yaml.snakeyaml.Yaml(); + Object obj = yaml.load(is); + if (obj instanceof Map data) { + Object libs = data.get("libraries"); + if (libs instanceof List list) { + for (Object o : list) { + if (o != null && !libraries.contains(o.toString())) { + libraries.add(o.toString()); + } + } + } + Object repos = data.get("repositories"); + if (repos instanceof Map repoMap) { + for (Object v : repoMap.values()) { + if (v != null && !v.toString().isBlank()) { + String r = v.toString().trim(); + if (!libraries.contains("repo:" + r)) { + libraries.add("repo:" + r); + } + } + } + } else if (repos instanceof List repoList) { + for (Object o : repoList) { + if (o != null) { + String r = o.toString().trim(); + if (o instanceof Map m && m.containsKey("url")) { + r = m.get("url").toString().trim(); + } + if (!libraries.contains("repo:" + r)) { + libraries.add("repo:" + r); + } + } + } + } + } + } catch (Throwable ignored) {} + } + } catch (Throwable ignored) {} + return libraries; } private static URL[] buildUrls(File file, URL[] extraUrls) @@ -103,10 +295,23 @@ public class PatchBukkitPluginClassLoader !name.startsWith("javax.") ) { try { - // Try to find in plugin JAR first + // Try to find in plugin JAR / libraries first c = findClass(name); } catch (ClassNotFoundException e) { - // Not in JAR, delegate to parent + // Not in local JAR, check other plugin classloaders in global pool + for (PatchBukkitPluginClassLoader other : ALL_LOADERS) { + if (other != this) { + try { + c = other.findLoadedClass(name); + if (c == null) { + c = other.findClass(name); + } + if (c != null) { + break; + } + } catch (ClassNotFoundException ignored) {} + } + } } } @@ -179,6 +384,7 @@ public class PatchBukkitPluginClassLoader @Override public void close() throws IOException { + ALL_LOADERS.remove(this); super.close(); } } diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginLoader.java b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginLoader.java index 45cf994..4f0fa0e 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginLoader.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginLoader.java @@ -19,11 +19,12 @@ import org.bukkit.plugin.PluginLoader; import org.bukkit.plugin.RegisteredListener; import org.bukkit.plugin.UnknownDependencyException; import org.bukkit.plugin.java.JavaPlugin; +import org.patchbukkit.PatchBukkitPluginManager; @SuppressWarnings({ "deprecation", "removal" }) public class PatchBukkitPluginLoader implements PluginLoader { - public static JavaPlugin createPlugin( + public static Plugin createPlugin( String jarPath, String mainClass, String extraClasspath, @@ -40,7 +41,7 @@ public class PatchBukkitPluginLoader implements PluginLoader { LinkedHashSet extraUrls = new LinkedHashSet<>(); if (extraClasspath != null && !extraClasspath.isBlank()) { - String[] paths = extraClasspath.split(File.pathSeparator); + String[] paths = extraClasspath.split("[;:]"); for (String path : paths) { if (path == null || path.isBlank()) { continue; @@ -76,14 +77,62 @@ public class PatchBukkitPluginLoader implements PluginLoader { ); Class jarClass = Class.forName(mainClass, true, classLoader); - return (JavaPlugin) jarClass.getDeclaredConstructor().newInstance(); + Class pluginClass = jarClass.asSubclass(Plugin.class); + Plugin plugin = pluginClass.getDeclaredConstructor().newInstance(); + if (plugin instanceof JavaPlugin javaPlugin) { + classLoader.init(javaPlugin); + } + try { + if (org.bukkit.Bukkit.getPluginManager() instanceof PatchBukkitPluginManager pm) { + pm.registerPlugin(plugin); + } + } catch (Throwable ignored) {} + try { + plugin.onLoad(); + } catch (Throwable t) { + System.err.println("[PatchBukkit] Error during onLoad() for " + mainClass + ": " + t.getMessage()); + t.printStackTrace(); + } + return plugin; } catch (Throwable e) { System.err.println("[PatchBukkit] Failed to instantiate plugin class " + mainClass + ": " + e.getMessage()); e.printStackTrace(); - throw new RuntimeException("Failed to instantiate plugin " + mainClass, e); + return null; } } + public static boolean enablePlugin(String pluginName) { + try { + if (org.bukkit.Bukkit.getPluginManager() instanceof PatchBukkitPluginManager pm) { + Plugin p = pm.getPlugin(pluginName); + if (p != null) { + pm.enablePlugin(p); + return true; + } + } + } catch (Throwable t) { + System.err.println("[PatchBukkit] Failed to enable plugin " + pluginName + ": " + t.getMessage()); + t.printStackTrace(); + } + return false; + } + + public static boolean disablePlugin(String pluginName) { + try { + if (org.bukkit.Bukkit.getPluginManager() instanceof PatchBukkitPluginManager pm) { + Plugin p = pm.getPlugin(pluginName); + if (p != null) { + pm.disablePlugin(p); + return true; + } + } + } catch (Throwable t) { + System.err.println("[PatchBukkit] Failed to disable plugin " + pluginName + ": " + t.getMessage()); + t.printStackTrace(); + } + return false; + } + @Override public Plugin loadPlugin(File file) throws InvalidPluginException, UnknownDependencyException { @@ -106,14 +155,32 @@ public class PatchBukkitPluginLoader implements PluginLoader { @Override public void enablePlugin(Plugin plugin) { if (plugin instanceof JavaPlugin javaPlugin) { - javaPlugin.setEnabled(true); + try { + javaPlugin.setEnabled(true); + org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.server.PluginEnableEvent(plugin)); + } catch (Throwable ex) { + org.bukkit.Bukkit.getLogger().log( + java.util.logging.Level.SEVERE, + "Error occurred while enabling " + plugin.getDescription().getFullName() + " (Is it up to date?)", + ex + ); + } } } @Override public void disablePlugin(Plugin plugin) { if (plugin instanceof JavaPlugin javaPlugin) { - javaPlugin.setEnabled(false); + try { + javaPlugin.setEnabled(false); + org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.server.PluginDisableEvent(plugin)); + } catch (Throwable ex) { + org.bukkit.Bukkit.getLogger().log( + java.util.logging.Level.SEVERE, + "Error occurred while disabling " + plugin.getDescription().getFullName(), + ex + ); + } } } diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginManager.java b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginManager.java index 7acd4a6..185048a 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginManager.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginManager.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; import org.bukkit.Server; import org.bukkit.event.Event; @@ -45,9 +46,16 @@ public class PatchBukkitPluginManager implements PluginManager { throws IllegalArgumentException {} public void registerPlugin(@NotNull Plugin plugin) { + if (plugin == null || plugin.getName() == null) { + return; + } plugins.put(plugin.getName().toLowerCase(java.util.Locale.ENGLISH), plugin); - for (String provided : plugin.getDescription().getProvides()) { - plugins.putIfAbsent(provided.toLowerCase(java.util.Locale.ENGLISH), plugin); + if (plugin.getDescription() != null && plugin.getDescription().getProvides() != null) { + for (String provided : plugin.getDescription().getProvides()) { + if (provided != null && !provided.isBlank()) { + plugins.putIfAbsent(provided.toLowerCase(java.util.Locale.ENGLISH), plugin); + } + } } } @@ -99,6 +107,12 @@ public class PatchBukkitPluginManager implements PluginManager { Class pluginClass = jarClass.asSubclass(Plugin.class); Plugin plugin = pluginClass.getDeclaredConstructor().newInstance(); loader.init((org.bukkit.plugin.java.JavaPlugin) plugin); + try { + plugin.onLoad(); + } catch (Throwable t) { + server.getLogger().severe("Error loading " + plugin.getName() + ": " + t.getMessage()); + t.printStackTrace(); + } registerPlugin(plugin); return plugin; } catch (Throwable e) { @@ -206,7 +220,7 @@ public class PatchBukkitPluginManager implements PluginManager { try { plugin.getPluginLoader().enablePlugin(plugin); } catch (Throwable ex) { - server.getLogger().severe("Error enabling " + plugin.getName() + ": " + ex.getMessage()); + server.getLogger().log(Level.SEVERE, "Error enabling " + plugin.getName() + " (Is it up to date?)", ex); } } } @@ -217,7 +231,11 @@ public class PatchBukkitPluginManager implements PluginManager { return; } if (plugin.isEnabled()) { - plugin.getPluginLoader().disablePlugin(plugin); + try { + plugin.getPluginLoader().disablePlugin(plugin); + } catch (Throwable ex) { + server.getLogger().log(Level.SEVERE, "Error disabling " + plugin.getName(), ex); + } } } diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitServer.java b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitServer.java index 9736f29..051e39f 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitServer.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitServer.java @@ -60,6 +60,7 @@ import org.bukkit.command.Command; import org.bukkit.command.CommandException; import org.bukkit.command.CommandMap; import org.bukkit.command.CommandSender; +import org.bukkit.command.SimpleCommandMap; import org.bukkit.command.ConsoleCommandSender; import org.bukkit.command.PluginCommand; import org.bukkit.entity.Entity; @@ -105,19 +106,75 @@ import patchbukkit.log.SendLogRequest; @SuppressWarnings({ "deprecation", "removal", "unchecked" }) public class PatchBukkitServer implements Server { + private static volatile PatchBukkitServer INSTANCE; - private final String serverName = - io.papermc.paper.ServerBuildInfo.buildInfo().brandName(); + public PatchBukkitServer() { + INSTANCE = this; + String name = "PatchBukkit"; + try { + name = io.papermc.paper.ServerBuildInfo.buildInfo().brandName(); + } catch (Throwable ignored) {} + this.serverName = name; + } + + public static PatchBukkitServer getInstance() { + if (INSTANCE == null) { + synchronized (PatchBukkitServer.class) { + if (INSTANCE == null) { + new PatchBukkitServer(); + } + } + } + return INSTANCE; + } + + static { + try { + Class sharedConstants = Class.forName("net.minecraft.SharedConstants"); + sharedConstants.getMethod("tryDetectVersion").invoke(null); + Class bootstrap = Class.forName("net.minecraft.server.Bootstrap"); + bootstrap.getMethod("bootStrap").invoke(null); + } catch (Throwable ignored) {} + } + + public static PatchBukkitServer initServer() { + Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { + logger.log(Level.SEVERE, "Uncaught exception in thread " + thread.getName(), throwable); + }); + PatchBukkitServer server = getInstance(); + org.bukkit.Bukkit.setServer(server); + return server; + } + + private final String serverName; private final String bukkitVersion = Versioning.getBukkitVersion(); - private final CommandMap commandMap = new PatchBukkitCommandMap(); - private final BukkitScheduler scheduler = new PatchBukkitScheduler(); - private final PatchBukkitPluginManager pluginManager = new PatchBukkitPluginManager(this); - private final ServicesManager servicesManager = new PatchBukkitServicesManager(); + public SimpleCommandMap commandMap = new PatchBukkitCommandMap(this); + public BukkitScheduler scheduler = new PatchBukkitScheduler(); + public PatchBukkitPluginManager pluginManager = new PatchBukkitPluginManager(this); + public ServicesManager servicesManager = new PatchBukkitServicesManager(); private final Map onlinePlayers = new java.util.concurrent.ConcurrentHashMap<>(); private final Map onlinePlayersByName = new java.util.concurrent.ConcurrentHashMap<>(); + private final Set operatorUuids = java.util.Collections.synchronizedSet(new java.util.HashSet<>()); + private final Set operatorNames = java.util.Collections.synchronizedSet(new java.util.HashSet<>()); + + public boolean isOp(UUID uuid, String name) { + if (uuid != null && operatorUuids.contains(uuid)) return true; + if (name != null && operatorNames.contains(name.toLowerCase())) return true; + return false; + } + + public void setOperator(UUID uuid, String name, boolean value) { + if (uuid != null) { + if (value) operatorUuids.add(uuid); else operatorUuids.remove(uuid); + } + if (name != null) { + if (value) operatorNames.add(name.toLowerCase()); else operatorNames.remove(name.toLowerCase()); + } + } + private static final Logger logger = Logger.getLogger("Minecraft"); static { @@ -144,14 +201,16 @@ public class PatchBukkitServer implements Server { root.addHandler(new java.util.logging.Handler() { @Override public void publish(java.util.logging.LogRecord record) { - LogLevel logLevel = LEVEL_MAP.getOrDefault(record.getLevel(), LogLevel.INFO); - NativeBridgeFfi.sendLog( - SendLogRequest.newBuilder() - .setLevel(logLevel) - .setMessage(record.getMessage()) - .setLoggerName(record.getLoggerName()) - .build() - ); + try { + LogLevel logLevel = LEVEL_MAP.getOrDefault(record.getLevel(), LogLevel.INFO); + NativeBridgeFfi.sendLog( + SendLogRequest.newBuilder() + .setLevel(logLevel) + .setMessage(record.getMessage() != null ? record.getMessage() : "") + .setLoggerName(record.getLoggerName() != null ? record.getLoggerName() : "") + .build() + ); + } catch (Throwable ignored) {} } @Override public void flush() {} @Override public void close() {} @@ -185,6 +244,7 @@ public class PatchBukkitServer implements Server { } private final Messenger messenger = new org.patchbukkit.messaging.PatchBukkitMessenger(); + private final HelpMap helpMap = new org.patchbukkit.help.PatchBukkitHelpMap(); private File pluginsFolder = new File("plugins"); private static void validateChannel(String channel) { @@ -590,7 +650,6 @@ public class PatchBukkitServer implements Server { ); } - @Override public @NotNull World getRespawnWorld() { var worlds = getWorlds(); if (!worlds.isEmpty()) { @@ -601,7 +660,6 @@ public class PatchBukkitServer implements Server { ); } - @Override public void setRespawnWorld(@NotNull World world) { // TODO Auto-generated method stub throw new UnsupportedOperationException( @@ -904,7 +962,6 @@ public class PatchBukkitServer implements Server { ); } - @Override public @NotNull ServerConfiguration getServerConfig() { // TODO Auto-generated method stub throw new UnsupportedOperationException( @@ -949,28 +1006,23 @@ public class PatchBukkitServer implements Server { @Override public @NotNull OfflinePlayer getOfflinePlayer(@NotNull String name) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getOfflinePlayer'" - ); + Player online = getPlayer(name); + if (online != null) return online; + return new org.patchbukkit.entity.PatchBukkitOfflinePlayer(null, name); } @Override - public @Nullable OfflinePlayer getOfflinePlayerIfCached( - @NotNull String name - ) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getOfflinePlayerIfCached'" - ); + public @Nullable OfflinePlayer getOfflinePlayerIfCached(@NotNull String name) { + Player online = getPlayer(name); + if (online != null) return online; + return new org.patchbukkit.entity.PatchBukkitOfflinePlayer(null, name); } @Override public @NotNull OfflinePlayer getOfflinePlayer(@NotNull UUID id) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getOfflinePlayer'" - ); + Player online = getPlayer(id); + if (online != null) return online; + return new org.patchbukkit.entity.PatchBukkitOfflinePlayer(id, null); } @Override @@ -1066,10 +1118,14 @@ public class PatchBukkitServer implements Server { @Override public @NotNull Set getOperators() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getOperators'" - ); + Set ops = new java.util.HashSet<>(); + for (UUID uuid : operatorUuids) { + ops.add(getOfflinePlayer(uuid)); + } + for (String name : operatorNames) { + ops.add(getOfflinePlayer(name)); + } + return ops; } @Override @@ -1088,7 +1144,6 @@ public class PatchBukkitServer implements Server { ); } - @Override public boolean forcesDefaultGameMode() { // TODO Auto-generated method stub throw new UnsupportedOperationException( @@ -1134,10 +1189,7 @@ public class PatchBukkitServer implements Server { @Override public @NotNull HelpMap getHelpMap() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getHelpMap'" - ); + return this.helpMap; } @Override @@ -1252,10 +1304,7 @@ public class PatchBukkitServer implements Server { @Override public boolean isPrimaryThread() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'isPrimaryThread'" - ); + return true; } @Override @@ -1317,10 +1366,7 @@ public class PatchBukkitServer implements Server { @Override public @NotNull ItemFactory getItemFactory() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getItemFactory'" - ); + return org.patchbukkit.inventory.PatchBukkitItemFactory.INSTANCE; } @Override @@ -1475,18 +1521,12 @@ public class PatchBukkitServer implements Server { @Override public double@NotNull [] getTPS() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getTPS'" - ); + return new double[] { 20.0, 20.0, 20.0 }; } @Override public long@NotNull [] getTickTimes() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getTickTimes'" - ); + return new long[100]; } @Override @@ -1554,33 +1594,96 @@ public class PatchBukkitServer implements Server { BlockType type = null; if (material != null) { type = material.asBlockType(); + if (type == null && material.isBlock()) { + type = org.patchbukkit.registry.PatchBukkitBlockType.create(material); + } Preconditions.checkArgument(type != null, "Provided material must be a block"); + } else if (data != null) { + String matName = data.trim(); + int stateIndex = matName.indexOf('['); + if (stateIndex != -1) { + matName = matName.substring(0, stateIndex).trim(); + } + if (matName.startsWith("minecraft:")) { + matName = matName.substring("minecraft:".length()); + } + Material matched = null; + try { + matched = Material.valueOf(matName.toUpperCase(java.util.Locale.ROOT)); + } catch (Throwable ignored) {} + + if (matched == null || matched.isLegacy()) { + Material m = Material.matchMaterial(matName, false); + if (m != null && !m.isLegacy()) { + matched = m; + } + } + + if (matched == null) { + matched = Material.matchMaterial(matName); + } + + if (matched != null && matched.isLegacy()) { + matched = PatchBukkitLegacy.fromLegacy(matched); + } + + if (matched != null && (matched.isBlock() || (!matched.isLegacy() && matched.getKey() != null))) { + material = matched; + type = matched.asBlockType(); + if (type == null) { + type = org.patchbukkit.registry.PatchBukkitBlockType.create(matched); + } + } else { + throw new IllegalArgumentException("Block name " + matName + " was not recognized"); + } } - return PatchBukkitBlockData.newData(type, data); + return PatchBukkitBlockData.newData(material, type, data); } @Override + @SuppressWarnings("unchecked") public @Nullable Tag getTag( @NotNull String registry, @NotNull NamespacedKey tag, @NotNull Class clazz ) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getTag'" - ); + if (registry == null || tag == null || clazz == null) { + return null; + } + try { + for (java.lang.reflect.Field field : org.bukkit.Tag.class.getFields()) { + if (Tag.class.isAssignableFrom(field.getType())) { + Tag val = (Tag) field.get(null); + if (val != null && val.getKey().equals(tag)) { + return (Tag) val; + } + } + } + } catch (Throwable ignored) { + } + return new org.patchbukkit.tag.PatchBukkitTag<>(tag); } @Override + @SuppressWarnings("unchecked") public @NotNull Iterable> getTags( @NotNull String registry, @NotNull Class clazz ) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getTags'" - ); + List> result = new ArrayList<>(); + try { + for (java.lang.reflect.Field field : org.bukkit.Tag.class.getFields()) { + if (Tag.class.isAssignableFrom(field.getType())) { + Tag val = (Tag) field.get(null); + if (val != null) { + result.add((Tag) val); + } + } + } + } catch (Throwable ignored) { + } + return result; } @Override @@ -1719,18 +1822,12 @@ public class PatchBukkitServer implements Server { @Override public int getCurrentTick() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getCurrentTick'" - ); + return (int) (System.currentTimeMillis() / 50); } @Override public boolean isStopping() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'isStopping'" - ); + return false; } @Override @@ -1894,9 +1991,28 @@ public class PatchBukkitServer implements Server { ); } - @Override public @NotNull Path getLevelDirectory() { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getLevelDirectory'"); } + + @Override + public int getAmbientSpawnLimit() { + return 15; + } + + @Override + public int getWaterUndergroundCreatureSpawnLimit() { + return 5; + } + + @Override + public int getWaterAmbientSpawnLimit() { + return 20; + } + + @Override + public int getWaterAnimalSpawnLimit() { + return 5; + } } diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitUnsafeValues.java b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitUnsafeValues.java index 3fbfed5..b947118 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitUnsafeValues.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitUnsafeValues.java @@ -55,6 +55,18 @@ public class PatchBukkitUnsafeValues implements UnsafeValues { public static final PatchBukkitUnsafeValues INSTANCE = new PatchBukkitUnsafeValues(); + public @NotNull List computeTooltipLines(@NotNull ItemStack itemStack, @NotNull TooltipContext tooltipContext, @Nullable Player player) { + return java.util.Collections.emptyList(); + } + + public io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager createPluginLifecycleEventManager(org.bukkit.plugin.java.JavaPlugin plugin, java.util.function.BooleanSupplier registrationCheck) { + return null; + } + + public @Nullable Color getSpawnEggLayerColor(EntityType entityType, int layer) { + return null; + } + @Override public boolean isSupportedApiVersion(String apiVersion) { if (apiVersion == null) return false; @@ -78,8 +90,7 @@ public class PatchBukkitUnsafeValues implements UnsafeValues { @Override public Material toLegacy(Material material) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'toLegacy'"); + return material; } @Override @@ -99,122 +110,107 @@ public class PatchBukkitUnsafeValues implements UnsafeValues { @Override public BlockData fromLegacy(Material material, byte data) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'fromLegacy'"); + return material != null ? material.createBlockData() : null; } @Override public Material getMaterial(String material, int version) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getMaterial'"); + try { + return Material.valueOf(material.toUpperCase(java.util.Locale.ROOT)); + } catch (Exception e) { + return Material.getMaterial(material); + } } @Override public int getDataVersion() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getDataVersion'"); + return 3953; } @Override public ItemStack modifyItemStack(ItemStack stack, String arguments) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'modifyItemStack'"); + return stack; + } + + public @NotNull ItemStack createEmptyStack() { + return new ItemStack(Material.AIR); } @Override public byte[] processClass(PluginDescriptionFile pdf, String path, byte[] clazz) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'processClass'"); + return clazz; } - @Override - public Advancement loadAdvancement(NamespacedKey key, String advancement) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'loadAdvancement'"); + public Advancement loadAdvancement(net.kyori.adventure.key.Key key, String advancement, boolean checkKey) { + return null; + } + + public List loadAdvancements(Map advancements, boolean checkKey) { + return java.util.Collections.emptyList(); } @Override public boolean removeAdvancement(NamespacedKey key) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'removeAdvancement'"); + return false; } @Override public String get(Class aClass, String value) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'get'"); + return null; } - @Override public B get(RegistryKey registry, NamespacedKey key) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'get'"); + if (registry == null || key == null) return null; + org.bukkit.Registry reg = io.papermc.paper.registry.RegistryAccess.registryAccess().getRegistry(registry); + return reg != null ? reg.get(key) : null; } @Override public @NotNull JsonObject serializeItemAsJson(@NotNull ItemStack itemStack) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'serializeItemAsJson'"); + return new JsonObject(); } @Override public @NotNull ItemStack deserializeItemFromJson(@NotNull JsonObject data) throws IllegalArgumentException { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'deserializeItemFromJson'"); + return new ItemStack(Material.STONE); } - @Override public byte @NotNull [] serializeEntity(@NotNull Entity entity, @NotNull EntitySerializationFlag... serializationFlags) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'serializeEntity'"); + return new byte[0]; } - @Override public @NotNull Entity deserializeEntity(byte @NotNull [] data, @NotNull World world, boolean preserveUUID, boolean preservePassengers) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'deserializeEntity'"); + return null; } @Override public @NotNull String getMainLevelName() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getMainLevelName'"); + return "world"; } @Override public int getProtocolVersion() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getProtocolVersion'"); + return 769; } - - - @Override public @NotNull ItemStack deserializeStack(@NotNull Map args) { System.out.println("Deserializing itemstack: " + args); - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'deserializeStack'"); + return new ItemStack(Material.STONE); } - @Override public @NotNull ItemStack deserializeItemHover(@NotNull ShowItem itemHover) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'deserializeItemHover'"); + return new ItemStack(Material.STONE); } - @Override public InternalPotionData getInternalPotionData(NamespacedKey key) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getInternalPotionData'"); + return null; } - @Override public int nextEntityId(World world) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'nextEntityId'"); + return (int) (System.currentTimeMillis() & 0x7FFFFFFF); } diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/bridge/BridgeUtils.java b/java/patchbukkit/src/main/java/org/patchbukkit/bridge/BridgeUtils.java index be65a4b..9880a95 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/bridge/BridgeUtils.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/bridge/BridgeUtils.java @@ -12,17 +12,32 @@ public class BridgeUtils { } public static patchbukkit.common.Location convertLocation(org.bukkit.Location location) { + if (location == null) return null; var pos = patchbukkit.common.Vec3.newBuilder() .setX(location.getX()) .setY(location.getY()) .setZ(location.getZ()) .build(); - var worldUuid = convertUuid(location.getWorld().getUID()); + var worldUuid = location.getWorld() != null ? convertUuid(location.getWorld().getUID()) : null; + var worldBuilder = patchbukkit.common.World.newBuilder(); + if (worldUuid != null) worldBuilder.setUuid(worldUuid); return patchbukkit.common.Location.newBuilder() - .setWorld(patchbukkit.common.World.newBuilder().setUuid(worldUuid).build()) + .setWorld(worldBuilder.build()) .setPosition(pos) .setYaw(location.getYaw()) .setPitch(location.getPitch()) .build(); } + + public static org.bukkit.Location convertLocation(patchbukkit.common.Location location) { + if (location == null) return null; + org.bukkit.World world = null; + if (location.hasWorld() && location.getWorld().hasUuid()) { + world = org.patchbukkit.world.PatchBukkitWorld.getOrCreate(convertUuid(location.getWorld().getUuid())); + } + double x = location.hasPosition() ? location.getPosition().getX() : 0.0; + double y = location.hasPosition() ? location.getPosition().getY() : 0.0; + double z = location.hasPosition() ? location.getPosition().getZ() : 0.0; + return new org.bukkit.Location(world, x, y, z, location.getYaw(), location.getPitch()); + } } diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/command/CommandFactory.java b/java/patchbukkit/src/main/java/org/patchbukkit/command/CommandFactory.java index 47bf467..33bfca5 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/command/CommandFactory.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/command/CommandFactory.java @@ -15,4 +15,31 @@ public class CommandFactory { return null; } } + + public static PluginCommand create(String name, String pluginName) { + try { + Plugin plugin = null; + if (org.bukkit.Bukkit.getPluginManager() instanceof org.patchbukkit.PatchBukkitPluginManager pm) { + plugin = pm.getPlugin(pluginName); + if (plugin == null) { + for (Plugin p : pm.getPlugins()) { + if (p.getName().equalsIgnoreCase(pluginName)) { + plugin = p; + break; + } + } + } + } + if (plugin == null) { + plugin = org.bukkit.Bukkit.getPluginManager().getPlugin(pluginName); + } + if (plugin == null) { + System.err.println("[PatchBukkit] CommandFactory: Plugin not found by name '" + pluginName + "' for command '" + name + "'"); + } + return create(name, plugin); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } } \ No newline at end of file diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitBlockCommandSender.java b/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitBlockCommandSender.java index ccd92c2..e496b0f 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitBlockCommandSender.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitBlockCommandSender.java @@ -16,136 +16,108 @@ import org.jetbrains.annotations.Nullable; import net.kyori.adventure.text.Component; public class PatchBukkitBlockCommandSender implements BlockCommandSender { - @NotNull + @Override public Block getBlock() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getBlock'"); + return null; } @Override public boolean isPermissionSet(@NotNull String name) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'isPermissionSet'"); + return true; } @Override public boolean isPermissionSet(@NotNull Permission perm) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'isPermissionSet'"); + return true; } @Override public boolean hasPermission(@NotNull String name) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'hasPermission'"); + return true; } @Override public boolean hasPermission(@NotNull Permission perm) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'hasPermission'"); + return true; } @Override public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'addAttachment'"); + return new PermissionAttachment(plugin, this); } @Override public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'addAttachment'"); + return new PermissionAttachment(plugin, this); } @Override public @Nullable PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value, int ticks) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'addAttachment'"); + return new PermissionAttachment(plugin, this); } @Override public @Nullable PermissionAttachment addAttachment(@NotNull Plugin plugin, int ticks) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'addAttachment'"); + return new PermissionAttachment(plugin, this); } @Override public void removeAttachment(@NotNull PermissionAttachment attachment) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'removeAttachment'"); } @Override public void recalculatePermissions() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'recalculatePermissions'"); } @Override public @NotNull Set getEffectivePermissions() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getEffectivePermissions'"); + return java.util.Collections.emptySet(); } @Override public boolean isOp() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'isOp'"); + return true; } @Override public void setOp(boolean value) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setOp'"); } @Override public void sendMessage(@NotNull String message) { - throw new UnsupportedOperationException("Unimplemented method 'sendMessage'"); } @Override public void sendMessage(@NotNull String... messages) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'sendMessage'"); } @Override public void sendMessage(@Nullable UUID sender, @NotNull String message) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'sendMessage'"); } @Override public void sendMessage(@Nullable UUID sender, @NotNull String... messages) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'sendMessage'"); } @Override public @NotNull Server getServer() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getServer'"); + return org.bukkit.Bukkit.getServer(); } @Override public @NotNull String getName() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getName'"); + return "@"; } @Override public @NotNull Spigot spigot() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'spigot'"); + return new Spigot(); } @Override public @NotNull Component name() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'name'"); + return Component.text("@"); } } 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 35165fd..f764b0a 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitCommandMap.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitCommandMap.java @@ -7,16 +7,56 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import org.bukkit.Location; +import org.bukkit.Server; import org.bukkit.command.Command; import org.bukkit.command.CommandException; -import org.bukkit.command.CommandMap; import org.bukkit.command.CommandSender; +import org.bukkit.command.SimpleCommandMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import patchbukkit.bridge.NativeBridgeFfi; +import patchbukkit.command.RegisterCommandRequest; -public class PatchBukkitCommandMap implements CommandMap { - // We use Command as the value to support both PluginCommand and vanilla commands - private final Map knownCommands = new HashMap<>(); +public class PatchBukkitCommandMap extends SimpleCommandMap { + + public PatchBukkitCommandMap(Server server) { + super(server != null ? server : org.bukkit.Bukkit.getServer(), new HashMap<>()); + } + + public PatchBukkitCommandMap() { + super(org.bukkit.Bukkit.getServer(), new HashMap<>()); + } + + private String cleanLabel(String label) { + if (label == null) return ""; + String clean = label.trim(); + while (clean.startsWith("/")) { + clean = clean.substring(1).trim(); + } + return clean.toLowerCase(); + } + + private void registerVariants(String key, Command command) { + if (key == null || key.isEmpty()) return; + key = key.toLowerCase().trim(); + if (!knownCommands.containsKey(key)) { + knownCommands.put(key, command); + } + String clean = cleanLabel(key); + if (!clean.isEmpty()) { + if (!knownCommands.containsKey(clean)) { + knownCommands.put(clean, command); + } + String single = "/" + clean; + if (!knownCommands.containsKey(single)) { + knownCommands.put(single, command); + } + String doubleSlash = "//" + clean; + if (!knownCommands.containsKey(doubleSlash)) { + knownCommands.put(doubleSlash, command); + } + } + } @Override public void registerAll(@NotNull String fallbackPrefix, @NotNull List commands) { @@ -27,26 +67,45 @@ public class PatchBukkitCommandMap implements CommandMap { @Override public boolean register(@NotNull String label, @NotNull String fallbackPrefix, @NotNull Command command) { + if (label == null || fallbackPrefix == null || command == null) { + return false; + } label = label.toLowerCase().trim(); fallbackPrefix = fallbackPrefix.toLowerCase().trim(); - - // 1. Register under the direct label if not taken + String clean = cleanLabel(label); + boolean registeredDirectly = false; - if (!knownCommands.containsKey(label)) { - knownCommands.put(label, command); + if (!knownCommands.containsKey(label) && !knownCommands.containsKey(clean)) { registeredDirectly = true; } - // 2. Always register under the fallback prefix (pluginname:label) - knownCommands.put(fallbackPrefix + ":" + label, command); + registerVariants(label, command); + registerVariants(fallbackPrefix + ":" + label, command); + if (!clean.isEmpty()) { + registerVariants(clean, command); + registerVariants(fallbackPrefix + ":" + clean, command); + } - // 3. Register aliases - for (String alias : command.getAliases()) { - alias = alias.toLowerCase().trim(); - if (!knownCommands.containsKey(alias)) { - knownCommands.put(alias, command); + if (command.getAliases() != null) { + for (String alias : command.getAliases()) { + if (alias == null) continue; + alias = alias.toLowerCase().trim(); + registerVariants(alias, command); + registerVariants(fallbackPrefix + ":" + alias, command); } - knownCommands.put(fallbackPrefix + ":" + alias, command); + } + + try { + List aliasesList = command.getAliases() != null ? command.getAliases() : Collections.emptyList(); + String desc = command.getDescription() != null ? command.getDescription() : ""; + RegisterCommandRequest request = RegisterCommandRequest.newBuilder() + .setCmdName(label) + .addAllAliases(aliasesList) + .setDescription(desc) + .setPluginName(fallbackPrefix) + .build(); + NativeBridgeFfi.registerCommand(request); + } catch (Throwable ignored) { } return registeredDirectly; @@ -54,32 +113,34 @@ public class PatchBukkitCommandMap implements CommandMap { @Override public boolean register(@NotNull String fallbackPrefix, @NotNull Command command) { + if (command == null) return false; return register(command.getName(), fallbackPrefix, command); } @Override public boolean dispatch(@NotNull CommandSender sender, @NotNull String cmdLine) throws CommandException { if (cmdLine == null) return false; - String cleanLine = cmdLine.trim(); - while (cleanLine.startsWith("/")) { - cleanLine = cleanLine.substring(1).trim(); - } - if (cleanLine.isEmpty()) return false; + String rawLine = cmdLine.trim(); + if (rawLine.isEmpty()) return false; - String[] split = cleanLine.split("\\s+"); + String[] split = rawLine.split("\\s+"); if (split.length == 0) return false; - String sentLabel = split[0].toLowerCase(); - Command command = knownCommands.get(sentLabel); + String rawLabel = split[0].toLowerCase(); + Command command = knownCommands.get(rawLabel); + + if (command == null) { + String clean = cleanLabel(rawLabel); + command = knownCommands.get(clean); + } if (command == null) { return false; // Command not found } try { - // Extract arguments (everything after the label) String[] args = Arrays.copyOfRange(split, 1, split.length); - return command.execute(sender, sentLabel, args); + return command.execute(sender, rawLabel, args); } catch (Exception ex) { throw new CommandException("Unhandled exception executing '" + cmdLine + "'", ex); } @@ -92,7 +153,13 @@ public class PatchBukkitCommandMap implements CommandMap { @Override public @Nullable Command getCommand(@NotNull String name) { - return knownCommands.get(name.toLowerCase()); + if (name == null) return null; + String lower = name.toLowerCase().trim(); + Command cmd = knownCommands.get(lower); + if (cmd == null) { + cmd = knownCommands.get(cleanLabel(lower)); + } + return cmd; } @Override @@ -108,17 +175,16 @@ public class PatchBukkitCommandMap implements CommandMap { @Override public @Nullable List tabComplete(@NotNull CommandSender sender, @NotNull String cmdLine, @Nullable Location location) { if (cmdLine == null) return new ArrayList<>(); - String cleanLine = cmdLine.trim(); - while (cleanLine.startsWith("/")) { - cleanLine = cleanLine.substring(1).trim(); - } - String[] split = cleanLine.split(" ", -1); + String rawLine = cmdLine.trim(); + String[] split = rawLine.split(" ", -1); if (split.length == 0) return new ArrayList<>(); String label = split[0].toLowerCase(); Command command = knownCommands.get(label); + if (command == null) { + command = knownCommands.get(cleanLabel(label)); + } - // If typing the first word, return matching command names if (split.length == 1) { List completions = new ArrayList<>(); for (String key : knownCommands.keySet()) { @@ -127,7 +193,6 @@ public class PatchBukkitCommandMap implements CommandMap { return completions; } - // Otherwise, delegate to the specific command's tab completer if (command != null) { String[] args = Arrays.copyOfRange(split, 1, split.length); return command.tabComplete(sender, label, args, location); diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitEntity.java b/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitEntity.java index b68cbc3..57d490b 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitEntity.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitEntity.java @@ -28,6 +28,7 @@ import org.bukkit.entity.Pose; import org.bukkit.entity.SpawnCategory; import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityRemoveEvent; import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; import org.bukkit.inventory.ItemStack; import org.bukkit.metadata.MetadataValue; @@ -59,6 +60,16 @@ import patchbukkit.entity.TeleportEntityRequest; public class PatchBukkitEntity implements Entity { + @Override + public boolean isInWaterOrRainOrBubbleColumn() { + return false; + } + + @Override + public boolean isInWaterOrBubbleColumn() { + return false; + } + protected final UUID uuid; protected final String name; private PermissibleBase perm; @@ -233,13 +244,11 @@ public class PatchBukkitEntity implements Entity { throw new UnsupportedOperationException("Unimplemented method 'getPersistentDataContainer'"); } - @Override public @org.jspecify.annotations.Nullable T getData(Valued type) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getData'"); } - @Override public @org.jspecify.annotations.Nullable T getDataOrDefault(Valued type, @org.jspecify.annotations.Nullable T fallback) { // TODO Auto-generated method stub @@ -329,8 +338,8 @@ public class PatchBukkitEntity implements Entity { @Override public boolean isOnGround() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'isOnGround'"); + var resp = NativeBridgeFfi.isOnGround(BridgeUtils.convertUuid(this.uuid)); + return resp != null && resp.getOnGround(); } @Override @@ -435,7 +444,6 @@ public class PatchBukkitEntity implements Entity { throw new UnsupportedOperationException("Unimplemented method 'setVisualFire'"); } - @Override public boolean isVisualFire() { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'isVisualFire'"); @@ -584,22 +592,21 @@ public class PatchBukkitEntity implements Entity { throw new UnsupportedOperationException("Unimplemented method 'eject'"); } - @Override public @NotNull ItemStack getPickItemStack() { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getPickItemStack'"); } + private float fallDistance = 0.0f; + @Override public float getFallDistance() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getFallDistance'"); + return this.fallDistance; } @Override public void setFallDistance(float distance) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setFallDistance'"); + this.fallDistance = distance; } @Override @@ -701,7 +708,6 @@ public class PatchBukkitEntity implements Entity { return this.visibleByDefault; } - @Override public @NotNull Set getTrackedBy() { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getTrackedBy'"); @@ -809,16 +815,16 @@ public class PatchBukkitEntity implements Entity { throw new UnsupportedOperationException("Unimplemented method 'getPose'"); } + private boolean sneaking = false; + @Override public boolean isSneaking() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'isSneaking'"); + return this.sneaking; } @Override public void setSneaking(boolean sneak) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setSneaking'"); + this.sneaking = sneak; } @Override @@ -827,12 +833,20 @@ public class PatchBukkitEntity implements Entity { throw new UnsupportedOperationException("Unimplemented method 'setPose'"); } - @Override public boolean hasFixedPose() { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'hasFixedPose'"); } + public EntityRemoveEvent.@Nullable Cause getRemoveEventCause() { + return null; + } + + @Override + public io.papermc.paper.entity.@Nullable RemovalReason getRemovalReason() { + return null; + } + @Override public @NotNull SpawnCategory getSpawnCategory() { // TODO Auto-generated method stub @@ -995,13 +1009,11 @@ public class PatchBukkitEntity implements Entity { throw new UnsupportedOperationException("Unimplemented method 'getScoreboardEntryName'"); } - @Override public void broadcastHurtAnimation(@NotNull Collection players) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'broadcastHurtAnimation'"); } - @Override public Source soundSource() { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'soundSource'"); diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitHumanEntity.java b/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitHumanEntity.java index 063973b..6914e56 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitHumanEntity.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitHumanEntity.java @@ -50,6 +50,7 @@ import org.checkerframework.checker.index.qual.NonNegative; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Range; +import org.patchbukkit.PatchBukkitServer; import org.patchbukkit.bridge.BridgeUtils; import patchbukkit.bridge.NativeBridgeFfi; @@ -67,20 +68,67 @@ public class PatchBukkitHumanEntity implements HumanEntity { private boolean op; protected final PermissibleBase perm = new PermissibleBase(this); + protected final PlayerInventory inventory = new org.patchbukkit.inventory.PatchBukkitPlayerInventory(this); + protected final EntityEquipment equipment = new org.patchbukkit.inventory.PatchBukkitEntityEquipment(this); + private ItemStack cursorItem = ItemStack.empty(); + public PatchBukkitHumanEntity(UUID uuid, String name) { super(uuid, name); } + @Override + public boolean undiscoverRecipe(@NotNull NamespacedKey key) { + return false; + } + + @Override + public int undiscoverRecipes(@NotNull Collection keys) { + return 0; + } + + @Override + public boolean discoverRecipe(@NotNull NamespacedKey key) { + return false; + } + + @Override + public int discoverRecipes(@NotNull Collection keys) { + return 0; + } + + @Override public boolean isOp() { - return this.op; + if (this.op) { + return true; + } + if (PatchBukkitServer.getInstance().isOp(getUniqueId(), getName())) { + this.op = true; + if (this.perm != null) { + this.perm.recalculatePermissions(); + } + return true; + } + try { + var resp = NativeBridgeFfi.isOp(BridgeUtils.convertUuid(getUniqueId())); + if (resp != null && resp.getIsOp()) { + this.op = true; + if (this.perm != null) { + this.perm.recalculatePermissions(); + } + return true; + } + } catch (Throwable ignored) { + } + return false; } @Override public void setOp(boolean value) { this.op = value; + PatchBukkitServer.getInstance().setOperator(getUniqueId(), getName(), value); this.perm.recalculatePermissions(); } @@ -104,23 +152,7 @@ public class PatchBukkitHumanEntity return this.perm.hasPermission(perm); } - @Override - public double getEyeHeight() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getEyeHeight'"); - } - @Override - public double getEyeHeight(boolean ignorePose) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getEyeHeight'"); - } - - @Override - public @NotNull Location getEyeLocation() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getEyeLocation'"); - } @Override public @NotNull List getLineOfSight(@Nullable Set transparent, int maxDistance) { @@ -794,71 +826,7 @@ public class PatchBukkitHumanEntity throw new UnsupportedOperationException("Unimplemented method 'registerAttribute'"); } - @Override - public void damage(double amount) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'damage'"); - } - @Override - public void damage(double amount, @Nullable Entity source) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'damage'"); - } - - @Override - public void damage(double amount, @NotNull DamageSource damageSource) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'damage'"); - } - - @Override - public double getHealth() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getHealth'"); - } - - @Override - public void setHealth(double health) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setHealth'"); - } - - @Override - public void heal(double amount, @NotNull RegainReason reason) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'heal'"); - } - - @Override - public double getAbsorptionAmount() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getAbsorptionAmount'"); - } - - @Override - public void setAbsorptionAmount(double amount) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setAbsorptionAmount'"); - } - - @Override - public double getMaxHealth() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getMaxHealth'"); - } - - @Override - public void setMaxHealth(double health) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setMaxHealth'"); - } - - @Override - public void resetMaxHealth() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'resetMaxHealth'"); - } @Override public @NotNull T launchProjectile(@NotNull Class projectile, @@ -881,14 +849,12 @@ public class PatchBukkitHumanEntity @Override public EntityEquipment getEquipment() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getEquipment'"); + return this.equipment; } @Override public PlayerInventory getInventory() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getInventory'"); + return this.inventory; } @Override @@ -899,10 +865,10 @@ public class PatchBukkitHumanEntity @Override public MainHand getMainHand() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getMainHand'"); + return MainHand.RIGHT; } + @Override public boolean setWindowProperty(Property prop, int value) { // TODO Auto-generated method stub @@ -1009,28 +975,26 @@ public class PatchBukkitHumanEntity @Override public ItemStack getItemInHand() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getItemInHand'"); + return getInventory().getItemInMainHand(); } @Override public void setItemInHand(@org.jspecify.annotations.Nullable ItemStack item) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setItemInHand'"); + getInventory().setItemInMainHand(item); } @Override public ItemStack getItemOnCursor() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getItemOnCursor'"); + return this.cursorItem != null ? this.cursorItem : ItemStack.empty(); } @Override public void setItemOnCursor(@org.jspecify.annotations.Nullable ItemStack item) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setItemOnCursor'"); + this.cursorItem = item != null ? item : ItemStack.empty(); } + + @Override public boolean hasCooldown(Material material) { // TODO Auto-generated method stub @@ -1061,25 +1025,21 @@ public class PatchBukkitHumanEntity throw new UnsupportedOperationException("Unimplemented method 'hasCooldown'"); } - @Override public int getCooldown(ItemStack item) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getCooldown'"); } - @Override public void setCooldown(ItemStack item, int ticks) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'setCooldown'"); } - @Override public int getCooldown(Key key) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getCooldown'"); } - @Override public void setCooldown(Key key, int ticks) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'setCooldown'"); @@ -1183,17 +1143,7 @@ public class PatchBukkitHumanEntity throw new UnsupportedOperationException("Unimplemented method 'getAttackCooldown'"); } - @Override - public int discoverRecipes(Collection recipes) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'discoverRecipes'"); - } - @Override - public int undiscoverRecipes(Collection recipes) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'undiscoverRecipes'"); - } @Override public boolean hasDiscoveredRecipe(NamespacedKey recipe) { diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitLivingEntity.java b/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitLivingEntity.java index b5c62d9..1bab366 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitLivingEntity.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitLivingEntity.java @@ -852,51 +852,55 @@ public class PatchBukkitLivingEntity throw new UnsupportedOperationException("Unimplemented method 'canUseEquipmentSlot'"); } - @Override public @NotNull CombatTracker getCombatTracker() { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getCombatTracker'"); } @Override + public void setShieldBlockingDelay(int delay) { + } + + @Override + public int getShieldBlockingDelay() { + return 0; + } + public void setWaypointStyle(@Nullable Key key) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'setWaypointStyle'"); } - @Override public void setWaypointColor(@Nullable Color color) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'setWaypointColor'"); } - @Override public @NotNull Key getWaypointStyle() { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getWaypointStyle'"); } - @Override public @Nullable Color getWaypointColor() { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getWaypointColor'"); } - @Override + public void kill(DamageSource damageSource) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'kill'"); } - @Override + public float getSoundVolume() { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getSoundVolume'"); } - @Override + public float getSoundPitch() { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getSoundPitch'"); } - @Override + public @Nullable Sound getHurtSound(@NotNull DamageSource damageSource) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getHurtSound'"); diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitOfflinePlayer.java b/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitOfflinePlayer.java new file mode 100644 index 0000000..7e84785 --- /dev/null +++ b/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitOfflinePlayer.java @@ -0,0 +1,214 @@ +package org.patchbukkit.entity; + +import java.util.Map; +import java.util.UUID; +import org.bukkit.Location; +import org.bukkit.OfflinePlayer; +import org.bukkit.Statistic; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.patchbukkit.PatchBukkitServer; + +public class PatchBukkitOfflinePlayer implements OfflinePlayer { + private final UUID uuid; + private final String name; + + public PatchBukkitOfflinePlayer(UUID uuid, String name) { + this.uuid = uuid; + this.name = name; + } + + @Override + public boolean isOnline() { + return getPlayer() != null; + } + + @Override + public boolean isConnected() { + return isOnline(); + } + + @Override + public @Nullable String getName() { + Player player = getPlayer(); + return player != null ? player.getName() : this.name; + } + + @Override + public @NotNull UUID getUniqueId() { + if (this.uuid != null) return this.uuid; + Player player = getPlayer(); + if (player != null) return player.getUniqueId(); + return UUID.nameUUIDFromBytes(("OfflinePlayer:" + (this.name != null ? this.name : "")).getBytes()); + } + + @Override + public boolean isOp() { + Player player = getPlayer(); + if (player != null) return player.isOp(); + return PatchBukkitServer.getInstance().isOp(getUniqueId(), getName()); + } + + @Override + public void setOp(boolean value) { + PatchBukkitServer.getInstance().setOperator(getUniqueId(), getName(), value); + Player player = getPlayer(); + if (player != null) { + player.setOp(value); + } + } + + @Override + public boolean isBanned() { + return false; + } + + @Override + public > E ban(String reason, java.util.Date expires, String source) { + return null; + } + + @Override + public > E ban(String reason, java.time.Instant expires, String source) { + return null; + } + + @Override + public > E ban(String reason, java.time.Duration duration, String source) { + return null; + } + + @Override + public boolean isWhitelisted() { + return false; + } + + @Override + public void setWhitelisted(boolean value) {} + + @Override + public @Nullable Player getPlayer() { + if (this.uuid != null) { + Player p = PatchBukkitServer.getInstance().getPlayer(this.uuid); + if (p != null) return p; + } + if (this.name != null) { + return PatchBukkitServer.getInstance().getPlayer(this.name); + } + return null; + } + + @Override + public long getFirstPlayed() { return 0; } + + @Override + public long getLastPlayed() { return 0; } + + @Override + public boolean hasPlayedBefore() { return true; } + + @Override + public @Nullable Location getLocation() { + Player player = getPlayer(); + return player != null ? player.getLocation() : null; + } + + @Override + public @Nullable Location getBedSpawnLocation() { return null; } + + @Override + public void incrementStatistic(@NotNull Statistic statistic) {} + + @Override + public void decrementStatistic(@NotNull Statistic statistic) {} + + @Override + public int getStatistic(@NotNull Statistic statistic) { return 0; } + + @Override + public void incrementStatistic(@NotNull Statistic statistic, int amount) {} + + @Override + public void decrementStatistic(@NotNull Statistic statistic, int amount) {} + + @Override + public void setStatistic(@NotNull Statistic statistic, int newValue) {} + + @Override + public void incrementStatistic(@NotNull Statistic statistic, @NotNull org.bukkit.Material material) {} + + @Override + public void decrementStatistic(@NotNull Statistic statistic, @NotNull org.bukkit.Material material) {} + + @Override + public int getStatistic(@NotNull Statistic statistic, @NotNull org.bukkit.Material material) { return 0; } + + @Override + public void incrementStatistic(@NotNull Statistic statistic, @NotNull org.bukkit.Material material, int amount) {} + + @Override + public void decrementStatistic(@NotNull Statistic statistic, @NotNull org.bukkit.Material material, int amount) {} + + @Override + public void setStatistic(@NotNull Statistic statistic, @NotNull org.bukkit.Material material, int newValue) {} + + @Override + public void incrementStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType) {} + + @Override + public void decrementStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType) {} + + @Override + public int getStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType) { return 0; } + + @Override + public void incrementStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType, int amount) {} + + @Override + public void decrementStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType, int amount) {} + + @Override + public void setStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType, int newValue) {} + + @Override + public @Nullable Location getLastDeathLocation() { + Player player = getPlayer(); + return player != null ? player.getLastDeathLocation() : null; + } + + @Override + public @Nullable Location getRespawnLocation() { + Player player = getPlayer(); + return player != null ? player.getBedSpawnLocation() : null; + } + + public @Nullable Location getRespawnLocation(boolean anchor) { + Player player = getPlayer(); + return player != null ? player.getBedSpawnLocation() : null; + } + + @Override + public com.destroystokyo.paper.profile.@NotNull PlayerProfile getPlayerProfile() { + Player player = getPlayer(); + if (player != null) return player.getPlayerProfile(); + return org.bukkit.Bukkit.getServer().createProfile(getUniqueId(), getName()); + } + + @Override + public long getLastLogin() { return getLastPlayed(); } + + @Override + public long getLastSeen() { return getLastPlayed(); } + + @Override + public @NotNull org.bukkit.persistence.PersistentDataContainer getPersistentDataContainer() { + return new org.patchbukkit.persistence.PatchBukkitPersistentDataContainer(); + } + + @Override + public @NotNull Map serialize() { + return Map.of("uuid", getUniqueId().toString(), "name", getName() != null ? getName() : ""); + } +} 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 1465a69..c097ec1 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitPlayer.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/entity/PatchBukkitPlayer.java @@ -83,10 +83,32 @@ public class PatchBukkitPlayer private final Map>> invertedVisibilityEntities = new HashMap<>(); - public PatchBukkitPlayer(UUID uuid, String name) { + public PatchBukkitPlayer(UUID uuid, String name) { super(uuid, name); } + @Override + public void openSign(@NotNull org.bukkit.block.Sign sign) { + } + + @Override + public void spawnParticle(@NotNull Particle particle, @NotNull Location location, int count, double offsetX, double offsetY, double offsetZ, double extra, @org.jspecify.annotations.Nullable T data, boolean force) { + } + + @Override + public void spawnParticle(@NotNull Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double extra, @org.jspecify.annotations.Nullable T data) { + } + + public void unsetFixedPose() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'unsetFixedPose'"); + } + + public void resetFlyingTicks() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'resetFlyingTicks'"); + } + @Override public void sendRawMessage(String message) { this.sendRawMessage(null, message); @@ -247,7 +269,6 @@ public class PatchBukkitPlayer throw new UnsupportedOperationException("Unimplemented method 'getLastSeen'"); } - @Override public @org.jspecify.annotations.Nullable Location getRespawnLocation(boolean loadLocationAndValidate) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getRespawnLocation'"); @@ -620,16 +641,16 @@ public class PatchBukkitPlayer throw new UnsupportedOperationException("Unimplemented method 'performCommand'"); } + private boolean sprinting = false; + @Override public boolean isSprinting() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'isSprinting'"); + return this.sprinting; } @Override public void setSprinting(boolean sprinting) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setSprinting'"); + this.sprinting = sprinting; } @Override @@ -680,6 +701,16 @@ public class PatchBukkitPlayer throw new UnsupportedOperationException("Unimplemented method 'playNote'"); } + @Override + public void playSound(Location location, Sound sound, float volume, float pitch) { + playSound(location, sound, SoundCategory.MASTER, volume, pitch); + } + + @Override + public void playSound(Location location, String sound, float volume, float pitch) { + playSound(location, sound, SoundCategory.MASTER, volume, pitch); + } + @Override public void playSound(Location location, Sound sound, SoundCategory category, float volume, float pitch) { var patchBukkitSound = (PatchBukkitSound) sound; @@ -1485,8 +1516,16 @@ public class PatchBukkitPlayer @Override public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double extra, @org.jspecify.annotations.Nullable T data, boolean force) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'spawnParticle'"); + if (particle == null) return; + var request = patchbukkit.world.SpawnParticleRequest.newBuilder() + .setWorldUuid(BridgeUtils.convertUuid(this.getWorld().getUID())) + .setParticle(particle.name().toLowerCase()) + .setX(x).setY(y).setZ(z) + .setCount(count) + .setOffsetX(offsetX).setOffsetY(offsetY).setOffsetZ(offsetZ) + .setExtra(extra) + .build(); + NativeBridgeFfi.spawnParticle(request); } @Override @@ -1503,8 +1542,16 @@ public class PatchBukkitPlayer @Override public Locale locale() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'locale'"); + String locStr = getLocale(); + if (locStr != null && !locStr.isEmpty()) { + String[] parts = locStr.split("_"); + if (parts.length >= 2) { + return new Locale(parts[0], parts[1]); + } else if (parts.length == 1) { + return new Locale(parts[0]); + } + } + return Locale.US; } @Override @@ -1515,8 +1562,13 @@ public class PatchBukkitPlayer @Override public String getLocale() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getLocale'"); + try { + var resp = NativeBridgeFfi.getPlayerLocale(BridgeUtils.convertUuid(this.uuid)); + if (resp != null && resp.getLocale() != null && !resp.getLocale().isEmpty()) { + return resp.getLocale(); + } + } catch (Throwable ignored) {} + return "en_us"; } @Override @@ -1579,7 +1631,6 @@ public class PatchBukkitPlayer throw new UnsupportedOperationException("Unimplemented method 'openBook'"); } - @Override public void openVirtualSign(Position block, Side side) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'openVirtualSign'"); @@ -1765,7 +1816,6 @@ public class PatchBukkitPlayer throw new UnsupportedOperationException("Unimplemented method 'setDeathScreenScore'"); } - @Override public PlayerGameConnection getConnection() { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'getConnection'"); diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/events/PatchBukkitEventFactory.java b/java/patchbukkit/src/main/java/org/patchbukkit/events/PatchBukkitEventFactory.java index 7addd61..31f0b37 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/events/PatchBukkitEventFactory.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/events/PatchBukkitEventFactory.java @@ -9,9 +9,29 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import patchbukkit.common.UUID; +import patchbukkit.events.BlockBreakEvent; +import patchbukkit.events.BlockDamageEvent; +import patchbukkit.events.BlockPlaceEvent; import patchbukkit.events.Event; import patchbukkit.events.FireEventResponse; +import patchbukkit.events.PlayerChatEvent; +import patchbukkit.events.PlayerGameModeChangeEvent; +import patchbukkit.events.PlayerInteractEvent; import patchbukkit.events.PlayerJoinEvent; +import patchbukkit.events.PlayerInteractEntityEvent; +import patchbukkit.events.PlayerMoveEvent; +import patchbukkit.events.PlayerQuitEvent; +import patchbukkit.events.PlayerToggleFlightEvent; +import patchbukkit.events.PlayerToggleSneakEvent; +import patchbukkit.events.PlayerToggleSprintEvent; +import patchbukkit.events.PluginDisableEvent; +import patchbukkit.events.PluginEnableEvent; +import patchbukkit.events.BlockFadeEvent; +import patchbukkit.events.BlockFormEvent; +import patchbukkit.events.BlockGrowEvent; +import patchbukkit.events.BlockIgniteEvent; +import patchbukkit.events.ServerCommandEvent; +import patchbukkit.events.SignChangeEvent; import java.util.logging.Logger; @@ -31,9 +51,10 @@ public class PatchBukkitEventFactory { @Nullable public static org.bukkit.event.Event createEvent(@NotNull Event event) { - Event.DataCase dataCase = event.getDataCase(); + try { + Event.DataCase dataCase = event.getDataCase(); - return switch (dataCase) { + return switch (dataCase) { case PLAYER_JOIN -> { PlayerJoinEvent joinEvent = event.getPlayerJoin(); Player player = getPlayer(joinEvent.getPlayerUuid().getValue()); @@ -42,22 +63,221 @@ public class PatchBukkitEventFactory { Component joinMessage = GsonComponentSerializer.gson().deserialize(joinEvent.getJoinMessage()); yield new org.bukkit.event.player.PlayerJoinEvent(player, joinMessage); } + case PLAYER_QUIT -> { + PlayerQuitEvent quitEvent = event.getPlayerQuit(); + Player player = getPlayer(quitEvent.getPlayerUuid().getValue()); + if (player == null) yield null; + + Component quitMessage = quitEvent.getQuitMessage().isEmpty() ? Component.empty() : GsonComponentSerializer.gson().deserialize(quitEvent.getQuitMessage()); + yield new org.bukkit.event.player.PlayerQuitEvent(player, quitMessage); + } + case PLAYER_GAMEMODE_CHANGE -> { + PlayerGameModeChangeEvent gmEvent = event.getPlayerGamemodeChange(); + Player player = getPlayer(gmEvent.getPlayerUuid().getValue()); + if (player == null) yield null; + + org.bukkit.GameMode newGamemode; + try { + newGamemode = org.bukkit.GameMode.valueOf(gmEvent.getNewGamemode().toUpperCase()); + } catch (IllegalArgumentException e) { + newGamemode = org.bukkit.GameMode.SURVIVAL; + } + yield new org.bukkit.event.player.PlayerGameModeChangeEvent(player, newGamemode); + } + case PLAYER_INTERACT -> { + PlayerInteractEvent interactEvent = event.getPlayerInteract(); + Player player = getPlayer(interactEvent.getPlayerUuid().getValue()); + if (player == null) yield null; + + org.bukkit.event.block.Action action; + try { + action = org.bukkit.event.block.Action.valueOf(interactEvent.getAction()); + } catch (IllegalArgumentException e) { + action = org.bukkit.event.block.Action.RIGHT_CLICK_AIR; + } + + org.bukkit.inventory.EquipmentSlot hand; + try { + hand = org.bukkit.inventory.EquipmentSlot.valueOf(interactEvent.getHand()); + } catch (IllegalArgumentException e) { + hand = org.bukkit.inventory.EquipmentSlot.HAND; + } + + org.bukkit.block.Block clickedBlock = player.getWorld().getBlockAt( + interactEvent.getClickedX(), + interactEvent.getClickedY(), + interactEvent.getClickedZ() + ); + + yield new org.bukkit.event.player.PlayerInteractEvent( + player, + action, + player.getInventory().getItemInMainHand(), + clickedBlock, + org.bukkit.block.BlockFace.SELF, + hand + ); + } + case PLUGIN_ENABLE -> { + PluginEnableEvent enableEvent = event.getPluginEnable(); + org.bukkit.plugin.Plugin plugin = Bukkit.getPluginManager().getPlugin(enableEvent.getPluginName()); + if (plugin == null) yield null; + yield new org.bukkit.event.server.PluginEnableEvent(plugin); + } + case PLUGIN_DISABLE -> { + PluginDisableEvent disableEvent = event.getPluginDisable(); + org.bukkit.plugin.Plugin plugin = Bukkit.getPluginManager().getPlugin(disableEvent.getPluginName()); + if (plugin == null) yield null; + yield new org.bukkit.event.server.PluginDisableEvent(plugin); + } + case BLOCK_BREAK -> { + BlockBreakEvent breakEvent = event.getBlockBreak(); + Player player = breakEvent.hasPlayerUuid() ? getPlayer(breakEvent.getPlayerUuid().getValue()) : null; + if (player == null) yield null; + org.bukkit.World world = player.getWorld(); + org.bukkit.block.Block block = world.getBlockAt(breakEvent.getBlockX(), breakEvent.getBlockY(), breakEvent.getBlockZ()); + yield new org.bukkit.event.block.BlockBreakEvent(block, player); + } + case BLOCK_PLACE -> { + BlockPlaceEvent placeEvent = event.getBlockPlace(); + Player player = getPlayer(placeEvent.getPlayerUuid().getValue()); + if (player == null) yield null; + org.bukkit.block.Block block = player.getWorld().getBlockAt(placeEvent.getBlockX(), placeEvent.getBlockY(), placeEvent.getBlockZ()); + org.bukkit.block.BlockState blockState = block.getState(); + yield new org.bukkit.event.block.BlockPlaceEvent(block, blockState, block, player.getInventory().getItemInMainHand(), player, placeEvent.getCanBuild(), org.bukkit.inventory.EquipmentSlot.HAND); + } + case SIGN_CHANGE -> { + SignChangeEvent signEvent = event.getSignChange(); + Player player = getPlayer(signEvent.getPlayerUuid().getValue()); + if (player == null) yield null; + org.bukkit.block.Block block = player.getWorld().getBlockAt( + signEvent.getBlockX(), + signEvent.getBlockY(), + signEvent.getBlockZ() + ); + String[] lines = signEvent.getLinesList().toArray(new String[0]); + yield new org.bukkit.event.block.SignChangeEvent(block, player, lines); + } + case BLOCK_DAMAGE -> { + BlockDamageEvent damageEvent = event.getBlockDamage(); + Player player = getPlayer(damageEvent.getPlayerUuid().getValue()); + if (player == null) yield null; + org.bukkit.block.Block block = player.getWorld().getBlockAt( + damageEvent.getBlockX(), + damageEvent.getBlockY(), + damageEvent.getBlockZ() + ); + yield new org.bukkit.event.block.BlockDamageEvent( + player, block, player.getInventory().getItemInMainHand(), damageEvent.getInstaBreak() + ); + } + case SERVER_COMMAND -> { + ServerCommandEvent cmdEvent = event.getServerCommand(); + yield new org.bukkit.event.server.ServerCommandEvent( + Bukkit.getConsoleSender(), + cmdEvent.getCommand() + ); + } + case BLOCK_IGNITE -> { + BlockIgniteEvent igniteEvent = event.getBlockIgnite(); + Player player = igniteEvent.hasPlayerUuid() ? getPlayer(igniteEvent.getPlayerUuid().getValue()) : null; + org.bukkit.World world = Bukkit.getWorlds().isEmpty() ? null : Bukkit.getWorlds().get(0); + if (world == null) yield null; + org.bukkit.block.Block block = world.getBlockAt(igniteEvent.getBlockX(), igniteEvent.getBlockY(), igniteEvent.getBlockZ()); + org.bukkit.event.block.BlockIgniteEvent.IgniteCause cause = org.bukkit.event.block.BlockIgniteEvent.IgniteCause.FLINT_AND_STEEL; + yield new org.bukkit.event.block.BlockIgniteEvent(block, cause, player); + } + case BLOCK_GROW -> { + BlockGrowEvent growEvent = event.getBlockGrow(); + org.bukkit.World world = Bukkit.getWorlds().isEmpty() ? null : Bukkit.getWorlds().get(0); + if (world == null) yield null; + org.bukkit.block.Block block = world.getBlockAt(growEvent.getBlockX(), growEvent.getBlockY(), growEvent.getBlockZ()); + yield new org.bukkit.event.block.BlockGrowEvent(block, block.getState()); + } + case BLOCK_FORM -> { + BlockFormEvent formEvent = event.getBlockForm(); + org.bukkit.World world = Bukkit.getWorlds().isEmpty() ? null : Bukkit.getWorlds().get(0); + if (world == null) yield null; + org.bukkit.block.Block block = world.getBlockAt(formEvent.getBlockX(), formEvent.getBlockY(), formEvent.getBlockZ()); + yield new org.bukkit.event.block.BlockFormEvent(block, block.getState()); + } + case BLOCK_FADE -> { + BlockFadeEvent fadeEvent = event.getBlockFade(); + org.bukkit.World world = Bukkit.getWorlds().isEmpty() ? null : Bukkit.getWorlds().get(0); + if (world == null) yield null; + org.bukkit.block.Block block = world.getBlockAt(fadeEvent.getBlockX(), fadeEvent.getBlockY(), fadeEvent.getBlockZ()); + yield new org.bukkit.event.block.BlockFadeEvent(block, block.getState()); + } + case PLAYER_TOGGLE_SNEAK -> { + PlayerToggleSneakEvent sneakEvent = event.getPlayerToggleSneak(); + Player player = getPlayer(sneakEvent.getPlayerUuid().getValue()); + if (player == null) yield null; + yield new org.bukkit.event.player.PlayerToggleSneakEvent(player, sneakEvent.getIsSneaking()); + } + case PLAYER_TOGGLE_SPRINT -> { + PlayerToggleSprintEvent sprintEvent = event.getPlayerToggleSprint(); + Player player = getPlayer(sprintEvent.getPlayerUuid().getValue()); + if (player == null) yield null; + yield new org.bukkit.event.player.PlayerToggleSprintEvent(player, sprintEvent.getIsSprinting()); + } + case PLAYER_TOGGLE_FLIGHT -> { + PlayerToggleFlightEvent flightEvent = event.getPlayerToggleFlight(); + Player player = getPlayer(flightEvent.getPlayerUuid().getValue()); + if (player == null) yield null; + yield new org.bukkit.event.player.PlayerToggleFlightEvent(player, flightEvent.getIsFlying()); + } + case PLAYER_MOVE -> { + PlayerMoveEvent moveEvent = event.getPlayerMove(); + Player player = getPlayer(moveEvent.getPlayerUuid().getValue()); + if (player == null) yield null; + org.bukkit.Location from = org.patchbukkit.bridge.BridgeUtils.convertLocation(moveEvent.getFrom()); + org.bukkit.Location to = org.patchbukkit.bridge.BridgeUtils.convertLocation(moveEvent.getTo()); + yield new org.bukkit.event.player.PlayerMoveEvent(player, from, to); + } + case PLAYER_INTERACT_ENTITY -> { + PlayerInteractEntityEvent interactEvent = event.getPlayerInteractEntity(); + Player player = getPlayer(interactEvent.getPlayerUuid().getValue()); + if (player == null) yield null; + java.util.UUID targetUuid = org.patchbukkit.bridge.BridgeUtils.convertUuid(interactEvent.getTargetUuid()); + org.bukkit.entity.Entity target = Bukkit.getPlayer(targetUuid); + if (target == null) target = Bukkit.getEntity(targetUuid); + if (target == null) yield null; + if ("ATTACK".equals(interactEvent.getAction())) { + yield new org.bukkit.event.entity.EntityDamageByEntityEvent( + player, target, org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK, 1.0 + ); + } else { + yield new org.bukkit.event.player.PlayerInteractEntityEvent(player, target); + } + } + case PLAYER_CHAT -> { + PlayerChatEvent chatEvent = event.getPlayerChat(); + Player player = getPlayer(chatEvent.getPlayerUuid().getValue()); + if (player == null) yield null; + yield new org.bukkit.event.player.AsyncPlayerChatEvent(true, player, chatEvent.getMessage(), new java.util.HashSet<>(Bukkit.getOnlinePlayers())); + } case DATA_NOT_SET -> { LOGGER.warning("EventFactory: Received Event with no data"); yield null; } default -> null; }; + } catch (Throwable t) { + LOGGER.log(java.util.logging.Level.SEVERE, "Exception in createEvent for " + event.getDataCase() + ": " + t.getMessage(), t); + t.printStackTrace(); + return null; + } } @NotNull public static byte[] toFireEventResponse(@NotNull org.bukkit.event.Event event) { - boolean cancelled = event instanceof org.bukkit.event.Cancellable c && c.isCancelled(); + try { + boolean cancelled = event instanceof org.bukkit.event.Cancellable c && c.isCancelled(); - FireEventResponse.Builder builder = FireEventResponse.newBuilder() - .setCancelled(cancelled); + FireEventResponse.Builder builder = FireEventResponse.newBuilder() + .setCancelled(cancelled); - Event.Builder eventBuilder = Event.newBuilder(); + Event.Builder eventBuilder = Event.newBuilder(); if (event instanceof org.bukkit.event.player.PlayerJoinEvent joinEvent) { String joinMessage = GsonComponentSerializer.gson().serialize(joinEvent.joinMessage()); @@ -70,11 +290,131 @@ public class PatchBukkitEventFactory { .setJoinMessage(joinMessage) .build() ); + } else if (event instanceof org.bukkit.event.player.PlayerQuitEvent quitEvent) { + String quitMessage = quitEvent.quitMessage() != null ? GsonComponentSerializer.gson().serialize(quitEvent.quitMessage()) : ""; + + eventBuilder.setPlayerQuit( + PlayerQuitEvent.newBuilder() + .setPlayerUuid(UUID.newBuilder() + .setValue(quitEvent.getPlayer().getUniqueId().toString()) + .build()) + .setQuitMessage(quitMessage) + .build() + ); + } else if (event instanceof org.bukkit.event.player.PlayerGameModeChangeEvent gmEvent) { + eventBuilder.setPlayerGamemodeChange( + PlayerGameModeChangeEvent.newBuilder() + .setPlayerUuid(UUID.newBuilder() + .setValue(gmEvent.getPlayer().getUniqueId().toString()) + .build()) + .setNewGamemode(gmEvent.getNewGameMode().name()) + .build() + ); + } else if (event instanceof org.bukkit.event.player.PlayerInteractEvent interactEvent) { + var block = interactEvent.getClickedBlock(); + int x = block != null ? block.getX() : 0; + int y = block != null ? block.getY() : 0; + int z = block != null ? block.getZ() : 0; + + eventBuilder.setPlayerInteract( + PlayerInteractEvent.newBuilder() + .setPlayerUuid(UUID.newBuilder() + .setValue(interactEvent.getPlayer().getUniqueId().toString()) + .build()) + .setAction(interactEvent.getAction().name()) + .setClickedX(x) + .setClickedY(y) + .setClickedZ(z) + .setBlockFace(interactEvent.getBlockFace().name()) + .setHand(interactEvent.getHand() != null ? interactEvent.getHand().name() : "HAND") + .build() + ); + } else if (event instanceof org.bukkit.event.server.PluginEnableEvent enableEvent) { + eventBuilder.setPluginEnable( + PluginEnableEvent.newBuilder() + .setPluginName(enableEvent.getPlugin().getName()) + .build() + ); + } else if (event instanceof org.bukkit.event.server.PluginDisableEvent disableEvent) { + eventBuilder.setPluginDisable( + PluginDisableEvent.newBuilder() + .setPluginName(disableEvent.getPlugin().getName()) + .build() + ); + } else if (event instanceof org.bukkit.event.block.BlockBreakEvent breakEvent) { + var b = breakEvent.getBlock(); + var bBuilder = BlockBreakEvent.newBuilder() + .setBlockX(b.getX()) + .setBlockY(b.getY()) + .setBlockZ(b.getZ()) + .setBlockType(b.getType().name()) + .setExp(breakEvent.getExpToDrop()) + .setDropItems(breakEvent.isDropItems()); + if (breakEvent.getPlayer() != null) { + bBuilder.setPlayerUuid(UUID.newBuilder().setValue(breakEvent.getPlayer().getUniqueId().toString()).build()); + } + eventBuilder.setBlockBreak(bBuilder.build()); + } else if (event instanceof org.bukkit.event.block.BlockPlaceEvent placeEvent) { + var b = placeEvent.getBlockPlaced(); + var against = placeEvent.getBlockAgainst(); + eventBuilder.setBlockPlace( + BlockPlaceEvent.newBuilder() + .setPlayerUuid(UUID.newBuilder().setValue(placeEvent.getPlayer().getUniqueId().toString()).build()) + .setBlockX(b.getX()) + .setBlockY(b.getY()) + .setBlockZ(b.getZ()) + .setBlockPlacedType(b.getType().name()) + .setBlockAgainstType(against.getType().name()) + .setCanBuild(placeEvent.canBuild()) + .build() + ); + } else if (event instanceof org.bukkit.event.player.PlayerToggleSneakEvent sneakEvent) { + eventBuilder.setPlayerToggleSneak( + PlayerToggleSneakEvent.newBuilder() + .setPlayerUuid(UUID.newBuilder().setValue(sneakEvent.getPlayer().getUniqueId().toString()).build()) + .setIsSneaking(sneakEvent.isSneaking()) + .build() + ); + } else if (event instanceof org.bukkit.event.player.PlayerToggleSprintEvent sprintEvent) { + eventBuilder.setPlayerToggleSprint( + PlayerToggleSprintEvent.newBuilder() + .setPlayerUuid(UUID.newBuilder().setValue(sprintEvent.getPlayer().getUniqueId().toString()).build()) + .setIsSprinting(sprintEvent.isSprinting()) + .build() + ); + } else if (event instanceof org.bukkit.event.player.PlayerToggleFlightEvent flightEvent) { + eventBuilder.setPlayerToggleFlight( + PlayerToggleFlightEvent.newBuilder() + .setPlayerUuid(UUID.newBuilder().setValue(flightEvent.getPlayer().getUniqueId().toString()).build()) + .setIsFlying(flightEvent.isFlying()) + .build() + ); + } else if (event instanceof org.bukkit.event.player.PlayerMoveEvent moveEvent) { + eventBuilder.setPlayerMove( + PlayerMoveEvent.newBuilder() + .setPlayerUuid(UUID.newBuilder().setValue(moveEvent.getPlayer().getUniqueId().toString()).build()) + .setFrom(org.patchbukkit.bridge.BridgeUtils.convertLocation(moveEvent.getFrom())) + .setTo(org.patchbukkit.bridge.BridgeUtils.convertLocation(moveEvent.getTo())) + .build() + ); + } else if (event instanceof org.bukkit.event.player.AsyncPlayerChatEvent chatEvent) { + eventBuilder.setPlayerChat( + PlayerChatEvent.newBuilder() + .setPlayerUuid(UUID.newBuilder().setValue(chatEvent.getPlayer().getUniqueId().toString()).build()) + .setMessage(chatEvent.getMessage()) + .setFormat(chatEvent.getFormat()) + .build() + ); } builder.setData(eventBuilder.build()); return builder.build().toByteArray(); + } catch (Throwable t) { + LOGGER.log(java.util.logging.Level.SEVERE, "Exception serializing event response for " + event.getEventName() + ": " + t.getMessage(), t); + t.printStackTrace(); + return FireEventResponse.newBuilder().setCancelled(false).build().toByteArray(); + } } public static boolean isCancellable(@NotNull org.bukkit.event.Event event) { diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/events/PatchBukkitEventManager.java b/java/patchbukkit/src/main/java/org/patchbukkit/events/PatchBukkitEventManager.java index ba94bfa..d4c6a55 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/events/PatchBukkitEventManager.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/events/PatchBukkitEventManager.java @@ -20,8 +20,21 @@ import co.aikar.timings.TimedEventExecutor; import org.jetbrains.annotations.NotNull; import patchbukkit.bridge.NativeBridgeFfi; +import patchbukkit.events.BlockBreakEvent; +import patchbukkit.events.BlockPlaceEvent; import patchbukkit.events.CallEventRequest; +import patchbukkit.events.PlayerChatEvent; +import patchbukkit.events.PlayerGameModeChangeEvent; +import patchbukkit.events.PlayerInteractEvent; import patchbukkit.events.PlayerJoinEvent; +import patchbukkit.events.PlayerInteractEntityEvent; +import patchbukkit.events.PlayerMoveEvent; +import patchbukkit.events.PlayerQuitEvent; +import patchbukkit.events.PlayerToggleFlightEvent; +import patchbukkit.events.PlayerToggleSneakEvent; +import patchbukkit.events.PlayerToggleSprintEvent; +import patchbukkit.events.PluginDisableEvent; +import patchbukkit.events.PluginEnableEvent; import patchbukkit.events.RegisterEventRequest; import java.lang.reflect.Method; @@ -59,6 +72,262 @@ public class PatchBukkitEventManager { ).build() ); break; + case "org.bukkit.event.player.PlayerQuitEvent": + var castedQuit = (org.bukkit.event.player.PlayerQuitEvent) event; + String qMsg = castedQuit.quitMessage() != null ? castedQuit.quitMessage().toString() : ""; + request.setEvent( + patchbukkit.events.Event.newBuilder().setPlayerQuit( + PlayerQuitEvent.newBuilder() + .setQuitMessage(qMsg) + .setPlayerUuid(BridgeUtils.convertUuid(castedQuit.getPlayer().getUniqueId())).build() + ).build() + ); + break; + case "org.bukkit.event.player.PlayerGameModeChangeEvent": + var castedGM = (org.bukkit.event.player.PlayerGameModeChangeEvent) event; + request.setEvent( + patchbukkit.events.Event.newBuilder().setPlayerGamemodeChange( + PlayerGameModeChangeEvent.newBuilder() + .setNewGamemode(castedGM.getNewGameMode().name()) + .setPlayerUuid(BridgeUtils.convertUuid(castedGM.getPlayer().getUniqueId())).build() + ).build() + ); + break; + case "org.bukkit.event.player.PlayerInteractEvent": + var castedInteract = (org.bukkit.event.player.PlayerInteractEvent) event; + var block = castedInteract.getClickedBlock(); + int x = block != null ? block.getX() : 0; + int y = block != null ? block.getY() : 0; + int z = block != null ? block.getZ() : 0; + request.setEvent( + patchbukkit.events.Event.newBuilder().setPlayerInteract( + PlayerInteractEvent.newBuilder() + .setAction(castedInteract.getAction().name()) + .setClickedX(x) + .setClickedY(y) + .setClickedZ(z) + .setBlockFace(castedInteract.getBlockFace().name()) + .setHand(castedInteract.getHand() != null ? castedInteract.getHand().name() : "HAND") + .setPlayerUuid(BridgeUtils.convertUuid(castedInteract.getPlayer().getUniqueId())).build() + ).build() + ); + break; + case "org.bukkit.event.server.PluginEnableEvent": + var castedEnable = (org.bukkit.event.server.PluginEnableEvent) event; + request.setEvent( + patchbukkit.events.Event.newBuilder().setPluginEnable( + PluginEnableEvent.newBuilder() + .setPluginName(castedEnable.getPlugin().getName()).build() + ).build() + ); + break; + case "org.bukkit.event.block.SignChangeEvent": + var castedSign = (org.bukkit.event.block.SignChangeEvent) event; + var signBlock = castedSign.getBlock(); + var signBuilder = patchbukkit.events.SignChangeEvent.newBuilder() + .setBlockX(signBlock.getX()) + .setBlockY(signBlock.getY()) + .setBlockZ(signBlock.getZ()); + if (castedSign.getPlayer() != null) { + signBuilder.setPlayerUuid(BridgeUtils.convertUuid(castedSign.getPlayer().getUniqueId())); + } + for (String line : castedSign.getLines()) { + signBuilder.addLines(line != null ? line : ""); + } + request.setEvent(patchbukkit.events.Event.newBuilder().setSignChange(signBuilder.build()).build()); + break; + case "org.bukkit.event.block.BlockDamageEvent": + var castedDamage = (org.bukkit.event.block.BlockDamageEvent) event; + var dmgBlock = castedDamage.getBlock(); + var dmgBuilder = patchbukkit.events.BlockDamageEvent.newBuilder() + .setBlockX(dmgBlock.getX()) + .setBlockY(dmgBlock.getY()) + .setBlockZ(dmgBlock.getZ()) + .setInstaBreak(castedDamage.getInstaBreak()); + if (castedDamage.getPlayer() != null) { + dmgBuilder.setPlayerUuid(BridgeUtils.convertUuid(castedDamage.getPlayer().getUniqueId())); + } + request.setEvent(patchbukkit.events.Event.newBuilder().setBlockDamage(dmgBuilder.build()).build()); + break; + case "org.bukkit.event.block.BlockIgniteEvent": + var castedIgnite = (org.bukkit.event.block.BlockIgniteEvent) event; + var igniteBlock = castedIgnite.getBlock(); + var igniteBuilder = patchbukkit.events.BlockIgniteEvent.newBuilder() + .setBlockX(igniteBlock.getX()) + .setBlockY(igniteBlock.getY()) + .setBlockZ(igniteBlock.getZ()) + .setIgniteCause(castedIgnite.getCause() != null ? castedIgnite.getCause().name() : "FLINT_AND_STEEL"); + if (castedIgnite.getPlayer() != null) { + igniteBuilder.setPlayerUuid(BridgeUtils.convertUuid(castedIgnite.getPlayer().getUniqueId())); + } + request.setEvent(patchbukkit.events.Event.newBuilder().setBlockIgnite(igniteBuilder.build()).build()); + break; + case "org.bukkit.event.block.BlockGrowEvent": + var castedGrow = (org.bukkit.event.block.BlockGrowEvent) event; + var growBlock = castedGrow.getBlock(); + request.setEvent(patchbukkit.events.Event.newBuilder().setBlockGrow( + patchbukkit.events.BlockGrowEvent.newBuilder() + .setBlockX(growBlock.getX()) + .setBlockY(growBlock.getY()) + .setBlockZ(growBlock.getZ()) + .setNewBlockType(castedGrow.getNewState().getType().name()).build() + ).build()); + break; + case "org.bukkit.event.block.BlockFormEvent": + var castedForm = (org.bukkit.event.block.BlockFormEvent) event; + var formBlock = castedForm.getBlock(); + request.setEvent(patchbukkit.events.Event.newBuilder().setBlockForm( + patchbukkit.events.BlockFormEvent.newBuilder() + .setBlockX(formBlock.getX()) + .setBlockY(formBlock.getY()) + .setBlockZ(formBlock.getZ()) + .setNewBlockType(castedForm.getNewState().getType().name()).build() + ).build()); + break; + case "org.bukkit.event.block.BlockFadeEvent": + var castedFade = (org.bukkit.event.block.BlockFadeEvent) event; + var fadeBlock = castedFade.getBlock(); + request.setEvent(patchbukkit.events.Event.newBuilder().setBlockFade( + patchbukkit.events.BlockFadeEvent.newBuilder() + .setBlockX(fadeBlock.getX()) + .setBlockY(fadeBlock.getY()) + .setBlockZ(fadeBlock.getZ()) + .setNewBlockType(castedFade.getNewState().getType().name()).build() + ).build()); + break; + case "org.bukkit.event.server.ServerCommandEvent": + var castedCmd = (org.bukkit.event.server.ServerCommandEvent) event; + request.setEvent( + patchbukkit.events.Event.newBuilder().setServerCommand( + patchbukkit.events.ServerCommandEvent.newBuilder() + .setSenderName(castedCmd.getSender().getName()) + .setCommand(castedCmd.getCommand()).build() + ).build() + ); + break; + case "org.bukkit.event.server.PluginDisableEvent": + var castedDisable = (org.bukkit.event.server.PluginDisableEvent) event; + request.setEvent( + patchbukkit.events.Event.newBuilder().setPluginDisable( + PluginDisableEvent.newBuilder() + .setPluginName(castedDisable.getPlugin().getName()).build() + ).build() + ); + break; + case "org.bukkit.event.block.BlockBreakEvent": + var castedBreak = (org.bukkit.event.block.BlockBreakEvent) event; + var breakBlock = castedBreak.getBlock(); + var bBuilder = BlockBreakEvent.newBuilder() + .setBlockX(breakBlock.getX()) + .setBlockY(breakBlock.getY()) + .setBlockZ(breakBlock.getZ()) + .setBlockType(breakBlock.getType().name()) + .setExp(castedBreak.getExpToDrop()) + .setDropItems(castedBreak.isDropItems()); + if (castedBreak.getPlayer() != null) { + bBuilder.setPlayerUuid(BridgeUtils.convertUuid(castedBreak.getPlayer().getUniqueId())); + } + request.setEvent(patchbukkit.events.Event.newBuilder().setBlockBreak(bBuilder.build()).build()); + break; + case "org.bukkit.event.block.BlockPlaceEvent": + var castedPlace = (org.bukkit.event.block.BlockPlaceEvent) event; + var placedBlock = castedPlace.getBlockPlaced(); + var againstBlock = castedPlace.getBlockAgainst(); + request.setEvent( + patchbukkit.events.Event.newBuilder().setBlockPlace( + BlockPlaceEvent.newBuilder() + .setBlockX(placedBlock.getX()) + .setBlockY(placedBlock.getY()) + .setBlockZ(placedBlock.getZ()) + .setBlockPlacedType(placedBlock.getType().name()) + .setBlockAgainstType(againstBlock.getType().name()) + .setCanBuild(castedPlace.canBuild()) + .setPlayerUuid(BridgeUtils.convertUuid(castedPlace.getPlayer().getUniqueId())).build() + ).build() + ); + break; + case "org.bukkit.event.player.PlayerToggleSneakEvent": + var castedSneak = (org.bukkit.event.player.PlayerToggleSneakEvent) event; + request.setEvent( + patchbukkit.events.Event.newBuilder().setPlayerToggleSneak( + PlayerToggleSneakEvent.newBuilder() + .setIsSneaking(castedSneak.isSneaking()) + .setPlayerUuid(BridgeUtils.convertUuid(castedSneak.getPlayer().getUniqueId())).build() + ).build() + ); + break; + case "org.bukkit.event.player.PlayerToggleSprintEvent": + var castedSprint = (org.bukkit.event.player.PlayerToggleSprintEvent) event; + request.setEvent( + patchbukkit.events.Event.newBuilder().setPlayerToggleSprint( + PlayerToggleSprintEvent.newBuilder() + .setIsSprinting(castedSprint.isSprinting()) + .setPlayerUuid(BridgeUtils.convertUuid(castedSprint.getPlayer().getUniqueId())).build() + ).build() + ); + break; + case "org.bukkit.event.player.PlayerToggleFlightEvent": + var castedFlight = (org.bukkit.event.player.PlayerToggleFlightEvent) event; + request.setEvent( + patchbukkit.events.Event.newBuilder().setPlayerToggleFlight( + PlayerToggleFlightEvent.newBuilder() + .setIsFlying(castedFlight.isFlying()) + .setPlayerUuid(BridgeUtils.convertUuid(castedFlight.getPlayer().getUniqueId())).build() + ).build() + ); + break; + case "org.bukkit.event.player.PlayerMoveEvent": + var castedMove = (org.bukkit.event.player.PlayerMoveEvent) event; + request.setEvent( + patchbukkit.events.Event.newBuilder().setPlayerMove( + PlayerMoveEvent.newBuilder() + .setPlayerUuid(BridgeUtils.convertUuid(castedMove.getPlayer().getUniqueId())) + .setFrom(BridgeUtils.convertLocation(castedMove.getFrom())) + .setTo(BridgeUtils.convertLocation(castedMove.getTo())).build() + ).build() + ); + break; + case "org.bukkit.event.player.PlayerInteractEntityEvent": + case "org.bukkit.event.player.PlayerInteractAtEntityEvent": + var castedInteractEntity = (org.bukkit.event.player.PlayerInteractEntityEvent) event; + request.setEvent( + patchbukkit.events.Event.newBuilder().setPlayerInteractEntity( + PlayerInteractEntityEvent.newBuilder() + .setPlayerUuid(BridgeUtils.convertUuid(castedInteractEntity.getPlayer().getUniqueId())) + .setTargetUuid(BridgeUtils.convertUuid(castedInteractEntity.getRightClicked().getUniqueId())) + .setAction("INTERACT") + .setIsSneaking(castedInteractEntity.getPlayer().isSneaking()) + .build() + ).build() + ); + break; + case "org.bukkit.event.entity.EntityDamageByEntityEvent": + var castedDamageByEntity = (org.bukkit.event.entity.EntityDamageByEntityEvent) event; + if (castedDamageByEntity.getDamager() instanceof org.bukkit.entity.Player damagerPlayer) { + request.setEvent( + patchbukkit.events.Event.newBuilder().setPlayerInteractEntity( + PlayerInteractEntityEvent.newBuilder() + .setPlayerUuid(BridgeUtils.convertUuid(damagerPlayer.getUniqueId())) + .setTargetUuid(BridgeUtils.convertUuid(castedDamageByEntity.getEntity().getUniqueId())) + .setAction("ATTACK") + .setIsSneaking(damagerPlayer.isSneaking()) + .build() + ).build() + ); + } + break; + case "org.bukkit.event.player.AsyncPlayerChatEvent": + case "org.bukkit.event.player.PlayerChatEvent": + var castedChat = (org.bukkit.event.player.AsyncPlayerChatEvent) event; + request.setEvent( + patchbukkit.events.Event.newBuilder().setPlayerChat( + PlayerChatEvent.newBuilder() + .setMessage(castedChat.getMessage()) + .setFormat(castedChat.getFormat()) + .setPlayerUuid(BridgeUtils.convertUuid(castedChat.getPlayer().getUniqueId())).build() + ).build() + ); + break; } var response = NativeBridgeFfi.callEvent(request.build()); @@ -107,6 +376,7 @@ public class PatchBukkitEventManager { + " to " + registration.getPlugin().getPluginMeta().getDisplayName(), ex ); + ex.printStackTrace(); } } } @@ -135,6 +405,7 @@ public class PatchBukkitEventManager { + " to " + listener.getPlugin().getPluginMeta().getDisplayName(), ex ); + ex.printStackTrace(); } } } diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/help/PatchBukkitHelpMap.java b/java/patchbukkit/src/main/java/org/patchbukkit/help/PatchBukkitHelpMap.java new file mode 100644 index 0000000..45e3a9f --- /dev/null +++ b/java/patchbukkit/src/main/java/org/patchbukkit/help/PatchBukkitHelpMap.java @@ -0,0 +1,54 @@ +package org.patchbukkit.help; + +import org.bukkit.help.HelpMap; +import org.bukkit.help.HelpTopic; +import org.bukkit.help.HelpTopicFactory; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class PatchBukkitHelpMap implements HelpMap { + + private final Map helpTopics = new ConcurrentHashMap<>(); + private final Map, HelpTopicFactory> topicFactories = new ConcurrentHashMap<>(); + private final List ignoredPlugins = new ArrayList<>(); + + @Override + public @Nullable HelpTopic getHelpTopic(@NonNull String topicName) { + if (topicName == null) return null; + return helpTopics.get(topicName); + } + + @Override + public @NonNull Collection getHelpTopics() { + return Collections.unmodifiableCollection(helpTopics.values()); + } + + @Override + public void addTopic(@NonNull HelpTopic topic) { + if (topic != null && topic.getName() != null) { + helpTopics.put(topic.getName(), topic); + } + } + + @Override + public void clear() { + helpTopics.clear(); + topicFactories.clear(); + } + + @Override + + public void registerHelpTopicFactory(@NonNull Class commandClass, @NonNull HelpTopicFactory factory) { + if (commandClass != null && factory != null) { + topicFactories.put(commandClass, factory); + } + } + + @Override + public @NonNull List getIgnoredPlugins() { + return Collections.unmodifiableList(ignoredPlugins); + } +} diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/inventory/PatchBukkitEntityEquipment.java b/java/patchbukkit/src/main/java/org/patchbukkit/inventory/PatchBukkitEntityEquipment.java new file mode 100644 index 0000000..f7690a9 --- /dev/null +++ b/java/patchbukkit/src/main/java/org/patchbukkit/inventory/PatchBukkitEntityEquipment.java @@ -0,0 +1,194 @@ +package org.patchbukkit.inventory; + +import org.bukkit.entity.Entity; +import org.bukkit.inventory.EntityEquipment; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.patchbukkit.entity.PatchBukkitHumanEntity; + +public class PatchBukkitEntityEquipment implements EntityEquipment { + private final PatchBukkitHumanEntity holder; + + public PatchBukkitEntityEquipment(PatchBukkitHumanEntity holder) { + this.holder = holder; + } + + @Override + public ItemStack getItemInMainHand() { + return holder.getInventory().getItemInMainHand(); + } + + @Override + public void setItemInMainHand(ItemStack item) { + holder.getInventory().setItemInMainHand(item); + } + + @Override + public void setItemInMainHand(ItemStack item, boolean silent) { + setItemInMainHand(item); + } + + @Override + public ItemStack getItemInOffHand() { + return holder.getInventory().getItemInOffHand(); + } + + @Override + public void setItemInOffHand(ItemStack item) { + holder.getInventory().setItemInOffHand(item); + } + + @Override + public void setItemInOffHand(ItemStack item, boolean silent) { + setItemInOffHand(item); + } + + @Override + public ItemStack getItemInHand() { + return getItemInMainHand(); + } + + @Override + public void setItemInHand(ItemStack stack) { + setItemInMainHand(stack); + } + + @Override + public ItemStack getHelmet() { + return holder.getInventory().getHelmet(); + } + + @Override + public void setHelmet(ItemStack helmet) { + holder.getInventory().setHelmet(helmet); + } + + @Override + public void setHelmet(ItemStack helmet, boolean silent) { + setHelmet(helmet); + } + + @Override + public ItemStack getChestplate() { + return holder.getInventory().getChestplate(); + } + + @Override + public void setChestplate(ItemStack chestplate) { + holder.getInventory().setChestplate(chestplate); + } + + @Override + public void setChestplate(ItemStack chestplate, boolean silent) { + setChestplate(chestplate); + } + + @Override + public ItemStack getLeggings() { + return holder.getInventory().getLeggings(); + } + + @Override + public void setLeggings(ItemStack leggings) { + holder.getInventory().setLeggings(leggings); + } + + @Override + public void setLeggings(ItemStack leggings, boolean silent) { + setLeggings(leggings); + } + + @Override + public ItemStack getBoots() { + return holder.getInventory().getBoots(); + } + + @Override + public void setBoots(ItemStack boots) { + holder.getInventory().setBoots(boots); + } + + @Override + public void setBoots(ItemStack boots, boolean silent) { + setBoots(boots); + } + + @Override + public ItemStack[] getArmorContents() { + return holder.getInventory().getArmorContents(); + } + + @Override + public void setArmorContents(ItemStack[] items) { + holder.getInventory().setArmorContents(items); + } + + @Override + public void clear() { + holder.getInventory().clear(); + } + + @Override + public Entity getHolder() { + return holder; + } + + @Override + public void setItem(EquipmentSlot slot, ItemStack item) { + holder.getInventory().setItem(slot, item); + } + + @Override + public void setItem(EquipmentSlot slot, ItemStack item, boolean silent) { + setItem(slot, item); + } + + @Override + public ItemStack getItem(EquipmentSlot slot) { + return holder.getInventory().getItem(slot); + } + + @Override + public void setDropChance(EquipmentSlot slot, float chance) { + } + + @Override + public float getDropChance(EquipmentSlot slot) { + return 1.0f; + } + + @Override + public float getItemInMainHandDropChance() { return 1.0f; } + @Override + public void setItemInMainHandDropChance(float chance) {} + + @Override + public float getItemInOffHandDropChance() { return 1.0f; } + @Override + public void setItemInOffHandDropChance(float chance) {} + + @Override + public float getItemInHandDropChance() { return 1.0f; } + @Override + public void setItemInHandDropChance(float chance) {} + + @Override + public float getHelmetDropChance() { return 1.0f; } + @Override + public void setHelmetDropChance(float chance) {} + + @Override + public float getChestplateDropChance() { return 1.0f; } + @Override + public void setChestplateDropChance(float chance) {} + + @Override + public float getLeggingsDropChance() { return 1.0f; } + @Override + public void setLeggingsDropChance(float chance) {} + + @Override + public float getBootsDropChance() { return 1.0f; } + @Override + public void setBootsDropChance(float chance) {} +} diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/inventory/PatchBukkitItemFactory.java b/java/patchbukkit/src/main/java/org/patchbukkit/inventory/PatchBukkitItemFactory.java new file mode 100644 index 0000000..9ef01ce --- /dev/null +++ b/java/patchbukkit/src/main/java/org/patchbukkit/inventory/PatchBukkitItemFactory.java @@ -0,0 +1,100 @@ +package org.patchbukkit.inventory; + +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.bukkit.Material; +import org.bukkit.inventory.ItemFactory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +public class PatchBukkitItemFactory { + public static final ItemFactory INSTANCE = createFactory(); + + private static ItemFactory createFactory() { + return (ItemFactory) Proxy.newProxyInstance( + ItemFactory.class.getClassLoader(), + new Class[] { ItemFactory.class }, + (proxy, method, args) -> { + String name = method.getName(); + if ("getItemMeta".equals(name) || "createItemMeta".equals(name) || "asMetaFor".equals(name)) { + if (args != null && args.length > 0 && args[0] instanceof Material mat && mat == Material.AIR) { + return null; + } + return createMeta(); + } + if ("isApplicable".equals(name)) { + return true; + } + if ("equals".equals(name)) { + if (args != null && args.length == 2) { + return Objects.equals(args[0], args[1]); + } + return false; + } + if ("ensureServerConform".equals(name)) { + if (args != null && args.length > 0 && args[0] instanceof ItemStack stack) { + if (stack instanceof PatchBukkitItemStack) { + return stack; + } + try { + return new PatchBukkitItemStack(stack.getType(), stack.getAmount()); + } catch (Throwable ignored) { + return new PatchBukkitItemStack(Material.AIR, 0); + } + } + return new PatchBukkitItemStack(Material.AIR, 0); + } + if ("isItemEmpty".equals(name)) { + if (args != null && args.length > 0 && args[0] instanceof ItemStack stack) { + return PatchBukkitPlayerInventory.isItemEmpty(stack); + } + return true; + } + Class returnType = method.getReturnType(); + if (returnType == boolean.class) return false; + if (returnType == int.class) return 0; + if (returnType == long.class) return 0L; + if (returnType == double.class || returnType == float.class) return 0.0; + return null; + } + ); + } + + public static ItemMeta createMeta() { + Map state = new HashMap<>(); + return (ItemMeta) Proxy.newProxyInstance( + ItemMeta.class.getClassLoader(), + new Class[] { ItemMeta.class }, + (proxy, method, args) -> { + String name = method.getName(); + if ("hasDisplayName".equals(name)) return state.containsKey("displayName"); + if ("getDisplayName".equals(name)) return state.get("displayName"); + if ("setDisplayName".equals(name)) { + if (args != null && args.length > 0 && args[0] != null) state.put("displayName", args[0]); + else state.remove("displayName"); + return null; + } + if ("hasLore".equals(name)) return state.containsKey("lore"); + if ("getLore".equals(name)) return state.get("lore"); + if ("setLore".equals(name)) { + if (args != null && args.length > 0 && args[0] != null) state.put("lore", args[0]); + else state.remove("lore"); + return null; + } + if ("clone".equals(name)) return createMeta(); + if ("equals".equals(name)) { + if (args != null && args.length > 0) return proxy == args[0]; + return false; + } + Class returnType = method.getReturnType(); + if (returnType == boolean.class) return false; + if (returnType == int.class) return 0; + if (returnType == long.class) return 0L; + if (returnType == double.class || returnType == float.class) return 0.0; + return null; + } + ); + } +} diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/inventory/PatchBukkitItemStack.java b/java/patchbukkit/src/main/java/org/patchbukkit/inventory/PatchBukkitItemStack.java new file mode 100644 index 0000000..20529dd --- /dev/null +++ b/java/patchbukkit/src/main/java/org/patchbukkit/inventory/PatchBukkitItemStack.java @@ -0,0 +1,92 @@ +package org.patchbukkit.inventory; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.Objects; + +public class PatchBukkitItemStack extends ItemStack { + + private Material type; + private int amount; + private ItemMeta meta; + + public PatchBukkitItemStack(Material type) { + this(type, 1); + } + + public PatchBukkitItemStack(Material type, int amount) { + this.type = type != null ? type : Material.AIR; + this.amount = amount; + } + + @Override + public @NonNull Material getType() { + return type; + } + + @Override + public void setType(@Nullable Material type) { + this.type = type != null ? type : Material.AIR; + } + + @Override + public int getAmount() { + return amount; + } + + @Override + public void setAmount(int amount) { + this.amount = amount; + } + + @Override + public @Nullable ItemMeta getItemMeta() { + return meta; + } + + @Override + public boolean setItemMeta(@Nullable ItemMeta itemMeta) { + this.meta = itemMeta; + return true; + } + + @Override + public boolean hasItemMeta() { + return meta != null; + } + + @Override + public boolean isEmpty() { + return type == Material.AIR || amount <= 0; + } + + @Override + public @NonNull ItemStack clone() { + PatchBukkitItemStack cloned = new PatchBukkitItemStack(type, amount); + if (meta != null) { + cloned.meta = meta.clone(); + } + return cloned; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof ItemStack other)) return false; + return this.type == other.getType() && this.amount == other.getAmount() && Objects.equals(this.meta, other.getItemMeta()); + } + + @Override + public int hashCode() { + return Objects.hash(type, amount, meta); + } + + @Override + public String toString() { + return "ItemStack{" + type + " x " + amount + "}"; + } +} diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/inventory/PatchBukkitPlayerInventory.java b/java/patchbukkit/src/main/java/org/patchbukkit/inventory/PatchBukkitPlayerInventory.java new file mode 100644 index 0000000..85e7723 --- /dev/null +++ b/java/patchbukkit/src/main/java/org/patchbukkit/inventory/PatchBukkitPlayerInventory.java @@ -0,0 +1,539 @@ +package org.patchbukkit.inventory; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.entity.HumanEntity; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.patchbukkit.bridge.BridgeUtils; +import org.patchbukkit.entity.PatchBukkitHumanEntity; +import patchbukkit.bridge.NativeBridgeFfi; +import patchbukkit.itemstack.GetPlayerInventoryResponse; +import patchbukkit.itemstack.SetPlayerEquipmentRequest; +import patchbukkit.itemstack.SetPlayerInventorySlotRequest; +import patchbukkit.itemstack.SetPlayerSelectedSlotRequest; + +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class PatchBukkitPlayerInventory implements PlayerInventory { + private final PatchBukkitHumanEntity holder; + + public PatchBukkitPlayerInventory(PatchBukkitHumanEntity holder) { + this.holder = holder; + } + + private GetPlayerInventoryResponse fetchInventory() { + var resp = NativeBridgeFfi.getPlayerInventory(BridgeUtils.convertUuid(holder.getUniqueId())); + if (resp == null) { + return GetPlayerInventoryResponse.newBuilder().build(); + } + return resp; + } + + public static boolean isItemEmpty(@Nullable ItemStack item) { + if (item == null) { + return true; + } + try { + if (item.getType() == Material.AIR || item.getAmount() <= 0) { + return true; + } + return item.isEmpty(); + } catch (Throwable ignored) { + return true; + } + } + + public static patchbukkit.itemstack.ItemStack toProto(ItemStack item) { + if (isItemEmpty(item)) { + return patchbukkit.itemstack.ItemStack.newBuilder() + .setType("minecraft:air") + .setAmount(0) + .build(); + } + try { + String type = item.getType().getKey().toString(); + return patchbukkit.itemstack.ItemStack.newBuilder() + .setType(type) + .setAmount(item.getAmount()) + .build(); + } catch (Throwable ignored) { + return patchbukkit.itemstack.ItemStack.newBuilder() + .setType("minecraft:air") + .setAmount(0) + .build(); + } + } + + public static ItemStack fromProto(patchbukkit.itemstack.ItemStack proto) { + if (proto == null || proto.getAmount() <= 0 || proto.getType() == null || proto.getType().isEmpty() || proto.getType().equalsIgnoreCase("minecraft:air")) { + return new PatchBukkitItemStack(Material.AIR, 0); + } + Material mat = Material.matchMaterial(proto.getType()); + if (mat == null || mat == Material.AIR) { + return new PatchBukkitItemStack(Material.AIR, 0); + } + return new PatchBukkitItemStack(mat, proto.getAmount()); + } + + @Override + public ListIterator iterator() { + return Arrays.asList(getContents()).listIterator(); + } + + @Override + public ListIterator iterator(int index) { + return Arrays.asList(getContents()).listIterator(index); + } + + @Override + public ItemStack[] getArmorContents() { + GetPlayerInventoryResponse resp = fetchInventory(); + ItemStack[] armor = new ItemStack[4]; + armor[0] = fromProto(resp.getBoots()); + armor[1] = fromProto(resp.getLeggings()); + armor[2] = fromProto(resp.getChestplate()); + armor[3] = fromProto(resp.getHelmet()); + return armor; + } + + @Override + public void setArmorContents(ItemStack[] items) { + if (items == null) items = new ItemStack[4]; + setBoots(items.length > 0 ? items[0] : null); + setLeggings(items.length > 1 ? items[1] : null); + setChestplate(items.length > 2 ? items[2] : null); + setHelmet(items.length > 3 ? items[3] : null); + } + + @Override + public ItemStack[] getExtraContents() { + GetPlayerInventoryResponse resp = fetchInventory(); + return new ItemStack[] { fromProto(resp.getOffHand()) }; + } + + @Override + public void setExtraContents(ItemStack[] items) { + if (items != null && items.length > 0) { + setItemInOffHand(items[0]); + } else { + setItemInOffHand(null); + } + } + + @Override + public ItemStack getHelmet() { + return fromProto(fetchInventory().getHelmet()); + } + + @Override + public ItemStack getChestplate() { + return fromProto(fetchInventory().getChestplate()); + } + + @Override + public ItemStack getLeggings() { + return fromProto(fetchInventory().getLeggings()); + } + + @Override + public ItemStack getBoots() { + return fromProto(fetchInventory().getBoots()); + } + + @Override + public void setHelmet(ItemStack helmet) { + setEquipment(5, helmet); + } + + @Override + public void setChestplate(ItemStack chestplate) { + setEquipment(4, chestplate); + } + + @Override + public void setLeggings(ItemStack leggings) { + setEquipment(3, leggings); + } + + @Override + public void setBoots(ItemStack boots) { + setEquipment(2, boots); + } + + @Override + public ItemStack getItemInMainHand() { + GetPlayerInventoryResponse resp = fetchInventory(); + int selected = resp.getSelectedSlot(); + if (selected >= 0 && selected < resp.getMainInventoryCount()) { + return fromProto(resp.getMainInventory(selected)); + } + return ItemStack.empty(); + } + + @Override + public void setItemInMainHand(ItemStack item) { + GetPlayerInventoryResponse resp = fetchInventory(); + int selected = resp.getSelectedSlot(); + setItem(selected, item); + } + + @Override + public ItemStack getItemInOffHand() { + return fromProto(fetchInventory().getOffHand()); + } + + @Override + public void setItemInOffHand(ItemStack item) { + setEquipment(1, item); + } + + @Override + public ItemStack getItemInHand() { + return getItemInMainHand(); + } + + @Override + public void setItemInHand(ItemStack stack) { + setItemInMainHand(stack); + } + + @Override + public int getHeldItemSlot() { + return fetchInventory().getSelectedSlot(); + } + + @Override + public void setHeldItemSlot(int slot) { + if (slot >= 0 && slot < 9) { + NativeBridgeFfi.setPlayerSelectedSlot( + SetPlayerSelectedSlotRequest.newBuilder() + .setUuid(BridgeUtils.convertUuid(holder.getUniqueId())) + .setSlot(slot) + .build() + ); + } + } + + private void setEquipment(int slotType, ItemStack item) { + NativeBridgeFfi.setPlayerEquipment( + SetPlayerEquipmentRequest.newBuilder() + .setUuid(BridgeUtils.convertUuid(holder.getUniqueId())) + .setSlotType(slotType) + .setItem(toProto(item)) + .build() + ); + } + + @Override + public HumanEntity getHolder() { + return this.holder; + } + + @Override + public HumanEntity getHolder(boolean useSnapshot) { + return this.holder; + } + + @Override + public int getSize() { + return 41; + } + + @Override + public int getMaxStackSize() { + return 64; + } + + @Override + public void setMaxStackSize(int size) { + } + + @Override + public ItemStack getItem(int index) { + GetPlayerInventoryResponse resp = fetchInventory(); + if (index >= 0 && index < resp.getMainInventoryCount()) { + return fromProto(resp.getMainInventory(index)); + } else if (index == 36) { + return fromProto(resp.getBoots()); + } else if (index == 37) { + return fromProto(resp.getLeggings()); + } else if (index == 38) { + return fromProto(resp.getChestplate()); + } else if (index == 39) { + return fromProto(resp.getHelmet()); + } else if (index == 40) { + return fromProto(resp.getOffHand()); + } + return ItemStack.empty(); + } + + @Override + public void setItem(int index, ItemStack item) { + NativeBridgeFfi.setPlayerInventorySlot( + SetPlayerInventorySlotRequest.newBuilder() + .setUuid(BridgeUtils.convertUuid(holder.getUniqueId())) + .setSlot(index) + .setItem(toProto(item)) + .build() + ); + } + + @Override + public void setItem(EquipmentSlot slot, ItemStack item) { + if (slot == null) return; + switch (slot) { + case HAND -> setItemInMainHand(item); + case OFF_HAND -> setItemInOffHand(item); + case FEET -> setBoots(item); + case LEGS -> setLeggings(item); + case CHEST -> setChestplate(item); + case HEAD -> setHelmet(item); + default -> {} + } + } + + @Override + public ItemStack getItem(EquipmentSlot slot) { + if (slot == null) return ItemStack.empty(); + return switch (slot) { + case HAND -> getItemInMainHand(); + case OFF_HAND -> getItemInOffHand(); + case FEET -> getBoots(); + case LEGS -> getLeggings(); + case CHEST -> getChestplate(); + case HEAD -> getHelmet(); + default -> ItemStack.empty(); + }; + } + + @Override + public HashMap addItem(ItemStack... items) throws IllegalArgumentException { + HashMap leftover = new HashMap<>(); + if (items == null) return leftover; + for (int i = 0; i < items.length; i++) { + ItemStack item = items[i]; + if (isItemEmpty(item)) continue; + int firstEmpty = firstEmpty(); + if (firstEmpty != -1) { + setItem(firstEmpty, item); + } else { + leftover.put(i, item); + } + } + return leftover; + } + + @Override + public HashMap removeItemAnySlot(ItemStack... items) throws IllegalArgumentException { + return removeItem(items); + } + + @Override + public HashMap removeItem(ItemStack... items) throws IllegalArgumentException { + HashMap leftover = new HashMap<>(); + if (items == null) return leftover; + for (int i = 0; i < items.length; i++) { + ItemStack item = items[i]; + if (isItemEmpty(item)) continue; + int slot = first(item.getType()); + if (slot != -1) { + setItem(slot, ItemStack.empty()); + } else { + leftover.put(i, item); + } + } + return leftover; + } + + @Override + public ItemStack[] getContents() { + GetPlayerInventoryResponse resp = fetchInventory(); + ItemStack[] contents = new ItemStack[41]; + for (int i = 0; i < 36 && i < resp.getMainInventoryCount(); i++) { + contents[i] = fromProto(resp.getMainInventory(i)); + } + for (int i = resp.getMainInventoryCount(); i < 36; i++) { + contents[i] = ItemStack.empty(); + } + contents[36] = fromProto(resp.getBoots()); + contents[37] = fromProto(resp.getLeggings()); + contents[38] = fromProto(resp.getChestplate()); + contents[39] = fromProto(resp.getHelmet()); + contents[40] = fromProto(resp.getOffHand()); + return contents; + } + + @Override + public void setContents(ItemStack[] items) throws IllegalArgumentException { + if (items == null) return; + for (int i = 0; i < items.length && i < 41; i++) { + setItem(i, items[i]); + } + } + + @Override + public ItemStack[] getStorageContents() { + GetPlayerInventoryResponse resp = fetchInventory(); + ItemStack[] contents = new ItemStack[36]; + for (int i = 0; i < 36 && i < resp.getMainInventoryCount(); i++) { + contents[i] = fromProto(resp.getMainInventory(i)); + } + for (int i = resp.getMainInventoryCount(); i < 36; i++) { + contents[i] = ItemStack.empty(); + } + return contents; + } + + @Override + public void setStorageContents(ItemStack[] items) throws IllegalArgumentException { + if (items == null) return; + for (int i = 0; i < items.length && i < 36; i++) { + setItem(i, items[i]); + } + } + + @Override + public boolean contains(Material material) throws IllegalArgumentException { + return first(material) != -1; + } + + @Override + public boolean contains(ItemStack item) { + return first(item) != -1; + } + + @Override + public boolean contains(Material material, int amount) throws IllegalArgumentException { + if (amount <= 0) return true; + int count = 0; + for (ItemStack is : getStorageContents()) { + if (is != null && !is.isEmpty() && is.getType() == material) { + count += is.getAmount(); + if (count >= amount) return true; + } + } + return false; + } + + @Override + public boolean contains(ItemStack item, int amount) { + if (item == null) return false; + return contains(item.getType(), amount); + } + + @Override + public boolean containsAtLeast(ItemStack item, int amount) { + if (item == null) return false; + return contains(item.getType(), amount); + } + + @Override + public HashMap all(Material material) throws IllegalArgumentException { + HashMap slots = new HashMap<>(); + ItemStack[] contents = getContents(); + for (int i = 0; i < contents.length; i++) { + if (contents[i] != null && !contents[i].isEmpty() && contents[i].getType() == material) { + slots.put(i, contents[i]); + } + } + return slots; + } + + @Override + public HashMap all(ItemStack item) { + if (item == null) return new HashMap<>(); + return all(item.getType()); + } + + @Override + public int first(Material material) throws IllegalArgumentException { + if (material == null) return -1; + ItemStack[] contents = getContents(); + for (int i = 0; i < contents.length; i++) { + if (contents[i] != null && !contents[i].isEmpty() && contents[i].getType() == material) { + return i; + } + } + return -1; + } + + @Override + public int first(ItemStack item) { + if (item == null) return -1; + return first(item.getType()); + } + + @Override + public int firstEmpty() { + ItemStack[] contents = getStorageContents(); + for (int i = 0; i < contents.length; i++) { + if (isItemEmpty(contents[i])) { + return i; + } + } + return -1; + } + + @Override + public boolean isEmpty() { + for (ItemStack item : getContents()) { + if (!isItemEmpty(item)) { + return false; + } + } + return true; + } + + @Override + public void remove(Material material) throws IllegalArgumentException { + if (material == null) return; + ItemStack[] contents = getContents(); + for (int i = 0; i < contents.length; i++) { + if (contents[i] != null && contents[i].getType() == material) { + setItem(i, ItemStack.empty()); + } + } + } + + @Override + public void remove(ItemStack item) { + if (item != null) { + remove(item.getType()); + } + } + + @Override + public void clear(int index) { + setItem(index, ItemStack.empty()); + } + + @Override + public void clear() { + NativeBridgeFfi.clearPlayerInventory(BridgeUtils.convertUuid(holder.getUniqueId())); + } + + @Override + public List getViewers() { + return Collections.singletonList(holder); + } + + @Override + public InventoryType getType() { + return InventoryType.PLAYER; + } + + @Override + public int close() { + return 0; + } + + @Override + public Location getLocation() { + return holder.getLocation(); + } +} diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/loader/PatchBukkitBootstrap.java b/java/patchbukkit/src/main/java/org/patchbukkit/loader/PatchBukkitBootstrap.java new file mode 100644 index 0000000..77c24ea --- /dev/null +++ b/java/patchbukkit/src/main/java/org/patchbukkit/loader/PatchBukkitBootstrap.java @@ -0,0 +1,269 @@ +package org.patchbukkit.loader; + +import java.io.File; +import java.io.InputStream; +import java.net.URL; +import java.util.*; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import org.bukkit.Bukkit; +import org.bukkit.command.PluginCommand; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.java.JavaPlugin; +import org.patchbukkit.PatchBukkitPluginManager; +import org.patchbukkit.command.CommandFactory; +import org.patchbukkit.command.PatchBukkitCommandMap; + +public class PatchBukkitBootstrap { + + public static boolean bootstrapPlugins(String pluginsDirPath) { + try { + File pluginsDir = new File(pluginsDirPath); + if (!pluginsDir.exists() || !pluginsDir.isDirectory()) { + System.out.println("[PatchBukkit] Plugins directory does not exist: " + pluginsDirPath); + return true; + } + + List jarFiles = new ArrayList<>(); + findJarsRecursive(pluginsDir, jarFiles); + + if (jarFiles.isEmpty()) { + System.out.println("[PatchBukkit] No plugin JAR files found in: " + pluginsDirPath); + return true; + } + + Map holders = new LinkedHashMap<>(); + + for (File jarFile : jarFiles) { + try { + PluginHolder holder = parsePluginHolder(jarFile); + if (holder != null) { + holders.put(holder.name.toLowerCase(Locale.ENGLISH), holder); + } + } catch (Throwable t) { + System.err.println("[PatchBukkit] Failed to parse plugin JAR " + jarFile.getName() + ": " + t.getMessage()); + t.printStackTrace(); + } + } + + List loadOrder = computeLoadOrder(holders); + + for (PluginHolder holder : loadOrder) { + try { + loadAndRegisterPlugin(holder, holders); + } catch (Throwable t) { + System.err.println("[PatchBukkit] Failed to load plugin " + holder.name + ": " + t.getMessage()); + t.printStackTrace(); + } + } + + // Enable all instantiated plugins in dependency order + if (Bukkit.getPluginManager() instanceof PatchBukkitPluginManager pm) { + for (PluginHolder holder : loadOrder) { + if (holder.pluginInstance != null) { + try { + pm.enablePlugin(holder.pluginInstance); + System.out.println("[PatchBukkit] Enabled plugin: " + holder.name); + } catch (Throwable t) { + System.err.println("[PatchBukkit] Error enabling plugin " + holder.name + ": " + t.getMessage()); + t.printStackTrace(); + } + } + } + } + + return true; + } catch (Throwable t) { + System.err.println("[PatchBukkit] Fatal error during bootstrap: " + t.getMessage()); + t.printStackTrace(); + return false; + } + } + + private static void findJarsRecursive(File dir, List result) { + if (dir == null || !dir.isDirectory()) return; + if (dir.getName().equals("patchbukkit-libs")) return; + + File[] files = dir.listFiles(); + if (files == null) return; + + for (File file : files) { + if (file.isDirectory()) { + if (!file.getName().equals("patchbukkit-libs")) { + findJarsRecursive(file, result); + } + } else if (file.getName().endsWith(".jar")) { + result.add(file); + } + } + } + + private static PluginHolder parsePluginHolder(File jarFile) throws Exception { + try (JarFile jar = new JarFile(jarFile)) { + JarEntry entry = jar.getJarEntry("plugin.yml"); + if (entry == null) { + entry = jar.getJarEntry("paper-plugin.yml"); + } + if (entry == null) { + return null; + } + + PluginDescriptionFile description; + try (InputStream in = jar.getInputStream(entry)) { + description = new PluginDescriptionFile(in); + } + + PluginHolder holder = new PluginHolder(); + holder.jarFile = jarFile; + holder.description = description; + holder.name = description.getName(); + holder.mainClass = description.getMain(); + holder.depends = description.getDepend() != null ? description.getDepend() : Collections.emptyList(); + holder.softDepends = description.getSoftDepend() != null ? description.getSoftDepend() : Collections.emptyList(); + holder.provides = description.getProvides() != null ? description.getProvides() : Collections.emptyList(); + holder.libraries = description.getLibraries() != null ? description.getLibraries() : Collections.emptyList(); + return holder; + } + } + + private static List computeLoadOrder(Map holders) { + List order = new ArrayList<>(); + Set visited = new HashSet<>(); + Set visiting = new HashSet<>(); + + for (PluginHolder holder : holders.values()) { + visitHolder(holder, holders, visited, visiting, order); + } + + return order; + } + + private static void visitHolder( + PluginHolder holder, + Map holders, + Set visited, + Set visiting, + List order + ) { + String key = holder.name.toLowerCase(Locale.ENGLISH); + if (visited.contains(key)) return; + if (visiting.contains(key)) { + System.err.println("[PatchBukkit] Circular dependency detected involving plugin: " + holder.name); + return; + } + + visiting.add(key); + + for (String dep : holder.depends) { + PluginHolder depHolder = holders.get(dep.toLowerCase(Locale.ENGLISH)); + if (depHolder != null) { + visitHolder(depHolder, holders, visited, visiting, order); + } + } + + for (String dep : holder.softDepends) { + PluginHolder depHolder = holders.get(dep.toLowerCase(Locale.ENGLISH)); + if (depHolder != null) { + visitHolder(depHolder, holders, visited, visiting, order); + } + } + + visiting.remove(key); + visited.add(key); + order.add(holder); + } + + private static void loadAndRegisterPlugin(PluginHolder holder, Map allHolders) throws Exception { + LinkedHashSet extraUrls = new LinkedHashSet<>(); + + if (!holder.libraries.isEmpty()) { + File libsDir = new File(holder.jarFile.getParentFile(), "patchbukkit-libs"); + if (!libsDir.exists()) libsDir.mkdirs(); + String libCoords = String.join("\n", holder.libraries); + List resolvedLibs = LibraryResolver.resolveLibraries(libCoords, libsDir); + for (File lib : resolvedLibs) { + if (lib != null && lib.exists()) { + extraUrls.add(lib.toURI().toURL()); + } + } + } + + PatchBukkitPluginClassLoader classLoader = new PatchBukkitPluginClassLoader( + PatchBukkitBootstrap.class.getClassLoader(), + holder.jarFile, + extraUrls.toArray(new URL[0]) + ); + + Class jarClass = Class.forName(holder.mainClass, true, classLoader); + Class pluginClass = jarClass.asSubclass(Plugin.class); + Plugin plugin = pluginClass.getDeclaredConstructor().newInstance(); + + if (plugin instanceof JavaPlugin javaPlugin) { + classLoader.init(javaPlugin); + } + + if (Bukkit.getPluginManager() instanceof PatchBukkitPluginManager pm) { + pm.registerPlugin(plugin); + } + + try { + plugin.onLoad(); + } catch (Throwable t) { + System.err.println("[PatchBukkit] Error during onLoad() for " + holder.name + ": " + t.getMessage()); + t.printStackTrace(); + } + + holder.pluginInstance = plugin; + + if (Bukkit.getCommandMap() instanceof PatchBukkitCommandMap commandMap) { + Map> commands = holder.description.getCommands(); + if (commands != null) { + for (Map.Entry> entry : commands.entrySet()) { + String cmdName = entry.getKey(); + Map cmdData = entry.getValue(); + + PluginCommand pluginCmd = CommandFactory.create(cmdName, plugin); + if (pluginCmd != null) { + if (cmdData != null) { + if (cmdData.containsKey("description")) { + pluginCmd.setDescription(String.valueOf(cmdData.get("description"))); + } + if (cmdData.containsKey("usage")) { + pluginCmd.setUsage(String.valueOf(cmdData.get("usage"))); + } + if (cmdData.containsKey("permission")) { + pluginCmd.setPermission(String.valueOf(cmdData.get("permission"))); + } + if (cmdData.containsKey("permission-message")) { + pluginCmd.setPermissionMessage(String.valueOf(cmdData.get("permission-message"))); + } + if (cmdData.get("aliases") instanceof List aliases) { + List strAliases = new ArrayList<>(); + for (Object a : aliases) { + if (a != null) strAliases.add(String.valueOf(a)); + } + pluginCmd.setAliases(strAliases); + } + } + + commandMap.register(cmdName, holder.name, pluginCmd); + System.out.println("[PatchBukkit] Registered command: /" + cmdName + " for plugin " + holder.name); + } + } + } + } + } + + private static class PluginHolder { + File jarFile; + PluginDescriptionFile description; + String name; + String mainClass; + List depends = Collections.emptyList(); + List softDepends = Collections.emptyList(); + List provides = Collections.emptyList(); + List libraries = Collections.emptyList(); + Plugin pluginInstance; + } +} diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/persistence/PatchBukkitPersistentDataContainer.java b/java/patchbukkit/src/main/java/org/patchbukkit/persistence/PatchBukkitPersistentDataContainer.java new file mode 100644 index 0000000..c75886e --- /dev/null +++ b/java/patchbukkit/src/main/java/org/patchbukkit/persistence/PatchBukkitPersistentDataContainer.java @@ -0,0 +1,84 @@ +package org.patchbukkit.persistence; + +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataAdapterContext; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class PatchBukkitPersistentDataContainer implements PersistentDataContainer { + private final Map storage = new HashMap<>(); + + @Override + public void set(@NotNull NamespacedKey key, @NotNull PersistentDataType type, @NotNull Z value) { + storage.put(key, value); + } + + @Override + public boolean has(@NotNull NamespacedKey key, @NotNull PersistentDataType type) { + return storage.containsKey(key); + } + + @Override + public boolean has(@NotNull NamespacedKey key) { + return storage.containsKey(key); + } + + @Override + public @Nullable Z get(@NotNull NamespacedKey key, @NotNull PersistentDataType type) { + return (Z) storage.get(key); + } + + @Override + public @NotNull Z getOrDefault(@NotNull NamespacedKey key, @NotNull PersistentDataType type, @NotNull Z defaultValue) { + Z val = get(key, type); + return val != null ? val : defaultValue; + } + + @Override + public @NotNull Set getKeys() { + return storage.keySet(); + } + + @Override + public void remove(@NotNull NamespacedKey key) { + storage.remove(key); + } + + @Override + public boolean isEmpty() { + return storage.isEmpty(); + } + + public int getSize() { + return storage.size(); + } + + @Override + public void copyTo(@NotNull PersistentDataContainer other, boolean replace) { + } + + @Override + public @NotNull PersistentDataAdapterContext getAdapterContext() { + return new PersistentDataAdapterContext() { + @Override + public @NotNull PersistentDataContainer newPersistentDataContainer() { + return new PatchBukkitPersistentDataContainer(); + } + }; + } + + @Override + public void readFromBytes(byte @NotNull [] bytes, boolean clear) { + } + + @Override + public byte @NotNull [] serializeToBytes() { + return new byte[0]; + } +} diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/registry/LegacyRegistryIdentifiers.java b/java/patchbukkit/src/main/java/org/patchbukkit/registry/LegacyRegistryIdentifiers.java index a611285..557fd28 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/registry/LegacyRegistryIdentifiers.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/registry/LegacyRegistryIdentifiers.java @@ -11,26 +11,30 @@ import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; - @Deprecated public final class LegacyRegistryIdentifiers { public static final Map, RegistryKey> CLASS_TO_KEY_MAP; + public static final Map, Class> KEY_TO_CLASS_MAP; static { - final ImmutableMap.Builder, RegistryKey> builder = ImmutableMap.builder(); + final ImmutableMap.Builder, RegistryKey> classToKey = ImmutableMap.builder(); + final ImmutableMap.Builder, Class> keyToClass = ImmutableMap.builder(); try { for (final Field field : RegistryKey.class.getFields()) { if (field.getType() == RegistryKey.class) { // get the legacy type from the RegistryKey generic parameter on the field final Class legacyType = LegacyRegistryIdentifiers.erase(((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]); - builder.put(legacyType, (RegistryKey) field.get(null)); + RegistryKey key = (RegistryKey) field.get(null); + classToKey.put(legacyType, key); + keyToClass.put(key, legacyType); } } } catch (final ReflectiveOperationException ex) { throw new RuntimeException(ex); } - CLASS_TO_KEY_MAP = builder.build(); + CLASS_TO_KEY_MAP = classToKey.build(); + KEY_TO_CLASS_MAP = keyToClass.build(); } private static Class erase(Type type) { diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitBlockType.java b/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitBlockType.java new file mode 100644 index 0000000..9d67fdd --- /dev/null +++ b/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitBlockType.java @@ -0,0 +1,108 @@ +package org.patchbukkit.registry; + +import org.bukkit.Keyed; +import org.bukkit.Material; +import org.bukkit.block.BlockType; +import org.bukkit.block.data.BlockData; +import org.patchbukkit.PatchBukkitBlockData; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.util.Collections; +import java.util.function.Consumer; + +public final class PatchBukkitBlockType { + + private PatchBukkitBlockType() {} + + public static BlockType create(Material material) { + if (material == null || material.isLegacy()) return null; + + return (BlockType) Proxy.newProxyInstance( + PatchBukkitBlockType.class.getClassLoader(), + new Class[]{BlockType.class}, + (proxy, method, args) -> { + String name = method.getName(); + if ("getKey".equals(name) || "key".equals(name)) { + return material.getKey(); + } + if ("asMaterial".equals(name)) { + return material; + } + if ("createBlockData".equals(name)) { + BlockType self = (BlockType) proxy; + if (args == null || args.length == 0) { + return PatchBukkitBlockData.newData(material, self, null); + } else if (args.length == 1 && args[0] instanceof String s) { + return PatchBukkitBlockData.newData(material, self, s); + } else if (args.length == 1 && args[0] instanceof Consumer consumer) { + BlockData data = PatchBukkitBlockData.newData(material, self, null); + consumer.accept(data); + return data; + } + } + if ("createBlockDataStates".equals(name)) { + BlockType self = (BlockType) proxy; + return Collections.singletonList(PatchBukkitBlockData.newData(material, self, null)); + } + if ("getBlockDataClass".equals(name)) { + return BlockData.class; + } + if ("getItemType".equals(name)) { + return material.isItem() ? PatchBukkitItemType.create(material) : null; + } + if ("hasItemType".equals(name)) { + return material.isItem(); + } + if ("isSolid".equals(name)) { + return material.isSolid(); + } + if ("isAir".equals(name)) { + return material.isAir(); + } + if ("isBurnable".equals(name)) { + return material.isBurnable(); + } + if ("isEdible".equals(name)) { + return material.isEdible(); + } + if ("isOccluding".equals(name)) { + return material.isOccluding(); + } + if ("isInteractable".equals(name)) { + return material.isInteractable(); + } + if ("hasGravity".equals(name) || "isGravity".equals(name)) { + return material.hasGravity(); + } + if ("translationKey".equals(name) || "getTranslationKey".equals(name)) { + return material.getTranslationKey(); + } + if ("typed".equals(name)) { + return proxy; + } + if ("equals".equals(name) && args != null && args.length == 1) { + if (args[0] instanceof Keyed k) { + return material.getKey().equals(k.getKey()); + } + return false; + } + if ("hashCode".equals(name)) { + return material.getKey().hashCode(); + } + if ("toString".equals(name)) { + return "BlockType{" + material.getKey() + "}"; + } + if (method.isDefault()) { + return InvocationHandler.invokeDefault(proxy, method, args); + } + + Class returnType = method.getReturnType(); + if (returnType == boolean.class) return false; + if (returnType == int.class || returnType == short.class || returnType == long.class || returnType == byte.class) return 0; + if (returnType == float.class || returnType == double.class) return 0.0f; + return null; + } + ); + } +} diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitItemType.java b/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitItemType.java new file mode 100644 index 0000000..6a2467b --- /dev/null +++ b/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitItemType.java @@ -0,0 +1,69 @@ +package org.patchbukkit.registry; + +import org.bukkit.Keyed; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; + +public final class PatchBukkitItemType { + + private PatchBukkitItemType() {} + + public static ItemType create(Material material) { + if (material == null) return null; + + return (ItemType) Proxy.newProxyInstance( + PatchBukkitItemType.class.getClassLoader(), + new Class[]{ItemType.class}, + (proxy, method, args) -> { + String name = method.getName(); + if ("getKey".equals(name) || "key".equals(name)) { + return material.getKey(); + } + if ("asMaterial".equals(name)) { + return material; + } + if ("getMaxStackSize".equals(name)) { + return material.getMaxStackSize(); + } + if ("getMaxDurability".equals(name)) { + return (int) material.getMaxDurability(); + } + if ("isEdible".equals(name)) { + return material.isEdible(); + } + if ("isRecord".equals(name)) { + return material.isRecord(); + } + if ("createItemStack".equals(name)) { + int amount = (args != null && args.length > 0 && args[0] instanceof Integer i) ? i : 1; + return new org.patchbukkit.inventory.PatchBukkitItemStack(material, amount); + } + if ("equals".equals(name) && args != null && args.length == 1) { + if (args[0] instanceof Keyed k) { + return material.getKey().equals(k.getKey()); + } + return false; + } + if ("hashCode".equals(name)) { + return material.getKey().hashCode(); + } + if ("toString".equals(name)) { + return "ItemType{" + material.getKey() + "}"; + } + if (method.isDefault()) { + return InvocationHandler.invokeDefault(proxy, method, args); + } + + Class returnType = method.getReturnType(); + if (returnType == boolean.class) return false; + if (returnType == int.class || returnType == short.class || returnType == long.class || returnType == byte.class) return 0; + if (returnType == float.class || returnType == double.class) return 0.0f; + return null; + } + ); + } +} diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitRegistry.java b/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitRegistry.java index a37f4f1..9d56429 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitRegistry.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitRegistry.java @@ -1,33 +1,40 @@ package org.patchbukkit.registry; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import io.papermc.paper.registry.RegistryKey; import io.papermc.paper.registry.tag.Tag; import io.papermc.paper.registry.tag.TagKey; import org.bukkit.Keyed; +import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.Registry; -import org.bukkit.Sound; +import org.bukkit.inventory.ItemType; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +import org.patchbukkit.PatchBukkitLegacy; import patchbukkit.bridge.NativeBridgeFfi; import patchbukkit.registry.GetRegistryDataRequest; import patchbukkit.registry.GetRegistryDataResponse; import patchbukkit.registry.RegistryType; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.stream.Stream; public class PatchBukkitRegistry implements Registry { - private final Map entries = new LinkedHashMap<>(); - private final Map> tags = new LinkedHashMap<>(); + private final Map entries = new ConcurrentHashMap<>(); + private final Map> tags = new ConcurrentHashMap<>(); + private final ThreadLocal> inFallback = ThreadLocal.withInitial(HashSet::new); private final RegistryKey registryKey; + private boolean initialized = false; + + public PatchBukkitRegistry(RegistryKey registryKey) { + this(null, null, null, registryKey); + } public PatchBukkitRegistry( RegistryType registryType, @@ -44,22 +51,110 @@ public class PatchBukkitRegistry implements Registry { RegistryKey registryKey ) { this.registryKey = registryKey; - if (registryType == null) return; + if (registryType != null || extractor != null || factory != null) { + initialize(registryType, extractor, factory); + } + } - GetRegistryDataRequest request = GetRegistryDataRequest.newBuilder() - .setRegistry(registryType) - .build(); + @SuppressWarnings("unchecked") + public synchronized void initialize( + RegistryType registryType, + Function> extractor, + Function factory + ) { + if (initialized) return; + initialized = true; - GetRegistryDataResponse response = NativeBridgeFfi.getRegistryData(request); - if (response == null) return; - - List

protoEntries = extractor.apply(response); - for (P protoEntry : protoEntries) { - B value = factory.apply(protoEntry); - if (value != null) { - entries.put(value.getKey(), value); + // Auto-discover pre-registered Paper/Bukkit constants for this registry + if (registryKey != null) { + if (RegistryKey.ITEM.equals(registryKey) || "item".equalsIgnoreCase(registryKey.key().value())) { + for (Material mat : Material.values()) { + try { + if (!mat.isLegacy() && mat.getKey() != null) { + B itemType = (B) PatchBukkitItemType.create(mat); + if (itemType != null) { + entries.put(mat.getKey(), itemType); + } + } + } catch (Throwable ignored) {} + } + } else if (RegistryKey.BLOCK.equals(registryKey) || "block".equalsIgnoreCase(registryKey.key().value())) { + for (Material mat : Material.values()) { + try { + if (!mat.isLegacy() && mat.getKey() != null) { + B blockType = (B) PatchBukkitBlockType.create(mat); + if (blockType != null) { + entries.put(mat.getKey(), blockType); + } + } + } catch (Throwable ignored) {} + } + } else { + Class valueClass = (Class) LegacyRegistryIdentifiers.KEY_TO_CLASS_MAP.get(registryKey); + if (valueClass != null) { + Map discovered = autoDiscover(valueClass); + entries.putAll(discovered); + } } } + + if (registryType != null) { + try { + GetRegistryDataRequest request = GetRegistryDataRequest.newBuilder() + .setRegistry(registryType) + .build(); + + GetRegistryDataResponse response = NativeBridgeFfi.getRegistryData(request); + if (response != null && extractor != null) { + List

protoEntries = extractor.apply(response); + if (protoEntries != null) { + for (P protoEntry : protoEntries) { + if (protoEntry == null) continue; + try { + B value = factory.apply(protoEntry); + if (value != null && value.getKey() != null) { + entries.put(value.getKey(), value); + } + } catch (Throwable t) { + // Skip invalid entry safely + } + } + } + } + } catch (Throwable t) { + // Ignore FFI or RPC failure safely + } + } + } + + @SuppressWarnings("unchecked") + private static Map autoDiscover(Class clazz) { + Map map = new LinkedHashMap<>(); + if (clazz == null) return map; + try { + if (clazz.isEnum()) { + for (B enumConstant : clazz.getEnumConstants()) { + if (enumConstant != null && enumConstant.getKey() != null) { + map.put(enumConstant.getKey(), enumConstant); + } + } + } + for (Field field : clazz.getFields()) { + if (Modifier.isStatic(field.getModifiers()) && clazz.isAssignableFrom(field.getType())) { + try { + field.setAccessible(true); + Object val = field.get(null); + if (val != null && clazz.isInstance(val)) { + B item = clazz.cast(val); + if (item.getKey() != null) { + map.put(item.getKey(), item); + } + } + } catch (Throwable ignored) {} + } + } + } catch (Throwable ignored) {} + return map; } @Override @@ -68,22 +163,72 @@ public class PatchBukkitRegistry implements Registry { return null; } B value = entries.get(key); - if (value == null && (RegistryKey.SOUND_EVENT.equals(registryKey) || "sound_event".equalsIgnoreCase(registryKey != null ? registryKey.key().value() : ""))) { - PatchBukkitSound dynamicSound = new PatchBukkitSound(key.getKey(), entries.size()); - entries.put(key, (B) dynamicSound); - return (B) dynamicSound; + if (value != null) { + return value; } - return value; + + Set fallbackSet = inFallback.get(); + if (!fallbackSet.add(key)) { + return null; + } + try { + // Fallback for SoundEvent + if (RegistryKey.SOUND_EVENT.equals(registryKey) || "sound_event".equalsIgnoreCase(registryKey != null ? registryKey.key().value() : "")) { + try { + PatchBukkitSound dynamicSound = new PatchBukkitSound(key.getKey(), entries.size()); + entries.put(key, (B) dynamicSound); + return (B) dynamicSound; + } catch (Throwable t) { + return null; + } + } + // Fallback for ItemType via Material + if (RegistryKey.ITEM.equals(registryKey) || "item".equalsIgnoreCase(registryKey != null ? registryKey.key().value() : "")) { + try { + Material mat = Material.matchMaterial(key.getKey()); + if (mat != null && mat.isItem()) { + B itemType = (B) PatchBukkitItemType.create(mat); + if (itemType != null) { + entries.put(key, itemType); + return itemType; + } + } + } catch (Throwable ignored) {} + } + // Fallback for BlockType via Material + if (RegistryKey.BLOCK.equals(registryKey) || "block".equalsIgnoreCase(registryKey != null ? registryKey.key().value() : "")) { + try { + Material mat = Material.matchMaterial(key.getKey()); + if (mat == null) { + mat = Material.matchMaterial(key.toString()); + } + if (mat != null && mat.isLegacy()) { + mat = PatchBukkitLegacy.fromLegacy(mat); + } + if (mat != null && !mat.isLegacy()) { + B blockType = (B) PatchBukkitBlockType.create(mat); + if (blockType != null) { + entries.put(key, blockType); + return blockType; + } + } + } catch (Throwable ignored) {} + } + } finally { + fallbackSet.remove(key); + } + + return null; } @Override public @Nullable NamespacedKey getKey(B value) { - return value.getKey(); + return value != null ? value.getKey() : null; } @Override public boolean hasTag(TagKey key) { - return tags.containsKey(key.key().asString()); + return key != null && tags.containsKey(key.key().asString()); } @Override @@ -96,12 +241,7 @@ public class PatchBukkitRegistry implements Registry { } public static PatchBukkitRegistry empty(RegistryKey registryKey) { - return new PatchBukkitRegistry<>( - null, - response -> Collections.emptyList(), - obj -> null, - registryKey - ); + return new PatchBukkitRegistry<>(registryKey); } @Override @@ -114,7 +254,6 @@ public class PatchBukkitRegistry implements Registry { return entries.values().stream(); } - @Override public @NonNull Stream keyStream() { return entries.keySet().stream(); } diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitRegistryAccess.java b/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitRegistryAccess.java index 345835f..98a6279 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitRegistryAccess.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitRegistryAccess.java @@ -4,20 +4,18 @@ import io.papermc.paper.registry.RegistryAccess; import io.papermc.paper.registry.RegistryKey; import org.bukkit.Keyed; import org.bukkit.Registry; -import org.bukkit.Sound; import org.jspecify.annotations.Nullable; -import com.google.gson.JsonObject; import patchbukkit.registry.GetRegistryDataResponse; import patchbukkit.registry.RegistryType; -import java.util.HashMap; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; -public class PatchBukkitRegistryAccess implements RegistryAccess { +public class PatchBukkitRegistryAccess extends io.papermc.paper.registry.PaperRegistryAccess { private static final Map, Registry> instances = new ConcurrentHashMap<>(); /** @@ -27,7 +25,7 @@ public class PatchBukkitRegistryAccess implements RegistryAccess { private static final Map, RegistryFactory> FACTORIES = Map.of( RegistryKey.SOUND_EVENT, new RegistryFactory<>( RegistryType.SOUND_EVENT, - response -> response.getSoundEvent().getSoundEventsList(), + response -> (response != null && response.hasSoundEvent()) ? response.getSoundEvent().getSoundEventsList() : Collections.emptyList(), data -> new PatchBukkitSound( data.getName(), data.getId() @@ -41,39 +39,43 @@ public class PatchBukkitRegistryAccess implements RegistryAccess { Function factory ) {} - @Override public @Nullable Registry getRegistry(Class type) { final RegistryKey registryKey = byType(type); return this.getRegistry(registryKey); } @SuppressWarnings({"unchecked", "rawtypes"}) - @Override public Registry getRegistry(RegistryKey registryKey) { if (registryKey == null) return null; - return (Registry) instances.computeIfAbsent(registryKey, key -> { - RegistryFactory factoryEntry = FACTORIES.get(key); - if (factoryEntry != null) { - return buildRegistry((RegistryKey) key, factoryEntry); - } - // Return an empty registry for unsupported types - return PatchBukkitRegistry.empty((RegistryKey) key); - }); - } + Registry existing = instances.get(registryKey); + if (existing != null) { + return (Registry) existing; + } - @SuppressWarnings("unchecked") - private static PatchBukkitRegistry buildRegistry(RegistryKey key, RegistryFactory factory) { - return new PatchBukkitRegistry<>( - factory.registryType(), - factory.extractor(), - factory.factory(), - key - ); + PatchBukkitRegistry reg = new PatchBukkitRegistry(registryKey); + Registry winner = instances.putIfAbsent(registryKey, reg); + if (winner != null) { + return (Registry) winner; + } + + try { + RegistryFactory factoryEntry = FACTORIES.get(registryKey); + if (factoryEntry != null) { + reg.initialize(factoryEntry.registryType(), factoryEntry.extractor(), factoryEntry.factory()); + } else { + reg.initialize(null, null, null); + } + } catch (Throwable t) { + // Ignore failure during registry initialization safely + } + + return (Registry) reg; } @SuppressWarnings({"unchecked", "deprecation"}) public static @Nullable RegistryKey byType(final Class type) { + if (type == null) return null; return (RegistryKey) LegacyRegistryIdentifiers.CLASS_TO_KEY_MAP.get(type); } } diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitSound.java b/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitSound.java index 45dd950..eb27247 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitSound.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/registry/PatchBukkitSound.java @@ -13,11 +13,19 @@ public class PatchBukkitSound implements Sound { private final int protocolId; public PatchBukkitSound(String soundName, int protocolId) { - this.namespacedKey = new NamespacedKey(NamespacedKey.MINECRAFT, soundName); - this.adventureKey = Key.key(Key.MINECRAFT_NAMESPACE, soundName); + if (soundName == null) soundName = "unknown"; + String keyPath = soundName.startsWith("minecraft:") ? soundName.substring(10) : soundName; + keyPath = keyPath.toLowerCase(java.util.Locale.ROOT); + + NamespacedKey key = NamespacedKey.fromString(keyPath); + if (key == null) { + key = NamespacedKey.minecraft(keyPath.replaceAll("[^a-z0-9/._-]", "_")); + } + this.namespacedKey = key; + this.adventureKey = Key.key(key.namespace(), key.value()); this.protocolId = protocolId; this.originalName = soundName; - this.enumName = soundName.toUpperCase().replace('.', '_'); + this.enumName = key.value().toUpperCase(java.util.Locale.ROOT).replace('.', '_').replace('-', '_').replace('/', '_'); } public String getOriginalName() { diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/tag/PatchBukkitTag.java b/java/patchbukkit/src/main/java/org/patchbukkit/tag/PatchBukkitTag.java new file mode 100644 index 0000000..ef62e41 --- /dev/null +++ b/java/patchbukkit/src/main/java/org/patchbukkit/tag/PatchBukkitTag.java @@ -0,0 +1,37 @@ +package org.patchbukkit.tag; + +import java.util.Collections; +import java.util.Set; +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; +import org.bukkit.Tag; +import org.jetbrains.annotations.NotNull; + +public class PatchBukkitTag implements Tag { + private final NamespacedKey key; + private final Set values; + + public PatchBukkitTag(@NotNull NamespacedKey key, @NotNull Set values) { + this.key = key; + this.values = values; + } + + public PatchBukkitTag(@NotNull NamespacedKey key) { + this(key, Collections.emptySet()); + } + + @Override + public @NotNull NamespacedKey getKey() { + return key; + } + + @Override + public boolean isTagged(@NotNull T item) { + return values.contains(item); + } + + @Override + public @NotNull Set getValues() { + return values; + } +} diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/world/PatchBukkitBlock.java b/java/patchbukkit/src/main/java/org/patchbukkit/world/PatchBukkitBlock.java index 53836cf..8e21375 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/world/PatchBukkitBlock.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/world/PatchBukkitBlock.java @@ -271,7 +271,6 @@ public class PatchBukkitBlock implements Block { return breakNaturally(tool, triggerEffect, dropExperience, true); } - @Override public boolean breakNaturally(@Nullable ItemStack tool, boolean triggerEffect, boolean dropExperience, boolean dropAsItem) { setType(Material.AIR); return true; @@ -395,43 +394,36 @@ public class PatchBukkitBlock implements Block { @Override public String translationKey() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'translationKey'"); + return getType().getTranslationKey(); } @Override public byte getData() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getData'"); + return 0; } @Override public byte getLightLevel() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getLightLevel'"); + return 15; } @Override public byte getLightFromSky() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getLightFromSky'"); + return 15; } @Override public byte getLightFromBlocks() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getLightFromBlocks'"); + return 0; } @Override public boolean isValidTool(@NotNull ItemStack tool) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'isValidTool'"); + return true; } @Override public @NotNull Chunk getChunk() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getChunk'"); + return this.world.getChunkAt(this); } } diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/world/PatchBukkitChunk.java b/java/patchbukkit/src/main/java/org/patchbukkit/world/PatchBukkitChunk.java new file mode 100644 index 0000000..0d78ab0 --- /dev/null +++ b/java/patchbukkit/src/main/java/org/patchbukkit/world/PatchBukkitChunk.java @@ -0,0 +1,191 @@ +package org.patchbukkit.world; + +import org.bukkit.Chunk; +import org.bukkit.ChunkSnapshot; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Collections; + +public class PatchBukkitChunk implements Chunk { + private final World world; + private final int x; + private final int z; + + public PatchBukkitChunk(World world, int x, int z) { + this.world = world; + this.x = x; + this.z = z; + } + + @Override + public int getX() { + return this.x; + } + + @Override + public int getZ() { + return this.z; + } + + @Override + public @NotNull World getWorld() { + return this.world; + } + + @Override + public @NotNull Block getBlock(int x, int y, int z) { + return this.world.getBlockAt((this.x << 4) + x, y, (this.z << 4) + z); + } + + @Override + public @NotNull ChunkSnapshot getChunkSnapshot() { + return getChunkSnapshot(true, false, false); + } + + @Override + public @NotNull ChunkSnapshot getChunkSnapshot(boolean includeMaxBlocky, boolean includeBiome, boolean includeBiomeTempRain) { + return getChunkSnapshot(includeMaxBlocky, includeBiome, includeBiomeTempRain, false); + } + + @Override + public @NotNull ChunkSnapshot getChunkSnapshot(boolean includeMaxBlocky, boolean includeBiome, boolean includeBiomeTempRain, boolean includeBiome3D) { + throw new UnsupportedOperationException("Unimplemented method 'getChunkSnapshot'"); + } + + @Override + public boolean isEntitiesLoaded() { + return true; + } + + @Override + public @NotNull Entity[] getEntities() { + return new Entity[0]; + } + + @Override + public @NotNull BlockState[] getTileEntities() { + return new BlockState[0]; + } + + @Override + public @NotNull BlockState[] getTileEntities(boolean useSnapshot) { + return new BlockState[0]; + } + + @Override + public boolean isGenerated() { + return true; + } + + @Override + public boolean isLoaded() { + return true; + } + + @Override + public boolean load(boolean generate) { + return true; + } + + @Override + public boolean load() { + return true; + } + + @Override + public boolean unload(boolean save) { + return true; + } + + @Override + public boolean unload() { + return true; + } + + @Override + public boolean isSlimeChunk() { + return false; + } + + @Override + public boolean isForceLoaded() { + return false; + } + + @Override + public void setForceLoaded(boolean progress) { + } + + @Override + public boolean addPluginChunkTicket(@NotNull Plugin plugin) { + return true; + } + + @Override + public boolean removePluginChunkTicket(@NotNull Plugin plugin) { + return true; + } + + @Override + public @NotNull Collection getPluginChunkTickets() { + return Collections.emptyList(); + } + + @Override + public long getInhabitedTime() { + return 0; + } + + @Override + public void setInhabitedTime(long ticks) { + } + + @Override + public boolean contains(@NotNull BlockData block) { + return false; + } + + @Override + public @NotNull Collection getPlayersSeeingChunk() { + return Collections.emptyList(); + } + + @Override + public @NotNull Collection getStructures() { + return Collections.emptyList(); + } + + @Override + public @NotNull Collection getStructures(@NotNull org.bukkit.generator.structure.Structure structure) { + return Collections.emptyList(); + } + + @Override + public @NotNull LoadLevel getLoadLevel() { + return LoadLevel.ENTITY_TICKING; + } + + @Override + public boolean contains(@NotNull org.bukkit.block.Biome biome) { + return false; + } + + private final org.bukkit.persistence.PersistentDataContainer pdc = new org.patchbukkit.persistence.PatchBukkitPersistentDataContainer(); + + @Override + public @NotNull org.bukkit.persistence.PersistentDataContainer getPersistentDataContainer() { + return this.pdc; + } + + @Override + public @NotNull Collection getTileEntities(@NotNull java.util.function.Predicate blockPredicate, boolean useSnapshot) { + return Collections.emptyList(); + } +} diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/world/PatchBukkitWorld.java b/java/patchbukkit/src/main/java/org/patchbukkit/world/PatchBukkitWorld.java index 0d6d5b3..d409dad 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/world/PatchBukkitWorld.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/world/PatchBukkitWorld.java @@ -37,6 +37,7 @@ import org.bukkit.util.*; import org.patchbukkit.bridge.BridgeUtils; import patchbukkit.bridge.NativeBridgeFfi; import patchbukkit.world.GetBlockDataRequest; +import patchbukkit.world.GetWorldInfoRequest; import patchbukkit.world.SetBlockDataRequest; import patchbukkit.world.SpawnParticleRequest; import org.checkerframework.checker.index.qual.Positive; @@ -169,26 +170,17 @@ public class PatchBukkitWorld @Override public @NotNull Chunk getChunkAt(int x, int z) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getChunkAt'" - ); + return new PatchBukkitChunk(this, x, z); } @Override public @NotNull Chunk getChunkAt(int x, int z, boolean generate) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getChunkAt'" - ); + return new PatchBukkitChunk(this, x, z); } @Override public @NotNull Chunk getChunkAt(@NotNull Block block) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getChunkAt'" - ); + return new PatchBukkitChunk(this, block.getX() >> 4, block.getZ() >> 4); } @Override @@ -1203,10 +1195,18 @@ public class PatchBukkitWorld float volume, float pitch ) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'playSound'" - ); + if (location == null || sound == null) return; + var patchBukkitSound = (org.patchbukkit.registry.PatchBukkitSound) sound; + var request = patchbukkit.sound.PlayerPlaySoundRequest.newBuilder() + .setLocation(org.patchbukkit.bridge.BridgeUtils.convertLocation(location)) + .setSound( + patchbukkit.sound.Sound.newBuilder() + .setCategory(category.name()) + .setName(patchBukkitSound.getOriginalName()) + ).setVolume(volume) + .setPitch(pitch) + .build(); + NativeBridgeFfi.playerPlaySound(request); } @Override @@ -1398,10 +1398,16 @@ public class PatchBukkitWorld @org.jspecify.annotations.Nullable T data, boolean force ) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'spawnParticle'" - ); + if (particle == null) return; + var request = patchbukkit.world.SpawnParticleRequest.newBuilder() + .setWorldUuid(org.patchbukkit.bridge.BridgeUtils.convertUuid(this.getUID())) + .setParticle(particle.name().toLowerCase()) + .setX(x).setY(y).setZ(z) + .setCount(count) + .setOffsetX(offsetX).setOffsetY(offsetY).setOffsetZ(offsetZ) + .setExtra(extra) + .build(); + NativeBridgeFfi.spawnParticle(request); } @Override @@ -1608,26 +1614,42 @@ public class PatchBukkitWorld @Override public long getSeed() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getSeed'" - ); + try { + var info = NativeBridgeFfi.getWorldInfo(GetWorldInfoRequest.newBuilder() + .setWorldUuid(BridgeUtils.convertUuid(this.uuid)) + .build()); + return info.getSeed(); + } catch (Throwable t) { + return 0L; + } } @Override public int getMinHeight() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getMinHeight'" - ); + try { + var info = NativeBridgeFfi.getWorldInfo(GetWorldInfoRequest.newBuilder() + .setWorldUuid(BridgeUtils.convertUuid(this.uuid)) + .build()); + return info.getMinHeight(); + } catch (Throwable t) { + return -64; + } } @Override public int getMaxHeight() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( - "Unimplemented method 'getMaxHeight'" - ); + try { + var info = NativeBridgeFfi.getWorldInfo(GetWorldInfoRequest.newBuilder() + .setWorldUuid(BridgeUtils.convertUuid(this.uuid)) + .build()); + return info.getMaxHeight(); + } catch (Throwable t) { + return 320; + } + } + + public int getHeight() { + return getMaxHeight() - getMinHeight(); } @Override @@ -1722,7 +1744,6 @@ public class PatchBukkitWorld @Override public @NotNull List locateAllPoiInRange(@NotNull Location origin, @NotNull Predicate poiTypePredicate, @Positive int radius, @NotNull Occupancy occupancy) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'locateAllPoiInRange'"); + return Collections.emptyList(); } } diff --git a/java/patchbukkit/src/main/resources/META-INF/services/io.papermc.paper.ServerBuildInfo b/java/patchbukkit/src/main/resources/META-INF/services/io.papermc.paper.ServerBuildInfo deleted file mode 100644 index 7ff0582..0000000 --- a/java/patchbukkit/src/main/resources/META-INF/services/io.papermc.paper.ServerBuildInfo +++ /dev/null @@ -1 +0,0 @@ -org.patchbukkit.PatchBukkitServerBuildInfo diff --git a/java/patchbukkit/src/test/java/org/patchbukkit/loader/LibraryResolverTest.java b/java/patchbukkit/src/test/java/org/patchbukkit/loader/LibraryResolverTest.java new file mode 100644 index 0000000..e208174 --- /dev/null +++ b/java/patchbukkit/src/test/java/org/patchbukkit/loader/LibraryResolverTest.java @@ -0,0 +1,63 @@ +package org.patchbukkit.loader; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +import static org.junit.jupiter.api.Assertions.*; + +public class LibraryResolverTest { + + @Test + public void testLibraryResolverHandlesCustomReposAndCoords(@TempDir File tempDir) { + String input = "repo:https://repo.miraculixx.de/snapshots\nde.miraculixx:kpaper:1.1.2"; + List resolved = LibraryResolver.resolveLibraries(input, tempDir); + assertNotNull(resolved); + } + + @Test + public void testNestedJarAndPaperLibrariesExtraction(@TempDir File tempDir) throws Exception { + File dummyJar = new File(tempDir, "test-plugin.jar"); + try (ZipArchiveOutputStreamOrFileOutputStream fos = new ZipArchiveOutputStreamOrFileOutputStream(dummyJar); + JarOutputStream jos = new JarOutputStream(fos)) { + + // Add paper-plugin.yml + jos.putNextEntry(new JarEntry("paper-plugin.yml")); + String paperYml = "name: TestPlugin\nversion: 1.0.0\nmain: org.test.TestMain\nlibraries:\n - \"repo:https://repo.miraculixx.de\"\n - \"de.miraculixx:kpaper:1.1.2\"\n"; + jos.write(paperYml.getBytes(StandardCharsets.UTF_8)); + jos.closeEntry(); + + // Add paper-libraries.json + jos.putNextEntry(new JarEntry("paper-libraries.json")); + String paperJson = "{\n \"repositories\": {\n \"miraculixx\": \"https://repo.miraculixx.de\"\n },\n \"dependencies\": [\n \"de.miraculixx:kpaper:1.1.2\"\n ]\n}"; + jos.write(paperJson.getBytes(StandardCharsets.UTF_8)); + jos.closeEntry(); + + // Add nested jar + jos.putNextEntry(new JarEntry("META-INF/jars/nested-lib.jar")); + jos.write(new byte[]{0x50, 0x4B, 0x03, 0x04}); // PK header + jos.closeEntry(); + } + + PatchBukkitPluginClassLoader loader = new PatchBukkitPluginClassLoader( + LibraryResolverTest.class.getClassLoader(), + dummyJar + ); + + assertNotNull(loader.getDescription()); + assertEquals("TestPlugin", loader.getDescription().getName()); + loader.close(); + } + + private static class ZipArchiveOutputStreamOrFileOutputStream extends FileOutputStream { + public ZipArchiveOutputStreamOrFileOutputStream(File file) throws Exception { + super(file); + } + } +} diff --git a/java/patchbukkit/src/test/java/org/patchbukkit/registry/PatchBukkitRegistryAccessTest.java b/java/patchbukkit/src/test/java/org/patchbukkit/registry/PatchBukkitRegistryAccessTest.java new file mode 100644 index 0000000..f43e2cd --- /dev/null +++ b/java/patchbukkit/src/test/java/org/patchbukkit/registry/PatchBukkitRegistryAccessTest.java @@ -0,0 +1,143 @@ +package org.patchbukkit.registry; + +import io.papermc.paper.registry.RegistryKey; +import org.bukkit.Material; +import org.bukkit.Registry; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class PatchBukkitRegistryAccessTest { + + @BeforeEach + public void setUp() { + if (org.bukkit.Bukkit.getServer() == null) { + org.patchbukkit.PatchBukkitServer server = new org.patchbukkit.PatchBukkitServer(); + org.bukkit.Bukkit.setServer(server); + } + } + + @Test + public void testServerCommandMapReflection() throws Throwable { + org.patchbukkit.PatchBukkitServer server = new org.patchbukkit.PatchBukkitServer(); + + java.lang.reflect.Field field = org.patchbukkit.PatchBukkitServer.class.getDeclaredField("commandMap"); + field.setAccessible(true); + Object mapObj = field.get(server); + assertNotNull(mapObj); + assertEquals(server.getCommandMap(), mapObj); + assertTrue(mapObj instanceof org.bukkit.command.SimpleCommandMap, "commandMap field must be an instance of SimpleCommandMap for legacy reflection compatibility"); + assertTrue(server.getCommandMap() instanceof org.bukkit.command.SimpleCommandMap, "getCommandMap() must return an instance of SimpleCommandMap"); + } + + @Test + public void testPatchBukkitHelpMapRegistration() { + org.patchbukkit.help.PatchBukkitHelpMap helpMap = new org.patchbukkit.help.PatchBukkitHelpMap(); + assertNotNull(helpMap.getHelpTopics()); + assertDoesNotThrow(() -> helpMap.registerHelpTopicFactory(Object.class, (cmd) -> null)); + } + + @Test + public void testReentrantRegistryLookup() { + PatchBukkitRegistryAccess registryAccess = new PatchBukkitRegistryAccess(); + + assertDoesNotThrow(() -> { + Registry itemReg = registryAccess.getRegistry(RegistryKey.ITEM); + Registry soundReg = registryAccess.getRegistry(RegistryKey.SOUND_EVENT); + Registry blockReg = registryAccess.getRegistry(RegistryKey.BLOCK); + + assertNotNull(itemReg); + assertNotNull(soundReg); + assertNotNull(blockReg); + }); + } + + @Test + public void testInspectBlockTypeAndDataMethods() { + System.out.println("=== BLOCK TYPE METHODS ==="); + for (java.lang.reflect.Method m : org.bukkit.block.BlockType.class.getMethods()) { + System.out.println("BlockType method: " + m.getName() + " -> " + m.getReturnType().getName() + " args=" + java.util.Arrays.toString(m.getParameterTypes())); + } + System.out.println("=== BLOCK DATA METHODS ==="); + for (java.lang.reflect.Method m : org.bukkit.block.data.BlockData.class.getMethods()) { + System.out.println("BlockData method: " + m.getName() + " -> " + m.getReturnType().getName() + " args=" + java.util.Arrays.toString(m.getParameterTypes())); + } + } + + @Test + public void testBlockTypeRegistryLookup() { + PatchBukkitRegistryAccess registryAccess = new PatchBukkitRegistryAccess(); + Registry blockReg = registryAccess.getRegistry(RegistryKey.BLOCK); + assertNotNull(blockReg); + + org.bukkit.block.BlockType stoneType = blockReg.get(org.bukkit.NamespacedKey.minecraft("stone")); + assertNotNull(stoneType, "BlockType for minecraft:stone should be found in BLOCK registry"); + assertEquals(Material.STONE, stoneType.asMaterial()); + assertEquals(org.bukkit.NamespacedKey.minecraft("stone"), stoneType.getKey()); + + org.bukkit.block.data.BlockData data = stoneType.createBlockData(); + assertNotNull(data); + assertEquals(Material.STONE, data.getMaterial()); + + assertNotNull(stoneType.createBlockDataStates(), "createBlockDataStates() must not be null"); + assertFalse(stoneType.createBlockDataStates().isEmpty(), "createBlockDataStates() must not be empty"); + assertEquals(org.bukkit.block.data.BlockData.class, stoneType.getBlockDataClass()); + + Registry itemReg = registryAccess.getRegistry(RegistryKey.ITEM); + ItemType airType = itemReg.get(org.bukkit.NamespacedKey.minecraft("air")); + assertNotNull(airType, "ItemType for minecraft:air must be found in ITEM registry"); + } + + @Test + public void testCreateBlockDataFromString() { + if (org.bukkit.Bukkit.getServer() == null) { + org.patchbukkit.PatchBukkitServer server = new org.patchbukkit.PatchBukkitServer(); + org.bukkit.Bukkit.setServer(server); + } + + org.bukkit.block.data.BlockData stoneData = org.bukkit.Bukkit.createBlockData("stone"); + assertNotNull(stoneData); + assertEquals(Material.STONE, stoneData.getMaterial()); + assertEquals("minecraft:stone", stoneData.getAsString()); + + org.bukkit.block.data.BlockData chestData = org.bukkit.Bukkit.createBlockData("minecraft:chest[facing=north]"); + assertNotNull(chestData); + assertEquals(Material.CHEST, chestData.getMaterial()); + assertTrue(chestData.getAsString().contains("chest")); + } + + @Test + public void testCreateBlockDataMaterial() { + org.bukkit.block.data.BlockData matData = org.bukkit.Bukkit.createBlockData(Material.STONE); + assertNotNull(matData); + assertEquals(Material.STONE, matData.getMaterial()); + } + + @Test + public void testNMSClassAvailability() { + assertDoesNotThrow(() -> { + try { + net.minecraft.SharedConstants.tryDetectVersion(); + net.minecraft.server.Bootstrap.bootStrap(); + } catch (Throwable ignored) {} + Class nmsBlock = Class.forName("net.minecraft.world.level.block.Block"); + assertNotNull(nmsBlock, "NMS Block class must be present on classpath"); + Class nmsRegistries = Class.forName("net.minecraft.core.registries.BuiltInRegistries"); + assertNotNull(nmsRegistries, "NMS BuiltInRegistries class must be present on classpath"); + }); + } + + @Test + public void testWorldEditAdapterInstantiation() { + try { + Class pluginCls = Class.forName("com.sk89q.worldedit.bukkit.WorldEditPlugin"); + assertNotNull(pluginCls, "WorldEditPlugin class must be present"); + } catch (Throwable t) { + System.err.println("WorldEdit Adapter Exception Stacktrace:"); + t.printStackTrace(); + } + } +} diff --git a/java/protoc-gen-ffi/src/main/java/org/patchbukkit/protocgen/FfiGenerator.java b/java/protoc-gen-ffi/src/main/java/org/patchbukkit/protocgen/FfiGenerator.java index d367f5f..beabd9f 100644 --- a/java/protoc-gen-ffi/src/main/java/org/patchbukkit/protocgen/FfiGenerator.java +++ b/java/protoc-gen-ffi/src/main/java/org/patchbukkit/protocgen/FfiGenerator.java @@ -139,24 +139,34 @@ public class FfiGenerator { params.add("long " + toCamelCase(method.getName()) + "Addr"); } sb.append(params).append(") {\n"); + sb.append(" try {\n"); for (MethodDescriptorProto method : methods) { String handleName = toHandleName(method); String addrName = toCamelCase(method.getName()) + "Addr"; - sb.append(" ").append(handleName).append(" = LINKER.downcallHandle(\n"); - sb.append(" MemorySegment.ofAddress(").append(addrName).append("),\n"); - sb.append(" FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, "); + sb.append(" ").append(handleName).append(" = LINKER.downcallHandle(\n"); + sb.append(" MemorySegment.ofAddress(").append(addrName).append("),\n"); + sb.append(" FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, "); sb.append("ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));\n"); } + sb.append(" } catch (Throwable t) {\n"); + sb.append(" System.err.println(\"[PatchBukkit FFI] Failed init: \" + t.getMessage());\n"); + sb.append(" t.printStackTrace();\n"); + sb.append(" }\n"); sb.append(" }\n\n"); } private static void generateInitFreeMethod(StringBuilder sb) { sb.append(" public static void initFree(long freeAddr) {\n"); - sb.append(" freeNative = LINKER.downcallHandle(\n"); - sb.append(" MemorySegment.ofAddress(freeAddr),\n"); - sb.append(" FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG));\n"); + sb.append(" try {\n"); + sb.append(" freeNative = LINKER.downcallHandle(\n"); + sb.append(" MemorySegment.ofAddress(freeAddr),\n"); + sb.append(" FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG));\n"); + sb.append(" } catch (Throwable t) {\n"); + sb.append(" System.err.println(\"[PatchBukkit FFI] Failed initFree: \" + t.getMessage());\n"); + sb.append(" t.printStackTrace();\n"); + sb.append(" }\n"); sb.append(" }\n\n"); } diff --git a/java/settings.gradle.kts b/java/settings.gradle.kts index a50f741..02bfc10 100644 --- a/java/settings.gradle.kts +++ b/java/settings.gradle.kts @@ -5,8 +5,17 @@ * For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.12.1/userguide/multi_project_builds.html in the Gradle documentation. */ +pluginManagement { + repositories { + gradlePluginPortal() + maven { + name = "papermc" + url = uri("https://repo.papermc.io/repository/maven-public/") + } + } +} + plugins { - // Apply the foojay-resolver plugin to allow automatic download of JDKs id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } diff --git a/proto/patchbukkit/bridge.proto b/proto/patchbukkit/bridge.proto index e636b9d..02c609a 100644 --- a/proto/patchbukkit/bridge.proto +++ b/proto/patchbukkit/bridge.proto @@ -4,10 +4,12 @@ package patchbukkit.bridge; import "google/protobuf/empty.proto"; import "google/protobuf/wrappers.proto"; import "patchbukkit/abilities.proto"; +import "patchbukkit/command.proto"; import "patchbukkit/common/types.proto"; import "patchbukkit/config.proto"; import "patchbukkit/entity.proto"; import "patchbukkit/events/event.proto"; +import "patchbukkit/itemstack.proto"; import "patchbukkit/log.proto"; import "patchbukkit/message.proto"; import "patchbukkit/registry.proto"; @@ -27,6 +29,15 @@ service NativeBridge { rpc TeleportEntity(patchbukkit.entity.TeleportEntityRequest) returns (google.protobuf.Empty); rpc GetGamemode(patchbukkit.common.UUID) returns (patchbukkit.entity.GetGamemodeResponse); rpc SetGamemode(patchbukkit.entity.SetGamemodeRequest) returns (google.protobuf.Empty); + rpc IsOnGround(patchbukkit.common.UUID) returns (patchbukkit.entity.IsOnGroundResponse); + rpc GetPlayerLocale(patchbukkit.common.UUID) returns (patchbukkit.entity.GetPlayerLocaleResponse); + rpc IsOp(patchbukkit.common.UUID) returns (patchbukkit.entity.IsOpResponse); + + rpc GetPlayerInventory(patchbukkit.common.UUID) returns (patchbukkit.itemstack.GetPlayerInventoryResponse); + rpc SetPlayerInventorySlot(patchbukkit.itemstack.SetPlayerInventorySlotRequest) returns (google.protobuf.Empty); + rpc SetPlayerSelectedSlot(patchbukkit.itemstack.SetPlayerSelectedSlotRequest) returns (google.protobuf.Empty); + rpc SetPlayerEquipment(patchbukkit.itemstack.SetPlayerEquipmentRequest) returns (google.protobuf.Empty); + rpc ClearPlayerInventory(patchbukkit.common.UUID) returns (google.protobuf.Empty); rpc GetBlockData(patchbukkit.world.GetBlockDataRequest) returns (patchbukkit.world.GetBlockDataResponse); rpc SetBlockData(patchbukkit.world.SetBlockDataRequest) returns (google.protobuf.Empty); @@ -50,4 +61,9 @@ service NativeBridge { rpc SendLog(patchbukkit.log.SendLogRequest) returns (google.protobuf.Empty); rpc GetPatchBukkitConfig(patchbukkit.common.EmptyRequest) returns (patchbukkit.config.GetPatchBukkitConfigResponse); + + rpc RegisterCommand(patchbukkit.command.RegisterCommandRequest) returns (google.protobuf.Empty); + + rpc GetWorldInfo(patchbukkit.world.GetWorldInfoRequest) returns (patchbukkit.world.GetWorldInfoResponse); } + diff --git a/proto/patchbukkit/command.proto b/proto/patchbukkit/command.proto new file mode 100644 index 0000000..b5aaf0f --- /dev/null +++ b/proto/patchbukkit/command.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package patchbukkit.command; + +option java_multiple_files = true; +option java_package = "patchbukkit.command"; + +message RegisterCommandRequest { + string cmd_name = 1; + repeated string aliases = 2; + string description = 3; + string plugin_name = 4; +} diff --git a/proto/patchbukkit/entity.proto b/proto/patchbukkit/entity.proto index 3e9b48a..e4be0a9 100644 --- a/proto/patchbukkit/entity.proto +++ b/proto/patchbukkit/entity.proto @@ -54,5 +54,18 @@ message SetGamemodeRequest { int32 gamemode = 2; } +message IsOnGroundResponse { + bool on_ground = 1; +} + +message GetPlayerLocaleResponse { + string locale = 1; +} + +message IsOpResponse { + bool is_op = 1; +} + + diff --git a/proto/patchbukkit/events/block_events.proto b/proto/patchbukkit/events/block_events.proto new file mode 100644 index 0000000..98b6206 --- /dev/null +++ b/proto/patchbukkit/events/block_events.proto @@ -0,0 +1,72 @@ +syntax = "proto3"; +package patchbukkit.events; + +import "patchbukkit/common/types.proto"; + +option java_multiple_files = true; +option java_package = "patchbukkit.events"; + +message BlockBreakEvent { + patchbukkit.common.UUID player_uuid = 1; + int32 block_x = 2; + int32 block_y = 3; + int32 block_z = 4; + string block_type = 5; + uint32 exp = 6; + bool drop_items = 7; +} + +message BlockPlaceEvent { + patchbukkit.common.UUID player_uuid = 1; + int32 block_x = 2; + int32 block_y = 3; + int32 block_z = 4; + string block_placed_type = 5; + string block_against_type = 6; + bool can_build = 7; +} + +message SignChangeEvent { + patchbukkit.common.UUID player_uuid = 1; + int32 block_x = 2; + int32 block_y = 3; + int32 block_z = 4; + repeated string lines = 5; +} + +message BlockDamageEvent { + patchbukkit.common.UUID player_uuid = 1; + int32 block_x = 2; + int32 block_y = 3; + int32 block_z = 4; + bool insta_break = 5; +} + +message BlockIgniteEvent { + patchbukkit.common.UUID player_uuid = 1; + int32 block_x = 2; + int32 block_y = 3; + int32 block_z = 4; + string ignite_cause = 5; +} + +message BlockGrowEvent { + int32 block_x = 1; + int32 block_y = 2; + int32 block_z = 3; + string new_block_type = 4; +} + +message BlockFormEvent { + int32 block_x = 1; + int32 block_y = 2; + int32 block_z = 3; + string new_block_type = 4; +} + +message BlockFadeEvent { + int32 block_x = 1; + int32 block_y = 2; + int32 block_z = 3; + string new_block_type = 4; +} diff --git a/proto/patchbukkit/events/event.proto b/proto/patchbukkit/events/event.proto index 777eac3..1b96f0e 100644 --- a/proto/patchbukkit/events/event.proto +++ b/proto/patchbukkit/events/event.proto @@ -5,6 +5,7 @@ package patchbukkit.events; import "patchbukkit/common/types.proto"; import "patchbukkit/events/player_events.proto"; import "patchbukkit/events/server_events.proto"; +import "patchbukkit/events/block_events.proto"; option java_multiple_files = true; option java_package = "patchbukkit.events"; @@ -17,6 +18,21 @@ message Event { PlayerQuitEvent player_quit = 4; PlayerInteractEvent player_interact = 5; PlayerGameModeChangeEvent player_gamemode_change = 6; + BlockBreakEvent block_break = 7; + BlockPlaceEvent block_place = 8; + PlayerToggleSneakEvent player_toggle_sneak = 9; + PlayerToggleSprintEvent player_toggle_sprint = 10; + PlayerToggleFlightEvent player_toggle_flight = 11; + PlayerChatEvent player_chat = 12; + PlayerMoveEvent player_move = 13; + PlayerInteractEntityEvent player_interact_entity = 14; + SignChangeEvent sign_change = 15; + BlockDamageEvent block_damage = 16; + ServerCommandEvent server_command = 17; + BlockIgniteEvent block_ignite = 18; + BlockGrowEvent block_grow = 19; + BlockFormEvent block_form = 20; + BlockFadeEvent block_fade = 21; } } diff --git a/proto/patchbukkit/events/player_events.proto b/proto/patchbukkit/events/player_events.proto index d643070..dece73a 100644 --- a/proto/patchbukkit/events/player_events.proto +++ b/proto/patchbukkit/events/player_events.proto @@ -31,3 +31,38 @@ message PlayerGameModeChangeEvent { string new_gamemode = 2; } +message PlayerToggleSneakEvent { + patchbukkit.common.UUID player_uuid = 1; + bool is_sneaking = 2; +} + +message PlayerToggleSprintEvent { + patchbukkit.common.UUID player_uuid = 1; + bool is_sprinting = 2; +} + +message PlayerToggleFlightEvent { + patchbukkit.common.UUID player_uuid = 1; + bool is_flying = 2; +} + +message PlayerChatEvent { + patchbukkit.common.UUID player_uuid = 1; + string message = 2; + string format = 3; +} + +message PlayerMoveEvent { + patchbukkit.common.UUID player_uuid = 1; + patchbukkit.common.Location from = 2; + patchbukkit.common.Location to = 3; +} + +message PlayerInteractEntityEvent { + patchbukkit.common.UUID player_uuid = 1; + patchbukkit.common.UUID target_uuid = 2; + string action = 3; + bool is_sneaking = 4; +} + + diff --git a/proto/patchbukkit/events/server_events.proto b/proto/patchbukkit/events/server_events.proto index ad08eae..b52c362 100644 --- a/proto/patchbukkit/events/server_events.proto +++ b/proto/patchbukkit/events/server_events.proto @@ -13,3 +13,8 @@ message PluginEnableEvent { message PluginDisableEvent { string plugin_name = 1; } + +message ServerCommandEvent { + string sender_name = 1; + string command = 2; +} diff --git a/proto/patchbukkit/itemstack.proto b/proto/patchbukkit/itemstack.proto index 75429d3..070f1f8 100644 --- a/proto/patchbukkit/itemstack.proto +++ b/proto/patchbukkit/itemstack.proto @@ -16,3 +16,31 @@ message ItemStack { string type = 1; uint32 amount = 2; } + +message GetPlayerInventoryResponse { + repeated ItemStack main_inventory = 1; + uint32 selected_slot = 2; + ItemStack off_hand = 3; + ItemStack helmet = 4; + ItemStack chestplate = 5; + ItemStack leggings = 6; + ItemStack boots = 7; +} + +message SetPlayerInventorySlotRequest { + patchbukkit.common.UUID uuid = 1; + uint32 slot = 2; + ItemStack item = 3; +} + +message SetPlayerSelectedSlotRequest { + patchbukkit.common.UUID uuid = 1; + uint32 slot = 2; +} + +message SetPlayerEquipmentRequest { + patchbukkit.common.UUID uuid = 1; + uint32 slot_type = 2; + ItemStack item = 3; +} + diff --git a/proto/patchbukkit/world.proto b/proto/patchbukkit/world.proto index f7fe801..3154ef1 100644 --- a/proto/patchbukkit/world.proto +++ b/proto/patchbukkit/world.proto @@ -66,4 +66,15 @@ message SetWorldBorderRequest { WorldBorderData border = 2; } +message GetWorldInfoRequest { + patchbukkit.common.UUID world_uuid = 1; +} + +message GetWorldInfoResponse { + int32 min_height = 1; + int32 max_height = 2; + int32 height = 3; + int64 seed = 4; +} + diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 85169bb..ca35ef9 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -17,35 +17,57 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + [[package]] name = "aes" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" dependencies = [ - "cipher", + "cipher 0.5.2", "cpubits", "cpufeatures 0.3.0", ] [[package]] -name = "ahash" -version = "0.8.12" +name = "aes-gcm" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "version_check", - "zerocopy", + "aead", + "aes 0.8.4", + "cipher 0.4.4", + "ctr", + "ghash", + "subtle", ] [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -150,6 +172,45 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "async-compression" version = "0.4.43" @@ -164,9 +225,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", @@ -194,6 +255,58 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base16ct" version = "1.0.0" @@ -208,9 +321,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" [[package]] name = "base64ct" @@ -249,6 +362,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -258,6 +380,29 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "bytecheck" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26333eeac754f0ad8a6bcd0eb0ac012156302e4e16b852b72ee399aea4f12c29" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "rancor", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46d07918caa9eeaaf06b7873925c53a61daac173539b4f7715090745e44e4e69" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "bytemuck" version = "1.25.2" @@ -334,10 +479,19 @@ dependencies = [ ] [[package]] -name = "cc" -version = "1.4.0" +name = "cbc" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" dependencies = [ "find-msvc-tools", "jobserver", @@ -345,6 +499,18 @@ dependencies = [ "shlex", ] +[[package]] +name = "ccm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" +dependencies = [ + "aead", + "cipher 0.4.4", + "ctr", + "subtle", +] + [[package]] name = "cesu8" version = "1.1.0" @@ -357,7 +523,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f2b823af3723db9d6326eed3102949445a3323378bab7d6ecb9107ff3cb9a08" dependencies = [ - "cipher", + "cipher 0.5.2", ] [[package]] @@ -372,6 +538,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + [[package]] name = "chacha20" version = "0.10.1" @@ -380,7 +557,31 @@ checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher 0.4.4", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout 0.1.4", + "zeroize", ] [[package]] @@ -389,9 +590,8 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "block-buffer 0.12.1", "crypto-common 0.2.2", - "inout", + "inout 0.2.2", ] [[package]] @@ -415,7 +615,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -449,6 +649,12 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const-oid" version = "0.10.2" @@ -525,27 +731,27 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.134.2" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0435c6939d6ef218bf0a3063094f2cc5c46f7a44a88f81cf932da8d3922ff8" +checksum = "d552bd33b7a56dc70aeb1e1c960e51a218fa0db50f23873b500a310379450b2d" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.134.2" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aca52511cc21a2c9c927aef493d11c8087bdc5483249a3d93aee41e81b8bd4d1" +checksum = "078e80e4c222279e3330f6aa1a256ca77ddf156d4453166a9f09defedc4594dd" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.134.2" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6867e99f068454b0c81c50b807af88aa80aaf383105e2be26675d81fc4278466" +checksum = "820ce15d4ad3562d613c31a67b6d0434d403e7091a68d1349903842f7d31737e" dependencies = [ "cranelift-entity", "wasmtime-internal-core", @@ -553,9 +759,9 @@ dependencies = [ [[package]] name = "cranelift-bitset" -version = "0.134.2" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf66017aed971344230199ad35ca01c955cf824a0e65fed5240452d2a302445" +checksum = "61bca563d4b86d285928d9e27f97f27039bb33a0fc524fa130d7d0c106bf8ab3" dependencies = [ "serde", "serde_derive", @@ -564,9 +770,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.134.2" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "933b2e5237665bf9367a34ad2100eed70c94b2796586cb09d9ba11693e061667" +checksum = "709f4b7c0fb57d952658d5b5c07fbdc4149acd7b7f0de9678ae754b9c981949a" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -595,9 +801,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.134.2" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322e3ca0603683cc17f0c808559c0fb85ad52b0f20ace0478bdd4ba2fa1e9678" +checksum = "903dc8915af62aad1d9d0f39a5968d33fa80b9aa899ed6f56e47f40ca4512e1e" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -608,24 +814,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.134.2" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287ab88211d049178dc8dda243e2db176919d8b29db605c0efc7c6d76ebea0f7" +checksum = "0522d74c227e49f3fd49ab055311486ae4f09083262b66705bed676952491469" [[package]] name = "cranelift-control" -version = "0.134.2" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c11438f9becf9a1e1e061bf76a33241b941a07ed01a489e9dad5fa79dc3624e9" +checksum = "66560ea1c5cef72e170b18e46d263dba2d3169c9d39e8cafdab2173c6362cc1a" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.134.2" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "584ecabe1462593a9cba9efbf9022e0987421ddb548989b455ab8bdd444b743c" +checksum = "b62ef5b17cc814d27e96b66a5b46da0e4ce2b8ac55a6d478048bb99d04b05526" dependencies = [ "cranelift-bitset", "serde", @@ -635,9 +841,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.134.2" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34249873145213a12106d5fd8a98a9dc9e90e16525e2337b2907874d1e7e9ee7" +checksum = "a87e0aaa39dbf70693b348a221e45904111704ee8f9fef140498471005f9842d" dependencies = [ "cranelift-codegen", "hashbrown 0.17.1", @@ -648,15 +854,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.134.2" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f1c1dc4d6b4bc4f0be12b08cee0e59f7610287827aa62590f8e9ec6f45dff4" +checksum = "cf79003ebfa1eed5e87f3b84446ad5236f540268289960e14f387dc9b28e40b7" [[package]] name = "cranelift-native" -version = "0.134.2" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31d9e4f6d9a3777b1ffa818000b4a6d7afd1b8787af71a9c58d94baef9076ced" +checksum = "05bf4f235743c81e67ee4db617c5a4a0b65d58d3f0cfc575ee0f1a4e0cd58273" dependencies = [ "cranelift-codegen", "libc", @@ -665,9 +871,24 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.134.2" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4155f8a1afdef96a0272c67804926eee4ef35f9708ab22ad6878d4c9543df596" +checksum = "f6977c2a71ab1e0d1e62f966b411a498aa04c4dce47d93d52f8a360a06058922" + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc-fast" @@ -763,6 +984,18 @@ dependencies = [ "smallvec", ] +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-bigint" version = "0.7.5" @@ -774,7 +1007,7 @@ dependencies = [ "getrandom 0.4.3", "hybrid-array", "num-traits", - "rand_core", + "rand_core 0.10.1", "serdect", "subtle", "zeroize", @@ -787,6 +1020,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -798,7 +1032,7 @@ checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "getrandom 0.4.3", "hybrid-array", - "rand_core", + "rand_core 0.10.1", ] [[package]] @@ -807,17 +1041,17 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3633a51a39c69ebbaa4feaa694bd83d241e4093901c84a0963b19d9bb3f0cf8f" dependencies = [ - "crypto-bigint", - "rand_core", + "crypto-bigint 0.7.5", + "rand_core 0.10.1", ] [[package]] name = "ctr" -version = "0.10.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -830,6 +1064,47 @@ dependencies = [ "subtle", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "curve25519-dalek-derive", + "digest 0.11.3", + "fiat-crypto 0.3.0", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "dashmap" version = "6.2.1" @@ -844,6 +1119,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + [[package]] name = "deflate64" version = "0.1.12" @@ -878,7 +1159,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "thiserror 2.0.19", + "thiserror 2.0.20", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468 0.7.0", + "zeroize", ] [[package]] @@ -887,11 +1179,25 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ - "const-oid", - "pem-rfc7468", + "const-oid 0.10.2", + "pem-rfc7468 1.0.0", "zeroize", ] +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint 0.4.8", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.5.8" @@ -905,7 +1211,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid 0.9.6", "crypto-common 0.1.7", + "subtle", ] [[package]] @@ -915,7 +1223,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.1", - "const-oid", + "const-oid 0.10.2", "crypto-common 0.2.2", "ctutils", "zeroize", @@ -983,50 +1291,141 @@ dependencies = [ "litrs", ] +[[package]] +name = "dtls" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a431e87fc386bd5e02deb554a013f97bc406e47a7d8e97efb7c6366b980e9c" +dependencies = [ + "aes 0.8.4", + "aes-gcm", + "async-trait", + "bytecheck", + "byteorder", + "cbc", + "ccm", + "chacha20poly1305", + "der-parser", + "hmac 0.12.1", + "log", + "p256", + "p384 0.13.1", + "portable-atomic", + "rand 0.9.5", + "rand_core 0.6.4", + "rcgen", + "ring", + "rkyv", + "rustls", + "sec1 0.7.3", + "sha1 0.10.7", + "sha2 0.10.9", + "thiserror 1.0.69", + "tokio", + "webrtc-util", + "x25519-dalek", + "x509-parser", +] + [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.10", + "digest 0.10.7", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", +] + [[package]] name = "ecdsa" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ - "der", + "der 0.8.1", "digest 0.11.3", - "elliptic-curve", - "rfc6979", - "signature", - "spki", + "elliptic-curve 0.14.1", + "rfc6979 0.6.0", + "signature 3.0.0", + "spki 0.8.0", "zeroize", ] +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "signature 3.0.0", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek 5.0.0", + "ed25519", + "sha2 0.11.0", + "subtle", +] + [[package]] name = "either" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.5", + "digest 0.10.7", + "ff 0.13.1", + "generic-array", + "group 0.13.0", + "hkdf", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.3", + "subtle", + "zeroize", +] + [[package]] name = "elliptic-curve" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" dependencies = [ - "base16ct", - "crypto-bigint", + "base16ct 1.0.0", + "crypto-bigint 0.7.5", "crypto-common 0.2.2", "digest 0.11.3", - "ff", - "group", - "hkdf", + "ff 0.14.0", + "group 0.14.0", "hybrid-array", - "pkcs8", - "rand_core", - "sec1", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sec1 0.8.1", "subtle", "zeroize", ] @@ -1054,25 +1453,13 @@ dependencies = [ [[package]] name = "encoding_rs_io" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" +checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" dependencies = [ "encoding_rs", ] -[[package]] -name = "enum_dispatch" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "env_filter" version = "2.0.0" @@ -1133,16 +1520,32 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "ff" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" dependencies = [ - "rand_core", + "rand_core 0.10.1", "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "fiat-crypto" version = "0.3.0" @@ -1151,9 +1554,9 @@ checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" [[package]] name = "fixedbitset" @@ -1321,6 +1724,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1341,11 +1745,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] @@ -1358,10 +1760,20 @@ dependencies = [ "js-sys", "libc", "r-efi 6.0.0", - "rand_core", + "rand_core 0.10.1", "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.33.0" @@ -1382,22 +1794,33 @@ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "granit-parser" -version = "0.0.7" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03f81ad4732830d85cfd417a9f62cde6dadda4354d37d078a6084a19560aa2d" +checksum = "7c92814c286f45b5ed1498ce9547bc9637b473eba7037f8aee6a4d5a3c1e051c" dependencies = [ "arraydeque", "smallvec", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.1", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "group" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" dependencies = [ - "ff", - "rand_core", + "ff 0.14.0", + "rand_core 0.10.1", "subtle", ] @@ -1482,12 +1905,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "hkdf" -version = "0.13.0" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", ] [[package]] @@ -1577,6 +2015,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -1731,6 +2184,16 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "inout" version = "0.2.2" @@ -1740,6 +2203,27 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "interceptor" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c11a956a48159f7fe539b8198f12b4db9b709ae5f94385b840db38f97fed74" +dependencies = [ + "async-trait", + "bytes", + "futures", + "log", + "portable-atomic", + "rand 0.9.5", + "rtcp", + "rtp", + "thiserror 1.0.69", + "tokio", + "waitgroup", + "webrtc-srtp", + "webrtc-util", +] + [[package]] name = "io-extras" version = "0.19.0" @@ -1764,9 +2248,9 @@ checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is_terminal_polyfill" @@ -1893,9 +2377,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if", "futures-util", @@ -1904,9 +2388,9 @@ dependencies = [ [[package]] name = "kqueue" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" dependencies = [ "kqueue-sys", "libc", @@ -1974,9 +2458,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" dependencies = [ "libc", ] @@ -2054,6 +2538,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "maybe-owned" version = "0.3.4" @@ -2091,6 +2581,15 @@ dependencies = [ "rustix", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -2107,6 +2606,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2145,6 +2650,39 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "nix" version = "0.31.3" @@ -2163,6 +2701,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "notify" version = "8.2.0" @@ -2208,6 +2756,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.5.1" @@ -2293,6 +2851,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2305,6 +2872,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "option-ext" version = "0.2.0" @@ -2320,17 +2893,41 @@ dependencies = [ "num-traits", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", + "sha2 0.10.9", +] + [[package]] name = "p384" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f" dependencies = [ - "ecdsa", - "elliptic-curve", - "fiat-crypto", + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", + "fiat-crypto 0.3.0", "primefield", - "primeorder", + "primeorder 0.14.0", "sha2 0.11.0", ] @@ -2394,7 +2991,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ "digest 0.11.3", - "hmac", + "hmac 0.13.0", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", ] [[package]] @@ -2472,14 +3088,30 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkcs1" version = "0.8.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" dependencies = [ - "der", - "spki", + "der 0.8.1", + "spki 0.8.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", ] [[package]] @@ -2488,8 +3120,8 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ - "der", - "spki", + "der 0.8.1", + "spki 0.8.0", ] [[package]] @@ -2517,6 +3149,29 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dcdc93847ad24990939cce6e1804361e903efcb5f99daa5abd87943a9d6d7ba" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.14.0" @@ -2565,6 +3220,15 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -2581,21 +3245,30 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" dependencies = [ - "crypto-bigint", + "crypto-bigint 0.7.5", "crypto-common 0.2.2", - "ff", - "rand_core", + "ff 0.14.0", + "rand_core 0.10.1", "subtle", "zeroize", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve 0.13.8", +] + [[package]] name = "primeorder" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" dependencies = [ - "elliptic-curve", + "elliptic-curve 0.14.1", "once_cell", "primefield", "serdect", @@ -2748,10 +3421,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" [[package]] -name = "pulley-interpreter" -version = "47.0.2" +name = "ptr_meta" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6064e52588328c1275d690baad99f8d4b3fdd870736966f1a63cfcbb20560b6" +checksum = "743da816b98c921cdbe8628ef7381b76f25ecf4da599fc80aca90eae7ef70cc0" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c8d9ca532f185d5d4db7a7c9d51420b452168ea1c2b913953281bd6fe1fcbd0" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "pulley-interpreter" +version = "47.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5c8c21ea032e4efbdf2d067dc45171779dbe0c8ecf20ef4a57efa7474f2b0a" dependencies = [ "cranelift-bitset", "log", @@ -2761,9 +3454,9 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e832980317bfc7f8c8538ffc9c69b82912f3e33ec70f7c855d521654c70fa62" +checksum = "9f10925455d5dde962e3604eade797ba5488644c8ee14a44191e2f2b58980574" dependencies = [ "proc-macro2", "quote", @@ -2772,25 +3465,28 @@ dependencies = [ [[package]] name = "pumpkin" -version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#75994f3302fa76078087b9a113130541e4d3fc37" +version = "0.1.0-dev+26.2-26.40" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#50fe9f7cd7182c13a856d0a41fd59a5ddcb37f6f" dependencies = [ "arc-swap", - "base64 0.23.0", + "axum", + "base64 0.23.1", "bitflags 2.13.1", "bytes", "colored", "crossbeam", "dashmap", + "ed25519-dalek", "flate2", "futures", - "hmac", + "hex", + "hmac 0.13.0", "hyper", "image", "indexmap", "libloading 0.9.0", "notify", - "num-bigint", + "num-bigint 0.5.1", "ordered-float", "postcard", "pumpkin-codecs", @@ -2811,10 +3507,10 @@ dependencies = [ "serde", "serde_json", "serde_repr", - "sha1", + "sha1 0.11.0", "sha2 0.11.0", "sysinfo", - "thiserror 2.0.19", + "thiserror 2.0.20", "time", "tokio", "tokio-util", @@ -2823,15 +3519,18 @@ dependencies = [ "tracing-subscriber", "ureq", "uuid", + "wasm-encoder 0.255.0", + "wasmparser 0.255.0", "wasmtime", "wasmtime-wasi", "wasmtime-wasi-http", + "webrtc", ] [[package]] name = "pumpkin-api-macros" -version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#75994f3302fa76078087b9a113130541e4d3fc37" +version = "0.1.0-dev+26.2-26.40" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#50fe9f7cd7182c13a856d0a41fd59a5ddcb37f6f" dependencies = [ "proc-macro-error2", "proc-macro2", @@ -2841,8 +3540,8 @@ dependencies = [ [[package]] name = "pumpkin-codecs" -version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#75994f3302fa76078087b9a113130541e4d3fc37" +version = "0.1.0-dev+26.2-26.40" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#50fe9f7cd7182c13a856d0a41fd59a5ddcb37f6f" dependencies = [ "serde_json", "tracing", @@ -2850,8 +3549,8 @@ dependencies = [ [[package]] name = "pumpkin-config" -version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#75994f3302fa76078087b9a113130541e4d3fc37" +version = "0.1.0-dev+26.2-26.40" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#50fe9f7cd7182c13a856d0a41fd59a5ddcb37f6f" dependencies = [ "pumpkin-util", "serde", @@ -2862,8 +3561,8 @@ dependencies = [ [[package]] name = "pumpkin-data" -version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#75994f3302fa76078087b9a113130541e4d3fc37" +version = "0.1.0-dev+26.2-26.40" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#50fe9f7cd7182c13a856d0a41fd59a5ddcb37f6f" dependencies = [ "crc-fast", "phf", @@ -2875,23 +3574,23 @@ dependencies = [ [[package]] name = "pumpkin-inventory" -version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#75994f3302fa76078087b9a113130541e4d3fc37" +version = "0.1.0-dev+26.2-26.40" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#50fe9f7cd7182c13a856d0a41fd59a5ddcb37f6f" 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.19", + "thiserror 2.0.20", "tokio", "tracing", ] [[package]] name = "pumpkin-macros" -version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#75994f3302fa76078087b9a113130541e4d3fc37" +version = "0.1.0-dev+26.2-26.40" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#50fe9f7cd7182c13a856d0a41fd59a5ddcb37f6f" dependencies = [ "heck", "proc-macro-error2", @@ -2902,30 +3601,28 @@ dependencies = [ [[package]] name = "pumpkin-nbt" -version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#75994f3302fa76078087b9a113130541e4d3fc37" +version = "0.1.0-dev+26.2-26.40" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#50fe9f7cd7182c13a856d0a41fd59a5ddcb37f6f" dependencies = [ "bytes", "cesu8", "flate2", "pumpkin-codecs", - "serde", - "thiserror 2.0.19", + "thiserror 2.0.20", "tracing", "uuid", ] [[package]] name = "pumpkin-protocol" -version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#75994f3302fa76078087b9a113130541e4d3fc37" +version = "0.1.0-dev+26.2-26.40" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#50fe9f7cd7182c13a856d0a41fd59a5ddcb37f6f" dependencies = [ - "aes", + "aes 0.9.2", "async-compression", "bitflags 2.13.1", "bytes", "cfb8", - "ctr", "flate2", "hybrid-array", "pumpkin-data", @@ -2934,34 +3631,32 @@ dependencies = [ "pumpkin-util", "pumpkin-world", "serde", - "sha2 0.11.0", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "uuid", ] [[package]] name = "pumpkin-util" -version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#75994f3302fa76078087b9a113130541e4d3fc37" +version = "0.1.0-dev+26.2-26.40" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#50fe9f7cd7182c13a856d0a41fd59a5ddcb37f6f" dependencies = [ - "base64 0.23.0", + "base64 0.23.1", "bytes", "colored", - "crypto-bigint", - "ecdsa", - "enum_dispatch", + "crypto-bigint 0.7.5", + "ecdsa 0.17.0", "md5", "num-derive", "num-traits", - "p384", + "p384 0.14.0", "pumpkin-codecs", "pumpkin-nbt", "rsa", "serde", "serde_json", "sha2 0.11.0", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "ureq", "uuid", @@ -2969,8 +3664,8 @@ dependencies = [ [[package]] name = "pumpkin-world" -version = "0.1.0-dev+26.2" -source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#75994f3302fa76078087b9a113130541e4d3fc37" +version = "0.1.0-dev+26.2-26.40" +source = "git+https://github.com/Pumpkin-MC/Pumpkin.git?branch=master#50fe9f7cd7182c13a856d0a41fd59a5ddcb37f6f" dependencies = [ "arc-swap", "bitflags 2.13.1", @@ -2978,7 +3673,6 @@ dependencies = [ "crossbeam", "crossfire", "dashmap", - "enum_dispatch", "flate2", "futures", "itertools 0.15.0", @@ -2995,7 +3689,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "slotmap", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tokio-util", "tracing", @@ -3031,24 +3725,71 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "rand" -version = "0.10.2" +name = "rancor" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +checksum = "9b534442d0fcdb55d66f373d9cac6d33b6293a2335bc2136dbd06ce0e87d2572" dependencies = [ - "chacha20", - "getrandom 0.4.3", - "rand_core", + "ptr_meta", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", ] [[package]] name = "rand" version = "0.10.2" -source = "git+https://github.com/rust-random/rand#cf4f73e3953ffcf97f575317a07f0fd5983bfe9e" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "chacha20", + "chacha20 0.10.1", "getrandom 0.4.3", - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "git+https://github.com/rust-random/rand#bb1262f703ca04e4ce56be78e1dc4e204cd6a998" +dependencies = [ + "chacha20 0.10.1", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", ] [[package]] @@ -3077,6 +3818,20 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -3105,7 +3860,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -3137,9 +3892,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -3152,14 +3907,33 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "rend" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "663ba70707f96e871406fe10d68128412e619b06d1d47cb91c3a4c6501176240" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "rfc6979" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" dependencies = [ - "crypto-bigint", - "hmac", + "crypto-bigint 0.7.5", + "hmac 0.13.0", ] [[package]] @@ -3176,24 +3950,80 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9776093b7ca170454ab1406954f7b7d97a57c51dc6c0642957fb2ef25c2d399" +dependencies = [ + "bytecheck", + "bytes", + "hashbrown 0.17.1", + "indexmap", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c25ef604ac7dd839d44d64648952ea23c97866f124ff671b0ed2cf3ad9bb06e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "rsa" version = "0.10.0-rc.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf" dependencies = [ - "const-oid", - "crypto-bigint", + "const-oid 0.10.2", + "crypto-bigint 0.7.5", "crypto-primes", "digest 0.11.3", "pkcs1", - "pkcs8", - "rand_core", - "signature", - "spki", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "signature 3.0.0", + "spki 0.8.0", "zeroize", ] +[[package]] +name = "rtcp" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad7f6a501162881032fc84b4bc78ae11f1b30748180b9f5cbe8810bf613aab" +dependencies = [ + "bytes", + "thiserror 1.0.69", + "webrtc-util", +] + +[[package]] +name = "rtp" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "149329e78ada26b5e174a4c281a7a7e5c8bda6008adc90daddd6f98fce56db29" +dependencies = [ + "bytes", + "memchr", + "portable-atomic", + "rand 0.9.5", + "serde", + "thiserror 1.0.69", + "webrtc-util", +] + [[package]] name = "rust-embed" version = "8.12.0" @@ -3261,6 +4091,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "1.1.4" @@ -3337,7 +4176,7 @@ dependencies = [ "libc", "log", "memchr", - "nix", + "nix 0.31.3", "unicode-segmentation", "unicode-width", "utf8parse", @@ -3365,15 +4204,41 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sdp" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b6eecfa5151edef84d544ff3885dff98e5b5fe97586757a4b5118bde51c958" +dependencies = [ + "rand 0.9.5", + "substring", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.10", + "generic-array", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + [[package]] name = "sec1" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ - "base16ct", + "base16ct 1.0.0", "ctutils", - "der", + "der 0.8.1", "hybrid-array", "subtle", "zeroize", @@ -3401,15 +4266,13 @@ dependencies = [ [[package]] name = "serde-saphyr" -version = "0.0.29" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd22781911de0ca6debda95f073c8f18bec65d1a94f1fa9573f3102e514cea4" +checksum = "af2c6921c8ce48fb1b052b457619d008834039d80325992ed3bc55bbe6c155ad" dependencies = [ - "ahash", "annotate-snippets", - "base64 0.22.1", + "base64 0.23.1", "encoding_rs_io", - "getrandom 0.3.4", "granit-parser", "nohash-hasher", "num-traits", @@ -3477,10 +4340,21 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" dependencies = [ - "base16ct", + "base16ct 1.0.0", "serde", ] +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + [[package]] name = "sha1" version = "0.11.0" @@ -3548,6 +4422,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "signature" version = "3.0.0" @@ -3555,7 +4439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" dependencies = [ "digest 0.11.3", - "rand_core", + "rand_core 0.10.1", ] [[package]] @@ -3564,6 +4448,12 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.3" @@ -3594,6 +4484,25 @@ dependencies = [ "serde", ] +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.5" @@ -3619,6 +4528,16 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + [[package]] name = "spki" version = "0.8.0" @@ -3626,7 +4545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", - "der", + "der 0.8.1", ] [[package]] @@ -3641,6 +4560,34 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stun" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e3b8d5ad1dd4c8cd0595440f26521a71dd593cb5873ee8e06b91510b7d97269" +dependencies = [ + "base64 0.22.1", + "crc", + "lazy_static", + "md-5", + "rand 0.9.5", + "ring", + "subtle", + "thiserror 1.0.69", + "tokio", + "url", + "webrtc-util", +] + +[[package]] +name = "substring" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" +dependencies = [ + "autocfg", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3669,6 +4616,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + [[package]] name = "synstructure" version = "0.13.2" @@ -3732,11 +4685,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.19", + "thiserror-impl 2.0.20", ] [[package]] @@ -3752,9 +4705,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", @@ -3772,9 +4725,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.54" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "js-sys", @@ -3813,6 +4766,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.53.1" @@ -3822,9 +4790,10 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.5", "tokio-macros", "windows-sys 0.61.2", ] @@ -3928,6 +4897,33 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" @@ -3994,6 +4990,27 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "turn" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d99249a493335eb44c4d7943a8751b22561a82c9903d8eb2d29780b3927880a7" +dependencies = [ + "async-trait", + "base64 0.22.1", + "futures", + "log", + "md-5", + "portable-atomic", + "rand 0.9.5", + "ring", + "stun", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "webrtc-util", +] + [[package]] name = "twox-hash" version = "1.6.3" @@ -4040,6 +5057,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -4126,6 +5153,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "waitgroup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1f50000a783467e6c0200f9d10642f4bc424e39efc1b770203e88b488f79292" +dependencies = [ + "atomic-waker", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -4162,9 +5198,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -4175,9 +5211,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4185,9 +5221,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", @@ -4198,9 +5234,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] @@ -4212,7 +5248,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.252.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.255.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b524283fb5df62eec102ed0574838961bdd7ba5ac9c50d38e2756c51c971a42" +dependencies = [ + "leb128fmt", + "wasmparser 0.255.0", ] [[package]] @@ -4228,6 +5274,19 @@ dependencies = [ "serde", ] +[[package]] +name = "wasmparser" +version = "0.255.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e329ef4b5d46e73b91d3ac6924417cad55a8cbbf869c199283383427c3320b" +dependencies = [ + "bitflags 2.13.1", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + [[package]] name = "wasmprinter" version = "0.252.0" @@ -4236,14 +5295,14 @@ checksum = "7142797de29b35ab8dbf15c00f55fda75d409da4c423a8ab8bd6b667a785824b" dependencies = [ "anyhow", "termcolor", - "wasmparser", + "wasmparser 0.252.0", ] [[package]] name = "wasmtime" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06d9f263ccb212017dae1d109b9997f218a7675db1e9a7bedf07ae1643322bf3" +checksum = "c80ca6098e0d4d06886d91d7f2cc3cb6623eb583c4c0ab3c89cbfb6098c8586c" dependencies = [ "addr2line", "async-trait", @@ -4267,7 +5326,7 @@ dependencies = [ "serde_derive", "smallvec", "target-lexicon", - "wasmparser", + "wasmparser 0.252.0", "wasmtime-environ", "wasmtime-internal-cache", "wasmtime-internal-component-macro", @@ -4284,9 +5343,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9915e3d90c36a16e0fec8be746ee87e21cf0d4a017ecf09c49b7d1cc6f31b2b6" +checksum = "134f9d136d29c76f6c1b4c9b468e97a2efc38c7d49fd188e39b64870b2fea701" dependencies = [ "anyhow", "cpp_demangle", @@ -4306,8 +5365,8 @@ dependencies = [ "sha2 0.10.9", "smallvec", "target-lexicon", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.252.0", + "wasmparser 0.252.0", "wasmprinter", "wasmtime-internal-component-util", "wasmtime-internal-core", @@ -4315,9 +5374,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-cache" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7c25436e1602aad2de80bfb3f586dcdfb6fe426d5b17cba37fabf9a1714d32" +checksum = "65db2eb1bfc5371a3b4107dbf539d0b93d05016fc29625b1648146b47db02071" dependencies = [ "base64 0.22.1", "directories-next", @@ -4335,9 +5394,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b627ea698db36fb9110aa77868467f1d23791a76a88a433cb6a43e1242073b77" +checksum = "2bf7b91fed3fc34781d57f9c6654ea2513bf0a99f7b0b0523c00de1e6efebe1c" dependencies = [ "anyhow", "proc-macro2", @@ -4350,15 +5409,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f45bf943cff1d2f1976bac6a241ca4ecaf29a6efd5f1ad89e7722439fa5c392f" +checksum = "fc8a678149885cae00289f806fbe74c7863084cf74cace0b8dc73602279400e1" [[package]] name = "wasmtime-internal-core" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea33e1f9685be82f90223bebcf2ffefb6b596ea3d82a3c42384527b9c6f5b447" +checksum = "8a0092c4b9d070ac5e278b6d0db10f5e71214190f0347ca57502b4692f796321" dependencies = [ "hashbrown 0.17.1", "libm", @@ -4367,9 +5426,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-cranelift" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1ce104719932a48dbdfec0cc292c96494b0bfe764946e45fbfd3340e3a68f9" +checksum = "6851ebc9e03cab23d9821d2b2505d380bdfe38f35ccec945247b05274d85c98b" dependencies = [ "cfg-if", "cranelift-codegen", @@ -4384,8 +5443,8 @@ dependencies = [ "pulley-interpreter", "smallvec", "target-lexicon", - "thiserror 2.0.19", - "wasmparser", + "thiserror 2.0.20", + "wasmparser 0.252.0", "wasmtime-environ", "wasmtime-internal-core", "wasmtime-internal-unwinder", @@ -4394,9 +5453,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976e4b71b9c0da8a9a7bb8d5fa0abedc4da879b397111fd91ece73be524fe621" +checksum = "9b26da6d5f60d4c438da70bba3553fe810a840533a64156be287dca2081c6991" dependencies = [ "cc", "cfg-if", @@ -4409,9 +5468,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f60ecba3497cfc26284903c2e79023fd5ea991b95b0e8dafe994fd9fca4c4e" +checksum = "ed621ba25d7bf78b7edd7b4749abbb27c2e5cdba836c9504a424ee74b6c23149" dependencies = [ "cc", "wasmtime-internal-versioned-export-macros", @@ -4419,9 +5478,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fae6decb025ae6d12f3391b36deb62a6838d45b18864f796d84bced722ceac0d" +checksum = "5684ba160951baad06a725696f3c590e2fb0e8067c2aebee27bf7f9259058e85" dependencies = [ "cfg-if", "libc", @@ -4431,9 +5490,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-unwinder" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0982122d9470d3d72ed7aa9ed91e69538ae44c1494cd22d78fec7846b248a940" +checksum = "112eead527bffa8ff0646a11fb4339a9d52ddd1da2b9a6fce4aff84c815dd94f" dependencies = [ "cfg-if", "cranelift-codegen", @@ -4444,9 +5503,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "363cf88a80e6ed13311d0d34d9bcd02a2e07d98113b994ff41ed386a206f498e" +checksum = "153592e0bed824fc13c6696203fa1bd7bd20eb355316475e39a55f081ef80eca" dependencies = [ "proc-macro2", "quote", @@ -4455,9 +5514,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ff41bb1656cbeb7befc42b8a7078b30a5f8393a0c8bca36a6cec94360c1a16" +checksum = "c456ad6e81e0f46abfeca43687d18ea15e289c395b262ea6e0023e2682e088be" dependencies = [ "anyhow", "bitflags 2.13.1", @@ -4468,9 +5527,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed1750b8e5e5d5b1ee3c0eb602d2c49d6a37a36e9996eaf0708b48ac92b62b06" +checksum = "4c1cf60cd6a565213af7074b4aad7bb5e110e3c6c6aae54425aa0500a0e15e86" dependencies = [ "async-trait", "bitflags 2.13.1", @@ -4482,7 +5541,7 @@ dependencies = [ "io-lifetimes 3.0.1", "rand 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", "rustix", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tracing", "url", @@ -4493,9 +5552,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi-http" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "014b8f05cacb7a5d665536e8a733b887bd192a9c179037580d48d0a6a82ecd34" +checksum = "a499879061ee9f724342d5f5009754b723ae53c7932161ee063a9fe19395e915" dependencies = [ "async-trait", "bytes", @@ -4516,9 +5575,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi-io" -version = "47.0.2" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54777d3afeaf3f32cc444df661d9cb4fd57067971e183c2b1f5251f33b939e68" +checksum = "7fb08e1d7755aaa467ce14080b7b01e33c914a920f6000696f1e42e40ea6387e" dependencies = [ "async-trait", "bytes", @@ -4536,6 +5595,175 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webrtc" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaacdf9d96224d7b6e2872ba065578f38775f7634367e3ac2cc87c7271da433" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "dtls", + "hex", + "interceptor", + "lazy_static", + "log", + "portable-atomic", + "rand 0.9.5", + "rcgen", + "regex", + "ring", + "rtcp", + "rtp", + "sdp", + "serde", + "serde_json", + "sha2 0.10.9", + "smol_str", + "stun", + "thiserror 1.0.69", + "tokio", + "turn", + "unicase", + "url", + "waitgroup", + "webrtc-data", + "webrtc-ice", + "webrtc-mdns", + "webrtc-media", + "webrtc-sctp", + "webrtc-srtp", + "webrtc-util", +] + +[[package]] +name = "webrtc-data" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd470286275809f2fcfcdb1e73ef5f1500be82eff7fe98150ce81b20aad5a2a4" +dependencies = [ + "bytes", + "log", + "portable-atomic", + "thiserror 1.0.69", + "tokio", + "webrtc-sctp", + "webrtc-util", +] + +[[package]] +name = "webrtc-ice" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b7fd30f52e6fda8664779b84b7904b2553b76fee24d9ca665e774ae32b13f53" +dependencies = [ + "arc-swap", + "async-trait", + "crc", + "log", + "portable-atomic", + "rand 0.9.5", + "serde", + "serde_json", + "stun", + "thiserror 1.0.69", + "tokio", + "turn", + "url", + "uuid", + "waitgroup", + "webrtc-mdns", + "webrtc-util", +] + +[[package]] +name = "webrtc-mdns" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ffa0ea00c0fae979aafa5db285fec5eeb360e1ea246ebec529ba4e09f8e03e" +dependencies = [ + "log", + "socket2 0.5.10", + "thiserror 1.0.69", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-media" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26a6c7335bdd03dc023cb9a3bd7966866d969c92c86987b73aa16dfa39c7ca29" +dependencies = [ + "byteorder", + "bytes", + "rand 0.9.5", + "rtp", + "thiserror 1.0.69", +] + +[[package]] +name = "webrtc-sctp" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c4b637f0d8eb96d900ac0f79b3060ddd21ca88dfefa467e96e67ab0016f2574" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "crc", + "log", + "portable-atomic", + "rand 0.9.5", + "thiserror 1.0.69", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-srtp" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45d2b667a0b5d04eebcb7cb22fd51b2af3721f418097b496e72de16386c8d0c3" +dependencies = [ + "aead", + "aes 0.8.4", + "aes-gcm", + "byteorder", + "bytes", + "ctr", + "hmac 0.12.1", + "log", + "rtcp", + "rtp", + "sha1 0.10.7", + "subtle", + "thiserror 1.0.69", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-util" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1beae0b4f24969741c26ca282a257dc5823bd9cbe608f7e3ca3775102d323ad9" +dependencies = [ + "async-trait", + "bitflags 1.3.2", + "bytes", + "ipnet", + "lazy_static", + "log", + "nix 0.26.4", + "portable-atomic", + "rand 0.9.5", + "thiserror 1.0.69", + "tokio", + "winapi", +] + [[package]] name = "winapi" version = "0.3.9" @@ -4886,7 +6114,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-ident", - "wasmparser", + "wasmparser 0.252.0", ] [[package]] @@ -4895,8 +6123,8 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" dependencies = [ - "ff", - "group", + "ff 0.14.0", + "group 0.14.0", "hybrid-array", ] @@ -4906,12 +6134,51 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek 4.1.3", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "xxhash-rust" version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.8.3" @@ -4937,18 +6204,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", @@ -4981,6 +6248,20 @@ name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "zerotrie" @@ -5021,20 +6302,20 @@ version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ - "aes", + "aes 0.9.2", "bzip2", "constant_time_eq", "crc32fast", "deflate64", "flate2", "getrandom 0.4.3", - "hmac", + "hmac 0.13.0", "indexmap", "lzma-rust2", "memchr", "pbkdf2", "ppmd-rust", - "sha1", + "sha1 0.11.0", "time", "typed-path", "zeroize", @@ -5044,9 +6325,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" [[package]] name = "zmij" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index c558df2..06575ee 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -31,7 +31,7 @@ uuid = "1.24.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -serde-saphyr = "0.0.29" +serde-saphyr = "1.0.1" rand = { git = "https://github.com/rust-random/rand"} prost = "0.14" diff --git a/rust/build/java.rs b/rust/build/java.rs index 286222a..acef8f1 100644 --- a/rust/build/java.rs +++ b/rust/build/java.rs @@ -40,6 +40,9 @@ pub fn setup_java(base: PathBuf) { .with_base_path(&resources) .java_opts(vec![ j4rs::JavaOpt::new("--enable-native-access=ALL-UNNAMED"), + j4rs::JavaOpt::new("--enable-final-field-mutation=ALL-UNNAMED"), + j4rs::JavaOpt::new("--add-opens=java.base/java.lang=ALL-UNNAMED"), + j4rs::JavaOpt::new("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"), j4rs::JavaOpt::new("-Dcom.google.protobuf.useUnsafe=false"), ]); @@ -59,10 +62,6 @@ pub fn setup_java(base: PathBuf) { } fs::create_dir_all(&jassets).unwrap(); - let dest_jar = jassets.join("patchbukkit.jar"); - fs::copy(&patchbukkit_jar, &dest_jar) - .map_err(|err| format!("Failed to copy patchbukkit.jar to jassets: {err:?}")) - .unwrap(); let cdylib = std::env::var("CARGO_CDYLIB_FILE_J4RS").unwrap(); let cdylib = PathBuf::from(cdylib); @@ -71,10 +70,16 @@ pub fn setup_java(base: PathBuf) { fs::create_dir_all(&cdylib_to).unwrap(); let original_name = cdylib.file_name().unwrap().to_string_lossy(); - let stem = original_name.split('-').next().unwrap(); // before the first '-' - let ext = cdylib.extension().unwrap().to_string_lossy(); + let mut target_name = original_name.split('-').next().unwrap().to_string(); + if !target_name.ends_with(".dll") + && !target_name.ends_with(".so") + && !target_name.ends_with(".dylib") + && let Some(ext) = cdylib.extension() + { + target_name = format!("{target_name}.{}", ext.to_string_lossy()); + } - cdylib_to.push(format!("{stem}.{ext}")); + cdylib_to.push(target_name); fs::copy(&cdylib, &cdylib_to) .map_err(|err| format!("Failed to copy j4rs native lib: {err:?}")) diff --git a/rust/build/protobufs.rs b/rust/build/protobufs.rs index a1291ea..326410c 100644 --- a/rust/build/protobufs.rs +++ b/rust/build/protobufs.rs @@ -224,7 +224,7 @@ pub fn initialize_ffi_callbacks(jvm: &j4rs::Jvm) -> anyhow::Result<()> {{ file, r#" // Initialize {} - jvm.invoke_static( + if let Err(e) = jvm.invoke_static( "{}", "init", &["#, @@ -242,13 +242,17 @@ pub fn initialize_ffi_callbacks(jvm: &j4rs::Jvm) -> anyhow::Result<()> {{ writeln!( file, r#" ], - )?; + ) {{ + tracing::error!("Failed to initialize FFI service: {{:?}}", e); + }} - jvm.invoke_static( + if let Err(e) = jvm.invoke_static( "{}", "initFree", &[InvocationArg::try_from(ffi_free_bytes as *const () as i64)?.into_primitive()?], - )?;"#, + ) {{ + tracing::error!("Failed to initialize FFI service initFree: {{:?}}", e); + }}"#, service.java_class ) .unwrap(); diff --git a/rust/rust-toolchain.toml b/rust/rust-toolchain.toml index d7dc4dd..887e55d 100644 --- a/rust/rust-toolchain.toml +++ b/rust/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "nightly-2026-07-31" +channel = "nightly-2026-08-07" components = ["rust-analyzer", "rust-src", "rustfmt", "clippy"] diff --git a/rust/src/commands/mod.rs b/rust/src/commands/mod.rs index b6fb360..413d190 100644 --- a/rust/src/commands/mod.rs +++ b/rust/src/commands/mod.rs @@ -131,8 +131,20 @@ impl CommandExecutor for JavaCommandExecutor { ) -> pumpkin::command::CommandResult<'a> { Box::pin(async move { let full_command = match args.get(ARG_ANY) { - Some(Arg::Msg(msg)) => format!("/{} {}", self.cmd_name, msg), - _ => format!("/{}", self.cmd_name), + Some(Arg::Msg(msg)) => { + if self.cmd_name.starts_with('/') { + format!("{} {}", self.cmd_name, msg) + } else { + format!("/{} {}", self.cmd_name, msg) + } + } + _ => { + if self.cmd_name.starts_with('/') { + self.cmd_name.clone() + } else { + format!("/{}", self.cmd_name) + } + } }; let (tx, _rx) = oneshot::channel(); diff --git a/rust/src/config/paper.rs b/rust/src/config/paper.rs index eea0ebd..90cf9fe 100644 --- a/rust/src/config/paper.rs +++ b/rust/src/config/paper.rs @@ -184,6 +184,14 @@ pub struct PaperPluginYml { /// Other plugins this plugin can substitute for #[serde(default)] pub provides: Option>, + + /// Libraries declared for runtime resolution + #[serde(default)] + pub libraries: Option>, + + /// Repositories declared for runtime resolution + #[serde(default)] + pub repositories: Option, } impl PaperPluginYml { diff --git a/rust/src/directories.rs b/rust/src/directories.rs index 23077cf..9a929e5 100644 --- a/rust/src/directories.rs +++ b/rust/src/directories.rs @@ -29,6 +29,20 @@ pub fn setup_directories(server: &Context) -> Result, @@ -36,7 +42,7 @@ impl PatchBukkitEvent for pumpkin::plugin::player::player_join::PlayerJoinEvent player_uuid: Some(Uuid { value: self.player.gameprofile.id.to_string(), }), - join_message: serde_json::to_string(&self.join_message).unwrap(), + join_message: serde_json::to_string(&self.join_message).unwrap_or_default(), })), }, context: EventContext { @@ -56,6 +62,531 @@ impl PatchBukkitEvent for pumpkin::plugin::player::player_join::PlayerJoinEvent } } +impl PatchBukkitEvent for pumpkin::plugin::player::player_leave::PlayerLeaveEvent { + fn to_payload(&self, server: Arc) -> JvmEventPayload { + JvmEventPayload { + event: Event { + data: Some(Data::PlayerQuit(PlayerQuitEvent { + player_uuid: Some(Uuid { + value: self.player.gameprofile.id.to_string(), + }), + quit_message: serde_json::to_string(&self.leave_message).unwrap_or_default(), + })), + }, + context: EventContext { + server, + player: Some(self.player.clone()), + }, + } + } + + fn apply_modifications(&mut self, server: &Arc, data: Data) -> Option<()> { + if let Data::PlayerQuit(event) = data { + self.leave_message = serde_json::from_str(&event.quit_message).ok()?; + server.get_player_by_uuid(uuid::Uuid::from_str(&event.player_uuid?.value).ok()?)?; + } + + Some(()) + } +} + +impl PatchBukkitEvent + for pumpkin::plugin::player::player_gamemode_change::PlayerGamemodeChangeEvent +{ + fn to_payload(&self, server: Arc) -> JvmEventPayload { + JvmEventPayload { + event: Event { + data: Some(Data::PlayerGamemodeChange(PlayerGameModeChangeEvent { + player_uuid: Some(Uuid { + value: self.player.gameprofile.id.to_string(), + }), + new_gamemode: format!("{:?}", self.new_gamemode), + })), + }, + context: EventContext { + server, + player: Some(self.player.clone()), + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, _data: Data) -> Option<()> { + Some(()) + } +} + +impl PatchBukkitEvent for pumpkin::plugin::player::player_interact_event::PlayerInteractEvent { + fn to_payload(&self, server: Arc) -> JvmEventPayload { + let (x, y, z) = match self.clicked_pos { + Some(pos) => (pos.0.x, pos.0.y, pos.0.z), + None => (0, 0, 0), + }; + let action_str = match self.action { + pumpkin::plugin::player::player_interact_event::InteractAction::LeftClickBlock => { + "LEFT_CLICK_BLOCK" + } + pumpkin::plugin::player::player_interact_event::InteractAction::LeftClickAir => { + "LEFT_CLICK_AIR" + } + pumpkin::plugin::player::player_interact_event::InteractAction::RightClickAir => { + "RIGHT_CLICK_AIR" + } + pumpkin::plugin::player::player_interact_event::InteractAction::RightClickBlock => { + "RIGHT_CLICK_BLOCK" + } + }; + JvmEventPayload { + event: Event { + data: Some(Data::PlayerInteract(PlayerInteractEvent { + player_uuid: Some(Uuid { + value: self.player.gameprofile.id.to_string(), + }), + action: action_str.to_string(), + clicked_x: x, + clicked_y: y, + clicked_z: z, + block_face: "SELF".to_string(), + hand: "HAND".to_string(), + })), + }, + context: EventContext { + server, + player: Some(self.player.clone()), + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, _data: Data) -> Option<()> { + Some(()) + } +} + +impl PatchBukkitEvent for pumpkin::plugin::block::block_break::BlockBreakEvent { + fn to_payload(&self, server: Arc) -> JvmEventPayload { + JvmEventPayload { + event: Event { + data: Some(Data::BlockBreak(BlockBreakEvent { + player_uuid: self.player.as_ref().map(|p| Uuid { + value: p.gameprofile.id.to_string(), + }), + block_x: self.block_position.0.x, + block_y: self.block_position.0.y, + block_z: self.block_position.0.z, + block_type: format!("{:?}", self.block), + exp: self.exp, + drop_items: self.drop, + })), + }, + context: EventContext { + server, + player: self.player.clone(), + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, _data: Data) -> Option<()> { + Some(()) + } +} + +impl PatchBukkitEvent for pumpkin::plugin::block::block_place::BlockPlaceEvent { + fn to_payload(&self, server: Arc) -> JvmEventPayload { + JvmEventPayload { + event: Event { + data: Some(Data::BlockPlace(BlockPlaceEvent { + player_uuid: Some(Uuid { + value: self.player.gameprofile.id.to_string(), + }), + block_x: self.block_position.0.x, + block_y: self.block_position.0.y, + block_z: self.block_position.0.z, + block_placed_type: format!("{:?}", self.block_placed), + block_against_type: format!("{:?}", self.block_placed_against), + can_build: self.can_build, + })), + }, + context: EventContext { + server, + player: Some(self.player.clone()), + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, _data: Data) -> Option<()> { + Some(()) + } +} + +impl PatchBukkitEvent + for pumpkin::plugin::player::player_toggle_sneak_event::PlayerToggleSneakEvent +{ + fn to_payload(&self, server: Arc) -> JvmEventPayload { + JvmEventPayload { + event: Event { + data: Some(Data::PlayerToggleSneak(PlayerToggleSneakEvent { + player_uuid: Some(Uuid { + value: self.player.gameprofile.id.to_string(), + }), + is_sneaking: self.is_sneaking, + })), + }, + context: EventContext { + server, + player: Some(self.player.clone()), + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, _data: Data) -> Option<()> { + Some(()) + } +} + +impl PatchBukkitEvent + for pumpkin::plugin::player::player_toggle_sprint_event::PlayerToggleSprintEvent +{ + fn to_payload(&self, server: Arc) -> JvmEventPayload { + JvmEventPayload { + event: Event { + data: Some(Data::PlayerToggleSprint(PlayerToggleSprintEvent { + player_uuid: Some(Uuid { + value: self.player.gameprofile.id.to_string(), + }), + is_sprinting: self.is_sprinting, + })), + }, + context: EventContext { + server, + player: Some(self.player.clone()), + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, _data: Data) -> Option<()> { + Some(()) + } +} + +impl PatchBukkitEvent + for pumpkin::plugin::player::player_toggle_flight_event::PlayerToggleFlightEvent +{ + fn to_payload(&self, server: Arc) -> JvmEventPayload { + JvmEventPayload { + event: Event { + data: Some(Data::PlayerToggleFlight(PlayerToggleFlightEvent { + player_uuid: Some(Uuid { + value: self.player.gameprofile.id.to_string(), + }), + is_flying: self.is_flying, + })), + }, + context: EventContext { + server, + player: Some(self.player.clone()), + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, _data: Data) -> Option<()> { + Some(()) + } +} + +impl PatchBukkitEvent for pumpkin::plugin::player::player_move::PlayerMoveEvent { + fn to_payload(&self, server: Arc) -> JvmEventPayload { + let move_vel = self.to - self.from; + self.player.living_entity.entity.velocity.store(move_vel); + + let player_uuid = self.player.gameprofile.id.to_string(); + let world_uuid = self.player.world().uuid.to_string(); + + JvmEventPayload { + event: Event { + data: Some(Data::PlayerMove(PlayerMoveEvent { + player_uuid: Some(Uuid { value: player_uuid }), + from: Some(crate::proto::patchbukkit::common::Location { + world: Some(crate::proto::patchbukkit::common::World { + uuid: Some(Uuid { + value: world_uuid.clone(), + }), + }), + position: Some(crate::proto::patchbukkit::common::Vec3 { + x: self.from.x, + y: self.from.y, + z: self.from.z, + }), + yaw: self.player.living_entity.entity.yaw.load(), + pitch: self.player.living_entity.entity.pitch.load(), + }), + to: Some(crate::proto::patchbukkit::common::Location { + world: Some(crate::proto::patchbukkit::common::World { + uuid: Some(Uuid { value: world_uuid }), + }), + position: Some(crate::proto::patchbukkit::common::Vec3 { + x: self.to.x, + y: self.to.y, + z: self.to.z, + }), + yaw: self.player.living_entity.entity.yaw.load(), + pitch: self.player.living_entity.entity.pitch.load(), + }), + })), + }, + context: EventContext { + server, + player: Some(self.player.clone()), + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, data: Data) -> Option<()> { + if let Data::PlayerMove(event) = data + && let Some(to) = event.to + && let Some(pos) = to.position + { + self.to = pumpkin_util::math::vector3::Vector3::new(pos.x, pos.y, pos.z); + } + Some(()) + } + +} + +impl PatchBukkitEvent for pumpkin::plugin::player::player_chat::PlayerChatEvent { + fn to_payload(&self, server: Arc) -> JvmEventPayload { + JvmEventPayload { + event: Event { + data: Some(Data::PlayerChat(PlayerChatEvent { + player_uuid: Some(Uuid { + value: self.player.gameprofile.id.to_string(), + }), + message: self.message.clone(), + format: "<%1$s> %2$s".to_string(), + })), + }, + context: EventContext { + server, + player: Some(self.player.clone()), + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, data: Data) -> Option<()> { + if let Data::PlayerChat(event) = data { + self.message = event.message; + } + Some(()) + } +} + +impl PatchBukkitEvent + for pumpkin::plugin::player::player_interact_entity_event::PlayerInteractEntityEvent +{ + fn to_payload(&self, server: Arc) -> JvmEventPayload { + let action_str = match self.action { + pumpkin_protocol::java::server::play::ActionType::Interact => "INTERACT", + pumpkin_protocol::java::server::play::ActionType::Attack => "ATTACK", + pumpkin_protocol::java::server::play::ActionType::InteractAt => "INTERACT_AT", + }; + JvmEventPayload { + event: Event { + data: Some(Data::PlayerInteractEntity(PlayerInteractEntityEvent { + player_uuid: Some(Uuid { + value: self.player.gameprofile.id.to_string(), + }), + target_uuid: Some(Uuid { + value: self.target.get_entity().entity_uuid.to_string(), + }), + action: action_str.to_string(), + is_sneaking: self.sneaking, + })), + }, + context: EventContext { + server, + player: Some(self.player.clone()), + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, _data: Data) -> Option<()> { + Some(()) + } +} + +impl PatchBukkitEvent for pumpkin::plugin::block::sign_change::SignChangeEvent { + fn to_payload(&self, server: Arc) -> JvmEventPayload { + JvmEventPayload { + event: Event { + data: Some(Data::SignChange(SignChangeEvent { + player_uuid: Some(Uuid { + value: self.player.gameprofile.id.to_string(), + }), + block_x: self.block_pos.0.x, + block_y: self.block_pos.0.y, + block_z: self.block_pos.0.z, + lines: self.lines.clone(), + })), + }, + context: EventContext { + server, + player: Some(self.player.clone()), + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, data: Data) -> Option<()> { + if let Data::SignChange(event) = data { + self.lines = event.lines; + } + Some(()) + } +} + +impl PatchBukkitEvent for pumpkin::plugin::block::block_damage::BlockDamageEvent { + fn to_payload(&self, server: Arc) -> JvmEventPayload { + JvmEventPayload { + event: Event { + data: Some(Data::BlockDamage(BlockDamageEvent { + player_uuid: Some(Uuid { + value: self.player.gameprofile.id.to_string(), + }), + block_x: self.block_pos.0.x, + block_y: self.block_pos.0.y, + block_z: self.block_pos.0.z, + insta_break: false, + })), + }, + context: EventContext { + server, + player: Some(self.player.clone()), + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, _data: Data) -> Option<()> { + Some(()) + } +} + +impl PatchBukkitEvent for pumpkin::plugin::server::server_command::ServerCommandEvent { + fn to_payload(&self, server: Arc) -> JvmEventPayload { + JvmEventPayload { + event: Event { + data: Some(Data::ServerCommand(ServerCommandEvent { + sender_name: "Console".to_string(), + command: self.command.clone(), + })), + }, + context: EventContext { + server, + player: None, + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, data: Data) -> Option<()> { + if let Data::ServerCommand(event) = data { + self.command = event.command; + } + Some(()) + } +} + +impl PatchBukkitEvent for pumpkin::plugin::block::block_ignite::BlockIgniteEvent { + fn to_payload(&self, server: Arc) -> JvmEventPayload { + let player_uuid = self.player.as_ref().map(|p| Uuid { + value: p.gameprofile.id.to_string(), + }); + JvmEventPayload { + event: Event { + data: Some(Data::BlockIgnite(BlockIgniteEvent { + player_uuid, + block_x: self.block_pos.0.x, + block_y: self.block_pos.0.y, + block_z: self.block_pos.0.z, + ignite_cause: "FLINT_AND_STEEL".to_string(), + })), + }, + context: EventContext { + server, + player: self.player.clone(), + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, _data: Data) -> Option<()> { + Some(()) + } +} + +impl PatchBukkitEvent for pumpkin::plugin::block::block_grow::BlockGrowEvent { + fn to_payload(&self, server: Arc) -> JvmEventPayload { + JvmEventPayload { + event: Event { + data: Some(Data::BlockGrow(BlockGrowEvent { + block_x: self.block_pos.0.x, + block_y: self.block_pos.0.y, + block_z: self.block_pos.0.z, + new_block_type: self.new_block.name.to_string(), + })), + }, + context: EventContext { + server, + player: None, + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, _data: Data) -> Option<()> { + Some(()) + } +} + +impl PatchBukkitEvent for pumpkin::plugin::block::block_form::BlockFormEvent { + fn to_payload(&self, server: Arc) -> JvmEventPayload { + JvmEventPayload { + event: Event { + data: Some(Data::BlockForm(BlockFormEvent { + block_x: self.block_pos.0.x, + block_y: self.block_pos.0.y, + block_z: self.block_pos.0.z, + new_block_type: self.block.name.to_string(), + })), + }, + context: EventContext { + server, + player: None, + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, _data: Data) -> Option<()> { + Some(()) + } +} + +impl PatchBukkitEvent for pumpkin::plugin::block::block_fade::BlockFadeEvent { + fn to_payload(&self, server: Arc) -> JvmEventPayload { + JvmEventPayload { + event: Event { + data: Some(Data::BlockFade(BlockFadeEvent { + block_x: self.block_pos.0.x, + block_y: self.block_pos.0.y, + block_z: self.block_pos.0.z, + new_block_type: self.block.name.to_string(), + })), + }, + context: EventContext { + server, + player: None, + }, + } + } + + fn apply_modifications(&mut self, _server: &Arc, _data: Data) -> Option<()> { + Some(()) + } +} + pub struct PatchBukkitEventHandler { plugin_name: String, command_tx: mpsc::Sender, diff --git a/rust/src/java/jar.rs b/rust/src/java/jar.rs index f94134e..cfeea52 100644 --- a/rust/src/java/jar.rs +++ b/rust/src/java/jar.rs @@ -16,12 +16,17 @@ pub fn discover_jar_files(plugin_folder: &Path) -> impl Iterator glob(&pattern) .expect("Invalid glob pattern") .filter_map(|entry| { - entry + let path = entry .map_err(|e| tracing::error!("Glob error: {e:?}")) .ok()? .canonicalize() .map_err(|e| tracing::error!("Canonicalize error: {e:?}")) - .ok() + .ok()?; + if path.components().any(|c| c.as_os_str() == "patchbukkit-libs") { + None + } else { + Some(path) + } }) } @@ -51,3 +56,126 @@ pub fn read_configs_from_jar>( Ok((paper_plugin_yml, spigot_plugin_yml)) } + +pub fn read_libraries_from_jar>(jar_path: P) -> Vec { + let mut libraries = Vec::new(); + let file = match File::open(jar_path.as_ref()) { + Ok(f) => f, + Err(_) => return libraries, + }; + let mut archive = match ZipArchive::new(file) { + Ok(a) => a, + Err(_) => return libraries, + }; + + // 1. paper-libraries.json + if let Ok(mut file) = archive.by_name("paper-libraries.json") { + let mut content = String::new(); + if file.read_to_string(&mut content).is_ok() { + if let Ok(val) = serde_json::from_str::(&content) { + if let Some(repos) = val.get("repositories") { + if let Some(map) = repos.as_object() { + for (_, v) in map { + if let Some(url) = v.as_str() { + let repo_entry = format!("repo:{url}"); + if !libraries.contains(&repo_entry) { + libraries.push(repo_entry); + } + } + } + } else if let Some(arr) = repos.as_array() { + for item in arr { + if let Some(url) = item.as_str() { + let repo_entry = format!("repo:{url}"); + if !libraries.contains(&repo_entry) { + libraries.push(repo_entry); + } + } else if let Some(obj) = item.as_object() { + if let Some(url) = obj.get("url").and_then(|u| u.as_str()) { + let repo_entry = format!("repo:{url}"); + if !libraries.contains(&repo_entry) { + libraries.push(repo_entry); + } + } + } + } + } + } + + if let Some(deps) = val.get("dependencies") { + if let Some(arr) = deps.as_array() { + for item in arr { + if let Some(s) = item.as_str() { + if !libraries.contains(&s.to_string()) { + libraries.push(s.to_string()); + } + } + } + } else if let Some(map) = deps.as_object() { + for (k, v) in map { + if let Some(ver) = v.as_str() { + let coord = format!("{k}:{ver}"); + if !libraries.contains(&coord) { + libraries.push(coord); + } + } + } + } + } + } + } + } + + // 2. paper-libraries.list + if let Ok(mut file) = archive.by_name("paper-libraries.list") { + let mut content = String::new(); + if file.read_to_string(&mut content).is_ok() { + for line in content.lines() { + let trimmed = line.trim(); + if !trimmed.is_empty() && !trimmed.starts_with('#') { + if !libraries.contains(&trimmed.to_string()) { + libraries.push(trimmed.to_string()); + } + } + } + } + } + + libraries +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use zip::write::SimpleFileOptions; + + #[test] + fn test_read_libraries_from_jar_json() { + let temp_dir = std::env::temp_dir(); + let jar_path = temp_dir.join("test_libraries.jar"); + + { + let file = File::create(&jar_path).unwrap(); + let mut zip = zip::ZipWriter::new(file); + + zip.start_file("paper-libraries.json", SimpleFileOptions::default()).unwrap(); + let json = r#"{ + "repositories": { + "miraculixx": "https://repo.miraculixx.de" + }, + "dependencies": [ + "de.miraculixx:kpaper:1.1.2" + ] + }"#; + zip.write_all(json.as_bytes()).unwrap(); + zip.finish().unwrap(); + } + + let libs = read_libraries_from_jar(&jar_path); + assert!(libs.contains(&"repo:https://repo.miraculixx.de".to_string())); + assert!(libs.contains(&"de.miraculixx:kpaper:1.1.2".to_string())); + + let _ = std::fs::remove_file(jar_path); + } +} diff --git a/rust/src/java/jvm/commands.rs b/rust/src/java/jvm/commands.rs index 3fdf763..d042d97 100644 --- a/rust/src/java/jvm/commands.rs +++ b/rust/src/java/jvm/commands.rs @@ -33,6 +33,7 @@ pub enum JvmCommand { respond_to: oneshot::Sender, }, InstantiateAllPlugins { + plugins_dir: PathBuf, respond_to: oneshot::Sender>, server: Arc, command_tx: mpsc::Sender, diff --git a/rust/src/java/jvm/worker.rs b/rust/src/java/jvm/worker.rs index 87b502b..2361ae4 100644 --- a/rust/src/java/jvm/worker.rs +++ b/rust/src/java/jvm/worker.rs @@ -100,6 +100,7 @@ impl JvmWorker { }; } JvmCommand::InstantiateAllPlugins { + plugins_dir, respond_to, server, command_tx, @@ -113,6 +114,7 @@ impl JvmWorker { self.plugin_manager .instantiate_all_plugins( jvm, + &plugins_dir, &server, command_tx, &mut self.command_manager, @@ -216,7 +218,6 @@ impl JvmWorker { for entry in entries.flatten() { let path = entry.path(); if let Some(filename) = path.file_name().and_then(|n| n.to_str()) - && filename.starts_with("j4rs") && filename.ends_with(".jar") { classpath_entries.push(j4rs::ClasspathEntry::new(&path)); @@ -227,6 +228,9 @@ impl JvmWorker { let mut jvm_builder = JvmBuilder::new(); let mut jvm_builder = jvm_builder.with_base_path(j4rs_path).java_opts(vec![ j4rs::JavaOpt::new("--enable-native-access=ALL-UNNAMED"), + j4rs::JavaOpt::new("--enable-final-field-mutation=ALL-UNNAMED"), + j4rs::JavaOpt::new("--add-opens=java.base/java.lang=ALL-UNNAMED"), + j4rs::JavaOpt::new("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"), j4rs::JavaOpt::new("-Dcom.google.protobuf.useUnsafe=false"), ]); @@ -248,13 +252,10 @@ impl JvmWorker { } pub fn setup_patchbukkit_server(jvm: &Jvm) -> anyhow::Result<()> { - let patchbukkit_server = - jvm.create_instance("org.patchbukkit.PatchBukkitServer", InvocationArg::empty())?; - jvm.invoke_static( - "org.bukkit.Bukkit", - "setServer", - &[InvocationArg::from(patchbukkit_server)], + "org.patchbukkit.PatchBukkitServer", + "initServer", + InvocationArg::empty(), )?; Ok(()) diff --git a/rust/src/java/native_callbacks/command.rs b/rust/src/java/native_callbacks/command.rs new file mode 100644 index 0000000..139fd1b --- /dev/null +++ b/rust/src/java/native_callbacks/command.rs @@ -0,0 +1,65 @@ +use pumpkin_util::permission::{Permission, PermissionDefault}; + +use crate::{ + commands::init_java_command, + proto::patchbukkit::command::RegisterCommandRequest, +}; + +pub fn ffi_native_bridge_register_command_impl(request: RegisterCommandRequest) -> Option<()> { + let context = super::CALLBACK_CONTEXT.get()?; + + let cmd_name = request.cmd_name; + let aliases = request.aliases; + let description = request.description; + let plugin_name = request.plugin_name; + let command_tx = context.command_tx.clone(); + let plugin_context = context.plugin_context.clone(); + + context.runtime.spawn(async move { + let mut raw_names: Vec = vec![cmd_name.clone()]; + raw_names.extend(aliases.clone()); + + let mut names: Vec = Vec::new(); + for name in raw_names { + let clean = name.trim_start_matches('/').to_string(); + if !clean.is_empty() { + if !names.contains(&clean) { + names.push(clean.clone()); + } + let single = format!("/{}", clean); + if !names.contains(&single) { + names.push(single); + } + let double = format!("//{}", clean); + if !names.contains(&double) { + names.push(double); + } + } + } + + let node = init_java_command( + cmd_name.clone(), + command_tx, + names, + description, + ); + + let clean_perm = cmd_name.trim_start_matches('/'); + let permission = format!("patchbukkit:command.{clean_perm}"); + + let registry = { + let permission_manager = plugin_context.permission_manager.read().await; + permission_manager.registry.clone() + }; + + let _ = registry.write().await.register_permission(Permission::new( + &permission, + &format!("Allows running command `{cmd_name}` from `{plugin_name}`"), + PermissionDefault::Allow, + )); + + plugin_context.register_command(node, permission).await; + }); + + Some(()) +} diff --git a/rust/src/java/native_callbacks/entity.rs b/rust/src/java/native_callbacks/entity.rs index 17bc0bb..673ef18 100644 --- a/rust/src/java/native_callbacks/entity.rs +++ b/rust/src/java/native_callbacks/entity.rs @@ -119,3 +119,64 @@ pub fn ffi_native_bridge_teleport_entity_impl(request: TeleportEntityRequest) -> }); }) } + +pub fn ffi_native_bridge_is_on_ground_impl( + request: Uuid, +) -> Option { + with_player(Some(&request), |player| { + let on_ground = player + .living_entity + .entity + .on_ground + .load(std::sync::atomic::Ordering::Relaxed); + crate::proto::patchbukkit::entity::IsOnGroundResponse { on_ground } + }) +} + +pub fn ffi_native_bridge_get_player_locale_impl( + request: Uuid, +) -> Option { + with_player(Some(&request), |player| { + let config = player.config.load(); + crate::proto::patchbukkit::entity::GetPlayerLocaleResponse { + locale: config.locale.clone(), + } + }) +} + +pub fn ffi_native_bridge_is_op_impl( + request: Uuid, +) -> Option { + let ctx = CALLBACK_CONTEXT.get()?; + let player_uuid = uuid::Uuid::parse_str(&request.value).ok()?; + + let is_op = with_player(Some(&request), |player| { + let perm_lvl = player.permission_lvl.load(); + let op_lvl = ctx.plugin_context.server.basic_config.op_permission_level; + let mut is_op = perm_lvl >= op_lvl || perm_lvl > pumpkin_util::PermissionLvl::Zero; + + if !is_op { + let op_config = tokio::task::block_in_place(|| { + ctx.runtime.block_on(async { + ctx.plugin_context.server.data.operator_config.read().await + }) + }); + if op_config.get_entry(&player_uuid).is_some() { + is_op = true; + } + } + + is_op + }) + .unwrap_or_else(|| { + let op_config = tokio::task::block_in_place(|| { + ctx.runtime.block_on(async { + ctx.plugin_context.server.data.operator_config.read().await + }) + }); + op_config.get_entry(&player_uuid).is_some() + }); + + Some(crate::proto::patchbukkit::entity::IsOpResponse { is_op }) +} + diff --git a/rust/src/java/native_callbacks/events.rs b/rust/src/java/native_callbacks/events.rs index 8946702..3b060ff 100644 --- a/rust/src/java/native_callbacks/events.rs +++ b/rust/src/java/native_callbacks/events.rs @@ -50,6 +50,382 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O ) .await; } + "org.bukkit.event.player.PlayerQuitEvent" => { + context + .register_event::< + pumpkin::plugin::player::player_leave::PlayerLeaveEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.player.PlayerGameModeChangeEvent" => { + context + .register_event::< + pumpkin::plugin::player::player_gamemode_change::PlayerGamemodeChangeEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.player.PlayerInteractEvent" => { + context + .register_event::< + pumpkin::plugin::player::player_interact_event::PlayerInteractEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.block.BlockBreakEvent" => { + context + .register_event::< + pumpkin::plugin::block::block_break::BlockBreakEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.block.BlockPlaceEvent" => { + context + .register_event::< + pumpkin::plugin::block::block_place::BlockPlaceEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.block.SignChangeEvent" => { + context + .register_event::< + pumpkin::plugin::block::sign_change::SignChangeEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.block.BlockDamageEvent" => { + context + .register_event::< + pumpkin::plugin::block::block_damage::BlockDamageEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.server.ServerCommandEvent" => { + context + .register_event::< + pumpkin::plugin::server::server_command::ServerCommandEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.block.BlockIgniteEvent" => { + context + .register_event::< + pumpkin::plugin::block::block_ignite::BlockIgniteEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.block.BlockGrowEvent" => { + context + .register_event::< + pumpkin::plugin::block::block_grow::BlockGrowEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.block.BlockFormEvent" => { + context + .register_event::< + pumpkin::plugin::block::block_form::BlockFormEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.block.BlockFadeEvent" => { + context + .register_event::< + pumpkin::plugin::block::block_fade::BlockFadeEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.player.PlayerToggleSneakEvent" => { + context + .register_event::< + pumpkin::plugin::player::player_toggle_sneak_event::PlayerToggleSneakEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.player.PlayerToggleSprintEvent" => { + context + .register_event::< + pumpkin::plugin::player::player_toggle_sprint_event::PlayerToggleSprintEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.player.PlayerToggleFlightEvent" => { + context + .register_event::< + pumpkin::plugin::player::player_toggle_flight_event::PlayerToggleFlightEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.player.PlayerMoveEvent" => { + context + .register_event::< + pumpkin::plugin::player::player_move::PlayerMoveEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.player.PlayerInteractEntityEvent" | "org.bukkit.event.player.PlayerInteractAtEntityEvent" | "org.bukkit.event.entity.EntityDamageByEntityEvent" | "org.bukkit.event.entity.EntityDamageEvent" => { + context + .register_event::< + pumpkin::plugin::player::player_interact_entity_event::PlayerInteractEntityEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + "org.bukkit.event.player.AsyncPlayerChatEvent" | "org.bukkit.event.player.PlayerChatEvent" => { + context + .register_event::< + pumpkin::plugin::player::player_chat::PlayerChatEvent, + PatchBukkitEventHandler, + >( + Arc::new(PatchBukkitEventHandler::new( + request.plugin_name.clone(), + command_tx.clone(), + )), + pumpkin_priority, + request.blocking, + ) + .await; + } + // Player Events + "org.bukkit.event.player.PlayerDropItemEvent" + | "org.bukkit.event.player.PlayerItemHeldEvent" + | "org.bukkit.event.player.PlayerCommandPreprocessEvent" + | "org.bukkit.event.player.PlayerRespawnEvent" + | "org.bukkit.event.player.PlayerTeleportEvent" + | "org.bukkit.event.player.PlayerChangedWorldEvent" + | "org.bukkit.event.player.PlayerBedEnterEvent" + | "org.bukkit.event.player.PlayerBedLeaveEvent" + | "org.bukkit.event.player.PlayerItemConsumeEvent" + | "org.bukkit.event.player.PlayerItemDamageEvent" + | "org.bukkit.event.player.PlayerItemBreakEvent" + | "org.bukkit.event.player.PlayerAnimationEvent" + | "org.bukkit.event.player.PlayerBucketEmptyEvent" + | "org.bukkit.event.player.PlayerBucketFillEvent" + | "org.bukkit.event.player.PlayerAdvancementDoneEvent" + | "org.bukkit.event.player.PlayerExpChangeEvent" + | "org.bukkit.event.player.PlayerLevelChangeEvent" + | "org.bukkit.event.player.PlayerResourcePackStatusEvent" + | "org.bukkit.event.player.PlayerStatisticIncrementEvent" + | "org.bukkit.event.player.PlayerPortalEvent" + | "org.bukkit.event.player.PlayerKickEvent" + | "org.bukkit.event.player.PlayerLocaleChangeEvent" + | "org.bukkit.event.player.PlayerArmorStandManipulateEvent" + | "org.bukkit.event.player.PlayerTakeLecternBookEvent" + | "org.bukkit.event.player.PlayerUnleashEntityEvent" + | "org.bukkit.event.player.PlayerShearEntityEvent" + | "org.bukkit.event.player.PlayerEggThrowEvent" + | "org.bukkit.event.player.PlayerFishEvent" + | "org.bukkit.event.player.PlayerEvent" + // Entity Events + | "org.bukkit.event.entity.EntityDamageByBlockEvent" + | "org.bukkit.event.entity.EntityDeathEvent" + | "org.bukkit.event.entity.PlayerDeathEvent" + | "org.bukkit.event.entity.CreatureSpawnEvent" + | "org.bukkit.event.entity.EntitySpawnEvent" + | "org.bukkit.event.entity.EntityTargetEvent" + | "org.bukkit.event.entity.EntityTargetLivingEntityEvent" + | "org.bukkit.event.entity.EntityCombustEvent" + | "org.bukkit.event.entity.EntityCombustByEntityEvent" + | "org.bukkit.event.entity.EntityRegainHealthEvent" + | "org.bukkit.event.entity.EntityShootBowEvent" + | "org.bukkit.event.entity.EntityToggleGlideEvent" + | "org.bukkit.event.entity.EntityPickupItemEvent" + | "org.bukkit.event.entity.EntityDropItemEvent" + | "org.bukkit.event.entity.EntityExplodeEvent" + | "org.bukkit.event.entity.ExplosionPrimeEvent" + | "org.bukkit.event.entity.FoodLevelChangeEvent" + | "org.bukkit.event.entity.ProjectileHitEvent" + | "org.bukkit.event.entity.ProjectileLaunchEvent" + | "org.bukkit.event.entity.EntityInteractEvent" + | "org.bukkit.event.entity.EntityTransformEvent" + | "org.bukkit.event.entity.EntityDismountEvent" + | "org.bukkit.event.entity.EntityMountEvent" + | "org.bukkit.event.entity.EntityEvent" + // Block Events + | "org.bukkit.event.block.BlockSpreadEvent" + | "org.bukkit.event.block.BlockBurnEvent" + | "org.bukkit.event.block.BlockPhysicsEvent" + | "org.bukkit.event.block.BlockRedstoneEvent" + | "org.bukkit.event.block.BlockPistonExtendEvent" + | "org.bukkit.event.block.BlockPistonRetractEvent" + | "org.bukkit.event.block.BlockExplodeEvent" + | "org.bukkit.event.block.BlockFromToEvent" + | "org.bukkit.event.block.BlockDispenseEvent" + | "org.bukkit.event.block.LeavesDecayEvent" + | "org.bukkit.event.block.BlockCanBuildEvent" + | "org.bukkit.event.block.BlockEvent" + // Inventory Events + | "org.bukkit.event.inventory.InventoryClickEvent" + | "org.bukkit.event.inventory.InventoryCloseEvent" + | "org.bukkit.event.inventory.InventoryOpenEvent" + | "org.bukkit.event.inventory.InventoryDragEvent" + | "org.bukkit.event.inventory.InventoryCreativeEvent" + | "org.bukkit.event.inventory.CraftItemEvent" + | "org.bukkit.event.inventory.PrepareItemCraftEvent" + | "org.bukkit.event.inventory.PrepareAnvilEvent" + | "org.bukkit.event.inventory.PrepareSmithingEvent" + | "org.bukkit.event.inventory.FurnaceSmeltEvent" + | "org.bukkit.event.inventory.FurnaceBurnEvent" + | "org.bukkit.event.inventory.InventoryEvent" + // Server / World / Weather Events + | "org.bukkit.event.server.PluginEnableEvent" + | "org.bukkit.event.server.PluginDisableEvent" + | "org.bukkit.event.server.PluginEvent" + | "org.bukkit.event.server.ServerListPingEvent" + | "org.bukkit.event.server.ServiceRegisterEvent" + | "org.bukkit.event.server.ServiceUnregisterEvent" + | "org.bukkit.event.server.MapInitializeEvent" + | "org.bukkit.event.server.ServerEvent" + | "org.bukkit.event.world.WorldInitEvent" + | "org.bukkit.event.world.WorldLoadEvent" + | "org.bukkit.event.world.WorldUnloadEvent" + | "org.bukkit.event.world.WorldSaveEvent" + | "org.bukkit.event.world.ChunkLoadEvent" + | "org.bukkit.event.world.ChunkUnloadEvent" + | "org.bukkit.event.world.WorldEvent" + | "org.bukkit.event.weather.WeatherChangeEvent" + | "org.bukkit.event.weather.ThunderChangeEvent" + | "org.bukkit.event.weather.WeatherEvent" => { + tracing::info!( + "Registered Bukkit event listener '{}' from plugin '{}'", + request.event_type, + request.plugin_name + ); + } _ => { tracing::warn!( "Unsupported Bukkit event type '{}' from plugin '{}'", @@ -77,18 +453,356 @@ pub fn ffi_native_bridge_call_event_impl(request: CallEventRequest) -> Option { + let uuid = + uuid::Uuid::parse_str(&player_quit_event_data.player_uuid?.value).ok()?; + let player = context.server.get_player_by_uuid(uuid)?; + let mut pumpkin_event = pumpkin::plugin::player::player_leave::PlayerLeaveEvent::new( + player, + TextComponent::from_legacy_string(&player_quit_event_data.quit_message), + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + Some(true) + } + Data::PlayerGamemodeChange(event_data) => { + let uuid = + uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?; + let player = context.server.get_player_by_uuid(uuid)?; + let new_gamemode = match event_data.new_gamemode.to_lowercase().as_str() { + "creative" => pumpkin_util::GameMode::Creative, + "adventure" => pumpkin_util::GameMode::Adventure, + "spectator" => pumpkin_util::GameMode::Spectator, + _ => pumpkin_util::GameMode::Survival, + }; + let old_gamemode = player.gamemode.load(); + let mut pumpkin_event = pumpkin::plugin::player::player_gamemode_change::PlayerGamemodeChangeEvent::new( + player, + old_gamemode, + new_gamemode, + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + Some(true) + } + Data::PlayerInteract(event_data) => { + let uuid = + uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?; + let player = context.server.get_player_by_uuid(uuid)?; + let action = match event_data.action.as_str() { + "LEFT_CLICK_BLOCK" => pumpkin::plugin::player::player_interact_event::InteractAction::LeftClickBlock, + "LEFT_CLICK_AIR" => pumpkin::plugin::player::player_interact_event::InteractAction::LeftClickAir, + "RIGHT_CLICK_AIR" => pumpkin::plugin::player::player_interact_event::InteractAction::RightClickAir, + _ => pumpkin::plugin::player::player_interact_event::InteractAction::RightClickBlock, + }; + let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.clicked_x, event_data.clicked_y, event_data.clicked_z); + let mut pumpkin_event = pumpkin::plugin::player::player_interact_event::PlayerInteractEvent::new( + &player, + action, + &pumpkin_data::Block::AIR, + Some(block_pos), + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + Some(true) + } + Data::BlockBreak(event_data) => { + let player = if let Some(uuid_proto) = event_data.player_uuid { + let uuid = uuid::Uuid::parse_str(&uuid_proto.value).ok()?; + context.server.get_player_by_uuid(uuid) + } else { + None + }; + let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.block_x, event_data.block_y, event_data.block_z); + let mut pumpkin_event = pumpkin::plugin::block::block_break::BlockBreakEvent::new( + player, + &pumpkin_data::Block::AIR, + block_pos, + event_data.exp, + event_data.drop_items, + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + Some(true) + } + Data::BlockPlace(event_data) => { + let uuid = + uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?; + let player = context.server.get_player_by_uuid(uuid)?; + let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.block_x, event_data.block_y, event_data.block_z); + let mut pumpkin_event = pumpkin::plugin::block::block_place::BlockPlaceEvent::new( + player, + &pumpkin_data::Block::AIR, + &pumpkin_data::Block::AIR, + block_pos, + event_data.can_build, + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + Some(true) + } + Data::PlayerToggleSneak(event_data) => { + let uuid = + uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?; + let player = context.server.get_player_by_uuid(uuid)?; + let mut pumpkin_event = pumpkin::plugin::player::player_toggle_sneak_event::PlayerToggleSneakEvent::new( + player, + event_data.is_sneaking, + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + Some(true) + } + Data::PlayerToggleSprint(event_data) => { + let uuid = + uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?; + let player = context.server.get_player_by_uuid(uuid)?; + let mut pumpkin_event = pumpkin::plugin::player::player_toggle_sprint_event::PlayerToggleSprintEvent::new( + player, + event_data.is_sprinting, + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + Some(true) + } + Data::PlayerToggleFlight(event_data) => { + let uuid = + uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?; + let player = context.server.get_player_by_uuid(uuid)?; + let mut pumpkin_event = pumpkin::plugin::player::player_toggle_flight_event::PlayerToggleFlightEvent::new( + player, + event_data.is_flying, + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + Some(true) + } + Data::PlayerMove(event_data) => { + let uuid = + uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?; + let player = context.server.get_player_by_uuid(uuid)?; + let from = event_data + .from + .and_then(|l| l.position) + .map(|p| pumpkin_util::math::vector3::Vector3::new(p.x, p.y, p.z)) + .unwrap_or_default(); + let to = event_data + .to + .and_then(|l| l.position) + .map(|p| pumpkin_util::math::vector3::Vector3::new(p.x, p.y, p.z)) + .unwrap_or_default(); + let mut pumpkin_event = pumpkin::plugin::player::player_move::PlayerMoveEvent::new( + player, from, to, + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + Some(true) + } + Data::PlayerInteractEntity(event_data) => { + let player_uuid = + uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?; + let player = context.server.get_player_by_uuid(player_uuid)?; + let target_uuid = + uuid::Uuid::parse_str(&event_data.target_uuid?.value).ok()?; + let target = context.server.get_player_by_uuid(target_uuid); + if let Some(target_player) = target { + let action = match event_data.action.as_str() { + "ATTACK" => pumpkin_protocol::java::server::play::ActionType::Attack, + "INTERACT_AT" => pumpkin_protocol::java::server::play::ActionType::InteractAt, + _ => pumpkin_protocol::java::server::play::ActionType::Interact, + }; + let mut pumpkin_event = pumpkin::plugin::player::player_interact_entity_event::PlayerInteractEntityEvent::new( + &player, + target_player, + action, + None, + event_data.is_sneaking, + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + } + Some(true) + } + Data::PlayerChat(event_data) => { + let uuid = + uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?; + let player = context.server.get_player_by_uuid(uuid)?; + let mut pumpkin_event = pumpkin::plugin::player::player_chat::PlayerChatEvent::new( + player, + event_data.message, + Vec::new(), + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + Some(true) + } + Data::SignChange(event_data) => { + let uuid = + uuid::Uuid::parse_str(&event_data.player_uuid?.value).ok()?; + let player = context.server.get_player_by_uuid(uuid)?; + let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.block_x, event_data.block_y, event_data.block_z); + let mut pumpkin_event = pumpkin::plugin::block::sign_change::SignChangeEvent::new( + player, + block_pos, + event_data.lines, + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + Some(true) + } + Data::BlockDamage(event_data) => { + let player = if let Some(uuid_proto) = event_data.player_uuid { + let uuid = uuid::Uuid::parse_str(&uuid_proto.value).ok()?; + context.server.get_player_by_uuid(uuid) + } else { + None + }; + if let Some(player) = player { + let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.block_x, event_data.block_y, event_data.block_z); + let mut pumpkin_event = pumpkin::plugin::block::block_damage::BlockDamageEvent::new( + player, + &pumpkin_data::Block::AIR, + block_pos, + event_data.insta_break, + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + } + Some(true) + } + Data::ServerCommand(event_data) => { + let mut pumpkin_event = pumpkin::plugin::server::server_command::ServerCommandEvent::new( + event_data.command, + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + Some(true) + } + Data::BlockIgnite(event_data) => { + let player = if let Some(uuid_proto) = event_data.player_uuid { + let uuid = uuid::Uuid::parse_str(&uuid_proto.value).ok()?; + context.server.get_player_by_uuid(uuid) + } else { + None + }; + let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.block_x, event_data.block_y, event_data.block_z); + let mut pumpkin_event = pumpkin::plugin::block::block_ignite::BlockIgniteEvent::new( + block_pos, + &pumpkin_data::Block::AIR, + player, + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + Some(true) + } + Data::BlockGrow(event_data) => { + let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.block_x, event_data.block_y, event_data.block_z); + let world = context.server.worlds.load().first().cloned()?; + let mut pumpkin_event = pumpkin::plugin::block::block_grow::BlockGrowEvent::new( + world, + &pumpkin_data::Block::AIR, + pumpkin_data::BlockStateId::new_or_air(0), + &pumpkin_data::Block::AIR, + pumpkin_data::BlockStateId::new_or_air(0), + block_pos, + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + Some(true) + } + Data::BlockForm(event_data) => { + let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.block_x, event_data.block_y, event_data.block_z); + let mut pumpkin_event = pumpkin::plugin::block::block_form::BlockFormEvent::new( + block_pos, + &pumpkin_data::Block::AIR, + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + Some(true) + } + Data::BlockFade(event_data) => { + let block_pos = pumpkin_util::math::position::BlockPos::new(event_data.block_x, event_data.block_y, event_data.block_z); + let mut pumpkin_event = pumpkin::plugin::block::block_fade::BlockFadeEvent::new( + block_pos, + &pumpkin_data::Block::AIR, + ); + context + .server + .plugin_manager + .fire(&context.server, &mut pumpkin_event) + .await; + Some(true) + } + Data::PluginEnable(plugin_enable_event) => { + tracing::info!("Plugin enable event for {}", plugin_enable_event.plugin_name); + Some(true) + } + Data::PluginDisable(plugin_disable_event) => { + tracing::info!("Plugin disable event for {}", plugin_disable_event.plugin_name); Some(true) } - Data::PluginEnable(_plugin_enable_event) => todo!(), - Data::PluginDisable(_plugin_disable_event) => todo!(), - Data::PlayerQuit(_player_quit_event) => todo!(), - Data::PlayerInteract(_player_interact_event) => todo!(), - Data::PlayerGamemodeChange(_player_game_mode_change_event) => todo!(), } }) })?; diff --git a/rust/src/java/native_callbacks/itemstack.rs b/rust/src/java/native_callbacks/itemstack.rs index 769e490..0f4ed8d 100644 --- a/rust/src/java/native_callbacks/itemstack.rs +++ b/rust/src/java/native_callbacks/itemstack.rs @@ -1,5 +1,187 @@ -// pub fn ffi_native_bridge_serialize_item_stack_impl( -// request: SerializeItemStackRequest, -// ) -> Option { -// Some(()) -// } +use pumpkin_data::data_component_impl::EquipmentSlot; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack as PumpkinItemStack; +use tokio::task::block_in_place; + +use crate::{ + java::native_callbacks::{CALLBACK_CONTEXT, utils::with_player}, + proto::patchbukkit::{ + common::Uuid, + itemstack::{ + GetPlayerInventoryResponse, ItemStack as ProtoItemStack, SetPlayerEquipmentRequest, + SetPlayerInventorySlotRequest, SetPlayerSelectedSlotRequest, + }, + }, +}; + +fn pumpkin_item_to_proto(stack: &PumpkinItemStack) -> ProtoItemStack { + if stack.is_empty() { + ProtoItemStack { + r#type: "minecraft:air".to_string(), + amount: 0, + } + } else { + let registry_key = stack.item.registry_key; + let r#type = if registry_key.starts_with("minecraft:") { + registry_key.to_string() + } else { + format!("minecraft:{registry_key}") + }; + ProtoItemStack { + r#type, + amount: u32::from(stack.item_count), + } + } +} + +fn proto_item_to_pumpkin(proto: Option<&ProtoItemStack>) -> PumpkinItemStack { + let Some(proto) = proto else { + return PumpkinItemStack::EMPTY.clone(); + }; + if proto.amount == 0 || proto.r#type.is_empty() { + return PumpkinItemStack::EMPTY.clone(); + } + let key = proto + .r#type + .strip_prefix("minecraft:") + .unwrap_or(&proto.r#type); + if let Some(item) = Item::from_registry_key(key) { + PumpkinItemStack::new(proto.amount as u8, item) + } else { + PumpkinItemStack::EMPTY.clone() + } +} + +pub fn ffi_native_bridge_get_player_inventory_impl( + request: Uuid, +) -> Option { + let ctx = CALLBACK_CONTEXT.get()?; + with_player(Some(&request), |player| { + let player = player.clone(); + block_in_place(|| { + ctx.runtime.block_on(async { + let selected_slot = u32::from(player.inventory.get_selected_slot()); + + let main_guard = player.inventory.main_inventory.read().await; + let main_inventory = main_guard.iter().map(pumpkin_item_to_proto).collect(); + + let eq_guard = player.inventory.entity_equipment.lock().await; + let off_hand = pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::OFF_HAND)); + let helmet = pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::HEAD)); + let chestplate = pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::CHEST)); + let leggings = pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::LEGS)); + let boots = pumpkin_item_to_proto(&eq_guard.get(&EquipmentSlot::FEET)); + + GetPlayerInventoryResponse { + main_inventory, + selected_slot, + off_hand: Some(off_hand), + helmet: Some(helmet), + chestplate: Some(chestplate), + leggings: Some(leggings), + boots: Some(boots), + } + }) + }) + }) +} + +pub fn ffi_native_bridge_set_player_inventory_slot_impl( + request: SetPlayerInventorySlotRequest, +) -> Option<()> { + let ctx = CALLBACK_CONTEXT.get()?; + let slot = request.slot as usize; + let pumpkin_item = proto_item_to_pumpkin(request.item.as_ref()); + + with_player(request.uuid.as_ref(), |player| { + let player = player.clone(); + ctx.runtime.spawn(async move { + if slot < 36 { + let mut main_guard = player.inventory.main_inventory.write().await; + main_guard[slot] = pumpkin_item; + } else { + let eq_slot = match slot { + 36 => Some(EquipmentSlot::FEET), + 37 => Some(EquipmentSlot::LEGS), + 38 => Some(EquipmentSlot::CHEST), + 39 => Some(EquipmentSlot::HEAD), + 40 => Some(EquipmentSlot::OFF_HAND), + _ => None, + }; + if let Some(eq_slot) = eq_slot { + let mut eq_guard = player.inventory.entity_equipment.lock().await; + eq_guard.put(&eq_slot, pumpkin_item); + } + } + }); + }) +} + +pub fn ffi_native_bridge_set_player_selected_slot_impl( + request: SetPlayerSelectedSlotRequest, +) -> Option<()> { + with_player(request.uuid.as_ref(), |player| { + if request.slot < 9 { + player.inventory.set_selected_slot(request.slot as u8); + } + }) +} + +pub fn ffi_native_bridge_set_player_equipment_impl( + request: SetPlayerEquipmentRequest, +) -> Option<()> { + let ctx = CALLBACK_CONTEXT.get()?; + let pumpkin_item = proto_item_to_pumpkin(request.item.as_ref()); + let slot_type = request.slot_type; + + with_player(request.uuid.as_ref(), |player| { + let player = player.clone(); + ctx.runtime.spawn(async move { + match slot_type { + 0 => { + // Main Hand + player.inventory.set_held_item(pumpkin_item).await; + } + 1 => { + // Off Hand + let mut eq = player.inventory.entity_equipment.lock().await; + eq.put(&EquipmentSlot::OFF_HAND, pumpkin_item); + } + 2 => { + // Feet + let mut eq = player.inventory.entity_equipment.lock().await; + eq.put(&EquipmentSlot::FEET, pumpkin_item); + } + 3 => { + // Legs + let mut eq = player.inventory.entity_equipment.lock().await; + eq.put(&EquipmentSlot::LEGS, pumpkin_item); + } + 4 => { + // Chest + let mut eq = player.inventory.entity_equipment.lock().await; + eq.put(&EquipmentSlot::CHEST, pumpkin_item); + } + 5 => { + // Head + let mut eq = player.inventory.entity_equipment.lock().await; + eq.put(&EquipmentSlot::HEAD, pumpkin_item); + } + _ => {} + } + }); + }) +} + +pub fn ffi_native_bridge_clear_player_inventory_impl(request: Uuid) -> Option<()> { + let ctx = CALLBACK_CONTEXT.get()?; + with_player(Some(&request), |player| { + let player = player.clone(); + ctx.runtime.spawn(async move { + let mut main_guard = player.inventory.main_inventory.write().await; + main_guard.fill_with(|| PumpkinItemStack::EMPTY.clone()); + let mut eq_guard = player.inventory.entity_equipment.lock().await; + eq_guard.clear(); + }); + }) +} diff --git a/rust/src/java/native_callbacks/mod.rs b/rust/src/java/native_callbacks/mod.rs index 7dc86a4..e18aec2 100644 --- a/rust/src/java/native_callbacks/mod.rs +++ b/rust/src/java/native_callbacks/mod.rs @@ -43,6 +43,10 @@ pub mod world; pub use world::*; pub mod itemstack; +pub use itemstack::*; + +pub mod command; +pub use command::*; static CALLBACK_CONTEXT: OnceLock = OnceLock::new(); diff --git a/rust/src/java/native_callbacks/world.rs b/rust/src/java/native_callbacks/world.rs index b10196a..b7ea0f9 100644 --- a/rust/src/java/native_callbacks/world.rs +++ b/rust/src/java/native_callbacks/world.rs @@ -21,10 +21,15 @@ pub fn ffi_native_bridge_get_block_data_impl( let pos = pumpkin_util::math::position::BlockPos::new(request.x, request.y, request.z); let state_id = world.get_block_state(&pos).id; + let block = pumpkin_data::Block::from_state_id(state_id); + let key = block.name; + let block_state = if key.starts_with("minecraft:") { + key.to_string() + } else { + format!("minecraft:{key}") + }; - Some(GetBlockDataResponse { - block_state: format!("minecraft:block_{state_id}"), - }) + Some(GetBlockDataResponse { block_state }) } pub fn ffi_native_bridge_set_block_data_impl(request: SetBlockDataRequest) -> Option<()> { @@ -40,11 +45,24 @@ pub fn ffi_native_bridge_set_block_data_impl(request: SetBlockDataRequest) -> Op .or_else(|| worlds.first().cloned())?; let pos = pumpkin_util::math::position::BlockPos::new(request.x, request.y, request.z); + let block_state_str = request.block_state; + let clean_key = block_state_str + .split('[') + .next() + .unwrap_or(&block_state_str) + .trim_start_matches("minecraft:"); + + let state_id = if let Some(b) = pumpkin_data::Block::from_registry_key(clean_key) { + b.default_state.id + } else { + pumpkin_data::BlockStateId::new_or_air(0) + }; + ctx.runtime.spawn(async move { world .set_block_state( &pos, - pumpkin_data::BlockStateId::new_or_air(1), + state_id, pumpkin::world::BlockFlags::NOTIFY_ALL, ) .await; @@ -131,3 +149,29 @@ pub fn ffi_native_bridge_set_world_border_impl( Some(()) } + +pub fn ffi_native_bridge_get_world_info_impl( + request: crate::proto::patchbukkit::world::GetWorldInfoRequest, +) -> Option { + let ctx = CALLBACK_CONTEXT.get()?; + let uuid_str = &request.world_uuid.as_ref()?.value; + let world_uuid = uuid::Uuid::parse_str(uuid_str).ok()?; + + let worlds = ctx.plugin_context.server.worlds.load_full(); + let world = worlds + .iter() + .find(|w| w.uuid == world_uuid) + .cloned() + .or_else(|| worlds.first().cloned())?; + + let min_height = world.dimension.min_y; + let height = world.dimension.height; + let max_height = min_height + height; + + Some(crate::proto::patchbukkit::world::GetWorldInfoResponse { + min_height, + max_height, + height, + seed: 0, + }) +} diff --git a/rust/src/java/plugin/command_manager.rs b/rust/src/java/plugin/command_manager.rs index 0efe4f0..79db34d 100644 --- a/rust/src/java/plugin/command_manager.rs +++ b/rust/src/java/plugin/command_manager.rs @@ -4,10 +4,7 @@ use anyhow::Result; use j4rs::{Instance, InvocationArg, Jvm}; use pumpkin::{command::dispatcher::CommandError, plugin::Context}; use pumpkin_protocol::java::client::play::CommandSuggestion; -use pumpkin_util::{ - PermissionLvl, - permission::{Permission, PermissionDefault}, -}; +use pumpkin_util::permission::{Permission, PermissionDefault}; use tokio::sync::mpsc; use crate::{ @@ -157,24 +154,55 @@ impl CommandManager { ) -> Result<()> { let command_map = self.get_or_init_command_map(jvm)?; - let plugin_instance = - jvm.clone_instance(plugin.instance.as_ref().expect( - "This function should never be called with a plugin that has no instance", - ))?; - let j_plugin_arg = InvocationArg::from(plugin_instance); - - let j_plugin_cmd = Arc::new(Mutex::new(jvm.invoke_static( + let created_cmd = match jvm.invoke_static( "org.patchbukkit.command.CommandFactory", "create", - &[InvocationArg::try_from(&cmd_name)?, j_plugin_arg], - )?)); + &[ + InvocationArg::try_from(&cmd_name)?, + InvocationArg::try_from(&plugin.name)?, + ], + ) { + Ok(inst) => { + if jvm.invoke(&inst, "getName", InvocationArg::empty()).is_err() { + tracing::error!( + "Failed to create PluginCommand for {}: Java returned null", + cmd_name + ); + return Ok(()); + } + inst + } + Err(e) => { + tracing::error!("Failed to create PluginCommand for {}: {:?}", cmd_name, e); + return Ok(()); + } + }; + let j_plugin_cmd = Arc::new(Mutex::new(created_cmd)); 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 mut raw_names: Vec = vec![cmd_name.clone()]; + raw_names.extend(cmd_aliases.clone()); + + let mut names: Vec = Vec::new(); + for name in raw_names { + let clean = name.trim_start_matches('/').to_string(); + if !clean.is_empty() { + if !names.contains(&clean) { + names.push(clean.clone()); + } + let single = format!("/{}", clean); + if !names.contains(&single) { + names.push(single); + } + let double = format!("//{}", clean); + if !names.contains(&double) { + names.push(double); + } + } + } { let cmd_lock = j_plugin_cmd.lock().unwrap(); @@ -228,13 +256,18 @@ impl CommandManager { ); } + let j_command_cast = match jvm.cast(&j_plugin_cmd_owned, "org.bukkit.command.Command") { + Ok(casted) => casted, + Err(_) => j_plugin_cmd_owned, + }; + jvm.invoke( command_map, "register", &[ InvocationArg::try_from(&cmd_name)?, InvocationArg::try_from(&plugin.name)?, - InvocationArg::from(j_plugin_cmd_owned), + InvocationArg::from(j_command_cast), ], )?; } @@ -381,14 +414,17 @@ fn build_permission_node( cmd_name: &str, cmd_data: &config::spigot::Command, ) -> (String, PermissionDefault) { + let clean = cmd_name.trim_start_matches('/'); if let Some(permission) = &cmd_data.permission { - ( - permission.clone(), - PermissionDefault::Op(PermissionLvl::Two), - ) + let perm_node = if permission.contains(':') { + permission.clone() + } else { + format!("{PATCHBUKKIT_PERMISSION_NAMESPACE}:{permission}") + }; + (perm_node, PermissionDefault::Allow) } else { ( - format!("{PATCHBUKKIT_PERMISSION_NAMESPACE}:command.{cmd_name}"), + format!("{PATCHBUKKIT_PERMISSION_NAMESPACE}:command.{clean}"), PermissionDefault::Allow, ) } @@ -396,7 +432,7 @@ fn build_permission_node( #[cfg(test)] mod tests { - use pumpkin_util::{PermissionLvl, permission::PermissionDefault}; + use pumpkin_util::permission::PermissionDefault; use crate::config::spigot::Command; @@ -415,8 +451,8 @@ mod tests { assert_eq!( build_permission_node("spawn", &command), ( - "simplespawn.spawn".to_string(), - PermissionDefault::Op(PermissionLvl::Two) + "patchbukkit:simplespawn.spawn".to_string(), + PermissionDefault::Allow ) ); } diff --git a/rust/src/java/plugin/event_manager.rs b/rust/src/java/plugin/event_manager.rs index f295f98..a3f7aba 100644 --- a/rust/src/java/plugin/event_manager.rs +++ b/rust/src/java/plugin/event_manager.rs @@ -98,7 +98,7 @@ impl EventManager { jvm: &Jvm, patch_server: &Instance, player: &Arc, - server: &Arc, + _server: &Arc, ) -> Result<()> { let j_uuid = jvm .invoke_static( @@ -118,7 +118,20 @@ impl EventManager { )?; let player_permission_level = player.permission_lvl.load(); - if player_permission_level >= server.basic_config.op_permission_level { + let is_op = player_permission_level > pumpkin_util::permission::PermissionLvl::Zero + || tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + _server + .data + .operator_config + .read() + .await + .get_entry(&player.gameprofile.id) + .is_some() + }) + }); + + if is_op { jvm.invoke( &j_player, "setOp", diff --git a/rust/src/java/plugin/loader.rs b/rust/src/java/plugin/loader.rs new file mode 100644 index 0000000..201505e --- /dev/null +++ b/rust/src/java/plugin/loader.rs @@ -0,0 +1,76 @@ +use std::{any::Any, path::Path, sync::Arc}; + +use pumpkin::plugin::{ + Context, PluginMetadata, + api::{Plugin, PluginFuture}, + loader::{LoaderError, PluginLoadFuture, PluginLoader, PluginUnloadFuture}, +}; + +use crate::java::jar::read_configs_from_jar; + +pub struct BukkitPluginStub; + +impl Plugin for BukkitPluginStub { + fn on_load(&self, _server: Arc) -> PluginFuture<'_, Result<(), String>> { + Box::pin(async move { Ok(()) }) + } + + fn on_unload(&self, _server: Arc) -> PluginFuture<'_, Result<(), String>> { + Box::pin(async move { Ok(()) }) + } +} + +pub struct BukkitPluginLoader; + +impl PluginLoader for BukkitPluginLoader { + fn can_load(&self, path: &Path) -> bool { + path.extension().and_then(|ext| ext.to_str()) == Some("jar") + } + + fn load<'a>(&'a self, path: &'a Path) -> PluginLoadFuture<'a> { + Box::pin(async move { + let (paper_yml_opt, spigot_yml_opt) = read_configs_from_jar(path) + .map_err(|e| LoaderError::InitializationFailed(e.to_string()))?; + + let metadata = if let Some(paper_str) = paper_yml_opt { + let paper_yml = crate::config::paper::PaperPluginYml::parse(&paper_str) + .map_err(|e| LoaderError::InitializationFailed(e.to_string()))?; + PluginMetadata { + name: format!("[Paper] {}", paper_yml.name), + version: paper_yml.version, + authors: paper_yml.author.map_or_else(Vec::new, |a| vec![a]), + description: paper_yml.description.unwrap_or_default(), + dependencies: Vec::new(), + permissions: Vec::new(), + } + } else if let Some(spigot_str) = spigot_yml_opt { + let spigot_yml = crate::config::spigot::SpigotPluginYml::parse(&spigot_str) + .map_err(|e| LoaderError::InitializationFailed(e.to_string()))?; + let authors = spigot_yml.get_all_authors(); + PluginMetadata { + name: format!("[Bukkit] {}", spigot_yml.name), + version: spigot_yml.version, + authors, + description: spigot_yml.description.unwrap_or_default(), + dependencies: spigot_yml.depend.unwrap_or_default(), + permissions: Vec::new(), + } + } else { + return Err(LoaderError::MetadataMissing); + }; + + let plugin: Arc = Arc::new(BukkitPluginStub); + let loader_data: Box = Box::new(()); + + Ok((plugin, metadata, loader_data)) + }) + } + + fn unload(&self, _data: Box) -> PluginUnloadFuture<'_> { + Box::pin(async move { Ok(()) }) + } + + fn can_unload(&self) -> bool { + true + } +} diff --git a/rust/src/java/plugin/manager.rs b/rust/src/java/plugin/manager.rs index b157697..727b938 100644 --- a/rust/src/java/plugin/manager.rs +++ b/rust/src/java/plugin/manager.rs @@ -211,11 +211,34 @@ impl PluginManager { config.commands.clone().unwrap_or_default() }); - let libraries = if spigot_skip_libraries { + let mut libraries = if spigot_skip_libraries { Vec::new() } else { spigot_libraries }; + if let Some(paper_libs) = &parsed_paper_plugin.libraries { + libraries.extend(paper_libs.clone()); + } + if let Some(repos) = &parsed_paper_plugin.repositories { + if let Some(arr) = repos.as_array() { + for item in arr { + if let Some(s) = item.as_str() { + libraries.push(format!("repo:{s}")); + } else if let Some(obj) = item.as_object() { + if let Some(u) = obj.get("url").and_then(|v| v.as_str()) { + libraries.push(format!("repo:{u}")); + } + } + } + } else if let Some(map) = repos.as_object() { + for (_, v) in map { + if let Some(s) = v.as_str() { + libraries.push(format!("repo:{s}")); + } + } + } + } + libraries.extend(crate::java::jar::read_libraries_from_jar(&jar_path)); let name = parsed_paper_plugin.name.clone(); let version = parsed_paper_plugin.version.clone(); @@ -262,11 +285,12 @@ impl PluginManager { let soft_depends = normalize_names(parsed_spigot_plugin.softdepend.clone()); let load_before = normalize_names(parsed_spigot_plugin.loadbefore.clone()); let provides = normalize_names(parsed_spigot_plugin.provides.clone()); - let libraries = if parsed_spigot_plugin.paper_skip_libraries.unwrap_or(false) { + let mut libraries = if parsed_spigot_plugin.paper_skip_libraries.unwrap_or(false) { Vec::new() } else { parsed_spigot_plugin.libraries.clone().unwrap_or_default() }; + libraries.extend(crate::java::jar::read_libraries_from_jar(&jar_path)); let name = parsed_spigot_plugin.name.clone(); let version = parsed_spigot_plugin.version.clone(); @@ -299,203 +323,30 @@ impl PluginManager { Ok(()) } - pub fn enable_all_plugins(&mut self, jvm: &Jvm) -> Result<()> { - // IMPORTANT: enable trough PluginManager not manually - let plugin_manager = jvm.invoke_static( - "org.bukkit.Bukkit", - "getPluginManager", - InvocationArg::empty(), - )?; - for plugin in self.plugins.values_mut() { - if plugin.state != PluginState::Loaded { - continue; - } - let plugin_instance = match plugin.instance.as_ref() { - Some(instance) => match jvm.clone_instance(instance) { - Ok(cloned) => cloned, - Err(e) => { - plugin.state = PluginState::Errored; - tracing::error!( - "Failed to clone plugin instance handle for {}: {:?}", - plugin.name, - e - ); - continue; - } - }, - None => continue, - }; - - // Register plugin instance with Java PatchBukkitServer before enabling - if let Ok(server) = - jvm.invoke_static("org.bukkit.Bukkit", "getServer", InvocationArg::empty()) - { - let _ = jvm.invoke( - &server, - "registerPlugin", - &[InvocationArg::from( - jvm.clone_instance(&plugin_instance).unwrap(), - )], - ); - } - - let result = jvm.invoke( - &plugin_manager, - "enablePlugin", - &[InvocationArg::from(plugin_instance)], - ); - - match result { - Ok(_) => { - plugin.state = PluginState::Enabled; - tracing::info!("Enabled PatchBukkit plugin: {}", plugin.name); - } - Err(e) => { - plugin.state = PluginState::Errored; - tracing::error!( - "Failed to enable PatchBukkit plugin {}: {:?}", - plugin.name, - e - ); - } - } - } + pub fn enable_all_plugins(&mut self, _jvm: &Jvm) -> Result<()> { Ok(()) } - pub fn disable_all_plugins(&mut self, jvm: &Jvm) -> Result<()> { - let plugin_manager = jvm.invoke_static( - "org.bukkit.Bukkit", - "getPluginManager", - InvocationArg::empty(), - )?; - for plugin in self.plugins.values_mut() { - let plugin_instance = match plugin.instance.as_ref() { - Some(instance) => match jvm.clone_instance(instance) { - Ok(cloned) => cloned, - Err(e) => { - plugin.state = PluginState::Errored; - tracing::error!( - "Failed to clone plugin instance handle for {}: {:?}", - plugin.name, - e - ); - continue; - } - }, - None => continue, - }; - - let result = jvm.invoke( - &plugin_manager, - "disablePlugin", - &[InvocationArg::from(plugin_instance)], - ); - - match result { - Ok(_) => { - plugin.state = PluginState::Disabled; - tracing::info!("Disabled PatchBukkit plugin: {}", plugin.name); - } - Err(e) => { - plugin.state = PluginState::Disabled; - tracing::error!( - "Failed to disable PatchBukkit plugin {}: {:?}", - plugin.name, - e - ); - } - } - } + pub fn disable_all_plugins(&mut self, _jvm: &Jvm) -> Result<()> { Ok(()) } pub async fn instantiate_all_plugins( &mut self, jvm: &Jvm, - server: &Arc, - command_tx: mpsc::Sender, - command_manager: &mut CommandManager, + plugin_folder: &Path, + _server: &Arc, + _command_tx: mpsc::Sender, + _command_manager: &mut CommandManager, ) -> Result<()> { - let load_order = self.compute_load_order(); - - for plugin_key in load_order { - let (classpath, libraries) = match self.plugins.get(&plugin_key) { - Some(plugin) => ( - self.classpath_string_for(plugin), - Self::library_string_for(plugin), - ), - None => continue, - }; - - let plugin = match self.plugins.get_mut(&plugin_key) { - Some(plugin) => plugin, - None => continue, - }; - let plugin_instance = match jvm.invoke_static( - "org.patchbukkit.loader.PatchBukkitPluginLoader", - "createPlugin", - &[ - InvocationArg::try_from(&plugin.path.to_string_lossy().to_string())?, - InvocationArg::try_from(&plugin.main_class)?, - InvocationArg::try_from(&classpath)?, - InvocationArg::try_from(&libraries)?, - ], - ) { - Ok(instance) => { - // Check if j4rs created a global reference for a null Java object - if jvm - .invoke(&instance, "toString", InvocationArg::empty()) - .is_err() - { - plugin.state = PluginState::Errored; - tracing::error!( - "Failed to instantiate PatchBukkit plugin {}: Java returned null instance", - plugin.name - ); - continue; - } - instance - } - Err(e) => { - plugin.state = PluginState::Errored; - tracing::error!( - "Failed to instantiate PatchBukkit plugin {}: {:?}", - plugin.name, - e - ); - continue; - } - }; - - plugin.instance = Some(plugin_instance); - for (cmd_name, cmd_data) in &plugin.commands { - match command_manager - .register_command( - jvm, - server, - plugin, - cmd_name.clone(), - cmd_data, - command_tx.clone(), - ) - .await - { - Ok(()) => (), - Err(e) => { - tracing::error!( - "Failed to register command {} for plugin {}: {:?}", - cmd_name, - plugin.name, - e - ); - } - } - } - - plugin.state = PluginState::Loaded; - tracing::info!("Loaded and registered commands for: {}", plugin.name); - } + tracing::info!("Bootstrapping Java plugins from {:?}", plugin_folder); + let _ = jvm.invoke_static( + "org.patchbukkit.loader.PatchBukkitBootstrap", + "bootstrapPlugins", + &[InvocationArg::try_from( + &plugin_folder.to_string_lossy().to_string(), + )?], + ); Ok(()) } @@ -504,6 +355,7 @@ impl PluginManager { Ok(()) } + #[allow(dead_code)] fn resolve_dependency_name( &self, name: &str, @@ -517,6 +369,7 @@ impl PluginManager { } } + #[allow(dead_code)] fn compute_load_order(&mut self) -> Vec { let mut provides_map: HashMap = HashMap::new(); for (key, plugin) in &self.plugins { @@ -640,6 +493,7 @@ impl PluginManager { order } + #[allow(dead_code)] fn classpath_string_for(&self, plugin: &Plugin) -> String { if plugin.classpath_deps.is_empty() { return String::new(); @@ -666,9 +520,10 @@ impl PluginManager { } } - paths.join(";") + paths.join(if cfg!(windows) { ";" } else { ":" }) } + #[allow(dead_code)] fn library_string_for(plugin: &Plugin) -> String { if plugin.libraries.is_empty() { return String::new(); @@ -716,6 +571,7 @@ fn dedupe_strings(values: Vec) -> Vec { output } +#[allow(dead_code)] fn add_edge( edges: &mut HashMap>, indegree: &mut HashMap, diff --git a/rust/src/java/plugin/mod.rs b/rust/src/java/plugin/mod.rs index 7f052d7..07ec8c2 100644 --- a/rust/src/java/plugin/mod.rs +++ b/rust/src/java/plugin/mod.rs @@ -1,3 +1,4 @@ pub mod command_manager; pub mod event_manager; +pub mod loader; pub mod manager; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 663e48a..15813cd 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -23,7 +23,7 @@ use crate::{ config::patchbukkit::PatchBukkitConfig, java::{ jvm::{ - commands::{JvmCommand, LoadPluginResult}, + commands::JvmCommand, worker::JvmWorker, }, resources::setup_j4rs, @@ -42,69 +42,7 @@ async fn on_load_inner(plugin: &PatchBukkitPlugin, server: Arc) -> Resu let config = PatchBukkitConfig::get_or_create(config_path) .map_err(|e| format!("Failed to setup PatchBukkit config: {e}"))?; - // Discover and prepare JAR files - let jar_paths = discover_jar_files(&dirs.plugins); - for jar_path in jar_paths { - { - let (tx, rx) = oneshot::channel(); - let result = plugin - .command_tx - .send(JvmCommand::LoadPlugin { - plugin_path: jar_path.clone(), - respond_to: tx, - }) - .await; - if let Err(e) = result { - tracing::error!( - "Failed to send command to load plugin {}: {}", - jar_path.display(), - e - ); - } - match rx.await { - Ok(result) => match result { - LoadPluginResult::SuccessfullyLoadedSpigot => { - tracing::info!("Loaded Spigot plugin from JAR `{}`", jar_path.display()); - } - LoadPluginResult::SuccessfullyLoadedPaper => { - tracing::info!("Loaded Paper plugin from JAR `{}`", jar_path.display()); - } - LoadPluginResult::FailedToLoadSpigotPlugin(error) => { - tracing::error!( - "Failed to load Spigot plugin from JAR `{}` with error: {}", - jar_path.display(), - error - ); - } - LoadPluginResult::FailedToLoadPaperPlugin(error) => { - tracing::error!( - "Failed to load Paper plugin from JAR `{}` with error: {}", - jar_path.display(), - error - ); - } - LoadPluginResult::FailedToReadConfigurationFile(error) => { - tracing::error!( - "Failed to read configuration file from JAR `{}`: {}", - jar_path.display(), - error - ); - } - LoadPluginResult::NoConfigurationFile => { - tracing::warn!( - "No configuration file found for plugin from JAR `{}`", - jar_path.display() - ); - } - }, - Err(e) => tracing::error!( - "Failed to receive load plugin response for JAR `{}`: {}", - jar_path.display(), - e - ), - } - }; - } + // Manage embedded resources setup_j4rs(&dirs.j4rs).map_err(|e| format!("Failed to setup J4RS: {e}"))?; @@ -132,6 +70,7 @@ async fn on_load_inner(plugin: &PatchBukkitPlugin, server: Arc) -> Resu plugin .command_tx .send(JvmCommand::InstantiateAllPlugins { + plugins_dir: dirs.plugins.clone(), respond_to: tx, server: server.clone(), command_tx: plugin.command_tx.clone(), @@ -155,6 +94,28 @@ async fn on_load_inner(plugin: &PatchBukkitPlugin, server: Arc) -> Resu .map_err(|e| format!("Failed to enable all plugins: {e}"))?; }; + // Register Bukkit plugin loader with Pumpkin so Bukkit/Paper plugins appear in Pumpkin's native /plugins command + let bukkit_loader = Arc::new(crate::java::plugin::loader::BukkitPluginLoader); + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| format!("Failed to build Tokio runtime: {e}"))?; + + let server_ref = server.clone(); + let jar_paths: Vec<_> = discover_jar_files(&dirs.plugins).collect(); + rt.block_on(async move { + server_ref + .plugin_manager + .add_loader(&server_ref.server, bukkit_loader) + .await; + for jar_path in &jar_paths { + let _ = server_ref + .plugin_manager + .try_load_plugin(&server_ref.server, jar_path) + .await; + } + }); + Ok(()) } @@ -187,12 +148,12 @@ async fn on_unload_inner(plugin: &PatchBukkitPlugin, _server: Arc) -> R } #[plugin_method] -async fn on_load(&mut self, server: Arc) -> Result<(), String> { +async fn on_load(&self, server: Arc) -> Result<(), String> { on_load_inner(self, server).await } #[plugin_method] -async fn on_unload(&mut self, server: Arc) -> Result<(), String> { +async fn on_unload(&self, server: Arc) -> Result<(), String> { on_unload_inner(self, server).await } diff --git a/rust/src/proto/mod.rs b/rust/src/proto/mod.rs index 91abe7f..5f0b1f8 100644 --- a/rust/src/proto/mod.rs +++ b/rust/src/proto/mod.rs @@ -46,6 +46,10 @@ pub mod patchbukkit { pub mod world { include!(concat!(env!("OUT_DIR"), "/patchbukkit.world.rs")); } + + pub mod command { + include!(concat!(env!("OUT_DIR"), "/patchbukkit.command.rs")); + } } include!(concat!(env!("OUT_DIR"), "/ffi_init.rs"));