import { API_PATHS, DEAD_AIR_SOCKET_PATH, FLUX_SOCKET_PATH, GAME_SOCKET_PATH, MOVERS_SOCKET_PATH, ROYALE_SOCKET_PATH, createShooterGame, deadAirGame, fluxGame, moversGame, royaleGame, type ApiMessage, } from "@syncer/shared"; import { existsSync, readFileSync, readdirSync } from "node:fs"; import { extname, join, relative } from "node:path"; import { App, type HttpResponse } from "uWebSockets.js"; import { ClientProfileStore } from "./client-profile-store.js"; import { hostNetworkedGame } from "./host-networked-game.js"; import { ServerProcessProfiler } from "./server-profiler.js"; const port = Number(process.env.PORT ?? 3001); const app = App(); const processProfiler = new ServerProcessProfiler(); const clientProfiles = new ClientProfileStore(); const staticSite = loadStaticSite(process.env.STATIC_ROOT); const gameLoops = [ hostNetworkedGame(app, GAME_SOCKET_PATH, createShooterGame({ botCount: 0 })), hostNetworkedGame(app, DEAD_AIR_SOCKET_PATH, deadAirGame), hostNetworkedGame(app, FLUX_SOCKET_PATH, fluxGame), hostNetworkedGame(app, MOVERS_SOCKET_PATH, moversGame), hostNetworkedGame(app, ROYALE_SOCKET_PATH, royaleGame), ]; app .post("/api/client-profile", (response) => { const chunks: Buffer[] = []; let bytes = 0; let finished = false; response.onAborted(() => { finished = true; }); response.onData((chunk, isLast) => { if (finished) return; bytes += chunk.byteLength; if (bytes > 64 * 1_024) { finished = true; response.writeStatus("413 Payload Too Large").end(); return; } chunks.push(Buffer.from(new Uint8Array(chunk))); if (!isLast) return; finished = true; try { const sample: unknown = JSON.parse(Buffer.concat(chunks).toString("utf8")); if (!clientProfiles.record(sample, readRemoteAddress(response))) { response.writeStatus("400 Bad Request").end("Invalid profile sample"); return; } response .writeStatus("204 No Content") .writeHeader("Access-Control-Allow-Origin", "*") .end(); } catch { response.writeStatus("400 Bad Request").end("Invalid JSON"); } }); }) .get("/api/profile", (response) => { const now = performance.now(); response .writeHeader("Content-Type", "application/json; charset=utf-8") .writeHeader("Access-Control-Allow-Origin", "*") .end(JSON.stringify({ timestamp: new Date().toISOString(), process: processProfiler.snapshot(), clients: clientProfiles.snapshot(now), games: gameLoops.map((game) => game.profile(now)), }, null, 2)); }) .get(API_PATHS.message, (response) => { const body: ApiMessage = { message: "Authoritative multiplayer simulations online", timestamp: new Date().toISOString(), }; response .writeHeader("Content-Type", "application/json; charset=utf-8") .end(JSON.stringify(body)); }) .any("/*", (response, request) => { const url = request.getUrl(); const method = request.getMethod().toUpperCase(); const asset = staticSite?.files.get(url); const fallback = staticSite && (method === "GET" || method === "HEAD") && !url.startsWith("/api/") && !url.startsWith("/ws/") ? staticSite.index : undefined; const file = asset ?? fallback; if (!file || (method !== "GET" && method !== "HEAD")) { response.writeStatus("404 Not Found").end("Not found"); return; } response .writeHeader("Content-Type", file.contentType) .writeHeader( "Cache-Control", asset && url.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache", ); response.end(method === "HEAD" ? undefined : file.body); }) .listen(port, (listenSocket) => { if (!listenSocket) { console.error(`Could not listen on port ${port}`); process.exitCode = 1; return; } console.log( `Authoritative server listening on http://localhost:${port} (${gameLoops .map(({ path }) => path) .join(", ")}); profiler: /api/profile`, ); const timer = setInterval(() => { const now = performance.now(); for (const game of gameLoops) game.advance(now); }, 4); timer.unref(); }); function readRemoteAddress(response: HttpResponse): string { const port = response.getRemotePort(); const text = new TextDecoder().decode(response.getRemoteAddressAsText()); return text ? `${text}:${port}` : port > 0 ? `unknown:${port}` : "unknown"; } interface StaticAsset { body: Buffer; contentType: string; } interface StaticSite { files: Map; index: StaticAsset; } function loadStaticSite(root: string | undefined): StaticSite | null { if (!root || !existsSync(root)) return null; const files = new Map(); for (const filePath of walkFiles(root)) { const url = `/${relative(root, filePath).split("\\").join("/")}`; files.set(url, { body: readFileSync(filePath), contentType: contentType(filePath), }); } const index = files.get("/index.html"); if (!index) throw new Error(`STATIC_ROOT has no index.html: ${root}`); console.log(`Serving ${files.size} static files from ${root}`); return { files, index }; } function walkFiles(directory: string): string[] { return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { const path = join(directory, entry.name); return entry.isDirectory() ? walkFiles(path) : [path]; }); } function contentType(filePath: string): string { switch (extname(filePath)) { case ".html": return "text/html; charset=utf-8"; case ".js": return "text/javascript; charset=utf-8"; case ".css": return "text/css; charset=utf-8"; case ".json": return "application/json; charset=utf-8"; case ".svg": return "image/svg+xml"; case ".png": return "image/png"; case ".jpg": case ".jpeg": return "image/jpeg"; case ".webp": return "image/webp"; case ".wasm": return "application/wasm"; case ".ico": return "image/x-icon"; case ".woff2": return "font/woff2"; default: return "application/octet-stream"; } }