import { createHash } from "node:crypto"; import { spawn } from "node:child_process"; import { access, readFile, readdir, stat, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const scriptDirectory = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.resolve(scriptDirectory, "../.."); const manastormRoot = path.join(projectRoot, "public/assets/game/manastorm"); const dungeonRoot = path.join(projectRoot, "src/assets/game/dungeons"); const manastormRegistry = path.join(projectRoot, "src/game/generated/manastormAssetPackages.json"); async function exists(file) { try { await access(file); return true; } catch { return false; } } async function readJson(file) { return JSON.parse(await readFile(file, "utf8")); } async function writeJson(file, value) { await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8"); } async function sha256(file) { return createHash("sha256").update(await readFile(file)).digest("hex"); } async function refreshManastorm() { const packages = []; let visuals = 0; for (const entry of await readdir(manastormRoot, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const manifestFile = path.join(manastormRoot, entry.name, "stage-pack.json"); if (!await exists(manifestFile)) continue; const manifest = await readJson(manifestFile); for (const descriptor of manifest.visual ?? []) { const relativeUrl = descriptor.url.replace(/^\/+/, ""); const file = path.join(projectRoot, "public", relativeUrl); descriptor.byteLength = (await stat(file)).size; descriptor.checksum = await sha256(file); descriptor.compression = "meshopt+ktx2"; visuals += 1; } await writeJson(manifestFile, manifest); packages.push(manifest); } packages.sort((left, right) => left.id.localeCompare(right.id)); await writeJson(manastormRegistry, { schemaVersion: 1, packages }); return { packages, visuals }; } async function refreshCanonicalDungeonEnvironments(packages) { const packageById = new Map(packages.map((assetPackage) => [assetPackage.id, assetPackage])); const publicDungeonRoot = path.join(projectRoot, "public/assets/game/dungeons"); let manifests = 0; for (const entry of await readdir(publicDungeonRoot, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const manifestFile = path.join(publicDungeonRoot, entry.name, "dungeon-pack.json"); if (!await exists(manifestFile)) continue; const manifest = await readJson(manifestFile); const canonical = packageById.get(manifest.environment?.id); if (!canonical) continue; manifest.environment = canonical; await writeJson(manifestFile, manifest); manifests += 1; } return manifests; } function compileCampaignCatalog() { return new Promise((resolve, reject) => { const compiler = path.join(projectRoot, "scripts/dungeon-pipeline/compile-campaign-catalog.mjs"); const child = spawn(process.execPath, [compiler], { cwd: projectRoot, windowsHide: true, stdio: "inherit", }); child.once("error", reject); child.once("exit", (code) => code === 0 ? resolve() : reject(new Error(`Campaign catalog compiler exited with ${code}.`))); }); } async function refreshDungeonImports() { let imports = 0; let optimizationReports = 0; for (const entry of await readdir(dungeonRoot, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const directory = path.join(dungeonRoot, entry.name); const visualName = (await readdir(directory)).find((name) => /-visual\.glb$/i.test(name)); if (!visualName) continue; const visualFile = path.join(directory, visualName); const visualStat = await stat(visualFile); const visualHash = await sha256(visualFile); const importFile = path.join(directory, "import-metadata.json"); if (await exists(importFile)) { const metadata = await readJson(importFile); const descriptor = metadata.assets?.find((asset) => asset.role === "visual"); if (descriptor) { descriptor.fileName = visualName; descriptor.size = visualStat.size; descriptor.sha256 = visualHash; descriptor.compression = "meshopt+ktx2"; } await writeJson(importFile, metadata); imports += 1; } const optimizationFile = path.join(directory, "optimization-report.json"); if (await exists(optimizationFile)) { const report = await readJson(optimizationFile); report.optimized = { ...(report.optimized ?? {}), size: visualStat.size, checksum: visualHash, }; report.compression = "meshopt+ktx2"; const command = "node scripts/asset-pipeline/compress-ktx2.mjs --concurrency 1 --jobs 2"; report.commands = [...new Set([...(report.commands ?? []), command])]; await writeJson(optimizationFile, report); optimizationReports += 1; } } return { imports, optimizationReports }; } const manastorm = await refreshManastorm(); const canonicalDungeonEnvironments = await refreshCanonicalDungeonEnvironments(manastorm.packages); const dungeons = await refreshDungeonImports(); await compileCampaignCatalog(); console.log(JSON.stringify({ status: "green", manastorm: { packages: manastorm.packages.length, visuals: manastorm.visuals }, canonicalDungeonEnvironments, dungeons, }, null, 2));