// 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 { 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"; import { code_url, fetch_verified } from "./handshake.ts"; // each loaded mod's credits file, for the credits screen export const mod_credits: { name: string; version: string; text: string }[] = []; // what the chunk workers need to generate the same world as the server export const worldgen_mods: { scripts: { mod: string; url: string }[]; ores: OreJson[] } = { scripts: [], ores: [] }; // set once the game is running, mods can't look at it during setup let client: Client | undefined; export function set_mods_client(game_client: Client) { client = game_client; } // 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 { 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); } })); } // 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(bmods.map((bmod) => ({ id: bmod.manifest.id, data: bmod.data }))); worldgen_mods.ores = recipes.ores; worldgen_mods.scripts = bmods.flatMap((bmod) => bmod.scripts.worldgen ? [{ mod: bmod.manifest.id, url: script_url(bmod.scripts.worldgen) }] : [] ); 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(bmod.manifest.id, "the client script doesn't export a setup function"); } 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) => new Proxy({}, { get: (_, prop) => () => { throw new Error(`[${mod}] ctx.${name}.${String(prop)} isn't implemented yet (${where} in MODS.md)`); }, }); const need_client = () => { if (!client) throw new Error(`[${mod}] the world isn't there yet during setup`); return client; }; return { mod: { id: mod, version: listing.version }, ui: not_yet("ui", "step 7") as ClientContext["ui"], hud: not_yet("hud", "step 7") as ClientContext["hud"], input: not_yet("input", "step 8") as ClientContext["input"], net: not_yet("net", "step 8") as ClientContext["net"], player: { get name() { return need_client().connection.name; }, get position() { const { x, y, z } = need_client().player; return { x, y, z }; }, }, world: { get_block(x, y, z) { const nid = need_client().level.get_block(x, y, z); if (nid === AIR) return AIR_ID; return EverythingRegistry.get_by_id("blocks", nid)?.id; }, get time() { return need_client().level.time; }, }, log: (...args) => console.log(`[${mod}]`, ...args), }; }