Files
healer-man/scripts/runewaker-pipeline/lib/native-water-build-utils.mjs
T
2026-10-06 22:25:34 -04:00

97 lines
4.2 KiB
JavaScript

import path from "node:path";
function parseCoordinate(value, label) {
let x;
let z;
if (Array.isArray(value) && value.length === 2) {
[x, z] = value;
} else if (value && typeof value === "object") {
({ x, z } = value);
} else if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 0xffffffff) {
x = value >>> 16;
z = value & 0xffff;
} else if (typeof value === "string") {
const normalized = value.trim().replace(/^\[/, "").replace(/\]$/, "");
const match = /^(?:chunk-x)?(-?\d+)(?:-z|,)(-?\d+)$/i.exec(normalized);
if (match) [, x, z] = match;
}
x = Number(x);
z = Number(z);
if (!Number.isInteger(x) || !Number.isInteger(z) || x < 0 || x > 0xffff || z < 0 || z > 0xffff) {
throw new Error(`${label} must identify non-negative uint16 chunk coordinates.`);
}
return { x, z, key: ((x << 16) | z) >>> 0 };
}
export function parseNativeWaterBuildArgs(argv) {
const options = {};
const valueOptions = new Set([
"recipe", "source-root", "wdb-report", "output-root", "ground-manifest", "audit-cache-root",
]);
for (let index = 0; index < argv.length; index += 1) {
const token = argv[index];
if (token === "--help" || token === "-h") return { help: true };
if (!token.startsWith("--")) throw new Error(`Unexpected positional argument: ${token}`);
const name = token.slice(2);
if (!valueOptions.has(name)) throw new Error(`Unknown option --${name}.`);
if (options[name] !== undefined) throw new Error(`Option --${name} may only be supplied once.`);
const value = argv[index + 1];
if (!value || value.startsWith("--")) throw new Error(`Option --${name} requires a value.`);
options[name] = value;
index += 1;
}
return { help: false, ...options };
}
export function chunkCoordinateFromManifestRow(row) {
if (row?.coordinate !== undefined) return parseCoordinate(row.coordinate, "Ground-manifest chunk coordinate");
if (row?.key !== undefined) return parseCoordinate(row.key, "Ground-manifest chunk key");
if (row?.id !== undefined) return parseCoordinate(row.id, "Ground-manifest chunk id");
throw new Error("Ground-manifest chunk row has no coordinate, key, or id.");
}
export function selectNativeWaterChunkKeys({ wdbChunkKeys, groundManifest, sourceWdbSha256 }) {
const available = new Set(wdbChunkKeys.map((key) => Number(key) >>> 0));
if (!groundManifest) return { selectedKeys: [...available].sort((a, b) => a - b), selection: { kind: "all-wdb-chunks" } };
const manifestSha = groundManifest.source?.wdbSha256 ?? groundManifest.sourceWdbSha256;
if (typeof manifestSha !== "string" || !/^[a-f\d]{64}$/i.test(manifestSha)) {
throw new Error("Ground manifest must include source.wdbSha256 or sourceWdbSha256 to bind it to its source WDB.");
}
if (manifestSha.toLowerCase() !== sourceWdbSha256.toLowerCase()) {
throw new Error("Ground manifest source WDB SHA-256 does not match the water source WDB.");
}
if (!Array.isArray(groundManifest.chunks) || groundManifest.chunks.length === 0) {
throw new Error("Ground manifest must contain a non-empty chunks array.");
}
const coordinates = groundManifest.chunks.map(chunkCoordinateFromManifestRow);
const selectedKeys = new Set();
for (const coordinate of coordinates) {
if (!available.has(coordinate.key)) {
throw new Error(`Ground-manifest chunk [${coordinate.x},${coordinate.z}] is absent from the source WDB chunk table.`);
}
if (selectedKeys.has(coordinate.key)) {
throw new Error(`Ground manifest repeats chunk [${coordinate.x},${coordinate.z}].`);
}
selectedKeys.add(coordinate.key);
}
return {
selectedKeys: [...selectedKeys].sort((a, b) => a - b),
selection: {
kind: "ground-manifest-chunks",
chunkCount: selectedKeys.size,
coordinates: coordinates.map(({ x, z }) => ({ x, z })),
},
};
}
export function ensureWithinProject(projectRoot, outputPath) {
const resolved = path.resolve(outputPath);
const relative = path.relative(path.resolve(projectRoot), resolved);
if (!relative || relative === ".") return resolved;
if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`)) {
throw new Error(`Output path must stay inside the project workspace: ${outputPath}`);
}
return resolved;
}