import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { inflateSync, constants } from "node:zlib"; import { readDataStore, parseTerrainChunk } from "./sand-vortex/extract-native-terrain.mjs"; import { decodeNativeWaterPayload } from "./lib/native-water-payload.mjs"; import { buildNativeWaterGeometry } from "./lib/native-water-geometry.mjs"; import { nativeWaterRenderPolicy } from "./lib/native-water-render-policy.mjs"; // Metadata-only staging: exact original BIN bytes (geometry, UVs and embedded // images) remain unchanged. Source split planes are recovered before any mesh // optimization reorders vertices. No runtime enum name is invented for raw 4. const projectRoot = process.cwd(); const output = path.resolve(process.argv.find(value => value.startsWith("--output="))?.slice(9) ?? "artifacts/playtest/rom-deep-review/water-screen-effects"); const sha = bytes => createHash("sha256").update(bytes).digest("hex"); const report = JSON.parse(await readFile("src/assets/game/dungeons/the-origin/native-water-report.json", "utf8")); const wdbPath = path.resolve(report.source.sourceRoot, report.source.wdb); const wdb = await readFile(wdbPath); assert.equal(sha(wdb), report.source.wdbSha256); const tables = readDataStore(wdb).tables; const payload = row => wdb.subarray(row.position, row.position + row.size); const descriptors = [...tables.get("water_descriptor").records].map(([key, row]) => ({ key, payload: payload(row) })); const layers = new Map(); for (const chunk of report.payloadDecode.nativeWaterChunks) { const compressed = payload(tables.get("chunks").records.get(String(chunk.keyUint32))); assert.equal(sha(compressed), chunk.compressedSha256); const terrain = parseTerrainChunk(compressed, chunk.key, { allowUnparsedWaterPayload: true }); const decompressed = inflateSync(compressed, { finishFlush: constants.Z_SYNC_FLUSH }); const decoded = decodeNativeWaterPayload(decompressed.subarray(decompressed.length - terrain.unconsumedBytes), { layerCount: terrain.waterLayerCount, descriptors, }); assert.equal(decoded.sourcePayloadSha256, chunk.waterPayloadSha256); const geometry = buildNativeWaterGeometry(decoded.layers, report.source.selectedPrimaryPlacement, report.source.metersPerSourceUnit); decoded.layers.forEach((layer, index) => { assert.equal(layer.descriptorJoin.status, "matched"); const descriptor = layer.descriptorJoin.descriptors[0]; layers.set(`${chunk.coordinate.x},${chunk.coordinate.z},${index}`, { sourceSplitPlaneHeight: geometry.layers[index].summary.sourceSplitPlaneHeight, descriptorSha256: descriptor.sha256, waterBodyGuid: descriptor.guid, ...nativeWaterRenderPolicy(descriptor), }); }); } function patchGlb(bytes) { assert.equal(bytes.readUInt32LE(0), 0x46546c67); const jsonLength = bytes.readUInt32LE(12); const json = JSON.parse(bytes.toString("utf8", 20, 20 + jsonLength)); const remainder = bytes.subarray(20 + jsonLength); let patched = 0; for (const mesh of json.meshes ?? []) { for (const primitive of mesh.primitives) { const layer = primitive.extras?.runewakerNativeWaterLayer; if (!layer) continue; const key = `${layer.sourceChunk.x},${layer.sourceChunk.z},${layer.sourceLayerIndex}`; const source = layers.get(key); assert.ok(source, `Missing native water layer ${key}`); assert.equal(layer.waterBodyGuid, source.waterBodyGuid); layer.sourceSplitPlaneHeight = source.sourceSplitPlaneHeight; const metadata = json.materials[primitive.material].extras?.runewakerWater; assert.ok(metadata, `Missing native water material ${key}`); assert.equal(metadata.nativeDescriptorSha256, source.descriptorSha256); Object.assign(metadata, nativeWaterRenderPolicy({ bodyType: source.bodyTypeRaw, effectType: metadata.effectTypeRaw, baseObject: { properties: [{ name: "waterDesc:full:enable", type: 2, value: Number(source.fullModelRequested) }] }, })); patched++; } } assert.equal(patched, layers.size); const rawJson = Buffer.from(JSON.stringify(json)); const padded = Buffer.alloc((rawJson.length + 3) & ~3, 0x20); rawJson.copy(padded); const header = Buffer.from(bytes.subarray(0, 20)); header.writeUInt32LE(20 + padded.length + remainder.length, 8); header.writeUInt32LE(padded.length, 12); return { bytes: Buffer.concat([header, padded, remainder]), binSha256: sha(remainder), patched }; } await mkdir(output, { recursive: true }); const files = [ ["canonical", "runewaker-export-work/the-origin/converted/the-origin-visual.glb"], ["optimized", "runewaker-export-work/the-origin/converted/the-origin-visual.optimized.glb"], ["shipping", "src/assets/game/dungeons/the-origin/the-origin-visual.glb"], ]; const proof = []; for (const [kind, file] of files) { const previous = await readFile(file); const patched = patchGlb(previous); const destination = path.join(output, `${kind}-the-origin-visual.glb`); await writeFile(destination, patched.bytes); const newJsonLength = patched.bytes.readUInt32LE(12); assert.equal(sha(patched.bytes.subarray(20 + newJsonLength)), patched.binSha256); proof.push({ kind, file, staged: path.relative(projectRoot, destination).replaceAll("\\", "/"), previousSha256: sha(previous), sha256: sha(patched.bytes), bytes: patched.bytes.length, binaryChunksUnchanged: true, binaryChunksSha256: patched.binSha256, layerCount: patched.patched }); } await writeFile(path.join(output, "proof.json"), `${JSON.stringify({ sourceWdbSha256: sha(wdb), sourceBranch: "BuildWaterBodyMaterial non-DIRECT + full enable BOOL", planeSource: "RuWorld_Water::BuildWaterLayerMesh first vertex transformed to source world height", uniqueSourceSplitHeights: [...new Set([...layers.values()].map(row => row.sourceSplitPlaneHeight))], layers: Object.fromEntries(layers), files: proof, }, null, 2)}\n`); console.log(JSON.stringify(proof, null, 2));