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
|
# 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
|
## 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.
|
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
|
## Migration compatibility
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -27,10 +27,10 @@ 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")
|
||||||
compileOnly("net.dmulloy2:ProtocolLib:5.4.0")
|
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("net.dmulloy2:ProtocolLib:5.4.0")
|
||||||
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")
|
||||||
@@ -47,20 +47,20 @@ tasks.test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exercise real potion events, item components and recipes without starting a listening server.
|
// 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 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)
|
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)
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ val downloadStealthRuntime = tasks.register("downloadStealthRuntime") {
|
|||||||
val prepareStealthRuntime = tasks.register<JavaExec>("prepareStealthRuntime") {
|
val prepareStealthRuntime = tasks.register<JavaExec>("prepareStealthRuntime") {
|
||||||
dependsOn(downloadStealthRuntime)
|
dependsOn(downloadStealthRuntime)
|
||||||
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) }
|
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"
|
mainClass = "io.papermc.paperclip.Main"
|
||||||
jvmArgs("-Dpaperclip.patchonly=true")
|
jvmArgs("-Dpaperclip.patchonly=true")
|
||||||
workingDir(stealthRuntime)
|
workingDir(stealthRuntime)
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ class EyePacketListenerTest {
|
|||||||
try (var platform = mockStatic(org.bukkit.Bukkit.class, call -> switch (call.getMethod().getName()) {
|
try (var platform = mockStatic(org.bukkit.Bukkit.class, call -> switch (call.getMethod().getName()) {
|
||||||
case "getServer" -> server;
|
case "getServer" -> server;
|
||||||
case "isPrimaryThread" -> true;
|
case "isPrimaryThread" -> true;
|
||||||
case "getVersion" -> "Purpur 2618 (MC: 26.2)";
|
case "getVersion" -> "Purpur 2651 (MC: 26.3)";
|
||||||
case "getMinecraftVersion" -> "26.2";
|
case "getMinecraftVersion" -> "26.3";
|
||||||
case "getBukkitVersion" -> "26.2-R0.1-SNAPSHOT";
|
case "getBukkitVersion" -> "26.3-R0.1-SNAPSHOT";
|
||||||
default -> call.callRealMethod();
|
default -> call.callRealMethod();
|
||||||
})) {
|
})) {
|
||||||
UUID viewerId = UUID.randomUUID();
|
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));
|
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()) {
|
try (var platform = mockStatic(org.bukkit.Bukkit.class, call -> switch (call.getMethod().getName()) {
|
||||||
case "getServer" -> server;
|
case "getServer" -> server;
|
||||||
case "getVersion" -> "Purpur 2618 (MC: 26.2)";
|
case "getVersion" -> "Purpur 2651 (MC: 26.3)";
|
||||||
case "getMinecraftVersion" -> "26.2";
|
case "getMinecraftVersion" -> "26.3";
|
||||||
case "getBukkitVersion" -> "26.2-R0.1-SNAPSHOT";
|
case "getBukkitVersion" -> "26.3-R0.1-SNAPSHOT";
|
||||||
default -> call.callRealMethod();
|
default -> call.callRealMethod();
|
||||||
}); var protocol = mockStatic(ProtocolLibrary.class)) {
|
}); var protocol = mockStatic(ProtocolLibrary.class)) {
|
||||||
var protocolManager = mock(ProtocolManager.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()) {
|
try (var platform = mockStatic(org.bukkit.Bukkit.class, call -> switch (call.getMethod().getName()) {
|
||||||
case "getServer" -> server;
|
case "getServer" -> server;
|
||||||
case "isPrimaryThread" -> true;
|
case "isPrimaryThread" -> true;
|
||||||
case "getVersion" -> "Purpur 2618 (MC: 26.2)";
|
case "getVersion" -> "Purpur 2651 (MC: 26.3)";
|
||||||
case "getMinecraftVersion" -> "26.2";
|
case "getMinecraftVersion" -> "26.3";
|
||||||
case "getBukkitVersion" -> "26.2-R0.1-SNAPSHOT";
|
case "getBukkitVersion" -> "26.3-R0.1-SNAPSHOT";
|
||||||
default -> call.callRealMethod();
|
default -> call.callRealMethod();
|
||||||
})) {
|
})) {
|
||||||
var world = mock(World.class);
|
var world = mock(World.class);
|
||||||
|
|||||||
@@ -52,12 +52,13 @@ final class NativeChatWire {
|
|||||||
var decoded = ClientboundPlayerChatPacket.STREAM_CODEC.decode(buffer);
|
var decoded = ClientboundPlayerChatPacket.STREAM_CODEC.decode(buffer);
|
||||||
assertEquals(nextChatIndex++, decoded.globalIndex(), "system messages must not create signed packet-index gaps");
|
assertEquals(nextChatIndex++, decoded.globalIndex(), "system messages must not create signed packet-index gaps");
|
||||||
var body = decoded.body().unpack(clientCache).orElseThrow();
|
var body = decoded.body().unpack(clientCache).orElseThrow();
|
||||||
var message = new PlayerChatMessage(new SignedMessageLink(decoded.index(), decoded.sender(), session), decoded.signature(), body,
|
var signature = decoded.signature().orElseThrow();
|
||||||
decoded.unsignedContent(), decoded.filterMask());
|
var message = new PlayerChatMessage(new SignedMessageLink(decoded.index(), decoded.sender(), session), signature, body,
|
||||||
|
decoded.unsignedContent().orElse(null), decoded.filterMask());
|
||||||
assertEquals(expected, body.content());
|
assertEquals(expected, body.content());
|
||||||
assertNotNull(validator.updateAndValidate(message), "the retained native client signature chain must accept normal chat again");
|
assertNotNull(validator.updateAndValidate(message), "the retained native client signature chain must accept normal chat again");
|
||||||
assertTrue(clientSeen.addPending(decoded.signature(), true));
|
assertTrue(clientSeen.addPending(signature, true));
|
||||||
clientCache.push(body, decoded.signature());
|
clientCache.push(body, signature);
|
||||||
}
|
}
|
||||||
} finally { buffer.release(); }
|
} finally { buffer.release(); }
|
||||||
var acknowledgement = clientSeen.generateAndApplyUpdate();
|
var acknowledgement = clientSeen.generateAndApplyUpdate();
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ class NativeCombatTabRestoreTest {
|
|||||||
try (var platform = mockStatic(Bukkit.class, call -> switch (call.getMethod().getName()) {
|
try (var platform = mockStatic(Bukkit.class, call -> switch (call.getMethod().getName()) {
|
||||||
case "getServer" -> server;
|
case "getServer" -> server;
|
||||||
case "isPrimaryThread" -> true;
|
case "isPrimaryThread" -> true;
|
||||||
case "getVersion" -> "Purpur 2618 (MC: 26.2)";
|
case "getVersion" -> "Purpur 2651 (MC: 26.3)";
|
||||||
case "getMinecraftVersion" -> "26.2";
|
case "getMinecraftVersion" -> "26.3";
|
||||||
case "getBukkitVersion" -> "26.2-R0.1-SNAPSHOT";
|
case "getBukkitVersion" -> "26.3-R0.1-SNAPSHOT";
|
||||||
default -> call.callRealMethod();
|
default -> call.callRealMethod();
|
||||||
})) {
|
})) {
|
||||||
var target = mock(CraftPlayer.class);
|
var target = mock(CraftPlayer.class);
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ class NativePotionDamageTest {
|
|||||||
var nativeAttacker = mock(ServerPlayer.class);
|
var nativeAttacker = mock(ServerPlayer.class);
|
||||||
when(victim.getHandle()).thenReturn(nativeVictim);
|
when(victim.getHandle()).thenReturn(nativeVictim);
|
||||||
when(nativeVictim.getBukkitEntity()).thenReturn(victim);
|
when(nativeVictim.getBukkitEntity()).thenReturn(victim);
|
||||||
when(nativeVictim.getBukkitLivingEntity()).thenReturn(victim);
|
|
||||||
when(nativeAttacker.getBukkitEntity()).thenReturn(attacker);
|
when(nativeAttacker.getBukkitEntity()).thenReturn(attacker);
|
||||||
var active = new HashMap<net.minecraft.core.Holder<net.minecraft.world.effect.MobEffect>, MobEffectInstance>();
|
var active = new HashMap<net.minecraft.core.Holder<net.minecraft.world.effect.MobEffect>, MobEffectInstance>();
|
||||||
var field = LivingEntity.class.getDeclaredField("activeEffects");
|
var field = LivingEntity.class.getDeclaredField("activeEffects");
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ class NativeTabMaskingTest {
|
|||||||
try (var platform = mockStatic(org.bukkit.Bukkit.class, call -> switch (call.getMethod().getName()) {
|
try (var platform = mockStatic(org.bukkit.Bukkit.class, call -> switch (call.getMethod().getName()) {
|
||||||
case "getServer" -> server;
|
case "getServer" -> server;
|
||||||
case "isPrimaryThread" -> true;
|
case "isPrimaryThread" -> true;
|
||||||
case "getVersion" -> "Purpur 2618 (MC: 26.2)";
|
case "getVersion" -> "Purpur 2651 (MC: 26.3)";
|
||||||
case "getMinecraftVersion" -> "26.2";
|
case "getMinecraftVersion" -> "26.3";
|
||||||
case "getBukkitVersion" -> "26.2-R0.1-SNAPSHOT";
|
case "getBukkitVersion" -> "26.3-R0.1-SNAPSHOT";
|
||||||
default -> call.callRealMethod();
|
default -> call.callRealMethod();
|
||||||
})) {
|
})) {
|
||||||
UUID hiddenId = UUID.randomUUID(), otherId = UUID.randomUUID(), viewerId = UUID.randomUUID();
|
UUID hiddenId = UUID.randomUUID(), otherId = UUID.randomUUID(), viewerId = UUID.randomUUID();
|
||||||
|
|||||||
@@ -21,9 +21,22 @@ class PurpurDistributionTest {
|
|||||||
assertEquals(version, metadata.get("version"));
|
assertEquals(version, metadata.get("version"));
|
||||||
assertEquals("SpigotStealth", metadata.get("name"), "Retain the data directory and persistent namespace");
|
assertEquals("SpigotStealth", metadata.get("name"), "Retain the data directory and persistent namespace");
|
||||||
assertEquals("games.dmg.spigotstealth.SpigotStealthPlugin", metadata.get("main"));
|
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"));
|
assertTrue(((Map<?, ?>) metadata.get("permissions")).containsKey("spigotstealth.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().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
|
@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("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"));
|
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));
|
||||||
|
|||||||
Reference in new issue
Block a user