fix(platform): support Purpur 26.3
This commit is contained in:
1 parent
5b41190a09
commit
e33d19b94e
8 files changed
+51
-28
No files matched your search
@@ -1,12 +1,12 @@
|
|||||||
# Purpur Tree Feller
|
# 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).
|
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
|
## Requirements
|
||||||
|
|
||||||
- Purpur 26.2 build 2618
|
- Purpur 26.3 build 2651
|
||||||
- Java 25
|
- 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.
|
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
|
./gradlew clean check jar
|
||||||
```
|
```
|
||||||
|
|
||||||
The plugin JAR is written to `build/libs/purpur-tree-feller-<version>.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-<version>.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
|
## 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
|
```bash
|
||||||
./scripts/start-test-server.sh
|
./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.
|
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
|
## Releases
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -26,9 +26,9 @@ tasks.withType<JavaCompile>().configureEach {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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(platform("org.junit:junit-bom:5.13.4"))
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
testImplementation("org.mockito:mockito-core:5.18.0")
|
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.
|
// 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 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)
|
outputs.file(launcher)
|
||||||
doLast {
|
doLast {
|
||||||
val file = launcher.get().asFile
|
val file = launcher.get().asFile
|
||||||
file.parentFile.mkdirs()
|
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.connectTimeout = 30_000
|
||||||
connection.readTimeout = 120_000
|
connection.readTimeout = 120_000
|
||||||
val bytes = connection.getInputStream().use { it.readBytes() }
|
val bytes = connection.getInputStream().use { it.readBytes() }
|
||||||
val digest = MessageDigest.getInstance("SHA-256").digest(bytes).joinToString("") { "%02x".format(it) }
|
val digest = MessageDigest.getInstance("SHA-256").digest(bytes).joinToString("") { "%02x".format(it) }
|
||||||
check(digest == "4a32d046a118804d89ca74ba89b798c98f6d8d1f310c18077ac573597049de31") {
|
check(digest == "0677c1febecfbc2e5b0a7f76abd647b70a1f38d3cbdf3dd6a82e401b4262a598") {
|
||||||
"Purpur 2618 checksum mismatch"
|
"Purpur 2651 checksum mismatch"
|
||||||
}
|
}
|
||||||
file.writeBytes(bytes)
|
file.writeBytes(bytes)
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ val downloadTreeRuntime = tasks.register("downloadTreeRuntime") {
|
|||||||
val prepareTreeRuntime = tasks.register<JavaExec>("prepareTreeRuntime") {
|
val prepareTreeRuntime = tasks.register<JavaExec>("prepareTreeRuntime") {
|
||||||
dependsOn(downloadTreeRuntime)
|
dependsOn(downloadTreeRuntime)
|
||||||
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) }
|
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"
|
mainClass = "io.papermc.paperclip.Main"
|
||||||
jvmArgs("-Dpaperclip.patchonly=true")
|
jvmArgs("-Dpaperclip.patchonly=true")
|
||||||
workingDir(treeRuntime)
|
workingDir(treeRuntime)
|
||||||
|
|||||||
+2
-2
@@ -9,8 +9,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
EULA: "TRUE"
|
EULA: "TRUE"
|
||||||
TYPE: PURPUR
|
TYPE: PURPUR
|
||||||
VERSION: "26.2"
|
VERSION: "26.3"
|
||||||
PURPUR_BUILD: "2618"
|
PURPUR_BUILD: "2651"
|
||||||
MEMORY: 2G
|
MEMORY: 2G
|
||||||
ONLINE_MODE: "false"
|
ONLINE_MODE: "false"
|
||||||
OPS_FILE: /config/tree-feller-ops.json
|
OPS_FILE: /config/tree-feller-ops.json
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ if [[ -z "$container_id" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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))
|
deadline=$((SECONDS + wait_seconds))
|
||||||
while (( SECONDS < deadline )); do
|
while (( SECONDS < deadline )); do
|
||||||
container_state="$(docker inspect --format '{{.State.Status}}' "$container_id")"
|
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")"
|
health_state="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id")"
|
||||||
if [[ "$health_state" != "healthy" ]]; then
|
if [[ "$health_state" != "healthy" ]]; then
|
||||||
docker compose -f "$compose_file" logs --tail=150 minecraft >&2
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -41,12 +41,12 @@ final class NativeRuntime {
|
|||||||
try (var resources = new MultiPackResourceManager(PackType.SERVER_DATA, packs.openAllSelected())) {
|
try (var resources = new MultiPackResourceManager(PackType.SERVER_DATA, packs.openAllSelected())) {
|
||||||
LayeredRegistryAccess<RegistryLayer> layers = RegistryLayer.createRegistryAccess();
|
LayeredRegistryAccess<RegistryLayer> layers = RegistryLayer.createRegistryAccess();
|
||||||
List<Registry.PendingTags<?>> tags = TagLoader.loadTagsForExistingRegistries(resources, layers.getLayer(RegistryLayer.STATIC));
|
List<Registry.PendingTags<?>> tags = TagLoader.loadTagsForExistingRegistries(resources, layers.getLayer(RegistryLayer.STATIC));
|
||||||
List<HolderLookup.RegistryLookup<?>> lookups = TagLoader.buildUpdatedLookups(layers.getAccessForLoading(RegistryLayer.WORLDGEN), tags);
|
List<HolderLookup.RegistryLookup<?>> lookups = TagLoader.buildUpdatedLookups(layers.getAccessForLoading(RegistryLayer.WORLD), tags);
|
||||||
RegistryAccess.Frozen worldgen = RegistryDataLoader.load(resources, lookups,
|
RegistryAccess.Frozen world = RegistryDataLoader.load(resources, lookups,
|
||||||
RegistryDataLoader.WORLDGEN_REGISTRIES, Util.backgroundExecutor()).join();
|
RegistryDataLoader.WORLD_REGISTRIES, Util.backgroundExecutor()).join();
|
||||||
layers = layers.replaceFrom(RegistryLayer.WORLDGEN, worldgen);
|
layers = layers.replaceFrom(RegistryLayer.WORLD, world);
|
||||||
RegistryAccess.Frozen dimensions = RegistryDataLoader.load(resources,
|
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();
|
RegistryDataLoader.DIMENSION_REGISTRIES, Util.backgroundExecutor()).join();
|
||||||
layers = layers.replaceFrom(RegistryLayer.DIMENSIONS, dimensions);
|
layers = layers.replaceFrom(RegistryLayer.DIMENSIONS, dimensions);
|
||||||
Class.forName(org.bukkit.Registry.class.getName());
|
Class.forName(org.bukkit.Registry.class.getName());
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ class LocalTestHarnessTest {
|
|||||||
Map<?, ?> service = (Map<?, ?>) ((Map<?, ?>) compose.get("services")).get("minecraft");
|
Map<?, ?> service = (Map<?, ?>) ((Map<?, ?>) compose.get("services")).get("minecraft");
|
||||||
Map<?, ?> environment = (Map<?, ?>) service.get("environment");
|
Map<?, ?> environment = (Map<?, ?>) service.get("environment");
|
||||||
assertEquals("PURPUR", environment.get("TYPE"));
|
assertEquals("PURPUR", environment.get("TYPE"));
|
||||||
assertEquals("2618", environment.get("PURPUR_BUILD"));
|
assertEquals("2651", environment.get("PURPUR_BUILD"));
|
||||||
assertEquals("26.2", environment.get("VERSION"));
|
assertEquals("26.3", environment.get("VERSION"));
|
||||||
assertEquals("itzg/minecraft-server:java25", service.get("image"));
|
assertEquals("itzg/minecraft-server:java25", service.get("image"));
|
||||||
assertEquals("tree-feller-test", compose.get("name"));
|
assertEquals("tree-feller-test", compose.get("name"));
|
||||||
assertEquals("tree-feller-test", service.get("container_name"));
|
assertEquals("tree-feller-test", service.get("container_name"));
|
||||||
@@ -48,7 +48,7 @@ class LocalTestHarnessTest {
|
|||||||
void startupVerifiesBuildBeforeRecreateAndRetainsWorldState() throws Exception {
|
void startupVerifiesBuildBeforeRecreateAndRetainsWorldState() throws Exception {
|
||||||
Result result = runStartup(false);
|
Result result = runStartup(false);
|
||||||
assertEquals(0, result.exitCode(), result.output());
|
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.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().indexOf("gradle clean check jar") < result.calls().indexOf("up -d --force-recreate"));
|
||||||
assertTrue(result.calls().contains("rcon-cli plugins"));
|
assertTrue(result.calls().contains("rcon-cli plugins"));
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class PluginMetadataTest {
|
|||||||
|
|
||||||
assertEquals("TreeFeller", metadata.get("name"));
|
assertEquals("TreeFeller", metadata.get("name"));
|
||||||
assertEquals("games.dmg.treefeller.TreeFellerPlugin", metadata.get("main"));
|
assertEquals("games.dmg.treefeller.TreeFellerPlugin", metadata.get("main"));
|
||||||
|
assertEquals("1.20", metadata.get("api-version"));
|
||||||
Map<?, ?> commands = (Map<?, ?>) metadata.get("commands");
|
Map<?, ?> commands = (Map<?, ?>) metadata.get("commands");
|
||||||
assertNotNull(commands.get("treefeller"));
|
assertNotNull(commands.get("treefeller"));
|
||||||
assertNotNull(commands.get("treefelleradmin"));
|
assertNotNull(commands.get("treefelleradmin"));
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ class PurpurDistributionTest {
|
|||||||
assertEquals(version, metadata.get("version"));
|
assertEquals(version, metadata.get("version"));
|
||||||
assertEquals("TreeFeller", metadata.get("name"), "Retain the data directory and persistent namespace");
|
assertEquals("TreeFeller", metadata.get("name"), "Retain the data directory and persistent namespace");
|
||||||
assertEquals("games.dmg.treefeller.TreeFellerPlugin", metadata.get("main"));
|
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"));
|
assertTrue(((Map<?, ?>) metadata.get("permissions")).containsKey("treefeller.admin"));
|
||||||
assertFalse(jar.stream().anyMatch(entry -> entry.getName().startsWith("org/bukkit/")
|
assertFalse(jar.stream().anyMatch(entry -> isForbiddenDistributionEntry(entry.getName())));
|
||||||
|| entry.getName().startsWith("org/purpurmc/") || entry.getName().endsWith("Test.class")));
|
assertNotNull(jar.getJarEntry("games/dmg/treefeller/TreeFellerPlugin.class"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +42,14 @@ class PurpurDistributionTest {
|
|||||||
@Test
|
@Test
|
||||||
void releaseAndCiAgreeWithThePurpurDistributionNameAndToolchain() throws Exception {
|
void releaseAndCiAgreeWithThePurpurDistributionNameAndToolchain() throws Exception {
|
||||||
String build = Files.readString(Path.of("build.gradle.kts"));
|
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"));
|
assertFalse(build.contains("org.spigotmc:spigot-api"));
|
||||||
for (String workflow : new String[] {"ci.yml", "release.yml"}) {
|
for (String workflow : new String[] {"ci.yml", "release.yml"}) {
|
||||||
String content = Files.readString(Path.of(".gitea/workflows", workflow));
|
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);
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user