162 lines
6.0 KiB
JavaScript
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 };
|