fix(stealth): support Purpur 26.3
This commit is contained in:
1 parent
8b412141da
commit
fab7cb9f43
11 files changed
+57
-37
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Purpur Stealth
|
||||
|
||||
Progression-gated identity concealment for Purpur 26.2 build 2618, built and tested with Java 25. ProtocolLib 5.4.0 is required.
|
||||
Progression-gated identity concealment for Purpur 26.3 build 2651, built and tested with Java 25. ProtocolLib 5.4.0 is required.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -34,7 +34,7 @@ Revelation uses private client-only teams and recipient-specific metadata; it do
|
||||
|
||||
Eye records are independently UUID-keyed in `plugins/SpigotStealth/state.yml` (schema 2). Legacy Stealth records and extension fields are preserved. Crafting/use wait for successful save acknowledgement. Shutdown queues a final snapshot without blocking the tick thread, and initialization callbacks cannot revive a disabled or superseded lifecycle. Unacknowledged progress at abrupt process/power loss is not guaranteed.
|
||||
|
||||
`check` includes `nativeStealthTest`, which downloads SHA-256-pinned Purpur 2618, patches it without starting a listening server, and exercises real potion events, items, recipes, event dispatch and plugin lifecycle with external platform doubles. Live-client appearance and gameplay checks remain separate follow-ups.
|
||||
`check` includes `nativeStealthTest`, which downloads SHA-256-pinned Purpur 26.3 build 2651, patches it without starting a listening server, and exercises real potion events, items, recipes, event dispatch and plugin lifecycle with external platform doubles. Live-client appearance and gameplay checks remain separate follow-ups.
|
||||
|
||||
## Migration compatibility
|
||||
|
||||
|
||||
+8
-8
@@ -27,10 +27,10 @@ tasks.withType<JavaCompile>().configureEach {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable")
|
||||
compileOnly("org.purpurmc.purpur:purpur-api:26.3.build.2651-stable")
|
||||
compileOnly("net.dmulloy2:ProtocolLib:5.4.0")
|
||||
|
||||
testImplementation("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable")
|
||||
testImplementation("org.purpurmc.purpur:purpur-api:26.3.build.2651-stable")
|
||||
testImplementation("net.dmulloy2:ProtocolLib:5.4.0")
|
||||
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
@@ -47,20 +47,20 @@ tasks.test {
|
||||
}
|
||||
|
||||
// Exercise real potion events, item components and recipes without starting a listening server.
|
||||
val stealthRuntime = layout.buildDirectory.dir("stealth-runtime")
|
||||
val stealthRuntime = layout.buildDirectory.dir("stealth-runtime-26.3-2651")
|
||||
val downloadStealthRuntime = tasks.register("downloadStealthRuntime") {
|
||||
val launcher = stealthRuntime.map { it.file("purpur-26.2-2618.jar") }
|
||||
val launcher = stealthRuntime.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)
|
||||
}
|
||||
@@ -68,7 +68,7 @@ val downloadStealthRuntime = tasks.register("downloadStealthRuntime") {
|
||||
val prepareStealthRuntime = tasks.register<JavaExec>("prepareStealthRuntime") {
|
||||
dependsOn(downloadStealthRuntime)
|
||||
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) }
|
||||
classpath = files(stealthRuntime.map { it.file("purpur-26.2-2618.jar") })
|
||||
classpath = files(stealthRuntime.map { it.file("purpur-26.3-2651.jar") })
|
||||
mainClass = "io.papermc.paperclip.Main"
|
||||
jvmArgs("-Dpaperclip.patchonly=true")
|
||||
workingDir(stealthRuntime)
|
||||
|
||||
@@ -31,9 +31,9 @@ class EyePacketListenerTest {
|
||||
try (var platform = mockStatic(org.bukkit.Bukkit.class, call -> switch (call.getMethod().getName()) {
|
||||
case "getServer" -> server;
|
||||
case "isPrimaryThread" -> true;
|
||||
case "getVersion" -> "Purpur 2618 (MC: 26.2)";
|
||||
case "getMinecraftVersion" -> "26.2";
|
||||
case "getBukkitVersion" -> "26.2-R0.1-SNAPSHOT";
|
||||
case "getVersion" -> "Purpur 2651 (MC: 26.3)";
|
||||
case "getMinecraftVersion" -> "26.3";
|
||||
case "getBukkitVersion" -> "26.3-R0.1-SNAPSHOT";
|
||||
default -> call.callRealMethod();
|
||||
})) {
|
||||
UUID viewerId = UUID.randomUUID();
|
||||
|
||||
@@ -94,9 +94,9 @@ class EyePluginLifecycleTest {
|
||||
doAnswer(call -> { listeners.add(call.getArgument(0)); return null; }).when(plugins).registerEvents(any(), eq(plugin));
|
||||
try (var platform = mockStatic(org.bukkit.Bukkit.class, call -> switch (call.getMethod().getName()) {
|
||||
case "getServer" -> server;
|
||||
case "getVersion" -> "Purpur 2618 (MC: 26.2)";
|
||||
case "getMinecraftVersion" -> "26.2";
|
||||
case "getBukkitVersion" -> "26.2-R0.1-SNAPSHOT";
|
||||
case "getVersion" -> "Purpur 2651 (MC: 26.3)";
|
||||
case "getMinecraftVersion" -> "26.3";
|
||||
case "getBukkitVersion" -> "26.3-R0.1-SNAPSHOT";
|
||||
default -> call.callRealMethod();
|
||||
}); var protocol = mockStatic(ProtocolLibrary.class)) {
|
||||
var protocolManager = mock(ProtocolManager.class);
|
||||
|
||||
@@ -51,9 +51,9 @@ class EyeRevealRuntimeTest {
|
||||
try (var platform = mockStatic(org.bukkit.Bukkit.class, call -> switch (call.getMethod().getName()) {
|
||||
case "getServer" -> server;
|
||||
case "isPrimaryThread" -> true;
|
||||
case "getVersion" -> "Purpur 2618 (MC: 26.2)";
|
||||
case "getMinecraftVersion" -> "26.2";
|
||||
case "getBukkitVersion" -> "26.2-R0.1-SNAPSHOT";
|
||||
case "getVersion" -> "Purpur 2651 (MC: 26.3)";
|
||||
case "getMinecraftVersion" -> "26.3";
|
||||
case "getBukkitVersion" -> "26.3-R0.1-SNAPSHOT";
|
||||
default -> call.callRealMethod();
|
||||
})) {
|
||||
var world = mock(World.class);
|
||||
|
||||
@@ -52,12 +52,13 @@ final class NativeChatWire {
|
||||
var decoded = ClientboundPlayerChatPacket.STREAM_CODEC.decode(buffer);
|
||||
assertEquals(nextChatIndex++, decoded.globalIndex(), "system messages must not create signed packet-index gaps");
|
||||
var body = decoded.body().unpack(clientCache).orElseThrow();
|
||||
var message = new PlayerChatMessage(new SignedMessageLink(decoded.index(), decoded.sender(), session), decoded.signature(), body,
|
||||
decoded.unsignedContent(), decoded.filterMask());
|
||||
var signature = decoded.signature().orElseThrow();
|
||||
var message = new PlayerChatMessage(new SignedMessageLink(decoded.index(), decoded.sender(), session), signature, body,
|
||||
decoded.unsignedContent().orElse(null), decoded.filterMask());
|
||||
assertEquals(expected, body.content());
|
||||
assertNotNull(validator.updateAndValidate(message), "the retained native client signature chain must accept normal chat again");
|
||||
assertTrue(clientSeen.addPending(decoded.signature(), true));
|
||||
clientCache.push(body, decoded.signature());
|
||||
assertTrue(clientSeen.addPending(signature, true));
|
||||
clientCache.push(body, signature);
|
||||
}
|
||||
} finally { buffer.release(); }
|
||||
var acknowledgement = clientSeen.generateAndApplyUpdate();
|
||||
|
||||
@@ -24,9 +24,9 @@ class NativeCombatTabRestoreTest {
|
||||
try (var platform = mockStatic(Bukkit.class, call -> switch (call.getMethod().getName()) {
|
||||
case "getServer" -> server;
|
||||
case "isPrimaryThread" -> true;
|
||||
case "getVersion" -> "Purpur 2618 (MC: 26.2)";
|
||||
case "getMinecraftVersion" -> "26.2";
|
||||
case "getBukkitVersion" -> "26.2-R0.1-SNAPSHOT";
|
||||
case "getVersion" -> "Purpur 2651 (MC: 26.3)";
|
||||
case "getMinecraftVersion" -> "26.3";
|
||||
case "getBukkitVersion" -> "26.3-R0.1-SNAPSHOT";
|
||||
default -> call.callRealMethod();
|
||||
})) {
|
||||
var target = mock(CraftPlayer.class);
|
||||
|
||||
@@ -46,7 +46,6 @@ class NativePotionDamageTest {
|
||||
var nativeAttacker = mock(ServerPlayer.class);
|
||||
when(victim.getHandle()).thenReturn(nativeVictim);
|
||||
when(nativeVictim.getBukkitEntity()).thenReturn(victim);
|
||||
when(nativeVictim.getBukkitLivingEntity()).thenReturn(victim);
|
||||
when(nativeAttacker.getBukkitEntity()).thenReturn(attacker);
|
||||
var active = new HashMap<net.minecraft.core.Holder<net.minecraft.world.effect.MobEffect>, MobEffectInstance>();
|
||||
var field = LivingEntity.class.getDeclaredField("activeEffects");
|
||||
|
||||
@@ -41,12 +41,12 @@ final class NativeRuntime {
|
||||
try (var resources = new MultiPackResourceManager(PackType.SERVER_DATA, packs.openAllSelected())) {
|
||||
LayeredRegistryAccess<RegistryLayer> layers = RegistryLayer.createRegistryAccess();
|
||||
List<Registry.PendingTags<?>> tags = TagLoader.loadTagsForExistingRegistries(resources, layers.getLayer(RegistryLayer.STATIC));
|
||||
List<HolderLookup.RegistryLookup<?>> 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<HolderLookup.RegistryLookup<?>> 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());
|
||||
|
||||
@@ -30,9 +30,9 @@ class NativeTabMaskingTest {
|
||||
try (var platform = mockStatic(org.bukkit.Bukkit.class, call -> switch (call.getMethod().getName()) {
|
||||
case "getServer" -> server;
|
||||
case "isPrimaryThread" -> true;
|
||||
case "getVersion" -> "Purpur 2618 (MC: 26.2)";
|
||||
case "getMinecraftVersion" -> "26.2";
|
||||
case "getBukkitVersion" -> "26.2-R0.1-SNAPSHOT";
|
||||
case "getVersion" -> "Purpur 2651 (MC: 26.3)";
|
||||
case "getMinecraftVersion" -> "26.3";
|
||||
case "getBukkitVersion" -> "26.3-R0.1-SNAPSHOT";
|
||||
default -> call.callRealMethod();
|
||||
})) {
|
||||
UUID hiddenId = UUID.randomUUID(), otherId = UUID.randomUUID(), viewerId = UUID.randomUUID();
|
||||
|
||||
@@ -21,9 +21,22 @@ class PurpurDistributionTest {
|
||||
assertEquals(version, metadata.get("version"));
|
||||
assertEquals("SpigotStealth", metadata.get("name"), "Retain the data directory and persistent namespace");
|
||||
assertEquals("games.dmg.spigotstealth.SpigotStealthPlugin", metadata.get("main"));
|
||||
assertEquals("1.20", metadata.get("api-version"), "Retain the deliberate compatibility floor");
|
||||
assertEquals(java.util.List.of("ProtocolLib"), metadata.get("depend"));
|
||||
assertTrue(((Map<?, ?>) metadata.get("commands")).containsKey("stealth"));
|
||||
assertTrue(((Map<?, ?>) metadata.get("commands")).containsKey("stealthadmin"));
|
||||
assertTrue(((Map<?, ?>) metadata.get("permissions")).containsKey("spigotstealth.use"));
|
||||
assertTrue(((Map<?, ?>) metadata.get("permissions")).containsKey("spigotstealth.admin"));
|
||||
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().startsWith("org/junit/")
|
||||
|| entry.getName().startsWith("org/mockito/")
|
||||
|| entry.getName().startsWith("org/yaml/snakeyaml/")
|
||||
|| entry.getName().endsWith("Test.class")
|
||||
|| entry.getName().endsWith("NativeRuntime.class")
|
||||
|| entry.getName().endsWith("NativeChatWire.class")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +54,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"));
|
||||
assertFalse(build.contains("purpur-api:26.2.build.2618-stable"));
|
||||
assertFalse(build.contains("26.2"));
|
||||
assertFalse(build.contains("2618"));
|
||||
assertTrue(build.contains("stealth-runtime-26.3-2651"));
|
||||
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("org.spigotmc:spigot-api"));
|
||||
for (String workflow : new String[] {"ci.yml", "release.yml"}) {
|
||||
String content = Files.readString(Path.of(".gitea/workflows", workflow));
|
||||
|
||||
Reference in new issue
Block a user