feat(platform): support Purpur 26.3
This commit is contained in:
1 parent
4abddb5d4d
commit
7496564b94
4 files changed
+231
-10
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
# Purpur Quest Board
|
# Purpur Quest Board
|
||||||
|
|
||||||
A Purpur 26.2 plugin for shared physical quest boards. Players can escrow rewards for item deliveries or player bounties, fulfill them, and collect exact rewards through shared boards.
|
A Purpur 26.3 plugin for shared physical quest boards. Players can escrow rewards for item deliveries or player bounties, fulfill them, and collect exact rewards through shared boards.
|
||||||
|
|
||||||
The approved behavior is specified in the [SoMC OKF wiki](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/purpur-quest-board/index.md).
|
The approved behavior is specified in the [SoMC OKF wiki](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/purpur-quest-board/index.md).
|
||||||
|
|
||||||
@@ -36,8 +36,8 @@ The repository, checkout and new JARs use `purpur-quest-board`. Runtime `SpigotQ
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Purpur 26.2 build 2618
|
- Purpur 26.3 build 2651
|
||||||
- Java 25 or newer
|
- Java 25
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ The repository, checkout and new JARs use `purpur-quest-board`. Runtime `SpigotQ
|
|||||||
./gradlew clean check jar
|
./gradlew clean check jar
|
||||||
```
|
```
|
||||||
|
|
||||||
The plugin JAR is written to `build/libs/`.
|
The plugin JAR is written to `build/libs/`. The verification task downloads the fixed Purpur 26.3 build 2651 runtime, verifies SHA-256 `0677c1febecfbc2e5b0a7f76abd647b70a1f38d3cbdf3dd6a82e401b4262a598`, and starts an isolated server to prove plugin load, enable, and orderly disable. Mock/API tests remain separate coverage for dialogs, boards, escrow, claims, persistence, and ownership locking.
|
||||||
|
|
||||||
## Player commands
|
## Player commands
|
||||||
|
|
||||||
|
|||||||
+95
-3
@@ -1,3 +1,8 @@
|
|||||||
|
import java.net.URI
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
}
|
}
|
||||||
@@ -23,9 +28,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")
|
||||||
@@ -33,13 +38,100 @@ dependencies {
|
|||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val purpurVersion = "26.3"
|
||||||
|
val purpurBuild = "2651"
|
||||||
|
val purpurRuntimeSha256 = "0677c1febecfbc2e5b0a7f76abd647b70a1f38d3cbdf3dd6a82e401b4262a598"
|
||||||
|
val purpurRuntimeJar = layout.projectDirectory.file(".gradle/purpur-runtime/purpur-$purpurVersion-$purpurBuild.jar")
|
||||||
|
|
||||||
|
fun sha256(file: File): String {
|
||||||
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
file.inputStream().buffered().use { input ->
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
while (true) {
|
||||||
|
val read = input.read(buffer)
|
||||||
|
if (read < 0) break
|
||||||
|
digest.update(buffer, 0, read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return digest.digest().joinToString("") { "%02x".format(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadPurpurRuntime = tasks.register("downloadPurpurRuntime") {
|
||||||
|
description = "Downloads and verifies the exact supported Purpur runtime"
|
||||||
|
group = "verification"
|
||||||
|
outputs.file(purpurRuntimeJar)
|
||||||
|
outputs.upToDateWhen { false }
|
||||||
|
doLast {
|
||||||
|
val destination = purpurRuntimeJar.asFile
|
||||||
|
destination.parentFile.mkdirs()
|
||||||
|
if (!destination.isFile || sha256(destination) != purpurRuntimeSha256) {
|
||||||
|
Files.deleteIfExists(destination.toPath())
|
||||||
|
val temporary = Files.createTempFile(destination.parentFile.toPath(), destination.name, ".part")
|
||||||
|
try {
|
||||||
|
val connection = URI(
|
||||||
|
"https://api.purpurmc.org/v2/purpur/$purpurVersion/$purpurBuild/download"
|
||||||
|
).toURL().openConnection().apply {
|
||||||
|
connectTimeout = 30_000
|
||||||
|
readTimeout = 120_000
|
||||||
|
}
|
||||||
|
connection.getInputStream().use { input ->
|
||||||
|
Files.newOutputStream(temporary).use(input::copyTo)
|
||||||
|
}
|
||||||
|
val actual = sha256(temporary.toFile())
|
||||||
|
check(actual == purpurRuntimeSha256) {
|
||||||
|
"Purpur runtime SHA-256 mismatch: expected $purpurRuntimeSha256, got $actual"
|
||||||
|
}
|
||||||
|
Files.move(
|
||||||
|
temporary,
|
||||||
|
destination.toPath(),
|
||||||
|
StandardCopyOption.ATOMIC_MOVE,
|
||||||
|
StandardCopyOption.REPLACE_EXISTING
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(temporary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check(sha256(destination) == purpurRuntimeSha256) {
|
||||||
|
"Cached Purpur runtime failed SHA-256 verification"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val java25Launcher = javaToolchains.launcherFor {
|
||||||
|
languageVersion = JavaLanguageVersion.of(25)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pluginJar = tasks.jar.flatMap { it.archiveFile }
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
dependsOn(tasks.jar)
|
dependsOn(tasks.jar)
|
||||||
systemProperty("distribution.jar", tasks.jar.get().archiveFile.get().asFile.absolutePath)
|
exclude("**/PurpurRuntimeLifecycleTest.class")
|
||||||
|
javaLauncher = java25Launcher
|
||||||
|
systemProperty("distribution.jar", pluginJar.get().asFile.absolutePath)
|
||||||
systemProperty("distribution.version", project.version.toString())
|
systemProperty("distribution.version", project.version.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val purpurRuntimeTest = tasks.register<Test>("purpurRuntimeTest") {
|
||||||
|
description = "Loads, enables, and disables the plugin on the supported Purpur runtime"
|
||||||
|
group = "verification"
|
||||||
|
dependsOn(downloadPurpurRuntime, tasks.jar)
|
||||||
|
testClassesDirs = sourceSets.test.get().output.classesDirs
|
||||||
|
classpath = sourceSets.test.get().runtimeClasspath
|
||||||
|
useJUnitPlatform()
|
||||||
|
include("**/PurpurRuntimeLifecycleTest.class")
|
||||||
|
javaLauncher = java25Launcher
|
||||||
|
systemProperty("purpur.runtime.jar", purpurRuntimeJar.asFile.absolutePath)
|
||||||
|
systemProperty("purpur.runtime.version", purpurVersion)
|
||||||
|
systemProperty("purpur.runtime.build", purpurBuild)
|
||||||
|
systemProperty("distribution.jar", pluginJar.get().asFile.absolutePath)
|
||||||
|
shouldRunAfter(tasks.test)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.check {
|
||||||
|
dependsOn(purpurRuntimeTest)
|
||||||
|
}
|
||||||
|
|
||||||
val pluginVersion = version
|
val pluginVersion = version
|
||||||
|
|
||||||
tasks.processResources {
|
tasks.processResources {
|
||||||
|
|||||||
@@ -21,11 +21,27 @@ class PurpurDistributionTest {
|
|||||||
assertEquals(version, metadata.get("version"));
|
assertEquals(version, metadata.get("version"));
|
||||||
assertEquals("SpigotQuestBoard", metadata.get("name"), "Retain the data directory and persistent namespace");
|
assertEquals("SpigotQuestBoard", metadata.get("name"), "Retain the data directory and persistent namespace");
|
||||||
assertEquals("games.dmg.spigotquestboard.SpigotQuestBoardPlugin", metadata.get("main"));
|
assertEquals("games.dmg.spigotquestboard.SpigotQuestBoardPlugin", metadata.get("main"));
|
||||||
|
assertEquals("1.20", metadata.get("api-version"), "Retain the deliberate compatibility floor");
|
||||||
|
assertEquals(java.util.Set.of("quests", "questadmin"), ((Map<?, ?>) metadata.get("commands")).keySet());
|
||||||
assertTrue(((Map<?, ?>) metadata.get("permissions")).containsKey("spigotquestboard.admin"));
|
assertTrue(((Map<?, ?>) metadata.get("permissions")).containsKey("spigotquestboard.admin"));
|
||||||
|
assertNull(metadata.get("depend"));
|
||||||
|
assertNull(metadata.get("softdepend"));
|
||||||
|
assertNull(metadata.get("loadbefore"));
|
||||||
var description = new org.bukkit.plugin.PluginDescriptionFile(jar.getInputStream(jar.getJarEntry("plugin.yml")));
|
var description = new org.bukkit.plugin.PluginDescriptionFile(jar.getInputStream(jar.getJarEntry("plugin.yml")));
|
||||||
assertEquals("spigotquestboard", description.namespace());
|
assertEquals("spigotquestboard", description.namespace());
|
||||||
assertFalse(jar.stream().anyMatch(entry -> entry.getName().startsWith("org/bukkit/")
|
assertFalse(jar.stream().anyMatch(entry -> {
|
||||||
|| entry.getName().startsWith("org/purpurmc/") || entry.getName().endsWith("Test.class")));
|
String name = entry.getName();
|
||||||
|
return name.startsWith("org/bukkit/")
|
||||||
|
|| name.startsWith("org/purpurmc/")
|
||||||
|
|| name.startsWith("io/papermc/")
|
||||||
|
|| 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/");
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +59,8 @@ 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"));
|
||||||
|
assertFalse(build.contains("26.2.build.2618"));
|
||||||
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));
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package games.dmg.spigotquestboard;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
class PurpurRuntimeLifecycleTest {
|
||||||
|
private static final Duration STARTUP_TIMEOUT = Duration.ofMinutes(3);
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path serverDirectory;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadsEnablesAndDisablesOnExactSupportedPurpurRuntime() throws Exception {
|
||||||
|
Path runtimeJar = requiredPath("purpur.runtime.jar");
|
||||||
|
Path pluginJar = requiredPath("distribution.jar");
|
||||||
|
String version = System.getProperty("purpur.runtime.version");
|
||||||
|
String build = System.getProperty("purpur.runtime.build");
|
||||||
|
|
||||||
|
Path plugins = Files.createDirectories(serverDirectory.resolve("plugins"));
|
||||||
|
Files.copy(pluginJar, plugins.resolve(pluginJar.getFileName()), StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
Files.writeString(serverDirectory.resolve("eula.txt"), "eula=true\n", UTF_8);
|
||||||
|
Files.writeString(serverDirectory.resolve("server.properties"), String.join("\n",
|
||||||
|
"level-name=runtime-world",
|
||||||
|
"online-mode=false",
|
||||||
|
"server-port=0",
|
||||||
|
"spawn-protection=0",
|
||||||
|
"sync-chunk-writes=true",
|
||||||
|
"") , UTF_8);
|
||||||
|
|
||||||
|
Process process = new ProcessBuilder(List.of(
|
||||||
|
javaExecutable().toString(),
|
||||||
|
"-Xms256M",
|
||||||
|
"-Xmx768M",
|
||||||
|
"-jar",
|
||||||
|
runtimeJar.toString(),
|
||||||
|
"--nogui"
|
||||||
|
)).directory(serverDirectory.toFile()).redirectErrorStream(true).start();
|
||||||
|
|
||||||
|
StringBuffer output = new StringBuffer();
|
||||||
|
AtomicBoolean stopSent = new AtomicBoolean();
|
||||||
|
try (var readerExecutor = Executors.newSingleThreadExecutor(
|
||||||
|
Thread.ofPlatform().name("purpur-runtime-output").factory())) {
|
||||||
|
var outputRead = readerExecutor.submit(() -> readOutputAndStop(process, output, stopSent));
|
||||||
|
boolean exited = process.waitFor(STARTUP_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
|
||||||
|
if (!exited) {
|
||||||
|
process.destroyForcibly();
|
||||||
|
process.waitFor(30, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
outputRead.get(30, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
String log = output.toString();
|
||||||
|
assertTrue(exited, () -> "Purpur did not stop within " + STARTUP_TIMEOUT + "\n" + log);
|
||||||
|
assertEquals(0, process.exitValue(), log);
|
||||||
|
assertTrue(stopSent.get(), () -> "Purpur never completed startup\n" + log);
|
||||||
|
assertTrue(log.contains("Purpur") && log.contains(version) && log.contains(build),
|
||||||
|
() -> "Expected Purpur " + version + " build " + build + " identity\n" + log);
|
||||||
|
assertTrue(log.contains("Enabling SpigotQuestBoard"), log);
|
||||||
|
assertTrue(log.contains("Disabling SpigotQuestBoard"), log);
|
||||||
|
assertFalse(log.contains("Could not load plugin"), log);
|
||||||
|
assertFalse(log.contains("Error occurred while enabling SpigotQuestBoard"), log);
|
||||||
|
} finally {
|
||||||
|
if (process.isAlive()) {
|
||||||
|
process.destroyForcibly();
|
||||||
|
process.waitFor(30, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Void readOutputAndStop(Process process, StringBuffer output, AtomicBoolean stopSent)
|
||||||
|
throws IOException {
|
||||||
|
try (var reader = new BufferedReader(new InputStreamReader(process.getInputStream(), UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
output.append(line).append('\n');
|
||||||
|
if (line.contains("Done (") && stopSent.compareAndSet(false, true)) {
|
||||||
|
process.getOutputStream().write("stop\n".getBytes(UTF_8));
|
||||||
|
process.getOutputStream().flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path requiredPath(String property) {
|
||||||
|
String value = System.getProperty(property);
|
||||||
|
assertFalse(value == null || value.isBlank(), property + " must be configured");
|
||||||
|
Path path = Path.of(value);
|
||||||
|
assertTrue(Files.isRegularFile(path), () -> property + " does not exist: " + path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path javaExecutable() {
|
||||||
|
String executable = System.getProperty("os.name").startsWith("Windows") ? "java.exe" : "java";
|
||||||
|
return Path.of(System.getProperty("java.home"), "bin", executable);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user