- 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)
114 lines
4 KiB
Java
114 lines
4 KiB
Java
package dev.loki.loparkour.ghost;
|
|
|
|
import dev.loki.loparkour.ghost.core.GhostManager;
|
|
import dev.loki.loparkour.ghost.model.GhostData;
|
|
import dev.loki.loparkour.ghost.model.GhostFrame;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.io.TempDir;
|
|
|
|
import java.io.DataOutputStream;
|
|
import java.io.File;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.nio.file.Path;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.UUID;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
|
|
/**
|
|
* Unit tests for the per-player personal replay storage of {@link GhostManager}.
|
|
*/
|
|
class GhostManagerTest {
|
|
|
|
private static final int EVICTION_CAP = 2;
|
|
|
|
/**
|
|
* A manager whose run cap is fixed, overriding the config-driven {@link #maxRuns()}.
|
|
* Chosen over mutating {@code Config} statics, which the plugin initializes at
|
|
* enable time and are not settable from unit tests.
|
|
*/
|
|
private static final class FixedCapManager extends GhostManager {
|
|
|
|
private FixedCapManager(File ghostFolder) {
|
|
super(ghostFolder);
|
|
}
|
|
|
|
@Override
|
|
public int maxRuns() {
|
|
return EVICTION_CAP;
|
|
}
|
|
}
|
|
|
|
private static GhostData run(UUID uuid, String name, int score, long timestamp) {
|
|
final List<GhostFrame> frames = new ArrayList<>();
|
|
frames.add(new GhostFrame(0, 0, 100, 0, 0, 0));
|
|
return new GhostData(uuid, name, score, timestamp, "world", frames);
|
|
}
|
|
|
|
@Test
|
|
void saveRunAndGetRunsReturnNewestFirst(@TempDir Path tempDir) throws IOException {
|
|
final UUID uuid = UUID.randomUUID();
|
|
final GhostManager manager = new GhostManager(tempDir.toFile());
|
|
final long base = 1_700_000_000_000L;
|
|
|
|
manager.saveRun(uuid, run(uuid, "A", 10, base));
|
|
manager.saveRun(uuid, run(uuid, "B", 20, base + 1_000));
|
|
manager.saveRun(uuid, run(uuid, "C", 30, base + 2_000));
|
|
|
|
final List<GhostData> runs = manager.getRuns(uuid);
|
|
|
|
assertEquals(3, runs.size());
|
|
assertEquals(base + 2_000, runs.get(0).getTimestamp());
|
|
assertEquals(30, runs.get(0).getScore());
|
|
assertEquals(base, runs.get(2).getTimestamp());
|
|
assertEquals(10, runs.get(2).getScore());
|
|
}
|
|
|
|
@Test
|
|
void saveRunEvictsOldestBeyondCap(@TempDir Path tempDir) throws IOException {
|
|
final UUID uuid = UUID.randomUUID();
|
|
final GhostManager manager = new FixedCapManager(tempDir.toFile());
|
|
final long base = 1_700_000_000_000L;
|
|
|
|
for (int i = 0; i < 5; i++) {
|
|
manager.saveRun(uuid, run(uuid, "P" + i, i * 10, base + i * 1_000L));
|
|
}
|
|
|
|
final List<GhostData> runs = manager.getRuns(uuid);
|
|
|
|
assertEquals(EVICTION_CAP, runs.size(), "Old runs beyond the cap must be evicted");
|
|
assertEquals(base + 4_000, runs.get(0).getTimestamp());
|
|
assertEquals(base + 3_000, runs.get(1).getTimestamp());
|
|
}
|
|
|
|
@Test
|
|
void getRunsWithEmptyFolderReturnsEmptyList(@TempDir Path tempDir) {
|
|
final GhostManager manager = new GhostManager(tempDir.toFile());
|
|
|
|
final List<GhostData> runs = manager.getRuns(UUID.randomUUID());
|
|
|
|
assertTrue(runs.isEmpty(), "Player with no saved runs should return an empty list");
|
|
}
|
|
|
|
@Test
|
|
void getRunsIgnoresStaleFormatFiles(@TempDir Path tempDir) throws IOException {
|
|
final UUID uuid = UUID.randomUUID();
|
|
final GhostManager manager = new GhostManager(tempDir.toFile());
|
|
manager.saveRun(uuid, run(uuid, "New", 10, 1_700_000_000_000L));
|
|
|
|
// Corrupt one file so it reads as an unsupported version
|
|
final File playerFolder = new File(tempDir.toFile(), uuid.toString());
|
|
final File[] files = playerFolder.listFiles((dir, name) -> name.endsWith(".ghost"));
|
|
assertNotNull(files);
|
|
try (DataOutputStream out = new DataOutputStream(
|
|
new FileOutputStream(files[0], false))) {
|
|
out.writeInt(2);
|
|
}
|
|
|
|
final List<GhostData> runs = manager.getRuns(uuid);
|
|
|
|
assertTrue(runs.isEmpty(), "Stale-format files must be skipped and deleted");
|
|
}
|
|
}
|