import { serveDir } from "@std/http/file-server"; import { GameToHost, HostToGame } from "./game/host_protocol.ts"; import type { ServerModIndex } from "../build.ts"; const PORT = Number(Deno.env.get("PORT") ?? 8000); const WORLD_FILE = Deno.env.get("WORLD_FILE") ?? "world.json"; const STATIC_ROOT = Deno.env.get("BUILD_DIR") ?? "build"; const SERVER_MODS_DIR = Deno.env.get("SERVER_MODS_DIR") ?? "server_mods"; const MAX_MESSAGE_SIZE = 4096; const SHUTDOWN_TIMEOUT_MS = 5000; // the host only does files and networking, the game runs in a worker with no permissions const game = new Worker(new URL("./game/worker.ts", import.meta.url), { type: "module", deno: { permissions: "none" }, } as WorkerOptions); const sockets = new Map(); let next_conn = 1; function post(message: HostToGame) { game.postMessage(message); } function read_world(): string | undefined { try { return Deno.readTextFileSync(WORLD_FILE); } catch (e) { if (e instanceof Deno.errors.NotFound) { return undefined; } throw e; } } function write_world(data: string) { // write then rename so a crash mid write doesnt eat the world Deno.writeTextFileSync(`${WORLD_FILE}.tmp`, data); Deno.renameSync(`${WORLD_FILE}.tmp`, WORLD_FILE); } game.onmessage = (event: MessageEvent) => { const message = event.data; switch (message.type) { case "ready": console.log(`World ${WORLD_FILE} ready, seed ${message.seed}`); break; case "failed": console.error(`Couldn't start the game: ${message.error}`); Deno.exit(1); break; case "send": { const socket = sockets.get(message.conn); if (socket?.readyState === WebSocket.OPEN) { socket.send(message.data); } break; } case "close": sockets.get(message.conn)?.close(); break; case "save": write_world(message.data); if (message.final) { console.log("World saved"); Deno.exit(0); } break; } }; // what deno task build made. no index means the build failed or never ran, and the game never runs without its mods function read_mods(): Extract["mods"] { let index: ServerModIndex; try { index = JSON.parse(Deno.readTextFileSync(`${SERVER_MODS_DIR}/index.json`)); } catch { console.error(`No ${SERVER_MODS_DIR}/index.json, run deno task build first (it also reports mod errors)`); Deno.exit(1); } return index.mods.map(({ listing, server }) => ({ listing, data: JSON.parse(Deno.readTextFileSync(`${STATIC_ROOT}/${listing.data}`)), server_code: server ? Deno.readTextFileSync(server) : undefined, worldgen_code: listing.worldgen ? Deno.readTextFileSync(`${STATIC_ROOT}/${listing.worldgen}`) : undefined, })); } game.onerror = (event) => { console.error("Game server crashed:", event.message); Deno.exit(1); }; const mods = read_mods(); console.log(`Mods: ${mods.map((mod) => `${mod.listing.id} ${mod.listing.version}`).join(", ") || "none"}`); const save = read_world(); post({ type: "init", save, default_seed: Deno.env.get("SEED") ?? crypto.randomUUID(), mods }); function handle_socket(socket: WebSocket) { const conn = next_conn++; socket.addEventListener("open", () => { sockets.set(conn, socket); post({ type: "connect", conn }); }); socket.addEventListener("message", (event) => { if (typeof event.data !== "string" || event.data.length > MAX_MESSAGE_SIZE) { return; } post({ type: "message", conn, data: event.data }); }); socket.addEventListener("close", () => { if (sockets.delete(conn)) { post({ type: "disconnect", conn }); } }); } function shutdown() { console.log("Saving world..."); post({ type: "shutdown" }); setTimeout(() => { console.error("Game server didn't save in time"); Deno.exit(1); }, SHUTDOWN_TIMEOUT_MS); } Deno.addSignalListener("SIGINT", shutdown); if (Deno.build.os !== "windows") { Deno.addSignalListener("SIGTERM", shutdown); } Deno.serve({ port: PORT, onListen: ({ port }) => console.log(`bworld server on http://localhost:${port}/`) }, (req) => { const url = new URL(req.url); if (url.pathname === "/ws") { if (req.headers.get("upgrade")?.toLowerCase() !== "websocket") { return new Response("Expected a websocket", { status: 426 }); } const { socket, response } = Deno.upgradeWebSocket(req); handle_socket(socket); return response; } if (url.pathname === "/") { return Response.redirect(new URL("/client/", url), 302); } return serveDir(req, { fsRoot: STATIC_ROOT, quiet: true }); });