New modding system

This commit is contained in:
2026-09-26 17:33:07 -03:00
parent 86d5c39eab
commit 243ff52063
25 changed files with 1089 additions and 512 deletions
+33 -32
View File
@@ -1,9 +1,10 @@
// loads the mods the server lists: registers their data, then runs their client scripts.
// loads the mods the server lists: downloads their .bmod files, registers their data, then runs their client scripts.
// see "Delivery to clients" in MODS.md
import { AIR } from "$/common/constants.ts";
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import type { ClientContext } from "$/common/mod_api/client.ts";
import { ModData, ModListing, ModLoadError, register_mod_data } from "$/common/mod_loader.ts";
import { ModListing, ModLoadError, register_mod_data } from "$/common/mod_loader.ts";
import { type Bmod, read_bmod } from "$/common/bmod.ts";
import type { OreJson } from "$/common/mod_data.ts";
import { AIR_ID } from "$/common/protocol.ts";
import type { Client } from "./client.ts";
@@ -22,50 +23,50 @@ export function set_mods_client(game_client: Client) {
client = game_client;
}
// downloads every mod's files and checks them against the hashes the server listed, then registers the data and
// runs the client scripts. everything a mod runs is imported from the checked bytes, never fetched twice
export async function load_client_mods(listings: ModListing[], base: URL) {
const downloads = await Promise.all(listings.map(async (listing) => {
const get = async (path: string | undefined, sha256: string | undefined, what: string) => {
if (!path) return undefined;
try {
return await fetch_verified(new URL(path, base), sha256 ?? "", what);
} catch (e) {
throw new ModLoadError(listing.id, (e as Error).message);
}
};
const [data, client, worldgen, credits] = await Promise.all([
get(listing.data, listing.sha256.data, "its data"),
get(listing.client, listing.sha256.client, "its client script"),
get(listing.worldgen, listing.sha256.worldgen, "its worldgen script"),
get(listing.credits, listing.sha256.credits, "its credits"),
]);
return { listing, data: JSON.parse(new TextDecoder().decode(data)) as ModData, client, worldgen, credits };
// downloads every mod's .bmod and checks it against the hash the server listed. everything a mod runs comes from
// the checked bytes, never fetched twice
export async function download_mods(listings: ModListing[], base: URL): Promise<Bmod[]> {
return await Promise.all(listings.map(async (listing) => {
try {
const bytes = await fetch_verified(new URL(listing.file, base), listing.sha256, `${listing.name}`);
const bmod = read_bmod(bytes, `${listing.id}.bmod`);
if (bmod.manifest.id !== listing.id) throw new Error(`the server listed it as ${listing.id}`);
return bmod;
} catch (e) {
throw new ModLoadError(listing.id, (e as Error).message);
}
}));
}
for (const { listing, credits } of downloads) {
if (credits) {
mod_credits.push({ name: listing.name, version: listing.version, text: new TextDecoder().decode(credits) });
// registers the mods' data and runs their client scripts, in the order the server listed them
export async function load_client_mods(listings: ModListing[], bmods: Bmod[]) {
for (const [i, bmod] of bmods.entries()) {
if (bmod.credits) {
mod_credits.push({ name: listings[i].name, version: listings[i].version, text: bmod.credits });
}
}
const recipes = register_mod_data(downloads.map(({ listing, data }) => ({ id: listing.id, data })));
const recipes = register_mod_data(bmods.map((bmod) => ({ id: bmod.manifest.id, data: bmod.data })));
worldgen_mods.ores = recipes.ores;
worldgen_mods.scripts = downloads.flatMap(({ listing, worldgen }) =>
worldgen ? [{ mod: listing.id, url: code_url(worldgen) }] : []
worldgen_mods.scripts = bmods.flatMap((bmod) =>
bmod.scripts.worldgen ? [{ mod: bmod.manifest.id, url: script_url(bmod.scripts.worldgen) }] : []
);
for (const { listing, client } of downloads) {
if (!client) continue;
const module = await import(code_url(client));
for (const [i, bmod] of bmods.entries()) {
if (!bmod.scripts.client) continue;
const module = await import(script_url(bmod.scripts.client));
if (typeof module.setup !== "function") {
throw new ModLoadError(listing.id, "the client script doesn't export a setup function");
throw new ModLoadError(bmod.manifest.id, "the client script doesn't export a setup function");
}
await module.setup(client_context(listing));
await module.setup(client_context(listings[i]));
}
}
function script_url(code: string) {
return code_url(new TextEncoder().encode(code));
}
function client_context(listing: ModListing): ClientContext {
const mod = listing.id;
const not_yet = (name: string, where: string) =>