import { spawn } from "node:child_process"; import { mkdir, readFile } from "node:fs/promises"; import path from "node:path"; import { pipelineDirectory, projectRoot } from "./recipe.mjs"; export const nativeExporter = path.join( pipelineDirectory, "native", "bin", "runewaker_model_exporter.exe", ); export function run(executable, argumentsList, label) { return new Promise((resolve, reject) => { console.log("\n[runewaker] " + label); const child = spawn(executable, argumentsList, { cwd: projectRoot, stdio: "inherit", windowsHide: true, }); child.once("error", reject); child.once("exit", (code) => { if (code === 0) resolve(); else reject(new Error(label + " exited with code " + code + ".")); }); }); } export function rankEnvironmentCandidates(report) { const byResource = new Map(); for (const descriptor of report.descriptors ?? []) { const resource = String(descriptor.resource ?? ""); if (!resource.toLowerCase().endsWith(".ros")) continue; const key = resource.replaceAll("/", "\\").toLowerCase(); const row = byResource.get(key) ?? { resource, placements: 0, maximumLocalRadius: 0, maximumWorldRadius: 0, descriptor: null, }; row.placements += 1; const localRadius = Number(descriptor.localBounds?.radius ?? 0); const worldRadius = Number(descriptor.worldBounds?.radius ?? 0); if (localRadius > row.maximumLocalRadius) { row.maximumLocalRadius = localRadius; row.descriptor = descriptor; } row.maximumWorldRadius = Math.max(row.maximumWorldRadius, worldRadius); byResource.set(key, row); } return [...byResource.values()] .map((row) => ({ ...row, isDungeonModel: /^model[\\/]dungeon[\\/]/i.test(row.resource), score: Number(( Math.log10(Math.max(1, row.maximumLocalRadius)) * 10 + (/^model[\\/]dungeon[\\/]/i.test(row.resource) ? 40 : 0) - Math.log10(Math.max(1, row.placements)) ).toFixed(6)), })) .sort((left, right) => ( right.score - left.score || right.maximumLocalRadius - left.maximumLocalRadius || left.resource.localeCompare(right.resource) )); } export async function inspectWdb({ wdbFile, reportFile }) { await mkdir(path.dirname(reportFile), { recursive: true }); await run( nativeExporter, ["--wdb-report", wdbFile, reportFile], "inspect " + path.basename(wdbFile) + " with the preserved object factory", ); const report = JSON.parse(await readFile(reportFile, "utf8")); const candidates = rankEnvironmentCandidates(report); const first = candidates[0] ?? null; const second = candidates[1] ?? null; const dominance = first && second ? first.maximumLocalRadius / Math.max(1, second.maximumLocalRadius) : first ? Number.POSITIVE_INFINITY : 0; return { report, candidates, recommendation: first ? { resource: first.resource.replaceAll("\\", "/"), placement: first.descriptor, dominance: Number.isFinite(dominance) ? Number(dominance.toFixed(4)) : null, confidence: first.isDungeonModel && dominance >= 3 ? "high" : "review-required", reason: first.isDungeonModel && dominance >= 3 ? "Largest model/dungeon ROS dominates the next candidate by at least 3x." : "Primary environment selection is ambiguous and requires visual review.", } : null, }; }