Compare commits

..
3 Commits
Author SHA1 Message Date
dmg ed02f46272 docs: point story tracking to Kaneo
CI / build (push) Successful in 1m51s
Release / release (push) Successful in 1m59s
2026-10-28 20:36:17 -04:00
dmg e4b2e004cd feat(platform): support Purpur 26.3
Release / release (push) Successful in 2m6s
CI / build (push) Successful in 58s
2026-10-17 04:03:03 +00:00
dmg 95a7ac41e1 feat(platform): support Purpur 26.3
CI / build (push) Successful in 54s
CI / build (pull_request) Successful in 1m43s
2026-10-16 23:46:50 -04:00
16 changed files with 85 additions and 58 deletions

No files matched your search

+5 -3
View File
@@ -1,9 +1,11 @@
# purpur-inventory-helper agent entrypoint
The canonical stories, engineering guidance, and **all process documents** are in the private [SoMC OKF wiki](https://git.garvis.dev/dmg/somc-okf/src/branch/main/index.md).
Canonical engineering guidance and technical/process documents are in the private [SoMC OKF wiki](https://git.garvis.dev/dmg/somc-okf/src/branch/main/index.md).
Before work, read the sibling `../somc-okf/index.md`, `../somc-okf/processes/index.md`, `../somc-okf/projects/purpur-inventory-helper/index.md`, `engineering.md` in that project section, and relevant `../somc-okf/user-stories/purpur-inventory-helper/` stories. Also follow the parent workspace `AGENTS.md` when present.
Stories, acceptance criteria, status and evidence belong in [Kaneo Social Minecraft](https://cloud.kaneo.app/dmg/SM), with subjects `[purpur-inventory-helper] Descriptive title` (no `US-###` or `FS-*` prefix). Use the configured authenticated `kaneo` MCP server per [Kaneo access and migration](https://git.garvis.dev/dmg/somc-okf/src/branch/main/processes/development.md#kaneo-access-and-migration); report unavailable access rather than creating local story copies. OpenProject is read-only history.
Before work, read the sibling `../somc-okf/index.md`, `../somc-okf/processes/index.md`, `../somc-okf/projects/purpur-inventory-helper/index.md`, `engineering.md` in that project section, and relevant Kaneo tickets with subject prefix `[purpur-inventory-helper]`. Also follow the parent workspace `AGENTS.md` when present.
For standalone checkouts, start at the [project page](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/purpur-inventory-helper/index.md) and [shared process](https://git.garvis.dev/dmg/somc-okf/src/branch/main/processes/development.md). Obtain wiki access before feature work; do not recreate a local knowledge bundle. Source builds do not require private wiki access.
Development follows [Development cycle](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/development-cycle.md): approved stories, failing tests, passing implementation, verification, then source/wiki commit and push. GitOps updates are committed locally **without pushing**; only [Do release](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/do-release.md) authorizes a reviewed GitOps push.
Follow the full [development process](https://git.garvis.dev/dmg/somc-okf/src/branch/main/processes/development.md) and [development cycle](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/development-cycle.md) for approvals, test-first/full-suite verification, required Gitea PR checks/reviews and merge, and merged-tree verification before ticket completion. Never push source `main` directly. GitOps updates are committed locally **without pushing**; only [Do release](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/do-release.md) authorizes a reviewed GitOps push.
+5 -5
View File
@@ -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.
+7 -7
View File
@@ -26,11 +26,11 @@ tasks.withType<JavaCompile>().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<JavaExec>("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)
@@ -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()) {
@@ -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() {}
@@ -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.<java.util.function.Consumer<org.bukkit.entity.Item>>isNull());
}
}
@@ -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());
@@ -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.<java.util.function.Consumer<org.bukkit.entity.Item>>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);
@@ -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.<net.minecraft.world.item.ItemStack>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.<java.util.function.Consumer<org.bukkit.entity.Item>>isNull());
when(nativeServer.packetProcessor()).thenReturn(processor);
when(craftServer.getPluginManager()).thenReturn(anvil.events);
when(anvil.nativePlayer.isWithinBlockInteractionRange(anvil.position, 4.0)).thenReturn(true);
@@ -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);
@@ -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()
@@ -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<RecipeHolder<?>>(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
@@ -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);
@@ -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.<Integer>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;
@@ -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<Registry.PendingTags<?>> tags = TagLoader.loadTagsForExistingRegistries(
resources, layers.getLayer(RegistryLayer.STATIC));
List<HolderLookup.RegistryLookup<?>> 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);
}
}
@@ -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"));