LoParkour/release/src/main/java/dev/loki/loparkour/ghost/GhostData.java
2026-03-17 13:36:23 +01:00

102 lines
4.2 KiB
Java

package dev.loki.loparkour.ghost;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Immutable snapshot of a player's run: who ran it, their score, and every recorded frame.
*
* <p>Bugs fixed vs original:
* <ul>
* <li>Files were named {@code playerName.ghost} — two players with the same name
* overwrote each other, and a name like {@code ../../config} caused path traversal.
* UUID is now stored and used as the filename instead.</li>
* <li>File format is backwards-compatible: UUID is written first (new field),
* then the existing layout. Old files without a UUID are handled in
* {@link #loadFromFile} by catching the format mismatch and returning null.</li>
* </ul>
*/
public class GhostData {
/** Binary format version — bump when changing the serialised layout. */
private static final int FORMAT_VERSION = 2;
private final UUID playerUuid;
private final String playerName;
private final int score;
private final List<GhostFrame> frames;
public GhostData(@NotNull UUID playerUuid, @NotNull String playerName,
int score, @NotNull List<GhostFrame> frames) {
this.playerUuid = playerUuid;
this.playerName = playerName;
this.score = score;
this.frames = new ArrayList<>(frames);
}
// ── Getters ───────────────────────────────────────────────────────────────
public UUID getPlayerUuid() { return playerUuid; }
public String getPlayerName() { return playerName; }
public int getScore() { return score; }
public List<GhostFrame> getFrames() { return frames; }
// ── Serialisation ─────────────────────────────────────────────────────────
public void saveToFile(@NotNull File file) throws IOException {
try (DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream(file)))) {
out.writeInt(FORMAT_VERSION); // version header
out.writeUTF(playerUuid.toString()); // UUID (filename-safe, collision-free)
out.writeUTF(playerName); // display name (for the ghost label)
out.writeInt(score);
out.writeInt(frames.size());
for (GhostFrame frame : frames) {
out.writeLong(frame.getTimestamp());
out.writeDouble(frame.getX());
out.writeDouble(frame.getY());
out.writeDouble(frame.getZ());
out.writeFloat(frame.getYaw());
out.writeFloat(frame.getPitch());
}
}
}
/**
* Loads a ghost from file.
*
* @return The loaded data, or {@code null} if the file is an old v1 format
* (no version header / UUID) — caller should delete and ignore it.
*/
public static GhostData loadFromFile(@NotNull File file) throws IOException {
try (DataInputStream in = new DataInputStream(
new BufferedInputStream(new FileInputStream(file)))) {
int version = in.readInt();
if (version != FORMAT_VERSION) {
// Old format (v1) started with readUTF for playerName, not an int version.
// Those files are stale — return null so the manager can discard them.
return null;
}
UUID uuid = UUID.fromString(in.readUTF());
String name = in.readUTF();
int score = in.readInt();
int frameCount = in.readInt();
List<GhostFrame> frames = new ArrayList<>(frameCount);
for (int i = 0; i < frameCount; i++) {
frames.add(new GhostFrame(
in.readLong(),
in.readDouble(), in.readDouble(), in.readDouble(),
in.readFloat(), in.readFloat()));
}
return new GhostData(uuid, name, score, frames);
}
}
}