97 lines
3.7 KiB
TypeScript
97 lines
3.7 KiB
TypeScript
// 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 type { Client } from "./client.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 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 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_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<BlockRegistry>("blocks", nid)?.id;
|
|
},
|
|
},
|
|
log: (...args) => console.log(`[${mod}]`, ...args),
|
|
};
|
|
}
|