From e33d19b94e891776044dbd666d5b90c2a2360c7f Mon Sep 17 00:00:00 2001 From: Dylan Garvis Date: Fri, 16 Oct 2026 23:46:19 -0400 Subject: [PATCH] fix(platform): support Purpur 26.3 --- README.md | 10 +++---- build.gradle.kts | 16 +++++------ compose.test.yml | 4 +-- scripts/start-test-server.sh | 4 +-- .../games/dmg/treefeller/NativeRuntime.java | 10 +++---- .../dmg/treefeller/LocalTestHarnessTest.java | 6 ++-- .../dmg/treefeller/PluginMetadataTest.java | 1 + .../treefeller/PurpurDistributionTest.java | 28 +++++++++++++++++-- 8 files changed, 51 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index c095df6..15a500c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Purpur Tree Feller -Tree Feller is a Purpur 26.2 plugin that lets Survival players earn animated automatic felling independently for each vanilla tree family. +Tree Feller is a Purpur 26.3 plugin that lets Survival players earn animated automatic felling independently for each vanilla tree family. The approved behavior and implementation record are in the [SoMC OKF wiki](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/purpur-tree-feller/index.md). ## Requirements -- Purpur 26.2 build 2618 +- Purpur 26.3 build 2651 - Java 25 Repository, checkout and new artifacts use `purpur-tree-feller`. Runtime `TreeFeller`, Java packages/entrypoint, `plugins/TreeFeller/`, commands/permissions and player progression/preferences remain unchanged. Replace the old JAR rather than installing both distributions. Old release tags/assets remain available; new releases no longer support Java 17. @@ -17,11 +17,11 @@ Repository, checkout and new artifacts use `purpur-tree-feller`. Runtime `TreeFe ./gradlew clean check jar ``` -The plugin JAR is written to `build/libs/purpur-tree-feller-.jar`. `check` includes `nativeTreeTest`, loading real item registries/services from SHA-256-verified Purpur 2618 under `build/tree-runtime/` without starting a listening server. Existing item/felling/undo assertions are retained. `LocalTestHarnessTest` checks parsed Compose settings and runs the actual startup script with isolated command doubles; it does not start Docker. +The plugin JAR is written to `build/libs/purpur-tree-feller-.jar`. `check` includes `nativeTreeTest`, loading real item registries/services from SHA-256-verified Purpur 26.3 build 2651 under `build/tree-runtime-26.3-2651/` without starting a listening server. The versioned runtime directory prevents verification from reusing extracted 26.2 state. Existing item/felling/undo assertions are retained. `LocalTestHarnessTest` checks parsed Compose settings and runs the actual startup script with isolated command doubles; it does not start Docker. ## Local Docker test server -Build the current plugin and start it on an isolated Java 25/Purpur 26.2 build 2618 server: +Build the current plugin and start it on an isolated Java 25/Purpur 26.3 build 2651 server: ```bash ./scripts/start-test-server.sh @@ -95,7 +95,7 @@ Player preferences, administrative locks, progress, unlocks, and known names are Tree detection searches matching natural trunk blocks laterally and upward, never below the initiating chop, and requires corresponding foliage or caps. Search bounds prevent unbounded traversal. Leaves, caps, roots, vines, and decorations are not automatically broken. -Automatic felling supports Purpur 26.2 wooden, stone, copper, iron, golden, diamond, and netherite axes. Additional trunk blocks use the server's `Player.breakBlock` path so block-break cancellation, drops, experience, enchantments, and axe durability remain authoritative. Protection plugins should cancel `BlockBreakEvent` normally. A cancelled additional break stops the remaining felling. +Automatic felling supports Purpur 26.3 wooden, stone, copper, iron, golden, diamond, and netherite axes. Additional trunk blocks use the server's `Player.breakBlock` path so block-break cancellation, drops, experience, enchantments, and axe durability remain authoritative. Protection plugins should cancel `BlockBreakEvent` normally. A cancelled additional break stops the remaining felling. ## Releases diff --git a/build.gradle.kts b/build.gradle.kts index a94ae6f..b0c34fc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,9 +26,9 @@ 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") - 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") @@ -44,20 +44,20 @@ tasks.test { } // Real item/felling/undo regressions require the declared runtime's registries and service providers. -val treeRuntime = layout.buildDirectory.dir("tree-runtime") +val treeRuntime = layout.buildDirectory.dir("tree-runtime-26.3-2651") val downloadTreeRuntime = tasks.register("downloadTreeRuntime") { - val launcher = treeRuntime.map { it.file("purpur-26.2-2618.jar") } + val launcher = treeRuntime.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").toURL().openConnection() + 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) } @@ -65,7 +65,7 @@ val downloadTreeRuntime = tasks.register("downloadTreeRuntime") { val prepareTreeRuntime = tasks.register("prepareTreeRuntime") { dependsOn(downloadTreeRuntime) javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) } - classpath = files(treeRuntime.map { it.file("purpur-26.2-2618.jar") }) + classpath = files(treeRuntime.map { it.file("purpur-26.3-2651.jar") }) mainClass = "io.papermc.paperclip.Main" jvmArgs("-Dpaperclip.patchonly=true") workingDir(treeRuntime) diff --git a/compose.test.yml b/compose.test.yml index 2bc4d1d..fd1c92b 100644 --- a/compose.test.yml +++ b/compose.test.yml @@ -9,8 +9,8 @@ services: environment: EULA: "TRUE" TYPE: PURPUR - VERSION: "26.2" - PURPUR_BUILD: "2618" + VERSION: "26.3" + PURPUR_BUILD: "2651" MEMORY: 2G ONLINE_MODE: "false" OPS_FILE: /config/tree-feller-ops.json diff --git a/scripts/start-test-server.sh b/scripts/start-test-server.sh index 7fc8065..3ffea79 100755 --- a/scripts/start-test-server.sh +++ b/scripts/start-test-server.sh @@ -32,7 +32,7 @@ if [[ -z "$container_id" ]]; then exit 1 fi -echo "Waiting up to ${wait_seconds}s for Purpur 26.2 build 2618..." +echo "Waiting up to ${wait_seconds}s for Purpur 26.3 build 2651..." deadline=$((SECONDS + wait_seconds)) while (( SECONDS < deadline )); do container_state="$(docker inspect --format '{{.State.Status}}' "$container_id")" @@ -51,7 +51,7 @@ done health_state="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id")" if [[ "$health_state" != "healthy" ]]; then docker compose -f "$compose_file" logs --tail=150 minecraft >&2 - echo "Timed out waiting for Purpur 26.2 build 2618 (status: $health_state)." >&2 + echo "Timed out waiting for Purpur 26.3 build 2651 (status: $health_state)." >&2 exit 1 fi diff --git a/src/nativeTest/java/games/dmg/treefeller/NativeRuntime.java b/src/nativeTest/java/games/dmg/treefeller/NativeRuntime.java index 4fd9750..b308917 100644 --- a/src/nativeTest/java/games/dmg/treefeller/NativeRuntime.java +++ b/src/nativeTest/java/games/dmg/treefeller/NativeRuntime.java @@ -41,12 +41,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/test/java/games/dmg/treefeller/LocalTestHarnessTest.java b/src/test/java/games/dmg/treefeller/LocalTestHarnessTest.java index 9de439c..c82883b 100644 --- a/src/test/java/games/dmg/treefeller/LocalTestHarnessTest.java +++ b/src/test/java/games/dmg/treefeller/LocalTestHarnessTest.java @@ -21,8 +21,8 @@ class LocalTestHarnessTest { Map service = (Map) ((Map) compose.get("services")).get("minecraft"); Map environment = (Map) service.get("environment"); assertEquals("PURPUR", environment.get("TYPE")); - assertEquals("2618", environment.get("PURPUR_BUILD")); - assertEquals("26.2", environment.get("VERSION")); + assertEquals("2651", environment.get("PURPUR_BUILD")); + assertEquals("26.3", environment.get("VERSION")); assertEquals("itzg/minecraft-server:java25", service.get("image")); assertEquals("tree-feller-test", compose.get("name")); assertEquals("tree-feller-test", service.get("container_name")); @@ -48,7 +48,7 @@ class LocalTestHarnessTest { void startupVerifiesBuildBeforeRecreateAndRetainsWorldState() throws Exception { Result result = runStartup(false); assertEquals(0, result.exitCode(), result.output()); - assertTrue(result.output().contains("Purpur 26.2 build 2618")); + assertTrue(result.output().contains("Purpur 26.3 build 2651")); assertTrue(result.output().contains("Tree Feller test server is ready")); assertTrue(result.calls().indexOf("gradle clean check jar") < result.calls().indexOf("up -d --force-recreate")); assertTrue(result.calls().contains("rcon-cli plugins")); diff --git a/src/test/java/games/dmg/treefeller/PluginMetadataTest.java b/src/test/java/games/dmg/treefeller/PluginMetadataTest.java index 9189208..0ce4bbe 100644 --- a/src/test/java/games/dmg/treefeller/PluginMetadataTest.java +++ b/src/test/java/games/dmg/treefeller/PluginMetadataTest.java @@ -17,6 +17,7 @@ class PluginMetadataTest { assertEquals("TreeFeller", metadata.get("name")); assertEquals("games.dmg.treefeller.TreeFellerPlugin", metadata.get("main")); + assertEquals("1.20", metadata.get("api-version")); Map commands = (Map) metadata.get("commands"); assertNotNull(commands.get("treefeller")); assertNotNull(commands.get("treefelleradmin")); diff --git a/src/test/java/games/dmg/treefeller/PurpurDistributionTest.java b/src/test/java/games/dmg/treefeller/PurpurDistributionTest.java index fcf1f59..679175a 100644 --- a/src/test/java/games/dmg/treefeller/PurpurDistributionTest.java +++ b/src/test/java/games/dmg/treefeller/PurpurDistributionTest.java @@ -21,9 +21,10 @@ class PurpurDistributionTest { assertEquals(version, metadata.get("version")); assertEquals("TreeFeller", metadata.get("name"), "Retain the data directory and persistent namespace"); assertEquals("games.dmg.treefeller.TreeFellerPlugin", metadata.get("main")); + assertEquals("1.20", metadata.get("api-version"), "Retain the deliberate compatibility floor"); assertTrue(((Map) metadata.get("permissions")).containsKey("treefeller.admin")); - assertFalse(jar.stream().anyMatch(entry -> entry.getName().startsWith("org/bukkit/") - || entry.getName().startsWith("org/purpurmc/") || entry.getName().endsWith("Test.class"))); + assertFalse(jar.stream().anyMatch(entry -> isForbiddenDistributionEntry(entry.getName()))); + assertNotNull(jar.getJarEntry("games/dmg/treefeller/TreeFellerPlugin.class")); } } @@ -41,7 +42,14 @@ class PurpurDistributionTest { @Test void releaseAndCiAgreeWithThePurpurDistributionNameAndToolchain() 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("tree-runtime-26.3-2651")); + assertFalse(build.contains("26.2.build.2618-stable") || build.contains("purpur-26.2-2618.jar") + || build.contains("/purpur/26.2/2618/download") + || build.contains("4a32d046a118804d89ca74ba89b798c98f6d8d1f310c18077ac573597049de31")); assertFalse(build.contains("org.spigotmc:spigot-api")); for (String workflow : new String[] {"ci.yml", "release.yml"}) { String content = Files.readString(Path.of(".gitea/workflows", workflow)); @@ -50,4 +58,18 @@ class PurpurDistributionTest { assertFalse(content.contains("build/libs/tree-feller-") || content.contains("name: tree-feller-"), workflow); } } + + private static boolean isForbiddenDistributionEntry(String name) { + return name.startsWith("org/bukkit/") + || name.startsWith("org/purpurmc/") + || name.startsWith("io/papermc/paper/") + || name.startsWith("com/destroystokyo/paper/") + || name.startsWith("net/minecraft/") + || name.startsWith("com/comphenix/protocol/") + || name.startsWith("org/junit/") + || name.startsWith("org/mockito/") + || name.endsWith("Test.class") + || name.contains("Test$") + || name.endsWith("NativeRuntime.class"); + } }