#!/usr/bin/env node import { createHash } from "node:crypto"; import { createReadStream } from "node:fs"; import { open, readFile, stat } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const creaturesOnly = process.argv.includes("--creatures-only"); const readJson = async (file) => JSON.parse(await readFile(path.join(projectRoot, file), "utf8")); const allowedExternalBlockers = new Set([ "tinkertech-showdown", "frozen-reach", "forgotten-mine", ]); function assert(condition, message) { if (!condition) throw new Error(message); } function sha256(file) { return new Promise((resolve, reject) => { const hash = createHash("sha256"); createReadStream(file) .on("error", reject) .on("data", (chunk) => hash.update(chunk)) .on("end", () => resolve(hash.digest("hex"))); }); } function assetDescriptors(value) { if (!value) return []; if (Array.isArray(value)) return value.flatMap(assetDescriptors); if (value.url) return [value]; if (typeof value === "object") return Object.values(value).flatMap(assetDescriptors); return []; } function shippingIdentity(descriptor) { return { url: descriptor.url, checksum: descriptor.checksum, size: Number(descriptor.size ?? descriptor.byteLength), triangleCount: Number(descriptor.triangleCount), }; } async function verifyAsset(slug, descriptor, expectedPrefixes) { const prefixes = Array.isArray(expectedPrefixes) ? expectedPrefixes : [expectedPrefixes]; assert( prefixes.some((prefix) => descriptor.url.startsWith(prefix)), `${slug}: unexpected asset URL ${descriptor.url}; expected one of ${prefixes.join(", ")}`, ); const file = path.join(projectRoot, "public", descriptor.url.replace(/^\/+/, "")); const details = await stat(file); const expectedSize = Number(descriptor.byteLength ?? descriptor.size); if (Number.isFinite(expectedSize)) { assert(details.size === expectedSize, `${slug}: size mismatch for ${descriptor.url}`); } if (descriptor.checksum) { assert(await sha256(file) === descriptor.checksum, `${slug}: checksum mismatch for ${descriptor.url}`); } } async function readGlbDrawCalls(url) { const file = path.join(projectRoot, "public", url.replace(/^\/+/, "")); const handle = await open(file, "r"); try { const header = Buffer.alloc(20); await handle.read(header, 0, header.length, 0); assert(header.toString("ascii", 0, 4) === "glTF", `${url}: invalid GLB magic.`); const jsonLength = header.readUInt32LE(12); const json = Buffer.alloc(jsonLength); await handle.read(json, 0, jsonLength, 20); const gltf = JSON.parse(json.toString("utf8").trimEnd()); return (gltf.meshes ?? []).reduce((total, mesh) => total + (mesh.primitives?.length ?? 0), 0); } finally { await handle.close(); } } const [campaign, registry, catalog, runReport, packageRegistry] = await Promise.all([ readJson("dungeon-pipeline/dungeon-campaign.json"), readJson("src/game/generated/fivePlayerDungeonImports.json"), readJson("src/game/generated/dungeonCampaignCatalog.json"), readJson("dungeon-pipeline/FIVE_PLAYER_IMPORT_RUN_REPORT.json"), readJson("src/game/generated/manastormAssetPackages.json"), ]); const failed = runReport.results.filter((result) => result.status === "error"); const blockedSlugs = new Set(failed.map((result) => result.slug)); assert(campaign.dungeons.length === 54, `Expected 54 campaign dungeons; found ${campaign.dungeons.length}.`); assert( [...blockedSlugs].every((slug) => allowedExternalBlockers.has(slug)), `Unexpected batch failures: ${[...blockedSlugs].join(", ")}`, ); assert( registry.imports.length === campaign.dungeons.length - blockedSlugs.size - 1, `Expected ${campaign.dungeons.length - blockedSlugs.size - 1} packaged native imports; found ${registry.imports.length}.`, ); assert(catalog.definitions.length === 54, `Expected 54 compiled definitions; found ${catalog.definitions.length}.`); const definitions = new Map(catalog.definitions.map((definition) => [definition.id, definition])); const coverage = new Map(catalog.coverage.map((entry) => [entry.dungeonId, entry])); const packagesByMap = new Map( packageRegistry.packages.map((assetPackage) => [Number(assetPackage.mapId), assetPackage]), ); const uniqueAssets = new Map(); let creatureEntries = 0; for (const imported of registry.imports) { assert(imported.status === "green", `${imported.slug}: native pack is not green.`); assert(!blockedSlugs.has(imported.slug), `${imported.slug}: blocked slug unexpectedly has a native pack.`); const definition = definitions.get(imported.slug); assert(definition, `${imported.slug}: compiled definition is missing.`); assert(coverage.get(imported.slug)?.source === "azerothcore", `${imported.slug}: compiled source is not authoritative.`); if (!creaturesOnly) { const canonicalEnvironment = packagesByMap.get(Number(imported.mapId)); assert( canonicalEnvironment, `${imported.slug}: optimized canonical environment is missing.`, ); for (const role of ["visual", "collision", "navigation"]) { assert( JSON.stringify(assetDescriptors(imported.environment[role]).map(shippingIdentity)) === JSON.stringify(assetDescriptors(canonicalEnvironment[role]).map(shippingIdentity)), `${imported.slug}: manifest ${role} does not use its canonical optimized package.`, ); assert( JSON.stringify(assetDescriptors(definition.assets[role]).map(shippingIdentity)) === JSON.stringify(assetDescriptors(canonicalEnvironment[role]).map(shippingIdentity)), `${imported.slug}: compiled ${role} does not use its canonical optimized package.`, ); } for (const descriptor of assetDescriptors(imported.environment)) { uniqueAssets.set(descriptor.url, [imported.slug, descriptor, "/assets/game/manastorm/"]); } } for (const descriptor of assetDescriptors(imported.creatureModels)) { uniqueAssets.set(descriptor.url, [imported.slug, descriptor, [ `/assets/game/dungeons/${imported.slug}/creatures/`, "/assets/game/creatures/shared/", ]]); } for (const [entry, model] of Object.entries(imported.creatureModels)) { creatureEntries += 1; assert(model.animationClips > 0, `${imported.slug}: entry ${entry} has no native animation clips.`); assert( model.meshOptimization?.policy === "wow-creature-material-join-v1", `${imported.slug}: entry ${entry} is missing the creature mesh optimization policy.`, ); assert( Number(model.meshOptimization.runtimeDrawCalls) >= 0 && Number(model.meshOptimization.sourceDrawCalls) >= 0 && Number(model.meshOptimization.runtimeDrawCalls) <= Number(model.meshOptimization.sourceDrawCalls), `${imported.slug}: entry ${entry} has invalid optimized draw-call counts.`, ); const entityModel = definition.entities?.[`entry-${entry}`]?.visual?.model; assert(entityModel?.url === model.url, `${imported.slug}: entry ${entry} did not compile with its native model.`); } } for (const slug of blockedSlugs) { assert(!registry.imports.some((entry) => entry.slug === slug), `${slug}: unexpected native pack.`); assert(coverage.get(slug)?.source === "procedural-fallback", `${slug}: expected documented fallback.`); } assert( coverage.get("wailing-caverns")?.source === "azerothcore", "wailing-caverns: golden fixture is not authoritative.", ); assert( runReport.results.filter((result) => result.status === "green").length === runReport.requested.length - blockedSlugs.size, "Batch green/error totals do not cover every requested dungeon.", ); assert( failed.length === blockedSlugs.size, `Unexpected batch failures: ${failed.map((result) => result.slug).join(", ")}`, ); for (const [slug, descriptor, expectedPrefix] of uniqueAssets.values()) { await verifyAsset(slug, descriptor, expectedPrefix); if (descriptor.meshOptimization) { const actualDrawCalls = await readGlbDrawCalls(descriptor.url); assert( actualDrawCalls === Number(descriptor.meshOptimization.runtimeDrawCalls), `${slug}: draw-call audit mismatch for ${descriptor.url}`, ); } } console.log(JSON.stringify({ status: blockedSlugs.size ? "green-with-documented-blockers" : "green", campaignDungeons: campaign.dungeons.length, authoritativeDungeons: [...coverage.values()].filter((entry) => entry.source === "azerothcore").length, packagedNativeImports: registry.imports.length, nativeCreatureEntries: creatureEntries, verifiedUniqueAssets: uniqueAssets.size, documentedBlockers: [...blockedSlugs], scope: creaturesOnly ? "creatures" : "all-assets", }, null, 2));