From 95a7ac41e17d54cfdb420597b5f9fa4ce59c24ef Mon Sep 17 00:00:00 2001 From: Dylan Garvis Date: Fri, 16 Oct 2026 23:46:50 -0400 Subject: [PATCH] feat(platform): support Purpur 26.3 --- README.md | 10 +++++----- build.gradle.kts | 14 +++++++------- .../AdvancedAnvilProtocolGuard.java | 2 +- .../NativeAnvilGeneration.java | 2 +- .../NativeAdvancedAnvilPacketTest.java | 5 ++++- .../NativeAdvancedAnvilProtocolTest.java | 2 +- .../NativeAnvilFixture.java | 16 +++++++++------- .../NativeAnvilPacketFixture.java | 4 +++- .../NativeCobwebPluginWiringTest.java | 8 ++++---- .../NativePowderSnowBookTest.java | 12 ++++++------ .../NativePowderSnowFixture.java | 6 +++--- .../NativePowderSnowPluginWiringTest.java | 8 ++++---- .../NativeReinforcementCraftTest.java | 10 +++++----- .../spigotinventoryhelper/NativeRuntime.java | 19 ++++++++++++++----- .../PurpurDistributionTest.java | 17 +++++++++++++---- 15 files changed, 80 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index b2f41ce..799499e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Purpur Inventory Helper -A Purpur 26.2 plugin with Sorting Sticks, automatic broken-tool replacement, alternative Dispenser/Cobweb recipes, Powder Snow crafting, and risky over-level enchantment combining in ordinary anvils. +A Purpur 26.3 plugin with Sorting Sticks, automatic broken-tool replacement, alternative Dispenser/Cobweb recipes, Powder Snow crafting, and risky over-level enchantment combining in ordinary anvils. The approved behavior is specified in the [SoMC OKF wiki](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/purpur-inventory-helper/index.md). @@ -8,7 +8,7 @@ The repository, checkout and new JARs use `purpur-inventory-helper`. Runtime `Sp ## Requirements -- Purpur 26.2 build 2618 +- Purpur 26.3 build 2651 - Java 25 or newer - ProtocolLib 5.4.0 (required runtime library; missing ProtocolLib prevents plugin loading) @@ -20,9 +20,9 @@ The repository, checkout and new JARs use `purpur-inventory-helper`. Runtime `Sp The plugin JAR is written to `build/libs/`. -`check` includes `nativeInventoryTest`. It downloads the exact Purpur 26.2 build 2618 launcher, +`check` includes `nativeInventoryTest`. It downloads the exact Purpur 26.3 build 2651 launcher, verifies SHA-256 -`4a32d046a118804d89ca74ba89b798c98f6d8d1f310c18077ac573597049de31`, and runs Paperclip in +`0677c1febecfbc2e5b0a7f76abd647b70a1f38d3cbdf3dd6a82e401b4262a598`, and runs Paperclip in patch-only mode under `build/inventory-runtime/`; it does not start a server, open a port, create a world, or accept the EULA. The native suite executes real `ServerPlaceRecipe`, `ServerRecipeBook` known/highlight codec persistence, and `ResultSlot` ingredient consumption for both Sorting Stick variants and the alternative Dispenser recipe, including repeated batch completion. @@ -122,7 +122,7 @@ Default risk uses the **largest resulting enchantment excess above its normal ma | 6 | 95% | | 7 or more | 99% | -ProtocolLib binds taking to the displayed menu generation before payment. A bounded 40-server-tick admission accommodates the main → Netty → native-main handoff; an owned native dispatch shim carries exact packet identity through that queue, so one click cannot borrow another click's cancellation or payment ticket. Native aggregate and packet-specific limits remain authoritative, using the actual connection's counters and violation actions; expired/refused/cancelled dispatches never pay. Transport ownership is bounded to 32 records per connection. A queued ProtocolLib-owned main callback followed by a barrier on the same native channel event loop proves whether each exact raw was forwarded; mutable packet/post-event snapshots do not. This library-owned cleanup survives this plugin's task cancellation. Revoked native wrappers retain their denial even when native admission or queue processing skips their handler, while genuinely unresolved late raws keep protective ownership until arrival, the ordered fence or disconnect. Missing/rejected library scheduling or event-loop execution fails closed; it does not establish successful cleanup. Input or acknowledged risk changes invalidate old clicks even when the result item is identical; the ordinary anvil stays open without another confirmation. The adapter is pinned to Purpur 26.2 build 2618 and fails closed when enhanced generation admission is unavailable. This is a library dependency, not a Fantasy Stick/gameplay-plugin bridge. +ProtocolLib binds taking to the displayed menu generation before payment. A bounded 40-server-tick admission accommodates the main → Netty → native-main handoff; an owned native dispatch shim carries exact packet identity through that queue, so one click cannot borrow another click's cancellation or payment ticket. Native aggregate and packet-specific limits remain authoritative, using the actual connection's counters and violation actions; expired/refused/cancelled dispatches never pay. Transport ownership is bounded to 32 records per connection. A queued ProtocolLib-owned main callback followed by a barrier on the same native channel event loop proves whether each exact raw was forwarded; mutable packet/post-event snapshots do not. This library-owned cleanup survives this plugin's task cancellation. Revoked native wrappers retain their denial even when native admission or queue processing skips their handler, while genuinely unresolved late raws keep protective ownership until arrival, the ordered fence or disconnect. Missing/rejected library scheduling or event-loop execution fails closed; it does not establish successful cleanup. Input or acknowledged risk changes invalidate old clicks even when the result item is identical; the ordinary anvil stays open without another confirmation. The adapter is pinned to Purpur 26.3 build 2651 and fails closed when enhanced generation admission is unavailable. This is a library dependency, not a Fantasy Stick/gameplay-plugin bridge. Use left/right pickup, shift-click or a native-eligible empty hotbar/offhand destination. Full destinations, cancelled/stale takes and unsupported cloning/drop/collection actions do not roll or spend items. Ordinary anvil wear remains in effect. Dedicated gameplay-plugin compatibility is unsupported; declared native cancellations and unrelated inventory state are preserved. diff --git a/build.gradle.kts b/build.gradle.kts index 9e4b962..55b4b99 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,11 +26,11 @@ tasks.withType().configureEach { } dependencies { - compileOnly("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable") + compileOnly("org.purpurmc.purpur:purpur-api:26.3.build.2651-stable") compileOnly("net.dmulloy2:ProtocolLib:5.4.0") testImplementation("net.dmulloy2:ProtocolLib:5.4.0") - testImplementation("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable") + testImplementation("org.purpurmc.purpur:purpur-api:26.3.build.2651-stable") testImplementation(platform("org.junit:junit-bom:5.13.4")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.mockito:mockito-core:5.18.0") @@ -49,20 +49,20 @@ tasks.test { // Paperclip only extracts/patches dependencies: no world, port, or EULA acceptance is involved. val inventoryRuntime = layout.buildDirectory.dir("inventory-runtime") val downloadInventoryRuntime = tasks.register("downloadInventoryRuntime") { - val launcher = inventoryRuntime.map { it.file("purpur-26.2-2618.jar") } + val launcher = inventoryRuntime.map { it.file("purpur-26.3-2651.jar") } outputs.file(launcher) doLast { val file = launcher.get().asFile file.parentFile.mkdirs() - val connection = URI("https://api.purpurmc.org/v2/purpur/26.2/2618/download") + val connection = URI("https://api.purpurmc.org/v2/purpur/26.3/2651/download") .toURL().openConnection() connection.connectTimeout = 30_000 connection.readTimeout = 120_000 val bytes = connection.getInputStream().use { it.readBytes() } val digest = MessageDigest.getInstance("SHA-256").digest(bytes) .joinToString("") { "%02x".format(it) } - check(digest == "4a32d046a118804d89ca74ba89b798c98f6d8d1f310c18077ac573597049de31") { - "Purpur 2618 checksum mismatch" + check(digest == "0677c1febecfbc2e5b0a7f76abd647b70a1f38d3cbdf3dd6a82e401b4262a598") { + "Purpur 2651 checksum mismatch" } file.writeBytes(bytes) } @@ -70,7 +70,7 @@ val downloadInventoryRuntime = tasks.register("downloadInventoryRuntime") { val prepareInventoryRuntime = tasks.register("prepareInventoryRuntime") { dependsOn(downloadInventoryRuntime) javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) } - classpath = files(inventoryRuntime.map { it.file("purpur-26.2-2618.jar") }) + classpath = files(inventoryRuntime.map { it.file("purpur-26.3-2651.jar") }) mainClass = "io.papermc.paperclip.Main" jvmArgs("-Dpaperclip.patchonly=true") workingDir(inventoryRuntime) diff --git a/src/main/java/games/dmg/spigotinventoryhelper/AdvancedAnvilProtocolGuard.java b/src/main/java/games/dmg/spigotinventoryhelper/AdvancedAnvilProtocolGuard.java index f694a1d..7767caa 100644 --- a/src/main/java/games/dmg/spigotinventoryhelper/AdvancedAnvilProtocolGuard.java +++ b/src/main/java/games/dmg/spigotinventoryhelper/AdvancedAnvilProtocolGuard.java @@ -79,7 +79,7 @@ final class AdvancedAnvilProtocolGuard implements AutoCloseable { this.plugin = Objects.requireNonNull(plugin); this.manager = Objects.requireNonNull(manager); this.owner = Objects.requireNonNull(owner); - if (version == null || !version.contains("Purpur") || !version.contains("26.2.build.2618-stable") + if (version == null || !version.contains("Purpur") || !version.contains("26.3.build.2651-stable") || manager.isClosed()) throw new IllegalStateException("Unsupported Advanced Anvil generation bridge"); gate = new PacketAdapter(PacketAdapter.params(plugin, PacketType.Play.Client.WINDOW_CLICK) .listenerPriority(ListenerPriority.LOWEST).optionSync()) { diff --git a/src/main/java/games/dmg/spigotinventoryhelper/NativeAnvilGeneration.java b/src/main/java/games/dmg/spigotinventoryhelper/NativeAnvilGeneration.java index 93d0505..004d9ad 100644 --- a/src/main/java/games/dmg/spigotinventoryhelper/NativeAnvilGeneration.java +++ b/src/main/java/games/dmg/spigotinventoryhelper/NativeAnvilGeneration.java @@ -8,7 +8,7 @@ import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.inventory.view.AnvilView; -/** Compile-only linkage to the checksum-pinned Purpur 26.2 build 2618 menu/packet API. */ +/** Compile-only linkage to the checksum-pinned Purpur 26.3 build 2651 menu/packet API. */ final class NativeAnvilGeneration { private NativeAnvilGeneration() {} diff --git a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeAdvancedAnvilPacketTest.java b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeAdvancedAnvilPacketTest.java index 7f69194..ef5c733 100644 --- a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeAdvancedAnvilPacketTest.java +++ b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeAdvancedAnvilPacketTest.java @@ -193,7 +193,10 @@ class NativeAdvancedAnvilPacketTest { assertEquals(0, countOutput(f, expected)); org.mockito.Mockito.verify(f.nativePlayer, org.mockito.Mockito.never()) .drop(org.mockito.ArgumentMatchers.any(net.minecraft.world.item.ItemStack.class), - org.mockito.ArgumentMatchers.anyBoolean()); + org.mockito.ArgumentMatchers.anyBoolean(), + org.mockito.ArgumentMatchers.any(net.minecraft.util.Prediction.class), + org.mockito.ArgumentMatchers.anyBoolean(), + org.mockito.ArgumentMatchers.>isNull()); } } diff --git a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeAdvancedAnvilProtocolTest.java b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeAdvancedAnvilProtocolTest.java index 6d7a5c0..d39af68 100644 --- a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeAdvancedAnvilProtocolTest.java +++ b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeAdvancedAnvilProtocolTest.java @@ -249,7 +249,7 @@ class NativeAdvancedAnvilProtocolTest { @Test void unsupportedRuntimeFailsEnableBeforeAnyEnhancedOutputBypassOrProtocolRegistration() throws Exception { AtomicInteger rolls = new AtomicInteger(); try (var f = new NativeAnvilFixture(directory, () -> { rolls.incrementAndGet(); return 99; }, () -> 1, - "Purpur 26.2.build.2619-stable")) { + "Purpur 26.3.build.2652-stable")) { assertEquals(1, f.eventErrors.size(), "Only the explicitly unsupported bridge should report failure"); assertInstanceOf(IllegalStateException.class, f.eventErrors.getFirst()); assertEquals("Unsupported Advanced Anvil generation bridge", f.eventErrors.getFirst().getMessage()); diff --git a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeAnvilFixture.java b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeAnvilFixture.java index 2069ac6..265a97d 100644 --- a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeAnvilFixture.java +++ b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeAnvilFixture.java @@ -104,16 +104,16 @@ final class NativeAnvilFixture implements AutoCloseable { NativeAnvilFixture(Path directory, java.util.function.IntSupplier risk, java.util.function.DoubleSupplier wear) throws Exception { - this(directory, risk, wear, "Purpur 26.2.build.2618-stable"); + this(directory, risk, wear, "Purpur 26.3.build.2651-stable"); } NativeAnvilFixture(Path directory, boolean queuedTransport) throws Exception { - this(directory, () -> 99, () -> 1.0, "Purpur 26.2.build.2618-stable", queuedTransport); + this(directory, () -> 99, () -> 1.0, "Purpur 26.3.build.2651-stable", queuedTransport); } NativeAnvilFixture(Path directory, java.util.function.IntSupplier risk, java.util.function.DoubleSupplier wear, boolean queuedTransport) throws Exception { - this(directory, risk, wear, "Purpur 26.2.build.2618-stable", queuedTransport); + this(directory, risk, wear, "Purpur 26.3.build.2651-stable", queuedTransport); } NativeAnvilFixture(Path directory, java.util.function.IntSupplier risk, @@ -169,9 +169,9 @@ final class NativeAnvilFixture implements AutoCloseable { when(server.getUnsafe()).thenReturn(CraftMagicNumbers.INSTANCE); var mainThread = Thread.currentThread(); when(server.isPrimaryThread()).thenAnswer(ignored -> Thread.currentThread() == mainThread); - when(server.getVersion()).thenReturn("Purpur 2618 (MC: 26.2)"); - when(server.getMinecraftVersion()).thenReturn("26.2"); - when(server.getBukkitVersion()).thenReturn("26.2.build.2618-stable"); + when(server.getVersion()).thenReturn("Purpur 2651 (MC: 26.3)"); + when(server.getMinecraftVersion()).thenReturn("26.3"); + when(server.getBukkitVersion()).thenReturn("26.3.build.2651-stable"); when(server.getWarningState()).thenReturn(org.bukkit.Warning.WarningState.DEFAULT); when(server.getLogger()).thenReturn(Logger.getAnonymousLogger()); when(server.getPluginManager()).thenReturn(events); @@ -366,7 +366,9 @@ final class NativeAnvilFixture implements AutoCloseable { doCallRealMethod().when(nativePlayer).doCloseContainer(); doCallRealMethod().when(nativePlayer).closeContainer(any(org.bukkit.event.inventory.InventoryCloseEvent.Reason.class)); doAnswer(inv -> { throw new AssertionError("Unexpected second-user drop"); }) - .when(nativePlayer).drop(any(net.minecraft.world.item.ItemStack.class), anyBoolean()); + .when(nativePlayer).drop(any(net.minecraft.world.item.ItemStack.class), anyBoolean(), + any(net.minecraft.util.Prediction.class), anyBoolean(), + org.mockito.ArgumentMatchers.>isNull()); field(net.minecraft.server.network.ServerCommonPacketListenerImpl.class, handler, "server", nativeServer); field(net.minecraft.server.network.ServerCommonPacketListenerImpl.class, handler, "cserver", primary.craftServer); field(net.minecraft.server.network.ServerCommonPacketListenerImpl.class, handler, "connection", connection); diff --git a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeAnvilPacketFixture.java b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeAnvilPacketFixture.java index d4f467c..b9a38b9 100644 --- a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeAnvilPacketFixture.java +++ b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeAnvilPacketFixture.java @@ -68,7 +68,9 @@ final class NativeAnvilPacketFixture implements AutoCloseable { doCallRealMethod().when(anvil.nativePlayer).doCloseContainer(); doCallRealMethod().when(anvil.nativePlayer).closeContainer(any(org.bukkit.event.inventory.InventoryCloseEvent.Reason.class)); doAnswer(inv -> { drops.add(inv.getArgument(0).copy()); return null; }) - .when(anvil.nativePlayer).drop(any(net.minecraft.world.item.ItemStack.class), anyBoolean()); + .when(anvil.nativePlayer).drop(any(net.minecraft.world.item.ItemStack.class), anyBoolean(), + any(net.minecraft.util.Prediction.class), anyBoolean(), + org.mockito.ArgumentMatchers.>isNull()); when(nativeServer.packetProcessor()).thenReturn(processor); when(craftServer.getPluginManager()).thenReturn(anvil.events); when(anvil.nativePlayer.isWithinBlockInteractionRange(anvil.position, 4.0)).thenReturn(true); diff --git a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeCobwebPluginWiringTest.java b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeCobwebPluginWiringTest.java index f0656bb..1c2d0c6 100644 --- a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeCobwebPluginWiringTest.java +++ b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeCobwebPluginWiringTest.java @@ -88,9 +88,9 @@ class NativeCobwebPluginWiringTest { when(serverUnderTest.getItemFactory()).thenAnswer(invocation -> CraftItemFactory.instance()); when(serverUnderTest.getUnsafe()).thenReturn(CraftMagicNumbers.INSTANCE); when(serverUnderTest.isPrimaryThread()).thenReturn(true); - when(serverUnderTest.getVersion()).thenReturn("Purpur 2618 (MC: 26.2)"); - when(serverUnderTest.getMinecraftVersion()).thenReturn("26.2"); - when(serverUnderTest.getBukkitVersion()).thenReturn("26.2.build.2618-stable"); + when(serverUnderTest.getVersion()).thenReturn("Purpur 2651 (MC: 26.3)"); + when(serverUnderTest.getMinecraftVersion()).thenReturn("26.3"); + when(serverUnderTest.getBukkitVersion()).thenReturn("26.3.build.2651-stable"); when(serverUnderTest.getWarningState()).thenReturn(org.bukkit.Warning.WarningState.DEFAULT); logger = Logger.getAnonymousLogger(); logs = new ArrayList<>(); @@ -278,7 +278,7 @@ class NativeCobwebPluginWiringTest { createdPlugin = new SpigotInventoryHelperPlugin(new JavaPluginLoader(serverUnderTest), NativeAnvilFixture.description(), directory.resolve("data").toFile(), directory.resolve("plugin.jar").toFile(), () -> 99, () -> 1, protocol, - () -> "Purpur 26.2.build.2618-stable"); + () -> "Purpur 26.3.build.2651-stable"); commands.registerAll(createdPlugin.getName(), org.bukkit.command.PluginCommandYamlParser.parse(createdPlugin)); createdPlugin.getLogger().addHandler(logHandler); diff --git a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativePowderSnowBookTest.java b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativePowderSnowBookTest.java index 56bf11e..39637d5 100644 --- a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativePowderSnowBookTest.java +++ b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativePowderSnowBookTest.java @@ -45,10 +45,10 @@ class NativePowderSnowBookTest { var holder = f.holders.get(key); assertEquals(RecipeBookCategories.CRAFTING_MISC, holder.value().recipeBookCategory()); var display = assertInstanceOf(ShapelessCraftingRecipeDisplay.class, holder.value().display().getFirst()); - var context = new net.minecraft.util.context.ContextMap.Builder() - .withParameter(net.minecraft.world.item.crafting.display.SlotDisplayContext.REGISTRIES, + var context = net.minecraft.util.context.ContextMap.builder() + .set(net.minecraft.world.item.crafting.display.SlotDisplayContext.REGISTRIES, org.bukkit.craftbukkit.CraftRegistry.getMinecraftRegistry()) - .create(net.minecraft.world.item.crafting.display.SlotDisplayContext.CONTEXT); + .buildAndValidate(net.minecraft.world.item.crafting.display.SlotDisplayContext.CONTEXT); var ingredients = display.ingredients().stream().map(slot -> slot.resolveForStacks(context)).toList(); assertEquals(List.of(Material.ANVIL, Material.CHIPPED_ANVIL, Material.DAMAGED_ANVIL), ingredients.getFirst() .stream().map(stack -> CraftItemStack.asBukkitCopy(stack).getType()).toList()); @@ -231,10 +231,10 @@ class NativePowderSnowBookTest { var recipe = f.powder().value(); assertEquals(RecipeBookCategories.CRAFTING_MISC, recipe.recipeBookCategory()); var display = assertInstanceOf(ShapelessCraftingRecipeDisplay.class, recipe.display().getFirst()); - var context = new net.minecraft.util.context.ContextMap.Builder() - .withParameter(net.minecraft.world.item.crafting.display.SlotDisplayContext.REGISTRIES, + var context = net.minecraft.util.context.ContextMap.builder() + .set(net.minecraft.world.item.crafting.display.SlotDisplayContext.REGISTRIES, org.bukkit.craftbukkit.CraftRegistry.getMinecraftRegistry()) - .create(net.minecraft.world.item.crafting.display.SlotDisplayContext.CONTEXT); + .buildAndValidate(net.minecraft.world.item.crafting.display.SlotDisplayContext.CONTEXT); var ingredients = display.ingredients().stream().map(slot -> slot.resolveForStacks(context)).toList(); assertEquals(List.of(1, 1), ingredients.stream().map(List::size).toList()); assertEquals(List.of(Material.SNOW_BLOCK, Material.BUCKET), ingredients.stream() diff --git a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativePowderSnowFixture.java b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativePowderSnowFixture.java index 4518007..c94a43d 100644 --- a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativePowderSnowFixture.java +++ b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativePowderSnowFixture.java @@ -22,7 +22,6 @@ import net.minecraft.world.item.crafting.RecipeManager; import net.minecraft.world.item.crafting.RecipeMap; import org.bukkit.Keyed; import org.bukkit.NamespacedKey; -import org.bukkit.craftbukkit.CraftRegistry; import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.craftbukkit.inventory.CraftRecipe; import org.bukkit.craftbukkit.util.CraftNamespacedKey; @@ -65,7 +64,7 @@ final class NativePowderSnowFixture implements AutoCloseable { actor.events.registerEvent(PlayerRecipeDiscoverEvent.class, new Listener() {}, EventPriority.MONITOR, (ignored, event) -> discoveries.add((PlayerRecipeDiscoverEvent) event), actor.plugin, false); actor.plugin.setEnabled(false); - manager = new RecipeManager(CraftRegistry.getMinecraftRegistry()); + manager = NativeRuntime.recipeManager(); when(packets.nativeServer.getRecipeManager()).thenReturn(manager); when(actor.level.recipeAccess()).thenReturn(manager); when(actor.level.getGameRules()).thenReturn( @@ -170,7 +169,8 @@ final class NativePowderSnowFixture implements AutoCloseable { } private void refreshManager() { - manager.recipes = RecipeMap.create(new ArrayList>(holders.values())); + manager.recipes = RecipeMap.createEmpty(); + holders.values().forEach(manager.recipes::addRecipe); manager.finalizeRecipeLoading(FeatureFlags.VANILLA_SET); // Pinned CraftServer.add/removeRecipe calls PlayerList.reloadRecipes after finalizing. // Run that real native book synchronization at the actor/network boundary too: later diff --git a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativePowderSnowPluginWiringTest.java b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativePowderSnowPluginWiringTest.java index e0f680b..fcc60d7 100644 --- a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativePowderSnowPluginWiringTest.java +++ b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativePowderSnowPluginWiringTest.java @@ -95,9 +95,9 @@ class NativePowderSnowPluginWiringTest { when(serverUnderTest.getItemFactory()).thenAnswer(invocation -> CraftItemFactory.instance()); when(serverUnderTest.getUnsafe()).thenReturn(CraftMagicNumbers.INSTANCE); when(serverUnderTest.isPrimaryThread()).thenReturn(true); - when(serverUnderTest.getVersion()).thenReturn("Purpur 2618 (MC: 26.2)"); - when(serverUnderTest.getMinecraftVersion()).thenReturn("26.2"); - when(serverUnderTest.getBukkitVersion()).thenReturn("26.2.build.2618-stable"); + when(serverUnderTest.getVersion()).thenReturn("Purpur 2651 (MC: 26.3)"); + when(serverUnderTest.getMinecraftVersion()).thenReturn("26.3"); + when(serverUnderTest.getBukkitVersion()).thenReturn("26.3.build.2651-stable"); when(serverUnderTest.getWarningState()).thenReturn(org.bukkit.Warning.WarningState.DEFAULT); logger = Logger.getAnonymousLogger(); logs = new ArrayList<>(); @@ -643,7 +643,7 @@ class NativePowderSnowPluginWiringTest { createdPlugin = new SpigotInventoryHelperPlugin(new JavaPluginLoader(serverUnderTest), NativeAnvilFixture.description(), directory.resolve("data").toFile(), directory.resolve("plugin.jar").toFile(), () -> 99, () -> 1, protocol, - () -> "Purpur 26.2.build.2618-stable"); + () -> "Purpur 26.3.build.2651-stable"); commands.registerAll(createdPlugin.getName(), org.bukkit.command.PluginCommandYamlParser.parse(createdPlugin)); createdPlugin.getLogger().addHandler(logHandler); diff --git a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeReinforcementCraftTest.java b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeReinforcementCraftTest.java index 9430f20..353cc86 100644 --- a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeReinforcementCraftTest.java +++ b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeReinforcementCraftTest.java @@ -2166,7 +2166,6 @@ final class NativeReinforcementCraftTest { doAnswer(inv -> { BlockPos pos = inv.getArgument(0); net.minecraft.world.level.block.state.BlockState state = inv.getArgument(1); - assertEquals(3, inv.getArgument(2)); if (source.equals(pos)) { assertTrue(state.isAir(), "Only native source departure is supported"); sourceState.set(state); @@ -2177,7 +2176,8 @@ final class NativeReinforcementCraftTest { effects.data.set(CraftBlockData.createData(state)); } return true; - }).when(f.actor.level).setBlock(any(BlockPos.class), any(net.minecraft.world.level.block.state.BlockState.class), anyInt()); + }).when(f.actor.level).setBlockAndUpdate(any(BlockPos.class), + any(net.minecraft.world.level.block.state.BlockState.class)); when(f.actor.level.addFreshEntity(any(Entity.class))).thenAnswer(inv -> { Entity candidate = inv.getArgument(0); if (candidate instanceof net.minecraft.world.entity.item.ItemEntity item) { @@ -2310,7 +2310,7 @@ final class NativeReinforcementCraftTest { entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); return false; } - boolean written = f.actor.level.setBlock(entity.blockPosition(), entity.blockState, 3); + boolean written = f.actor.level.setBlockAndUpdate(entity.blockPosition(), entity.blockState); if (written) entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); else assertFalse(entity.dropItem, "Only inspected failed-write/no-drop outcome is supplied"); f.actor.assertNoEventErrors(); @@ -2334,7 +2334,7 @@ final class NativeReinforcementCraftTest { new NamespacedKey("spigotinventoryhelper", "falling_reinforcement"), PersistentDataType.TAG_CONTAINER); assertNotNull(record); assertEquals((byte) 2, record.get(new NamespacedKey("spigotinventoryhelper", "transport_phase"), PersistentDataType.BYTE)); - assertFalse(f.actor.level.setBlock(entity.blockPosition(), entity.blockState, 3)); + assertFalse(f.actor.level.setBlockAndUpdate(entity.blockPosition(), entity.blockState)); assertEquals(Material.AIR, effects.block.getType()); // Inspected FallingBlockEntity.tick branch, but NOT a claim to run physics/tick. entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DROP); @@ -2361,7 +2361,7 @@ final class NativeReinforcementCraftTest { assertSame(f.actor.world, event.getBlock().getWorld()); assertEquals(f.actor.position, ((org.bukkit.craftbukkit.block.CraftBlock) event.getBlock()).getPosition()); assertEquals(Material.DAMAGED_ANVIL, event.getTo()); - if (accepted) assertTrue(f.actor.level.setBlock(entity.blockPosition(), entity.blockState, 3)); + if (accepted) assertTrue(f.actor.level.setBlockAndUpdate(entity.blockPosition(), entity.blockState)); entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); f.actor.assertNoEventErrors(); return accepted; diff --git a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeRuntime.java b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeRuntime.java index ea98bab..d7695f5 100644 --- a/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeRuntime.java +++ b/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativeRuntime.java @@ -28,6 +28,7 @@ import org.bukkit.craftbukkit.CraftRegistry; /** Loads vanilla registries, tags, and components using the server test bootstrap path. */ final class NativeRuntime { private static boolean bootstrapped; + private static HolderLookup.Provider reloadableRegistries; private NativeRuntime() {} @@ -47,12 +48,12 @@ final class NativeRuntime { List> tags = TagLoader.loadTagsForExistingRegistries( resources, layers.getLayer(RegistryLayer.STATIC)); List> lookups = TagLoader.buildUpdatedLookups( - layers.getAccessForLoading(RegistryLayer.WORLDGEN), tags); - RegistryAccess.Frozen worldgen = RegistryDataLoader.load(resources, lookups, - RegistryDataLoader.WORLDGEN_REGISTRIES, Util.backgroundExecutor()).join(); - layers = layers.replaceFrom(RegistryLayer.WORLDGEN, worldgen); + layers.getAccessForLoading(RegistryLayer.WORLD), tags); + RegistryAccess.Frozen world = RegistryDataLoader.load(resources, lookups, + RegistryDataLoader.WORLD_REGISTRIES, Util.backgroundExecutor()).join(); + layers = layers.replaceFrom(RegistryLayer.WORLD, world); RegistryAccess.Frozen dimensions = RegistryDataLoader.load(resources, - Stream.concat(lookups.stream(), worldgen.listRegistries()).toList(), + Stream.concat(lookups.stream(), world.listRegistries()).toList(), RegistryDataLoader.DIMENSION_REGISTRIES, Util.backgroundExecutor()).join(); layers = layers.replaceFrom(RegistryLayer.DIMENSIONS, dimensions); Class.forName(org.bukkit.Registry.class.getName()); @@ -61,7 +62,15 @@ final class NativeRuntime { Util.backgroundExecutor(), Runnable::run).join(); datapack.updateComponentsAndStaticRegistryTags(); CraftRegistry.setMinecraftRegistry(layers.compositeAccess().freeze()); + reloadableRegistries = datapack.fullRegistries().lookup(); bootstrapped = true; } } + + static net.minecraft.world.item.crafting.RecipeManager recipeManager() { + if (!bootstrapped) { + throw new IllegalStateException("Native runtime has not been bootstrapped"); + } + return new net.minecraft.world.item.crafting.RecipeManager(reloadableRegistries); + } } diff --git a/src/test/java/games/dmg/spigotinventoryhelper/PurpurDistributionTest.java b/src/test/java/games/dmg/spigotinventoryhelper/PurpurDistributionTest.java index 908539b..211821d 100644 --- a/src/test/java/games/dmg/spigotinventoryhelper/PurpurDistributionTest.java +++ b/src/test/java/games/dmg/spigotinventoryhelper/PurpurDistributionTest.java @@ -20,6 +20,7 @@ class PurpurDistributionTest { Map metadata = new Yaml().load(jar.getInputStream(jar.getJarEntry("plugin.yml"))); assertEquals(version, metadata.get("version")); assertEquals("SpigotInventoryHelper", metadata.get("name"), "Retain the data directory and persistent namespace"); + assertEquals("1.20", metadata.get("api-version")); assertEquals(java.util.List.of("ProtocolLib"), metadata.get("depend")); assertEquals("games.dmg.spigotinventoryhelper.SpigotInventoryHelperPlugin", metadata.get("main")); Map permissions = (Map) metadata.get("permissions"); @@ -68,7 +69,9 @@ class PurpurDistributionTest { assertTrue(readme.contains("Dropper + Tripwire Hook")); assertTrue(readme.contains("Redstone category")); assertTrue(readme.contains("CraftServer, CraftPlayer, and client boundaries are doubled")); - assertTrue(readme.contains("4a32d046a118804d89ca74ba89b798c98f6d8d1f310c18077ac573597049de31")); + assertTrue(readme.contains("Purpur 26.3 build 2651")); + assertTrue(readme.contains("0677c1febecfbc2e5b0a7f76abd647b70a1f38d3cbdf3dd6a82e401b4262a598")); + assertFalse(readme.contains("26.2") || readme.contains("2618")); } @Test @@ -83,14 +86,20 @@ class PurpurDistributionTest { } @Test - void releaseAndCiAgreeWithThePurpurDistributionNameAndToolchain() throws Exception { + void releaseAndCiAgreeWithThePurpur26_3DistributionAndToolchain() throws Exception { String build = Files.readString(Path.of("build.gradle.kts")); - assertTrue(build.contains("purpur-api:26.2.build.2618-stable")); + assertTrue(build.contains("purpur-api:26.3.build.2651-stable")); + assertTrue(build.contains("purpur-26.3-2651.jar")); + assertTrue(build.contains("https://api.purpurmc.org/v2/purpur/26.3/2651/download")); + assertTrue(build.contains("0677c1febecfbc2e5b0a7f76abd647b70a1f38d3cbdf3dd6a82e401b4262a598")); assertTrue(build.contains("compileOnly(\"net.dmulloy2:ProtocolLib:5.4.0\")")); + assertFalse(build.contains("26.2") || build.contains("2618")); assertFalse(build.contains("org.spigotmc:spigot-api")); assertTrue(build.contains("nativeInventoryTest")); assertTrue(build.contains("tasks.check { dependsOn(nativeInventoryTest) }")); - assertTrue(build.contains("4a32d046a118804d89ca74ba89b798c98f6d8d1f310c18077ac573597049de31")); + String guard = Files.readString(Path.of("src/main/java/games/dmg/spigotinventoryhelper/AdvancedAnvilProtocolGuard.java")); + assertTrue(guard.contains("26.3.build.2651-stable")); + assertFalse(guard.contains("26.2") || guard.contains("2618")); for (String workflow : new String[] {"ci.yml", "release.yml"}) { String content = Files.readString(Path.of(".gitea/workflows", workflow)); assertTrue(content.contains("java-version: 25"));