Files
MCMapper-Backend/api/src/players.ts
T
octoturge 826233e10c 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
2026-08-09 20:07:11 +02:00

47 lines
1.6 KiB
TypeScript

import { eq } from "drizzle-orm";
import { db } from "./db/client";
import { servers } from "./db/schema";
import { redis } from "./redis";
export interface PlayerPosition {
uuid: string;
username: string;
x: number;
y: number;
z: number;
}
export function playerChannel(serverId: string): string {
return `mcmapper:players:${serverId}`;
}
function snapshotKey(serverId: string): string {
return `mcmapper:players:snapshot:${serverId}`;
}
export async function arePlayerPositionsVisible(serverId: string): Promise<boolean> {
const [row] = await db
.select({ playerPositionsVisible: servers.playerPositionsVisible })
.from(servers)
.where(eq(servers.id, serverId))
.limit(1);
return row?.playerPositionsVisible ?? false;
}
/**
* Fans a player roster out to every browser subscribed to this server's channel, and stashes it
* in Redis so a browser tab that connects between mod flushes gets the current roster immediately
* instead of waiting up to `playerPositionIntervalTicks` for the next one — see
* players-gateway.ts's `open()`. No history/persistence beyond the latest snapshot: unlike chat,
* player positions have no meaningful "history" to keep.
*/
export async function publishPlayerPositions(serverId: string, dimension: number, players: PlayerPosition[]) {
const payload = JSON.stringify({ dimension, players });
await redis.set(snapshotKey(serverId), payload);
await redis.publish(playerChannel(serverId), payload);
}
export async function getPlayerSnapshot(serverId: string): Promise<string | null> {
return redis.get(snapshotKey(serverId));
}