112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
// 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<ArrayBuffer>;
|
|
}
|
|
|
|
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<string>): Promise<ServerMod[]> {
|
|
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<string, { file: string; bmod: Bmod }>();
|
|
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 = <T>(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<string, unknown>,
|
|
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<ArrayBuffer>) {
|
|
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
}
|