/// // the desktop app: the web client in a bundled chromium (deno desktop's cef backend), so people whose browser has no // webgpu (firefox) can still play. it joins servers like the browser does, and runs its own game server for // singleplayer, like minecraft java. build it with deno task desktop, see the desktop/ docs for how deno desktop works import { fromFileUrl, join } from "@std/path"; import { type Host, serve_static, start_host } from "../server/host.ts"; // where the project was built from: build/ and server_mods/ (the .bmod files) are bundled into the app with --include const ROOT = fromFileUrl(new URL("../", import.meta.url)); const BUILD_DIR = join(ROOT, "build"); // the server the address field starts with. "" leaves it empty for the player to fill in const config: { server?: string } = JSON.parse( await Deno.readTextFile(new URL("./config.json", import.meta.url)), ); const default_server = Deno.env.get("BWORLD_SERVER") ?? config.server ?? ""; const win = new Deno.BrowserWindow({ title: "bworld", width: 1280, height: 720 }); // the singleplayer game server, started the first time singleplayer connects so players who only join other servers // don't get a world made for them. it plays the .bmod files the app was built with let host: Promise | undefined; function singleplayer_host(): Promise { host ??= (async () => { const worlds = join(data_dir(), "worlds"); Deno.mkdirSync(worlds, { recursive: true }); return await start_host({ build_dir: BUILD_DIR, server_mods_dir: join(ROOT, "server_mods"), world_file: join(worlds, "world.json"), on_fatal(message) { console.error(message); alert(`The singleplayer world stopped: ${message}`); Deno.exit(1); }, }); })(); return host; } // the world gets saved before the app closes win.addEventListener("close", async (event) => { if (!host) { return; } event.preventDefault(); try { await (await host).shutdown(); } catch (e) { console.error((e as Error).message); } Deno.exit(0); }); // deno desktop binds this to a private local address and opens the window on it, the port given here is ignored Deno.serve(async (req) => { const url = new URL(req.url); if (url.pathname === "/") { // ?server= fills in the address field, without it the field would start with this app's own address. // ?singleplayer shows the singleplayer button, which joins this app's own server const start = new URL("/client/", url); start.searchParams.set("server", default_server); start.searchParams.set("singleplayer", ""); return Response.redirect(start, 302); } // the singleplayer server's socket and its .bmod downloads if (url.pathname === "/ws" || url.pathname.startsWith("/mods/")) { try { return (await singleplayer_host()).handle(req); } catch (e) { // its mods have problems, the title screen shows why the connection failed console.error((e as Error).message); host = undefined; return new Response((e as Error).message, { status: 500 }); } } return serve_static(BUILD_DIR, req, url); }); // where the app keeps its worlds, the usual place for app data on each os function data_dir() { const home = Deno.env.get("HOME") ?? Deno.env.get("USERPROFILE") ?? "."; switch (Deno.build.os) { case "windows": return join(Deno.env.get("APPDATA") ?? join(home, "AppData", "Roaming"), "bworld"); case "darwin": return join(home, "Library", "Application Support", "bworld"); default: return join(Deno.env.get("XDG_DATA_HOME") ?? join(home, ".local", "share"), "bworld"); } }