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

145 lines
5.4 KiB
JavaScript

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 <optimized.glb> --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));