#!/usr/bin/env node import { access, copyFile, mkdir, readFile, readdir, stat, writeFile } 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 shippingRoot = path.join(projectRoot, "public", "assets", "game", "dungeons"); const sharedRoot = path.join(projectRoot, "public", "assets", "game", "creatures", "shared"); const registryFile = path.join(projectRoot, "src", "game", "generated", "fivePlayerDungeonImports.json"); const exists = async (file) => access(file).then(() => true, () => false); const readJson = async (file) => JSON.parse(await readFile(file, "utf8")); const writeJson = (file, value) => writeFile(file, `${JSON.stringify(value, null, 2)}\n`); const records = []; for (const entry of await readdir(shippingRoot, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const manifestFile = path.join(shippingRoot, entry.name, "dungeon-pack.json"); if (!await exists(manifestFile)) continue; records.push({ manifestFile, manifest: await readJson(manifestFile) }); } const referencesByChecksum = new Map(); for (const { manifest } of records) { for (const model of Object.values(manifest.creatureModels ?? {})) { if (!model.checksum || !model.url) continue; const references = referencesByChecksum.get(model.checksum) ?? []; references.push(model); referencesByChecksum.set(model.checksum, references); } } await mkdir(sharedRoot, { recursive: true }); let duplicateGroups = 0; let relinkedModels = 0; let createdSharedFiles = 0; for (const [checksum, references] of referencesByChecksum) { const urls = [...new Set(references.map((model) => model.url))]; if (urls.length <= 1) continue; const sharedUrl = `/assets/game/creatures/shared/${checksum}.glb`; const sharedFile = path.join(projectRoot, "public", sharedUrl.replace(/^\/+/, "")); const preferredUrl = urls.find((url) => url === sharedUrl) ?? urls[0]; const sourceFile = path.join(projectRoot, "public", preferredUrl.replace(/^\/+/, "")); if (!await exists(sourceFile)) throw new Error(`Missing shared-creature source: ${preferredUrl}`); const sourceDetails = await stat(sourceFile); const expectedSize = Number(references[0].size ?? references[0].byteLength); if (Number.isFinite(expectedSize) && sourceDetails.size !== expectedSize) { throw new Error(`Size mismatch for shared-creature source: ${preferredUrl}`); } if (!await exists(sharedFile)) { await copyFile(sourceFile, sharedFile); createdSharedFiles += 1; } for (const model of references) { model.url = sharedUrl; relinkedModels += 1; } duplicateGroups += 1; } for (const { manifestFile, manifest } of records) await writeJson(manifestFile, manifest); const imports = records.map(({ manifest }) => manifest).sort((left, right) => ( left.slug.localeCompare(right.slug) )); await writeJson(registryFile, { schemaVersion: 1, generatedAt: "deterministic", imports, }); console.log(JSON.stringify({ status: "green", duplicateGroups, relinkedModels, createdSharedFiles, sharedRoot: path.relative(projectRoot, sharedRoot).replaceAll("\\", "/"), }, null, 2));