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
184 lines
5.4 KiB
TypeScript
184 lines
5.4 KiB
TypeScript
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;
|
|
}
|