New modding system
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
// 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("");
|
||||
}
|
||||
Reference in New Issue
Block a user