feat: personal run replays (ghost system)

- GhostData: worldName field, format v4; GhostManager stores last N runs per player (ghosts/<uuid>/<ts>.ghost)
- GhostService: per-player recording (start on run start, every tick, finalize on fall), gated by ghost-mode.enabled + per-player toggle
- PlayerGhost: packet-based fake-player NPC with viewer skin (NMS via remapNms gradle task, reflection send, graceful degradation)
- GhostReplay: teleport viewer to run start, replay frames via PlayerGhost, restore on finish
- /lp replay [index] command + tab completion; settings menu ghost_recording toggle (persisted disk/SQL)
- config: ghost-mode.{enabled,max-runs}, remove dead show-top/transparency + GHOST_* statics
- tests: GhostData v4 round-trip, GhostManager eviction/ordering, yawToFixed (79 total)
This commit is contained in:
loki5512344 2026-08-06 13:13:59 +02:00
parent 4c255111f0
commit 8c8046a4f5
Signed by: boba
GPG key ID: 253067914055423B
30 changed files with 1027 additions and 227 deletions

View file

@ -1,3 +1,19 @@
import java.io.File
import java.net.URI
import java.util.zip.ZipFile
// Remap tooling: rebuilds the Mojang-mapped 1.20.4 NMS classes at build time.
buildscript {
repositories {
mavenCentral()
maven("https://maven.fabricmc.net/")
}
dependencies {
classpath("net.fabricmc:mapping-io:0.6.1")
classpath("net.fabricmc:tiny-remapper:0.9.0")
}
}
plugins {
id("java")
id("io.github.goooler.shadow") version("8.1.8")
@ -77,6 +93,63 @@ repositories {
}
}
// --- Mojang-mapped NMS (net.minecraft.*) for the packet NPC renderer ---------
// repo.papermc.io purged the old io.papermc.paper:paper:1.20.4-R0.1-SNAPSHOT
// dev-bundle, so the 1.20.4 vanilla server jar (bundler/obfuscated) is remapped
// with Mojang's official mappings via tiny-remapper into the same Mojang-mapped
// classes the dev-bundle shipped. Result is cached under the Gradle user home.
val nmsVersion = "1.20.4"
val nmsCacheDir = gradle.gradleUserHomeDir.resolve("caches/loparkour-nms/$nmsVersion")
val nmsServerJar = nmsCacheDir.resolve("server.jar")
val nmsNestedJar = nmsCacheDir.resolve("server-nested.jar")
val nmsMappings = nmsCacheDir.resolve("server-mappings.txt")
val nmsTiny = nmsCacheDir.resolve("server-mappings.tiny")
val nmsMappedJar = nmsCacheDir.resolve("server-mojang-mapped.jar")
val nmsAuthlibJar = nmsCacheDir.resolve("authlib-6.0.52.jar")
val remapNms by tasks.registering {
outputs.files(nmsServerJar, nmsMappings, nmsMappedJar, nmsAuthlibJar)
doLast {
nmsCacheDir.mkdirs()
fun download(url: String, target: File) {
if (target.isFile) {
return
}
URI(url).toURL().openStream().use { input ->
target.outputStream().use { input.copyTo(it) }
}
}
download("https://piston-data.mojang.com/v1/objects/8dd1a28015f51b1803213892b50b7b4fc76e594d/server.jar", nmsServerJar)
download("https://piston-data.mojang.com/v1/objects/c1cafe916dd8b58ed1fe0564fc8f786885224e62/server.txt", nmsMappings)
download("https://libraries.minecraft.net/com/mojang/authlib/6.0.52/authlib-6.0.52.jar", nmsAuthlibJar)
if (!nmsNestedJar.isFile) {
ZipFile(nmsServerJar).use { zip ->
zip.getInputStream(zip.getEntry("META-INF/versions/$nmsVersion/server-$nmsVersion.jar")).use { input ->
nmsNestedJar.outputStream().use { input.copyTo(it) }
}
}
}
val tree = net.fabricmc.mappingio.tree.MemoryMappingTree(false)
net.fabricmc.mappingio.MappingReader.read(nmsMappings.toPath(), net.fabricmc.mappingio.format.MappingFormat.PROGUARD_FILE, tree)
net.fabricmc.mappingio.MappingWriter.create(nmsTiny.toPath(), net.fabricmc.mappingio.format.MappingFormat.TINY_2_FILE).use { writer ->
tree.accept(net.fabricmc.mappingio.adapter.MappingSourceNsSwitch(writer, "target"))
}
val remapper = net.fabricmc.tinyremapper.TinyRemapper.newRemapper()
.withMappings(net.fabricmc.tinyremapper.TinyUtils.createTinyMappingProvider(nmsTiny.toPath(), "target", "source"))
.ignoreConflicts(true)
.rebuildSourceFilenames(true)
.build()
net.fabricmc.tinyremapper.OutputConsumerPath.Builder(nmsMappedJar.toPath()).build().use { output ->
output.addNonClassFiles(nmsNestedJar.toPath())
remapper.readInputs(nmsNestedJar.toPath())
remapper.apply(output)
}
remapper.finish()
}
}
tasks.withType<JavaCompile>().configureEach { dependsOn(remapNms) }
// Resolve the org.bukkit:bukkit capability conflict that newer paper-api artifacts
// introduce (they both bundle the bukkit classes and declare the capability).
configurations.configureEach {
@ -100,6 +173,13 @@ dependencies {
} else {
compileOnly(apiCoordinate)
}
// NMS (net.minecraft.*) for packet NPC rendering — compileOnly, Mojang-mapped.
// Built once by the remapNms task above (vanilla 1.20.4 server + official
// mappings) because the io.papermc.paper:paper:1.20.4 dev-bundle was purged.
compileOnly(files(nmsMappedJar, nmsAuthlibJar))
// fastutil types are referenced by the NMS packet signatures (IntList).
compileOnly("it.unimi.dsi:fastutil:8.5.9")
// Adventure API (for Component)
compileOnly("net.kyori:adventure-api:4.14.0")
@ -133,6 +213,8 @@ dependencies {
testImplementation("org.mockito:mockito-junit-jupiter:5.10.0")
testImplementation("org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
// Test runtime classpath for loading PlayerGhost (references NMS + authlib types).
testImplementation(files(nmsMappedJar, nmsAuthlibJar))
}
tasks.shadowJar {