Improve networking

This commit is contained in:
2026-09-25 00:08:01 -03:00
parent 6458bc0440
commit 2a2ccae9ce
18 changed files with 600 additions and 162 deletions
+28 -12
View File
@@ -1,7 +1,7 @@
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 type { ModData, ModListing } from "./common/mod_loader.ts";
import type { AtlasListing, ModData, ModListing } from "./common/mod_loader.ts";
// all overridable so tests can build somewhere else
const BUILD_FOLDER = Deno.env.get("BUILD_DIR") ?? "build";
@@ -11,6 +11,7 @@ const SERVER_MODS_FOLDER = Deno.env.get("SERVER_MODS_DIR") ?? "server_mods";
// what the server reads at startup, see server/main.ts
export interface ServerModIndex {
atlas: AtlasListing;
mods: { listing: ModListing; server?: string }[];
}
@@ -74,8 +75,9 @@ function calculate_atlas_size(count: number) {
};
}
// one atlas with the engine's textures (engine:<file>) and every mod's (<mod>:<file>)
async function build_atlas(mods: LoadedMod[]) {
// one atlas with the engine's textures (engine:<file>) and every mod's (<mod>:<file>).
// named by its hash, so clients can cache it forever and download it from other origins
async function build_atlas(mods: LoadedMod[]): Promise<AtlasListing> {
const textures = new Map<string, string>();
for (const entry of Deno.readDirSync(ENGINE_TEXTURE_DIR)) {
if (entry.isFile && entry.name.endsWith(".png")) {
@@ -115,8 +117,13 @@ async function build_atlas(mods: LoadedMod[]) {
atlas_info[id] = { x: column, y: row };
}
Deno.writeFileSync(`${BUILD_FOLDER}/assets/sprites/textures.png`, canvas.toBuffer());
Deno.writeTextFileSync(`${BUILD_FOLDER}/assets/sprites/textures.json`, JSON.stringify(atlas_info));
const png = new Uint8Array(canvas.toBuffer());
const json = new TextEncoder().encode(JSON.stringify(atlas_info));
const hashes = { png: await sha256(png), json: await sha256(json) };
const name = `assets/sprites/textures.${(hashes.png + hashes.json).slice(0, 12)}`;
Deno.writeFileSync(`${BUILD_FOLDER}/${name}.png`, png);
Deno.writeFileSync(`${BUILD_FOLDER}/${name}.json`, json);
return { png: `${name}.png`, json: `${name}.json`, sha256: hashes };
}
async function build_sprites(mods: LoadedMod[]) {
@@ -125,16 +132,16 @@ async function build_sprites(mods: LoadedMod[]) {
await copy(`assets/sprites/${entry.name}`, `${BUILD_FOLDER}/assets/sprites/${entry.name}`);
}
}
await build_atlas(mods);
return await build_atlas(mods);
}
async function build_assets(mods: LoadedMod[]) {
async function build_assets(mods: LoadedMod[]): Promise<AtlasListing> {
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(mods);
return await build_sprites(mods);
}
// every mod in mods/, checked and sorted so dependencies load first. a broken mod fails the whole build,
@@ -156,15 +163,21 @@ async function bundle_script(path: string, platform: "browser" | "deno"): Promis
return result.outputFiles[0].text();
}
async function sha256(bytes: Uint8Array<ArrayBuffer> | string) {
const data = typeof bytes === "string" ? new TextEncoder().encode(bytes) : bytes;
const digest = await crypto.subtle.digest("SHA-256", data);
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
}
async function short_hash(parts: string[]) {
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(parts.join("\0")));
return [...new Uint8Array(digest)].slice(0, 6).map((b) => b.toString(16).padStart(2, "0")).join("");
}
// build/mods/<id>/<hash>/ gets what players download, server_mods/ the server scripts
async function build_mods(mods: LoadedMod[]) {
async function build_mods(mods: LoadedMod[], atlas: AtlasListing) {
clear_folder(SERVER_MODS_FOLDER);
const index: ServerModIndex = { mods: [] };
const index: ServerModIndex = { atlas, mods: [] };
for (const mod of mods) {
const manifest = mod.manifest as { name: string; version: string; scripts?: Record<string, string> };
@@ -193,14 +206,17 @@ async function build_mods(mods: LoadedMod[]) {
version: manifest.version,
hash,
data: `${public_dir}/data.json`,
sha256: { data: await sha256(data_json) },
};
Deno.writeTextFileSync(`${BUILD_FOLDER}/${listing.data}`, data_json);
if (client) {
listing.client = `${public_dir}/client.js`;
listing.sha256.client = await sha256(client);
Deno.writeTextFileSync(`${BUILD_FOLDER}/${listing.client}`, client);
}
if (worldgen) {
listing.worldgen = `${public_dir}/worldgen.js`;
listing.sha256.worldgen = await sha256(worldgen);
Deno.writeTextFileSync(`${BUILD_FOLDER}/${listing.worldgen}`, worldgen);
}
@@ -234,9 +250,9 @@ async function build() {
const now = performance.now();
clear_folder(BUILD_FOLDER);
const mods = load_mods();
await build_assets(mods);
const atlas = await build_assets(mods);
await build_client();
await build_mods(mods);
await build_mods(mods, atlas);
console.log(`Built in ${(performance.now() - now).toFixed(2)}ms`);
} catch (e) {
// no index means the server refuses to start, instead of running without some mods