// the server's mods: every .bmod in server_mods/, checked, sorted so dependencies load first, and with the copy // players download (the same mod without its server script) ready to serve import { join } from "@std/path"; import { type Bmod, BMOD_EXTENSION, client_copy, read_bmod, write_bmod } from "$/common/bmod.ts"; import { check_references, load_order, type LoadedMod } from "$/common/mod_check.ts"; import type { ModListing } from "$/common/mod_loader.ts"; export interface ServerMod { bmod: Bmod; listing: ModListing; // what's served at listing.file client_bytes: Uint8Array; } export class ModsError extends Error { constructor(problems: string[]) { super(`Mods have errors:\n${problems.map((p) => ` ${p}`).join("\n")}`); this.name = "ModsError"; } } // engine_textures are the engine: texture ids, for checking the textures mods use. a mod with problems stops the // whole server from starting, the game never runs with some mods missing export async function load_bmods(dir: string, engine_textures: Iterable): Promise { let files: string[]; try { files = [...Deno.readDirSync(dir)] .filter((entry) => entry.isFile && entry.name.endsWith(BMOD_EXTENSION)) .map((entry) => entry.name) .sort(); } catch (e) { if (e instanceof Deno.errors.NotFound) return []; throw e; } const problems: string[] = []; const by_id = new Map(); for (const file of files) { try { const bmod = read_bmod(Deno.readFileSync(join(dir, file)), file); const other = by_id.get(bmod.manifest.id); if (other) { problems.push(`${file} and ${other.file} are both the mod "${bmod.manifest.id}"`); continue; } by_id.set(bmod.manifest.id, { file, bmod }); } catch (e) { problems.push((e as Error).message); } } // the same checks check-mods runs on mod folders const loaded = [...by_id.values()].map(({ file, bmod }) => as_loaded_mod(file, bmod)); check_references(loaded, engine_textures); for (const mod of loaded) { const dependencies = (mod.manifest?.dependencies ?? []) as { id: string }[]; for (const { id } of dependencies) { if (!by_id.has(id)) mod.report.errors.push(`depends on "${id}", which isn't in ${dir}`); } problems.push(...mod.report.errors.map((error) => `${mod.dir}: ${error}`)); for (const warning of mod.report.warnings) console.warn(`${mod.dir}: ${warning}`); } let ordered: LoadedMod[] = []; try { ordered = load_order(loaded); } catch (e) { problems.push((e as Error).message); } if (problems.length > 0) throw new ModsError(problems); return await Promise.all(ordered.map(async (mod) => { const bmod = by_id.get(mod.id)!.bmod; const client_bytes = write_bmod(client_copy(bmod)); const sha256 = await hex_sha256(client_bytes); return { bmod, client_bytes, listing: { id: bmod.manifest.id, name: bmod.manifest.name, version: bmod.manifest.version, sha256, file: `mods/${sha256}${BMOD_EXTENSION}`, size: client_bytes.length, }, }; })); } // in the shape the checks in common/mod_check.ts take function as_loaded_mod(file: string, bmod: Bmod): LoadedMod { const entries = (list: T[]) => list.map((json) => ({ file: `${file} data.json`, json })); return { id: bmod.manifest.id, dir: file, report: { id: bmod.manifest.id, errors: [], warnings: [] }, manifest: bmod.manifest as unknown as Record, blocks: entries(bmod.data.blocks), models: entries(bmod.data.models), items: entries(bmod.data.items), recipes: entries(bmod.data.recipes), ores: entries(bmod.data.ores), textures: [...bmod.textures.keys()], texture_files: new Map(), }; } async function hex_sha256(bytes: Uint8Array) { const digest = await crypto.subtle.digest("SHA-256", bytes); return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join(""); }