fix(tyrant): support Purpur 26.3
This commit is contained in:
1 parent
8cfee3ce39
commit
755b9e8d03
4 files changed
+233
-5
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
# Purpur Tyrant
|
# Purpur Tyrant
|
||||||
|
|
||||||
The asymmetric Tyrant/Vigilante game plugin, targeting Java 25 and Purpur 26.2 build 2618.
|
The asymmetric Tyrant/Vigilante game plugin, targeting Java 25 and Purpur 26.3 build 2651.
|
||||||
|
|
||||||
## Build and releases
|
## Build and releases
|
||||||
|
|
||||||
@@ -10,6 +10,8 @@ The asymmetric Tyrant/Vigilante game plugin, targeting Java 25 and Purpur 26.2 b
|
|||||||
|
|
||||||
New artifacts use `build/libs/purpur-tyrant-<version>.jar`. Gitea CI checks pushes/pull requests; approved conventional commits on main drive versioned releases. Distribution tests inspect the actual JAR, version, Java 25 bytecode, retained runtime identity and workflow names.
|
New artifacts use `build/libs/purpur-tyrant-<version>.jar`. Gitea CI checks pushes/pull requests; approved conventional commits on main drive versioned releases. Distribution tests inspect the actual JAR, version, Java 25 bytecode, retained runtime identity and workflow names.
|
||||||
|
|
||||||
|
The full verification command compiles against Purpur API `26.3.build.2651-stable`, downloads build 2651 from the Purpur API, verifies SHA-256 `0677c1febecfbc2e5b0a7f76abd647b70a1f38d3cbdf3dd6a82e401b4262a598`, and starts a clean loopback-only Purpur server. The native lifecycle check loads, enables, commands, and disables the packaged plugin while proving persisted role/game state and a captured-entity snapshot survive the cycle. It uses generated build-local server state, not the untracked Compose environment, and is automated compatibility evidence rather than a live-player test.
|
||||||
|
|
||||||
## Migration compatibility
|
## Migration compatibility
|
||||||
|
|
||||||
The repository and checkout are now `purpur-tyrant`, previously `spigot-tyrant`. Runtime identity remains **SpigotTyrant**, including `plugins/SpigotTyrant/`, Java entrypoints/packages, command/permission names and persistent keys for roles, captured mobs and legacy items. The migration changes the build/distribution target, not gameplay or persisted schemas. New releases no longer support Java 17.
|
The repository and checkout are now `purpur-tyrant`, previously `spigot-tyrant`. Runtime identity remains **SpigotTyrant**, including `plugins/SpigotTyrant/`, Java entrypoints/packages, command/permission names and persistent keys for roles, captured mobs and legacy items. The migration changes the build/distribution target, not gameplay or persisted schemas. New releases no longer support Java 17.
|
||||||
|
|||||||
+62
-2
@@ -1,3 +1,6 @@
|
|||||||
|
import java.net.URI
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
}
|
}
|
||||||
@@ -24,9 +27,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")
|
||||||
@@ -41,6 +44,63 @@ tasks.test {
|
|||||||
systemProperty("distribution.version", project.version.toString())
|
systemProperty("distribution.version", project.version.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val nativeRuntime = layout.buildDirectory.dir("native-runtime")
|
||||||
|
val downloadNativeRuntime = tasks.register("downloadNativeRuntime") {
|
||||||
|
val launcher = nativeRuntime.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.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 == "0677c1febecfbc2e5b0a7f76abd647b70a1f38d3cbdf3dd6a82e401b4262a598") {
|
||||||
|
"Purpur 2651 checksum mismatch"
|
||||||
|
}
|
||||||
|
file.writeBytes(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val prepareNativeRuntime = tasks.register<JavaExec>("prepareNativeRuntime") {
|
||||||
|
dependsOn(downloadNativeRuntime)
|
||||||
|
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) }
|
||||||
|
classpath = files(nativeRuntime.map { it.file("purpur-26.3-2651.jar") })
|
||||||
|
mainClass = "io.papermc.paperclip.Main"
|
||||||
|
jvmArgs("-Dpaperclip.patchonly=true")
|
||||||
|
workingDir(nativeRuntime)
|
||||||
|
outputs.dir(nativeRuntime.map { it.dir("versions") })
|
||||||
|
outputs.dir(nativeRuntime.map { it.dir("libraries") })
|
||||||
|
}
|
||||||
|
val nativeTest = sourceSets.create("nativeTest")
|
||||||
|
dependencies {
|
||||||
|
add(nativeTest.implementationConfigurationName, platform("org.junit:junit-bom:5.13.4"))
|
||||||
|
add(nativeTest.implementationConfigurationName, "org.junit.jupiter:junit-jupiter")
|
||||||
|
add(nativeTest.runtimeOnlyConfigurationName, "org.junit.platform:junit-platform-launcher")
|
||||||
|
}
|
||||||
|
val nativeRuntimeJars = files(fileTree(nativeRuntime) {
|
||||||
|
include("versions/**/*.jar", "libraries/**/*.jar")
|
||||||
|
}).builtBy(prepareNativeRuntime)
|
||||||
|
nativeTest.compileClasspath += sourceSets.main.get().output + nativeRuntimeJars
|
||||||
|
nativeTest.runtimeClasspath += sourceSets.main.get().runtimeClasspath + nativeRuntimeJars
|
||||||
|
val nativeLifecycleTest = tasks.register<Test>("nativeLifecycleTest") {
|
||||||
|
description = "Loads, enables, and disables the packaged plugin on the pinned Purpur runtime"
|
||||||
|
dependsOn(tasks.jar)
|
||||||
|
testClassesDirs = nativeTest.output.classesDirs
|
||||||
|
classpath = nativeTest.runtimeClasspath
|
||||||
|
useJUnitPlatform()
|
||||||
|
maxHeapSize = "1G"
|
||||||
|
workingDir(nativeRuntime)
|
||||||
|
systemProperty("distribution.jar", tasks.jar.get().archiveFile.get().asFile.absolutePath)
|
||||||
|
systemProperty(
|
||||||
|
"purpur.launcher",
|
||||||
|
nativeRuntime.map { it.file("purpur-26.3-2651.jar") }.get().asFile.absolutePath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tasks.check { dependsOn(nativeLifecycleTest) }
|
||||||
|
|
||||||
val pluginVersion = version
|
val pluginVersion = version
|
||||||
|
|
||||||
tasks.processResources {
|
tasks.processResources {
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/** Exercises the packaged plugin through a real, isolated Purpur server lifecycle. */
|
||||||
|
final class SpigotTyrantLifecycleNativeTest {
|
||||||
|
private static final String TYRANT_ID = "11111111-1111-1111-1111-111111111111";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void packagedPluginLoadsStateAndCapturedEntitySnapshotAcrossEnableAndDisable()
|
||||||
|
throws Exception {
|
||||||
|
Path runtime = Path.of("").toAbsolutePath();
|
||||||
|
Path plugins = Files.createDirectories(runtime.resolve("plugins"));
|
||||||
|
Path dataDirectory = Files.createDirectories(plugins.resolve("SpigotTyrant"));
|
||||||
|
Files.copy(
|
||||||
|
Path.of(System.getProperty("distribution.jar")),
|
||||||
|
plugins.resolve("purpur-tyrant-native-test.jar"),
|
||||||
|
StandardCopyOption.REPLACE_EXISTING
|
||||||
|
);
|
||||||
|
try (var config = getClass().getClassLoader().getResourceAsStream("config.yml")) {
|
||||||
|
if (config == null) {
|
||||||
|
throw new AssertionError("Missing packaged default config");
|
||||||
|
}
|
||||||
|
Files.copy(config, dataDirectory.resolve("config.yml"));
|
||||||
|
}
|
||||||
|
Files.writeString(dataDirectory.resolve("state.yml"), """
|
||||||
|
game:
|
||||||
|
lifecycle: RUNNING
|
||||||
|
tyrant-id: 11111111-1111-1111-1111-111111111111
|
||||||
|
tyrant-level: 3
|
||||||
|
unspent-choices: 2
|
||||||
|
purchases: [ASSASSIN]
|
||||||
|
claimed-armor: [HELMET]
|
||||||
|
players:
|
||||||
|
11111111-1111-1111-1111-111111111111:
|
||||||
|
name: NativeTyrant
|
||||||
|
tyrant-class: TAMER
|
||||||
|
captured-mobs:
|
||||||
|
- entity-type: ZOMBIE
|
||||||
|
data:
|
||||||
|
snapshot: '{id:"minecraft:zombie",CustomName:"Native"}'
|
||||||
|
""");
|
||||||
|
Files.writeString(runtime.resolve("eula.txt"), "eula=true\n");
|
||||||
|
Files.writeString(runtime.resolve("server.properties"), """
|
||||||
|
online-mode=false
|
||||||
|
server-port=0
|
||||||
|
query.port=0
|
||||||
|
enable-query=false
|
||||||
|
enable-rcon=false
|
||||||
|
spawn-protection=0
|
||||||
|
view-distance=2
|
||||||
|
simulation-distance=2
|
||||||
|
level-name=native-lifecycle-world
|
||||||
|
motd=Spigot Tyrant native lifecycle
|
||||||
|
""");
|
||||||
|
|
||||||
|
Process process = new ProcessBuilder(
|
||||||
|
Path.of(System.getProperty("java.home"), "bin", "java").toString(),
|
||||||
|
"-Xms512M",
|
||||||
|
"-Xmx1G",
|
||||||
|
"-jar",
|
||||||
|
System.getProperty("purpur.launcher"),
|
||||||
|
"--nogui"
|
||||||
|
).directory(runtime.toFile()).redirectErrorStream(true).start();
|
||||||
|
CountDownLatch started = new CountDownLatch(1);
|
||||||
|
CompletableFuture<String> output = CompletableFuture.supplyAsync(() ->
|
||||||
|
captureOutput(process, started)
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
assertTrue(started.await(120, TimeUnit.SECONDS), () ->
|
||||||
|
"Purpur did not finish startup:\n" + completedOutput(output)
|
||||||
|
);
|
||||||
|
process.outputWriter(StandardCharsets.UTF_8).append("tyrantadmin status\n")
|
||||||
|
.append("tyrantadmin grantpoint\n").flush();
|
||||||
|
awaitPersistedProgress(dataDirectory.resolve("state.yml"));
|
||||||
|
process.outputWriter(StandardCharsets.UTF_8).append("stop\n").flush();
|
||||||
|
assertTrue(process.waitFor(60, TimeUnit.SECONDS), "Purpur did not stop cleanly");
|
||||||
|
} finally {
|
||||||
|
if (process.isAlive()) {
|
||||||
|
process.destroyForcibly();
|
||||||
|
process.waitFor(10, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String log = output.get(10, TimeUnit.SECONDS);
|
||||||
|
assertEquals(0, process.exitValue(), log);
|
||||||
|
assertTrue(log.contains("Enabling SpigotTyrant"), log);
|
||||||
|
assertTrue(log.contains("Spigot Tyrant enabled."), log);
|
||||||
|
assertTrue(log.contains("Lifecycle: RUNNING"), log);
|
||||||
|
assertTrue(log.contains("Tyrant: " + TYRANT_ID), log);
|
||||||
|
assertTrue(log.contains("NativeTyrant (" + TYRANT_ID + "): class=TAMER"), log);
|
||||||
|
assertTrue(log.contains("Disabling SpigotTyrant"), log);
|
||||||
|
|
||||||
|
PersistentState saved = new YamlTyrantStateRepository(
|
||||||
|
dataDirectory.resolve("state.yml")
|
||||||
|
).load();
|
||||||
|
assertEquals(4, saved.game().tyrantLevel());
|
||||||
|
assertEquals(3, saved.game().unspentChoices());
|
||||||
|
CapturedMob captured = saved.players().values().iterator().next().capturedMobs().getFirst();
|
||||||
|
assertEquals("ZOMBIE", captured.entityType());
|
||||||
|
assertEquals(
|
||||||
|
"{id:\"minecraft:zombie\",CustomName:\"Native\"}",
|
||||||
|
captured.data().get("snapshot")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String captureOutput(Process process, CountDownLatch started) {
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
try (var reader = new BufferedReader(new InputStreamReader(
|
||||||
|
process.getInputStream(), StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
output.append(line).append('\n');
|
||||||
|
if (line.contains("Done (") && line.contains("For help, type")) {
|
||||||
|
started.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output.toString();
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new CompletionException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void awaitPersistedProgress(Path stateFile) throws Exception {
|
||||||
|
long deadline = System.nanoTime() + Duration.ofSeconds(20).toNanos();
|
||||||
|
while (System.nanoTime() < deadline) {
|
||||||
|
PersistentState state = new YamlTyrantStateRepository(stateFile).load();
|
||||||
|
if (state.game().tyrantLevel() == 4 && state.game().unspentChoices() == 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Thread.sleep(100);
|
||||||
|
}
|
||||||
|
throw new TimeoutException("Tyrant state did not reflect the native grantpoint command");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String completedOutput(CompletableFuture<String> output) {
|
||||||
|
return output.isDone() ? output.join() : "<server still running>";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,12 @@ class PurpurDistributionTest {
|
|||||||
assertEquals("games.dmg.spigottyrant.SpigotTyrantPlugin", metadata.get("main"));
|
assertEquals("games.dmg.spigottyrant.SpigotTyrantPlugin", metadata.get("main"));
|
||||||
assertTrue(((Map<?, ?>) metadata.get("permissions")).containsKey("spigottyrant.admin"));
|
assertTrue(((Map<?, ?>) metadata.get("permissions")).containsKey("spigottyrant.admin"));
|
||||||
assertFalse(jar.stream().anyMatch(entry -> entry.getName().startsWith("org/bukkit/")
|
assertFalse(jar.stream().anyMatch(entry -> entry.getName().startsWith("org/bukkit/")
|
||||||
|| entry.getName().startsWith("org/purpurmc/") || entry.getName().endsWith("Test.class")));
|
|| entry.getName().startsWith("org/purpurmc/")
|
||||||
|
|| entry.getName().startsWith("net/minecraft/")
|
||||||
|
|| entry.getName().startsWith("com/comphenix/protocol/")
|
||||||
|
|| entry.getName().contains("/test/")
|
||||||
|
|| entry.getName().contains("/nativeTest/")
|
||||||
|
|| entry.getName().endsWith("Test.class")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,8 +46,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"));
|
||||||
|
assertFalse(build.contains("26.2.build.2618-stable"));
|
||||||
assertFalse(build.contains("org.spigotmc:spigot-api"));
|
assertFalse(build.contains("org.spigotmc:spigot-api"));
|
||||||
|
String readme = Files.readString(Path.of("README.md"));
|
||||||
|
assertTrue(readme.contains("Purpur 26.3 build 2651"));
|
||||||
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));
|
||||||
assertTrue(content.contains("java-version: 25"));
|
assertTrue(content.contains("java-version: 25"));
|
||||||
|
|||||||
Reference in new issue
Block a user