Files
2026-08-14 15:56:39 -04:00

162 lines
6.0 KiB
JavaScript

import { mkdir, readdir, writeFile } from "node:fs/promises";
import path from "node:path";
import {
exists,
optionValue,
pipelineDirectory,
projectPath,
projectRoot,
readJson,
slugify,
} from "./lib/recipe.mjs";
const argv = process.argv.slice(2);
const sourceRoot = path.resolve(
process.env.RUNEWAKER_ROOT
?? path.join(projectRoot, "../rom-pvt-server-backups/Runewaker"),
);
const configFile = path.join(sourceRoot, "Tools", "dungeon_config.json");
const wdbDirectory = path.join(sourceRoot, "Resource", "wdb");
const luaDirectory = path.join(sourceRoot, "Resource", "luascript");
const recipeDirectory = path.join(pipelineDirectory, "recipes");
const overrideFile = path.join(pipelineDirectory, "discovery-overrides.json");
const outputFile = path.resolve(
projectRoot,
optionValue(argv, "--output", "runewaker-export-work/instance-catalog.json"),
);
const STOP_WORDS = new Set([
"dgn", "inst", "instance", "the", "of", "and", "a", "an", "dungeon",
]);
async function listWdbFiles(directory, prefix = "") {
const result = [];
for (const entry of await readdir(directory, { withFileTypes: true })) {
const relative = prefix ? prefix + "/" + entry.name : entry.name;
const absolute = path.join(directory, entry.name);
if (entry.isDirectory()) result.push(...await listWdbFiles(absolute, relative));
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".wdb")) result.push(relative);
}
return result;
}
function tokens(value) {
return slugify(value)
.split("-")
.filter((token) => token && !STOP_WORDS.has(token));
}
function similarity(leftValue, rightValue) {
const left = new Set(tokens(leftValue));
const right = new Set(tokens(rightValue));
if (!left.size || !right.size) return 0;
let intersection = 0;
for (const token of left) if (right.has(token)) intersection += 1;
const union = new Set([...left, ...right]).size;
const containment = intersection / Math.min(left.size, right.size);
return Number((intersection / union * 0.65 + containment * 0.35).toFixed(4));
}
async function recipesByDungeon() {
const result = new Map();
for (const name of await readdir(recipeDirectory)) {
if (!name.endsWith(".json")) continue;
const file = path.join(recipeDirectory, name);
const recipe = await readJson(file);
const dungeonId = recipe.dungeonId ?? recipe.slug;
if (!dungeonId) continue;
const key = slugify(dungeonId);
const entry = result.get(key) ?? { environment: null, population: null };
if (name.includes("population")) entry.population = projectPath(file);
else entry.environment = projectPath(file);
result.set(key, entry);
}
return result;
}
if (!await exists(configFile)) throw new Error("Missing RuneWaker dungeon catalog: " + configFile);
if (!await exists(wdbDirectory)) throw new Error("Missing RuneWaker WDB directory: " + wdbDirectory);
const config = await readJson(configFile);
const overrides = await exists(overrideFile) ? await readJson(overrideFile) : {};
const knownRecipes = await recipesByDungeon();
const wdbFiles = (await listWdbFiles(wdbDirectory)).sort((left, right) => left.localeCompare(right));
const allWdb = new Set(wdbFiles.map((name) => name.toLowerCase()));
const dungeons = [];
for (const dungeon of config.dungeons ?? []) {
const override = overrides[dungeon.id] ?? {};
const ranked = wdbFiles
.map((fileName) => ({
fileName,
score: Math.max(
similarity(dungeon.id, path.parse(fileName).name),
similarity(dungeon.name, path.parse(fileName).name),
),
}))
.filter((candidate) => candidate.score > 0)
.sort((left, right) => right.score - left.score || left.fileName.localeCompare(right.fileName))
.slice(0, 5);
const selectedWdb = override.wdb
?? (ranked[0]?.score >= 0.62 && ranked[0].score - (ranked[1]?.score ?? 0) >= 0.12
? ranked[0].fileName
: null);
const luaFiles = [];
for (const name of dungeon.lua_files ?? []) {
luaFiles.push({ name, exists: await exists(path.join(luaDirectory, name)) });
}
const recipes = knownRecipes.get(slugify(dungeon.id)) ?? { environment: null, population: null };
dungeons.push({
id: dungeon.id,
slug: slugify(dungeon.id),
name: dungeon.name,
baseZone: dungeon.base_zone,
copyFromZone: dungeon.copy_from_zone,
mobTemplateCount: (dungeon.mob_guids ?? []).length,
configuredBossTemplateCount: (dungeon.boss_guids ?? []).length,
luaFiles,
entryLuaPrefix: dungeon.entry_lua_prefix || null,
selectedWdb,
selectedWdbExists: selectedWdb ? allWdb.has(selectedWdb.toLowerCase()) : false,
wdbCandidates: ranked,
recipes,
readiness: recipes.environment && recipes.population
? "packaged"
: selectedWdb
? "ready-to-scaffold"
: "needs-wdb-review",
});
}
const selected = new Set(dungeons.map((dungeon) => dungeon.selectedWdb?.toLowerCase()).filter(Boolean));
const payload = {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
source: {
dungeonConfig: configFile,
wdbDirectory,
luaDirectory,
},
counts: {
configuredDungeons: dungeons.length,
wdbFiles: wdbFiles.length,
packaged: dungeons.filter((dungeon) => dungeon.readiness === "packaged").length,
readyToScaffold: dungeons.filter((dungeon) => dungeon.readiness === "ready-to-scaffold").length,
needsWdbReview: dungeons.filter((dungeon) => dungeon.readiness === "needs-wdb-review").length,
},
dungeons,
unmatchedWdbFiles: wdbFiles.filter((name) => !selected.has(name.toLowerCase())),
};
await mkdir(path.dirname(outputFile), { recursive: true });
await writeFile(outputFile, JSON.stringify(payload, null, 2) + "\n", "utf8");
console.log("[runewaker] discovered " + payload.counts.configuredDungeons + " configured dungeons and "
+ payload.counts.wdbFiles + " WDB files.");
console.log("[runewaker] " + payload.counts.readyToScaffold + " ready to scaffold; "
+ payload.counts.needsWdbReview + " need a WDB choice; "
+ payload.counts.packaged + " already packaged.");
console.log("[runewaker] catalog: " + projectPath(outputFile));
export { similarity, tokens };