Files
bworld/common/mod_loader.ts
T
2026-09-25 17:52:59 -03:00

102 lines
2.9 KiB
TypeScript

// registers mods' data, the same way on the server and on clients
import { BlockRegistry, EverythingRegistry, ItemRegistry } from "./everything_registry.ts";
import {
block_from_json,
BlockJson,
grid_recipe_from_json,
GridRecipe,
item_from_json,
ItemJson,
OreJson,
RecipeJson,
} from "./mod_data.ts";
import { register_block_item } from "./utils.ts";
// everything a mod's json files hold, merged into one file by the build (build/mods/<id>/<hash>/data.json)
export interface ModData {
blocks: BlockJson[];
items: ItemJson[];
recipes: RecipeJson[];
ores: OreJson[];
}
// a mod as the server lists it to clients, in load order. paths are relative to the server's root
export interface ModListing {
id: string;
name: string;
version: string;
// short hash of everything public, the folder it's served from
hash: string;
data: string;
client?: string;
worldgen?: string;
// the manifest's credits file, markdown, shown on the credits screen
credits?: string;
// sha-256 in hex of each file, clients check these before using them
sha256: { data: string; client?: string; worldgen?: string; credits?: string };
}
// the texture atlas with every mod's textures, built by the server
export interface AtlasListing {
png: string;
json: string;
sha256: { png: string; json: string };
}
export class RecipeBook {
shaped: GridRecipe[] = [];
furnace = new Map<string, { output: { id: string; count: number }; cook_time: number }>();
fuel = new Map<string, number>();
ores: OreJson[] = [];
}
export class ModLoadError extends Error {
constructor(mod: string, message: string) {
super(`${mod}: ${message}`);
this.name = "ModLoadError";
}
}
// registers every mod's blocks, items and recipes, in the order given (dependencies first)
export function register_mod_data(mods: { id: string; data: ModData }[]): RecipeBook {
const recipes = new RecipeBook();
for (const { id, data } of mods) {
const fail = (message: string): never => {
throw new ModLoadError(id, message);
};
try {
for (const json of data.blocks) {
const { block, has_item } = block_from_json(json);
EverythingRegistry.register<BlockRegistry>("blocks", block.id, block);
if (has_item) {
register_block_item(block);
}
}
for (const json of data.items) {
EverythingRegistry.register<ItemRegistry>("items", json.id, item_from_json(json));
}
} catch (e) {
fail((e as Error).message);
}
for (const recipe of data.recipes) {
switch (recipe.type) {
case "shaped":
recipes.shaped.push(grid_recipe_from_json(recipe));
break;
case "furnace":
if (recipes.furnace.has(recipe.input)) fail(`two furnace recipes for ${recipe.input}`);
recipes.furnace.set(recipe.input, { output: recipe.output, cook_time: recipe.cook_time });
break;
case "fuel":
recipes.fuel.set(recipe.item, recipe.burn_time);
break;
}
}
recipes.ores.push(...data.ores);
}
return recipes;
}