From c701f75713d6b58823a9344f8ba38063a4c36696 Mon Sep 17 00:00:00 2001 From: Dylan Garvis Date: Fri, 16 Oct 2026 23:47:17 -0400 Subject: [PATCH] feat(platform): support Purpur 26.3 --- AGENTS.md | 2 +- README.md | 4 ++-- build.gradle.kts | 23 +++++++++++-------- src/main/resources/plugin.yml | 2 +- .../dmg/purpurpotato/NativeCombatTest.java | 2 ++ .../games/dmg/purpurpotato/NativeRuntime.java | 10 ++++---- .../dmg/purpurpotato/PluginLifecycleTest.java | 2 +- .../dmg/purpurpotato/DistributionTest.java | 17 ++++++++++---- 8 files changed, 39 insertions(+), 23 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7953a45..25d5e8d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,6 @@ Canonical knowledge and stories live in the sibling `somc-okf/` checkout, not th Standalone checkout entrypoint: https://git.garvis.dev/dmg/somc-okf/src/branch/main/index.md . If unavailable, request access; do not create a substitute knowledge bundle or copy private wiki content here. -Obtain explicit story-change approval before implementation and operational approval before infrastructure, tooling or secret changes. Use vertical-slice TDD, retain native combat/protection decisions and verify `./gradlew clean check jar`, including mandatory non-listening native tests. Java 25, Gradle 9.6.1, Purpur API `26.2.build.2618-stable`; do not silently upgrade. Keep blocking I/O off the tick thread. +Obtain explicit story-change approval before implementation and operational approval before infrastructure, tooling or secret changes. Use vertical-slice TDD, retain native combat/protection decisions and verify `./gradlew clean check jar`, including mandatory non-listening native tests. Java 25, Gradle 9.6.1, Purpur API `26.3.build.2651-stable`; do not silently upgrade. Keep blocking I/O off the tick thread. Publishing credentials and release-triggering pushes require separate readiness verification and authorization. Never put secret bytes in argv, URLs, logs or Git. No deployment/GitOps push follows implicitly from source work. Preserve unrelated changes and use conventional commits. diff --git a/README.md b/README.md index 20f8df3..7c27622 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Purpur Potato -A Java 25 plugin for Purpur 26.2 build 2618. Right-click a poisonous potato to throw it with native snowball flight and a potato visual. Each successful launch costs one potato (including Creative), has a 20-tick cooldown, and deals one health point of base damage without poison. Native damage modifiers, PvP rules and protection cancellations remain authoritative. Projectiles expire after 100 ticks; misses are not refunded. +A Java 25 plugin for Purpur 26.3 build 2651. Right-click a poisonous potato to throw it with native snowball flight and a potato visual. Each successful launch costs one potato (including Creative), has a 20-tick cooldown, and deals one health point of base damage without poison. Native damage modifiers, PvP rules and protection cancellations remain authoritative. Projectiles expire after 100 ticks; misses are not refunded. `/potato [help]` explains the mechanic to players and console. `purpurpotato.help` defaults to true. No recipe, unlock, persistent data or runtime credential is required. @@ -11,7 +11,7 @@ A Java 25 plugin for Purpur 26.2 build 2618. Right-click a poisonous potato to t ./gradlew clean check jar -PreleaseVersion=1.2.3 ``` -Gradle 9.6.1 selects Java 25. Full checks include Java tests, mandatory non-listening native tests against the checksum-pinned Purpur runtime, and TypeScript release-tool tests (`npm ci` requires Node 20+). Native preparation runs Paperclip **patch-only**: it does not start a server, listen on a port or access production. Build downloads require network access. Generated runtime files stay under ignored `build/`. +Gradle 9.6.1 selects Java 25 and compiles against Purpur API `26.3.build.2651-stable`. Full checks include Java tests, mandatory non-listening native tests against the checksum-pinned `purpur-26.3-2651.jar` runtime, and TypeScript release-tool tests (`npm ci` requires Node 20+). Native preparation runs Paperclip **patch-only** in a build-specific directory: it does not start a server, listen on a port, access production, or reuse an extracted runtime from another supported build. Build downloads require network access. Generated runtime files stay under ignored `build/`. Outputs: `build/libs/purpur-potato-.jar`. Server and test dependencies are not bundled. Do not deploy snapshot workflow artifacts. diff --git a/build.gradle.kts b/build.gradle.kts index ae1ed97..2295013 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,10 +25,15 @@ tasks.withType().configureEach { options.compilerArgs.addAll(listOf("-Xlint:all,-deprecation,-removal", "-Werror")) } -dependencies { - compileOnly("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable") +val purpurApi = "org.purpurmc.purpur:purpur-api:26.3.build.2651-stable" +val nativeRuntimeName = "purpur-26.3-2651.jar" +val nativeRuntimeUrl = "https://api.purpurmc.org/v2/purpur/26.3/2651/download" - testImplementation("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable") + +dependencies { + compileOnly(purpurApi) + + testImplementation(purpurApi) testImplementation(platform("org.junit:junit-bom:5.13.4")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.mockito:mockito-core:5.18.0") @@ -44,20 +49,20 @@ tasks.test { } // Verify native projectile adapters against the declared runtime, without starting a server. -val nativeRuntime = layout.buildDirectory.dir("native-runtime") +val nativeRuntime = layout.buildDirectory.dir("native-runtime/26.3-2651") val downloadNativeRuntime = tasks.register("downloadNativeRuntime") { - val launcher = nativeRuntime.map { it.file("purpur-26.2-2618.jar") } + val launcher = nativeRuntime.map { it.file(nativeRuntimeName) } 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").toURL().openConnection() + val connection = URI(nativeRuntimeUrl).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) } @@ -65,7 +70,7 @@ val downloadNativeRuntime = tasks.register("downloadNativeRuntime") { val prepareNativeRuntime = tasks.register("prepareNativeRuntime") { dependsOn(downloadNativeRuntime) javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) } - classpath = files(nativeRuntime.map { it.file("purpur-26.2-2618.jar") }) + classpath = files(nativeRuntime.map { it.file(nativeRuntimeName) }) mainClass = "io.papermc.paperclip.Main" jvmArgs("-Dpaperclip.patchonly=true") workingDir(nativeRuntime) diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 69d76b4..b323b90 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ name: PurpurPotato version: '${version}' main: games.dmg.purpurpotato.PurpurPotato -api-version: '26.2' +api-version: '26.3' description: Throw consumable poisonous potatoes with native snowball flight. commands: potato: diff --git a/src/nativeTest/java/games/dmg/purpurpotato/NativeCombatTest.java b/src/nativeTest/java/games/dmg/purpurpotato/NativeCombatTest.java index 2bb1d81..7a51721 100644 --- a/src/nativeTest/java/games/dmg/purpurpotato/NativeCombatTest.java +++ b/src/nativeTest/java/games/dmg/purpurpotato/NativeCombatTest.java @@ -101,6 +101,7 @@ class NativeCombatTest { when(nativePlayer.getBukkitEntity()).thenReturn(player); when(player.getScoreboard()).thenReturn(mock(org.bukkit.craftbukkit.scoreboard.CraftScoreboard.class)); when(nativePlayer.level()).thenReturn(world); + when(nativePlayer.position()).thenReturn(Vec3.ZERO); when(nativePlayer.getAdvancements()).thenReturn(mock(net.minecraft.server.PlayerAdvancements.class)); when(player.getUniqueId()).thenReturn(UUID.randomUUID()); var inventory = mock(PlayerInventory.class); @@ -140,6 +141,7 @@ class NativeCombatTest { if (target instanceof ServerPlayer nativeVictim) { when(world.isPvpAllowed()).thenReturn(!mode.equals("pvp-denied")); when(nativeVictim.canHarmPlayer(nativePlayer)).thenCallRealMethod(); + when(nativeVictim.doTeamsAllowDamage(nativePlayer)).thenCallRealMethod(); var abilities = net.minecraft.world.entity.player.Player.class.getDeclaredField("abilities"); abilities.setAccessible(true); abilities.set(nativeVictim, new net.minecraft.world.entity.player.Abilities()); diff --git a/src/nativeTest/java/games/dmg/purpurpotato/NativeRuntime.java b/src/nativeTest/java/games/dmg/purpurpotato/NativeRuntime.java index 6d4e6c2..f441aac 100644 --- a/src/nativeTest/java/games/dmg/purpurpotato/NativeRuntime.java +++ b/src/nativeTest/java/games/dmg/purpurpotato/NativeRuntime.java @@ -43,12 +43,12 @@ final class NativeRuntime { try (var resources = new MultiPackResourceManager(PackType.SERVER_DATA, packs.openAllSelected())) { LayeredRegistryAccess layers = RegistryLayer.createRegistryAccess(); 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); + List> lookups = TagLoader.buildUpdatedLookups(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()); diff --git a/src/nativeTest/java/games/dmg/purpurpotato/PluginLifecycleTest.java b/src/nativeTest/java/games/dmg/purpurpotato/PluginLifecycleTest.java index 72b1a26..a4a1442 100644 --- a/src/nativeTest/java/games/dmg/purpurpotato/PluginLifecycleTest.java +++ b/src/nativeTest/java/games/dmg/purpurpotato/PluginLifecycleTest.java @@ -47,7 +47,7 @@ class PluginLifecycleTest { when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("PotatoTest")); try (var stream = getClass().getResourceAsStream("/plugin.yml")) { var description = new PluginDescriptionFile(stream); - assertEquals("26.2", description.getAPIVersion()); + assertEquals("26.3", description.getAPIVersion()); when(plugin.getDescription()).thenReturn(description); } var constructor = PluginCommand.class.getDeclaredConstructor(String.class, org.bukkit.plugin.Plugin.class); diff --git a/src/test/java/games/dmg/purpurpotato/DistributionTest.java b/src/test/java/games/dmg/purpurpotato/DistributionTest.java index 12c2a54..1827d3d 100644 --- a/src/test/java/games/dmg/purpurpotato/DistributionTest.java +++ b/src/test/java/games/dmg/purpurpotato/DistributionTest.java @@ -19,7 +19,7 @@ class DistributionTest { assertEquals("PurpurPotato", metadata.get("name")); assertEquals(version, metadata.get("version")); assertEquals("games.dmg.purpurpotato.PurpurPotato", metadata.get("main")); - assertEquals("26.2", metadata.get("api-version")); + assertEquals("26.3", metadata.get("api-version")); var commands = (Map) metadata.get("commands"); assertEquals(1, commands.size()); var potato = (Map) commands.get("potato"); @@ -27,6 +27,9 @@ class DistributionTest { assertEquals("purpurpotato.help", potato.get("permission")); var permissions = (Map) metadata.get("permissions"); assertEquals(true, ((Map) permissions.get("purpurpotato.help")).get("default")); + assertFalse(metadata.containsKey("depend")); + assertFalse(metadata.containsKey("softdepend")); + assertFalse(metadata.containsKey("loadbefore")); var entry = jar.getEntry("games/dmg/purpurpotato/PurpurPotato.class"); assertNotNull(entry); try (var bytes = new DataInputStream(jar.getInputStream(entry))) { @@ -34,9 +37,15 @@ class DistributionTest { bytes.readUnsignedShort(); assertEquals(69, bytes.readUnsignedShort()); } - assertTrue(jar.stream().filter(e -> e.getName().endsWith(".class")) - .allMatch(e -> e.getName().startsWith("games/dmg/purpurpotato/") - && !e.getName().endsWith("Test.class")), "No server or test dependencies may be bundled"); + assertTrue(jar.stream().allMatch(e -> { + String name = e.getName(); + return e.isDirectory() + || name.equals("META-INF/MANIFEST.MF") + || name.equals("plugin.yml") + || name.startsWith("games/dmg/purpurpotato/") + && name.endsWith(".class") + && !name.endsWith("Test.class"); + }), "Only plugin classes, metadata, and the manifest may be bundled"); } } }