// loads the mods the server lists: 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 type { OreJson } from "$/common/mod_data.ts"; import { AIR_ID } from "$/common/protocol.ts"; import { Position } from "$/common/components/position.ts"; import type { ClientWorld } from "./client_world.ts"; import { code_url, fetch_verified } from "./handshake.ts"; // 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 world exists, mods can't look at it during setup let world: ClientWorld | undefined; export function set_mods_world(client_world: ClientWorld) { world = client_world; } // 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] = 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"), ]); return { listing, data: JSON.parse(new TextDecoder().decode(data)) as ModData, client, worldgen }; })); const recipes = register_mod_data(downloads.map(({ listing, data }) => ({ id: listing.id, data }))); worldgen_mods.ores = recipes.ores; worldgen_mods.scripts = downloads.flatMap(({ listing, worldgen }) => worldgen ? [{ mod: listing.id, url: code_url(worldgen) }] : [] ); for (const { listing, client } of downloads) { if (!client) continue; const module = await import(code_url(client)); if (typeof module.setup !== "function") { throw new ModLoadError(listing.id, "the client script doesn't export a setup function"); } await module.setup(client_context(listing)); } } 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_world = () => { if (!world) throw new Error(`[${mod}] the world isn't there yet during setup`); return world; }; 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_world().connection.name; }, get position() { const [player] = need_world().get_tag("player")!; const position = player.get(Position)!; return { x: position.x, y: position.y, z: position.z }; }, }, world: { get_block(x, y, z) { const nid = need_world().dimension.get_block(x, y, z); if (nid === AIR) return AIR_ID; return EverythingRegistry.get_by_id("blocks", nid)?.id; }, }, log: (...args) => console.log(`[${mod}]`, ...args), }; }