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:
parent
4c255111f0
commit
8c8046a4f5
30 changed files with 1027 additions and 227 deletions
|
|
@ -0,0 +1,99 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue