PUBG drop map: live squad map with WebSocket rooms
All checks were successful
Deploy production / deploy (push) Successful in 1m15s

- server.js wraps Bun.serve to add /ws/<channel> upgrades (same server and port) and
  serve the map tiles from the host mount; Dockerfile now runs bun ./server.js
- ws hub (channel registry, origin check) + pubg-map channel: rooms by unguessable
  code, validated + rate-limited messages, per-room write queue, presence
- games.pubg_rooms / pubg_matches / pubg_markers; per-match sessions with history
- /trackers/games/pubg/map hub (owner: rooms, landings over time, history) and
  /trackers/games/pubg/map/<code> fullscreen Leaflet map (all 11 maps)
- deploy-remote.sh bind-mounts ~/pubg-maps; scripts/build-pubg-tiles.py rebuilds it
This commit is contained in:
2026-10-25 22:46:24 +00:00
parent d93374bca2
commit 0fe0d36f87
26 changed files with 39201 additions and 3 deletions

View File

@@ -0,0 +1,183 @@
import { and, desc, eq, inArray, isNotNull, sql } from 'drizzle-orm';
import { db } from '#lib/server/db/index.js';
import { pubgMarkers, pubgMatches, pubgRooms } from '#lib/server/db/schema/index.js';
import type { Kind, Marker, Match, PastMatch } from '#lib/pubg-map/shared.js';
// Postgres side of the PUBG drop map (games.pubg_rooms, pubg_matches, pubg_markers).
const iso = (d: Date) => d.toISOString();
type MarkerRow = typeof pubgMarkers.$inferSelect;
export const toMarker = (r: MarkerRow): Marker => ({
id: r.id,
kind: r.kind as Kind,
x: r.x,
y: r.y,
x2: r.x2,
y2: r.y2,
label: r.label,
player: r.player,
color: r.color,
createdAt: iso(r.createdAt)
});
const toMatch = (r: typeof pubgMatches.$inferSelect): Match => ({
id: r.id,
number: r.number,
map: r.map,
startedAt: iso(r.startedAt)
});
export { newRoomCode } from './code';
import { newRoomCode } from './code';
export async function roomByCode(code: string) {
const [room] = await db.select().from(pubgRooms).where(eq(pubgRooms.code, code));
return room ?? null;
}
export async function listRooms() {
return db
.select({
id: pubgRooms.id,
code: pubgRooms.code,
name: pubgRooms.name,
createdAt: pubgRooms.createdAt,
matches: sql<number>`(select count(*)::int from games.pubg_matches m where m.room_id = ${pubgRooms.id})`
})
.from(pubgRooms)
.orderBy(desc(pubgRooms.createdAt));
}
export async function createRoom(name: string) {
const [room] = await db.insert(pubgRooms).values({ code: newRoomCode(), name }).returning();
return room;
}
export async function deleteRoom(id: number) {
await db.delete(pubgRooms).where(eq(pubgRooms.id, id));
}
/** The room's open match, created on Erangel when the room has none yet. */
export async function currentMatch(roomId: number): Promise<Match> {
const [open] = await db
.select()
.from(pubgMatches)
.where(and(eq(pubgMatches.roomId, roomId), sql`${pubgMatches.endedAt} is null`))
.orderBy(desc(pubgMatches.number))
.limit(1);
if (open) return toMatch(open);
return startMatch(roomId, 'erangel');
}
export async function startMatch(roomId: number, map: string, endId?: number): Promise<Match> {
return db.transaction(async (tx) => {
if (endId)
await tx.update(pubgMatches).set({ endedAt: new Date() }).where(eq(pubgMatches.id, endId));
const [{ next }] = await tx
.select({ next: sql<number>`coalesce(max(${pubgMatches.number}), 0)::int + 1` })
.from(pubgMatches)
.where(eq(pubgMatches.roomId, roomId));
const [row] = await tx.insert(pubgMatches).values({ roomId, number: next, map }).returning();
return toMatch(row);
});
}
export async function setMatchMap(matchId: number, map: string) {
await db.update(pubgMatches).set({ map }).where(eq(pubgMatches.id, matchId));
}
export async function markersOf(matchId: number) {
const rows = await db
.select()
.from(pubgMarkers)
.where(eq(pubgMarkers.matchId, matchId))
.orderBy(pubgMarkers.createdAt);
return rows.map(toMarker);
}
export async function insertMarker(matchId: number, m: Omit<Marker, 'createdAt'>) {
const [row] = await db
.insert(pubgMarkers)
.values({ ...m, matchId })
.onConflictDoNothing()
.returning();
return row ? toMarker(row) : null;
}
export async function updateMarker(
matchId: number,
id: string,
set: Partial<Pick<MarkerRow, 'x' | 'y' | 'x2' | 'y2' | 'label'>>
) {
const rows = await db
.update(pubgMarkers)
.set(set)
.where(and(eq(pubgMarkers.id, id), eq(pubgMarkers.matchId, matchId)))
.returning({ id: pubgMarkers.id });
return rows.length > 0;
}
export async function deleteMarkers(matchId: number, ids: string[]) {
if (!ids.length) return [];
const rows = await db
.delete(pubgMarkers)
.where(and(eq(pubgMarkers.matchId, matchId), inArray(pubgMarkers.id, ids)))
.returning({ id: pubgMarkers.id });
return rows.map((r) => r.id);
}
export async function clearMatch(matchId: number) {
await db.delete(pubgMarkers).where(eq(pubgMarkers.matchId, matchId));
}
/** Archived matches with their landings and plane path, newest first. */
export async function pastMatches(
opts: { roomId?: number; limit?: number } = {}
): Promise<(PastMatch & { roomId: number })[]> {
const matches = await db
.select()
.from(pubgMatches)
.where(
and(
isNotNull(pubgMatches.endedAt),
opts.roomId ? eq(pubgMatches.roomId, opts.roomId) : undefined
)
)
.orderBy(desc(pubgMatches.startedAt))
.limit(opts.limit ?? 200);
if (!matches.length) return [];
const markers = await db
.select()
.from(pubgMarkers)
.where(
inArray(
pubgMarkers.matchId,
matches.map((m) => m.id)
)
);
return matches.map((m) => {
const mine = markers.filter((r) => r.matchId === m.id);
const plane = mine.find((r) => r.kind === 'plane');
return {
...toMatch(m),
roomId: m.roomId,
endedAt: m.endedAt ? iso(m.endedAt) : null,
markers: mine.length,
landings: mine
.filter((r) => r.kind === 'landing')
.map((r) => ({ player: r.player, color: r.color, x: r.x, y: r.y })),
plane:
plane && plane.x2 != null && plane.y2 != null
? { x: plane.x, y: plane.y, x2: plane.x2, y2: plane.y2 }
: null
};
});
}
/** Owner only (checked by the caller): delete one archived match and its markers. */
export async function deletePastMatch(matchId: number) {
const rows = await db
.delete(pubgMatches)
.where(and(eq(pubgMatches.id, matchId), isNotNull(pubgMatches.endedAt)))
.returning({ roomId: pubgMatches.roomId });
return rows[0]?.roomId ?? null;
}