Files
bworld/server/main.ts
T
2026-09-24 19:34:07 -03:00

127 lines
3.2 KiB
TypeScript

import { serveDir } from "@std/http/file-server";
import { GameToHost, HostToGame } from "./game/host_protocol.ts";
const PORT = Number(Deno.env.get("PORT") ?? 8000);
const WORLD_FILE = Deno.env.get("WORLD_FILE") ?? "world.json";
const STATIC_ROOT = "build";
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<number, WebSocket>();
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<GameToHost>) => {
const message = event.data;
switch (message.type) {
case "ready":
console.log(`World ${WORLD_FILE} ready, seed ${message.seed}`);
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;
}
};
game.onerror = (event) => {
console.error("Game server crashed:", event.message);
Deno.exit(1);
};
const save = read_world();
post({ type: "init", save, default_seed: Deno.env.get("SEED") ?? crypto.randomUUID() });
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 });
});