158 lines
5.8 KiB
TypeScript
158 lines
5.8 KiB
TypeScript
// 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<string, unknown>;
|
|
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<string, string>;
|
|
}
|
|
|
|
// 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<string, "visiting" | "done">();
|
|
|
|
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<string>) {
|
|
const block_owners = new Map<string, string[]>();
|
|
const item_owners = new Map<string, string[]>();
|
|
const model_owners = new Map<string, string[]>();
|
|
const add = (map: Map<string, string[]>, id: string, mod: string) => map.set(id, [...(map.get(id) ?? []), mod]);
|
|
|
|
const textures = new Set<string>(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");
|
|
}
|
|
}
|
|
}
|