Add player position tracking relay + rendering (Phase 7b)

Relays the mod's throttled player_positions roster over a new
/ws/players/:serverId gateway (Redis pub/sub + snapshot key so a
tab connecting between mod flushes isn't empty), gated per-server
by a new playerPositionsVisible admin toggle independent of the
mod's own tracking config. Frontend renders the roster as map
markers with a show/hide toggle and online count. Covered by unit
tests (players.test.ts, ws-gateway.test.ts, admin.test.ts) and a
new e2e spec that plays the real mod WS protocol from inside a
browser context.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tKdPZt78zbPUZMXWzKEKt
This commit is contained in:
2026-08-09 20:07:11 +02:00
parent cc3860ce08
commit 826233e10c
16 changed files with 444 additions and 16 deletions
+17
View File
@@ -4,6 +4,7 @@ import { chunkColumns, chunkSections, servers } from "./db/schema";
import { markChunkDirty } from "./redis";
import { storeLinkCode } from "./link";
import { recordAndPublishChat } from "./chat";
import { arePlayerPositionsVisible, publishPlayerPositions, type PlayerPosition } from "./players";
// Wire protocol (mod <-> api), one JSON object per WS text frame:
//
@@ -49,6 +50,14 @@ import { recordAndPublishChat } from "./chat";
// (per-server `waypointFormat` config) and forwards the structured point — the mod owns building
// the actual chat text in that format (see WaypointShare.java's javadoc for the researched wire
// formats and their attribution).
//
// mod -> api {"type":"player_positions","dimension":0,"players":[{"uuid":"...","username":"...","x":..,"y":..,"z":..}]}
//
// Phase 7b: the mod's throttled online-player roster, always the full current list (not a diff —
// see PlayerPosition.java's javadoc). Not persisted (no meaningful history) — just fanned out via
// players.ts's publishPlayerPositions to whatever browsers are subscribed on /ws/players/:serverId
// (players-gateway.ts), gated on the per-server `playerPositionsVisible` admin toggle (independent
// of the mod-local `playerTrackingEnabled` config that decides whether this message is sent at all).
interface Column {
x: number;
@@ -202,6 +211,14 @@ export const wsGateway = {
});
return;
}
if (msg.type === "player_positions") {
if (!(await arePlayerPositionsVisible(state.serverId))) return;
const dimension: number = msg.dimension;
const players: PlayerPosition[] = msg.players ?? [];
await publishPlayerPositions(state.serverId, dimension, players);
return;
}
},
close(ws: any) {