108 lines
5.9 KiB
JavaScript
108 lines
5.9 KiB
JavaScript
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));
|