97 lines
4.2 KiB
JavaScript
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;
|
|
}
|