- 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)
99 lines
3.3 KiB
Java
99 lines
3.3 KiB
Java
package dev.loki.loparkour.ghost.service;
|
|
|
|
import dev.loki.loparkour.config.core.Config;
|
|
import dev.loki.loparkour.ghost.core.GhostManager;
|
|
import dev.loki.loparkour.ghost.core.GhostRecorder;
|
|
import dev.loki.loparkour.ghost.model.GhostData;
|
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
|
import org.jetbrains.annotations.NotNull;
|
|
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
|
|
/**
|
|
* Plugin-owned singleton that records each player's runs as {@link GhostData}
|
|
* while the server's {@code ghost-mode} is enabled and the player's per-player
|
|
* {@code ghostRecording} toggle is on.
|
|
*/
|
|
public class GhostService {
|
|
|
|
private final GhostManager manager = new GhostManager();
|
|
private final Map<UUID, GhostRecorder> recorders = new ConcurrentHashMap<>();
|
|
private final Set<UUID> replaying = ConcurrentHashMap.newKeySet();
|
|
|
|
public GhostManager getManager() {
|
|
return manager;
|
|
}
|
|
|
|
private boolean serverEnabled() {
|
|
return Config.CONFIG.getBoolean("ghost-mode.enabled");
|
|
}
|
|
|
|
private boolean recordingEnabled(ParkourPlayer pp) {
|
|
return serverEnabled()
|
|
&& !replaying.contains(pp.getUUID())
|
|
&& Boolean.TRUE.equals(pp.ghostRecording);
|
|
}
|
|
|
|
public void startRun(ParkourGenerator generator) {
|
|
for (final ParkourPlayer pp : generator.getPlayers()) {
|
|
if (recordingEnabled(pp)) {
|
|
recorders.computeIfAbsent(pp.getUUID(), k -> new GhostRecorder())
|
|
.startRecording(pp.getLocation());
|
|
} else {
|
|
recorders.remove(pp.getUUID());
|
|
}
|
|
}
|
|
}
|
|
|
|
public void recordPlayers(ParkourGenerator generator) {
|
|
for (final ParkourPlayer pp : generator.getPlayers()) {
|
|
final GhostRecorder recorder = recorders.get(pp.getUUID());
|
|
if (recorder != null) {
|
|
recorder.recordFrame(pp.getLocation());
|
|
}
|
|
}
|
|
}
|
|
|
|
public void endRun(ParkourGenerator generator, int score) {
|
|
for (final ParkourPlayer pp : generator.getPlayers()) {
|
|
final GhostRecorder recorder = recorders.remove(pp.getUUID());
|
|
if (recorder != null && recorder.isRecording()) {
|
|
final String worldName = pp.getLocation().getWorld() == null
|
|
? "" : pp.getLocation().getWorld().getName();
|
|
final GhostData data = recorder.stopRecording(
|
|
pp.getUUID(), pp.player.getName(), score, worldName);
|
|
manager.saveRun(pp.getUUID(), data);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void playerLeft(UUID uuid) {
|
|
recorders.remove(uuid);
|
|
}
|
|
|
|
public boolean isReplaying(UUID uuid) {
|
|
return replaying.contains(uuid);
|
|
}
|
|
|
|
public void setReplaying(UUID uuid, boolean replaying) {
|
|
if (replaying) {
|
|
this.replaying.add(uuid);
|
|
} else {
|
|
this.replaying.remove(uuid);
|
|
}
|
|
}
|
|
|
|
public void startReplay(@NotNull org.bukkit.entity.Player viewer, @NotNull GhostData run) {
|
|
playerLeft(viewer.getUniqueId());
|
|
setReplaying(viewer.getUniqueId(), true);
|
|
new GhostReplay(viewer, run, () -> setReplaying(viewer.getUniqueId(), false)).start();
|
|
}
|
|
|
|
public void shutdown() {
|
|
recorders.clear();
|
|
}
|
|
}
|