import { copy } from "@std/fs"; import { createCanvas, loadImage } from "@gfx/canvas-wasm"; import { ENGINE_TEXTURE_DIR, load_and_check, load_order, LoadedMod } from "./tools/check_mods.ts"; import { pack_mod } from "./tools/pack_mod.ts"; import { BMOD_EXTENSION } from "./common/bmod.ts"; import { ENGINE_TEXTURES_INDEX } from "./server/host.ts"; // all overridable so tests can build somewhere else const BUILD_FOLDER = Deno.env.get("BUILD_DIR") ?? "build"; const MODS_FOLDER = Deno.env.get("MODS_DIR") ?? "mods"; // where the server loads .bmod files from. the build puts every mod in mods/ there, next to any others const SERVER_MODS_FOLDER = Deno.env.get("SERVER_MODS_DIR") ?? "server_mods"; function clear_folder(folder: string) { try { Deno.removeSync(folder, { recursive: true }); } catch (e) { if (!(e instanceof Deno.errors.NotFound)) throw e; } Deno.mkdirSync(folder, { recursive: true }); } async function build_fonts() { // i used https://fonts.varg.dev/ to build the font, its confusing const canvas_size = 256; const font = await loadImage("assets/fonts/m6x11.png"); const canvas = createCanvas(canvas_size, canvas_size); const ctx = canvas.getContext("2d"); ctx.drawImage(font, 0, 0); const image_data = ctx.getImageData(0, 0, canvas_size, canvas_size); const data = image_data.data; for (let i = 0; i < data.length; i += 4) { const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; if (r === 0 && g === 0 && b === 0) { // black pixel, alpha = 0 data[i + 3] = 0; } else { // force white white data[i] = 255; data[i + 1] = 255; data[i + 2] = 255; data[i + 3] = 255; } } ctx.putImageData(image_data, 0, 0); Deno.mkdirSync(`${BUILD_FOLDER}/assets/fonts`); Deno.writeFileSync(`${BUILD_FOLDER}/assets/fonts/m6x11.png`, canvas.toBuffer()); await copy("assets/fonts/m6x11.fnt", `${BUILD_FOLDER}/assets/fonts/m6x11.fnt`); } async function build_sprites() { for (const entry of Deno.readDirSync("assets/sprites")) { if (entry.name.endsWith(".png")) { await copy(`assets/sprites/${entry.name}`, `${BUILD_FOLDER}/assets/sprites/${entry.name}`); } } } // the engine's textures (engine:), with a list of them. clients put them in the texture atlas with the mods' function build_engine_textures() { const folder = `${BUILD_FOLDER}/assets/textures`; Deno.mkdirSync(folder, { recursive: true }); const names: string[] = []; for (const entry of [...Deno.readDirSync(ENGINE_TEXTURE_DIR)].sort((a, b) => a.name.localeCompare(b.name))) { if (entry.isFile && entry.name.endsWith(".png")) { Deno.copyFileSync(`${ENGINE_TEXTURE_DIR}/${entry.name}`, `${folder}/${entry.name}`); names.push(entry.name.replace(/\.png$/, "")); } } Deno.writeTextFileSync(`${BUILD_FOLDER}/${ENGINE_TEXTURES_INDEX}`, JSON.stringify(names)); } async function build_assets() { Deno.mkdirSync(`${BUILD_FOLDER}/assets`, { recursive: true }); await copy("assets/ASSETS.md", `${BUILD_FOLDER}/assets/ASSETS.md`); await build_fonts(); Deno.mkdirSync(`${BUILD_FOLDER}/assets/sprites`, { recursive: true }); await build_sprites(); build_engine_textures(); } // every mod in mods/, checked and sorted so dependencies load first. a broken mod fails the whole build, // the game never runs with some mods missing function load_mods(): LoadedMod[] { const mods = load_and_check(MODS_FOLDER); const problems = mods.flatMap((mod) => mod.report.errors.map((error) => ` ${mod.id}: ${error}`)); if (problems.length > 0) { throw new Error(`Mods have errors (deno task check-mods for details):\n${problems.join("\n")}`); } return load_order(mods); } // every mod in mods/ into server_mods/.bmod. other .bmod files there are left alone, they're other people's mods async function build_mods(mods: LoadedMod[]) { Deno.mkdirSync(SERVER_MODS_FOLDER, { recursive: true }); remove_old_format(); for (const mod of mods) { Deno.writeFileSync(`${SERVER_MODS_FOLDER}/${mod.id}${BMOD_EXTENSION}`, await pack_mod(mod)); } console.log(`Mods: ${mods.map((m) => m.id).join(", ") || "none"}`); } // builds before .bmod files put each mod's server script in a folder here with an index.json function remove_old_format() { for (const entry of Deno.readDirSync(SERVER_MODS_FOLDER)) { if (entry.isDirectory || entry.name === "index.json") { Deno.removeSync(`${SERVER_MODS_FOLDER}/${entry.name}`, { recursive: true }); } } } async function build_client() { const _result = await Deno.bundle({ entrypoints: ["./client/main.ts", "./client/workers/chunk_worker.ts"], outputDir: `${BUILD_FOLDER}/client`, platform: "browser", minify: false, keepNames: true, }); await copy("./client/index.html", `${BUILD_FOLDER}/client/index.html`); } async function build() { try { const now = performance.now(); clear_folder(BUILD_FOLDER); // checked first: a mod with errors stops the build before any .bmod is written const mods = load_mods(); await build_assets(); await build_client(); await build_mods(mods); console.log(`Built in ${(performance.now() - now).toFixed(2)}ms`); } catch (e) { console.log(e instanceof Error ? e.message : e); return false; } return true; } let last_build = 0; if (import.meta.main) { const ok = await build(); // deno task build --once, for scripts and ci if (Deno.args.includes("--once")) { Deno.exit(ok ? 0 : 1); } const watcher = Deno.watchFs(["assets", "client", "common", MODS_FOLDER], { recursive: true }); for await (const event of watcher) { const now = performance.now(); if (now - last_build < 500) { continue; } if (["create", "modify", "rename", "remove"].includes(event.kind)) { last_build = now; console.log("Rebuilding..."); await build(); } } }