Files
bworld/build.ts
T
2026-09-26 17:33:07 -03:00

168 lines
5.6 KiB
TypeScript

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:<file>), 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/<id>.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();
}
}
}