// checks between mods: dependencies, load order, and ids one mod uses from another. pure, so the build, check-mods // and the server (on .bmod files) all use it import type { BlockJson, ItemJson, OreJson, RecipeJson } from "./mod_data.ts"; import { BUILTIN_MODELS, type ModelJson } from "./block_models.ts"; export interface ModReport { id: string; errors: string[]; warnings: string[]; } export interface LoadedMod { id: string; dir: string; report: ModReport; manifest?: Record; blocks: { file: string; json: BlockJson }[]; models: { file: string; json: ModelJson }[]; items: { file: string; json: ItemJson }[]; recipes: { file: string; json: RecipeJson }[]; ores: { file: string; json: OreJson }[]; textures: string[]; // absolute paths by texture id texture_files: Map; } // dependencies before the mods that need them, otherwise alphabetical export function load_order(mods: LoadedMod[]): LoadedMod[] { const by_id = new Map(mods.map((mod) => [mod.id, mod])); const ordered: LoadedMod[] = []; const state = new Map(); const visit = (mod: LoadedMod, path: string[]) => { if (state.get(mod.id) === "done") return; if (state.get(mod.id) === "visiting") { throw new Error(`mods depend on each other in a circle: ${[...path, mod.id].join(" -> ")}`); } state.set(mod.id, "visiting"); const dependencies = ((mod.manifest?.dependencies ?? []) as { id: string }[]).map((d) => d.id).sort(); for (const dependency of dependencies) { const other = by_id.get(dependency); if (other) visit(other, [...path, mod.id]); } state.set(mod.id, "done"); ordered.push(mod); }; for (const mod of [...mods].sort((a, b) => a.id.localeCompare(b.id))) { visit(mod, []); } return ordered; } // engine_textures are the engine: texture ids, for telling whether a texture a mod uses exists export function check_references(mods: LoadedMod[], engine_textures: Iterable) { const block_owners = new Map(); const item_owners = new Map(); const model_owners = new Map(); const add = (map: Map, id: string, mod: string) => map.set(id, [...(map.get(id) ?? []), mod]); const textures = new Set(engine_textures); for (const mod of mods) { for (const { json } of mod.blocks) { add(block_owners, json.id, mod.id); if (json.item !== false) add(item_owners, json.id, mod.id); } for (const { json } of mod.items) add(item_owners, json.id, mod.id); for (const { json } of mod.models) add(model_owners, json.id, mod.id); for (const texture of mod.textures) textures.add(texture); } const mod_ids = new Set(mods.map((mod) => mod.id)); for (const mod of mods) { const { errors, warnings } = mod.report; const dependencies = new Set( ((mod.manifest?.dependencies ?? []) as { id: string }[]).map((dep) => dep.id), ); for (const dep of dependencies) { if (!mod_ids.has(dep)) errors.push(`manifest.json: depends on "${dep}", which isn't installed`); } const uses = (file: string, id: string, what: "block" | "item" | "texture" | "model") => { const namespace = id.split(":")[0]; if (namespace !== mod.id && namespace !== "engine" && !dependencies.has(namespace)) { warnings.push(`${file}: uses ${id} but doesn't list "${namespace}" in dependencies`); } if (what === "model") { if (!BUILTIN_MODELS[id] && !model_owners.has(id)) errors.push(`${file}: model ${id} doesn't exist`); } else if (what === "texture") { if (!textures.has(id)) warnings.push(`${file}: texture ${id} doesn't exist, it will show as missing`); } else if (!(what === "block" ? block_owners : item_owners).has(id)) { errors.push(`${file}: ${what} ${id} doesn't exist`); } }; for (const { file, json } of mod.blocks) { if ((block_owners.get(json.id)?.length ?? 0) > 1) { errors.push(`${file}: block ${json.id} is also defined by ${block_owners.get(json.id)!.join(", ")}`); } for (const variant of [json, ...Object.values(json.variants ?? {})]) { if (variant.model) uses(file, variant.model, "model"); const textures = variant.textures ?? {}; for (const texture of typeof textures === "string" ? [textures] : Object.values(textures)) { uses(file, texture, "texture"); } } if (json.drops) uses(file, json.drops, "item"); } for (const { file, json } of mod.models) { if ((model_owners.get(json.id)?.length ?? 0) > 1 || BUILTIN_MODELS[json.id]) { errors.push(`${file}: model ${json.id} is defined more than once`); } for (const texture of Object.values(json.textures ?? {})) { if (!texture.startsWith("#")) uses(file, texture, "texture"); } for (const element of json.elements) { for (const face of Object.values(element.faces)) { if (face && !face.texture.startsWith("#")) uses(file, face.texture, "texture"); } } } for (const { file, json } of mod.items) { if ((item_owners.get(json.id)?.length ?? 0) > 1) { errors.push(`${file}: item ${json.id} is also defined by ${item_owners.get(json.id)!.join(", ")}`); } uses(file, json.texture, "texture"); if (json.places) uses(file, json.places, "block"); } for (const { file, json } of mod.recipes) { switch (json.type) { case "shaped": for (const id of Object.values(json.key)) uses(file, id, "item"); uses(file, json.result.id, "item"); break; case "furnace": uses(file, json.input, "item"); uses(file, json.output.id, "item"); break; case "fuel": uses(file, json.item, "item"); break; case "smithing": uses(file, json.tool, "item"); uses(file, json.material.id, "item"); if (json.addition) uses(file, json.addition, "item"); uses(file, json.result, "item"); break; } } for (const { file, json } of mod.ores) { uses(file, json.id, "block"); uses(file, json.replaces, "block"); } } }