Files
healer-man/scripts/runewaker-pipeline/lib/native-alpha-cache-validation.mjs
T
2026-10-06 22:25:34 -04:00

99 lines
4.3 KiB
JavaScript

const SHA256 = /^[\da-f]{64}$/i;
const REQUIRED_ALPHA_PARAMETERS = ["alphaTestMode", "alphaMode", "translucent", "alpha", "alphaModifier"];
function normalizeResource(value) {
return String(value ?? "").replaceAll("\\", "/").replace(/^\/+/, "").toLowerCase();
}
function reportErrors(report, label, expectedResource, expectedSourceSha256) {
const errors = [];
if (!report || typeof report !== "object") return [`${label} report is missing`];
if (normalizeResource(report.sourceModel) !== normalizeResource(expectedResource)) {
errors.push(`${label} source model does not match the cached resource`);
}
if (!SHA256.test(String(report.sourceResourceSha256 ?? ""))
|| report.sourceResourceSha256.toLowerCase() !== expectedSourceSha256.toLowerCase()) {
errors.push(`${label} source resource SHA-256 is missing or stale`);
}
if (!Array.isArray(report.materials) || report.materials.length === 0) {
errors.push(`${label} native material rows are missing`);
return errors;
}
for (const material of report.materials) {
const identity = String(material?.name ?? "");
if (!/^runewaker_material_\d+$/i.test(identity)) {
errors.push(`${label} has a material row without a native material index`);
continue;
}
if (!Array.isArray(material.sourceTextureChannels)) {
errors.push(`${label} ${identity} has no source texture channel inventory`);
}
const parameters = new Set((material.alphaBlendParameters ?? [])
.filter((parameter) => parameter && typeof parameter.name === "string")
.map((parameter) => parameter.name));
for (const required of REQUIRED_ALPHA_PARAMETERS) {
if (!parameters.has(required)) {
errors.push(`${label} ${identity} lacks native alpha parameter ${required}`);
}
}
const alphaValues = Object.fromEntries((material.alphaBlendParameters ?? [])
.filter(parameter => parameter && typeof parameter.name === "string")
.map(parameter => [parameter.name, parameter.value]));
if (alphaValues.alphaTestMode === 1
&& (!Number.isInteger(alphaValues.alphaTestValue) || alphaValues.alphaTestValue < 0 || alphaValues.alphaTestValue > 255)) {
errors.push(`${label} ${identity} lacks a valid native alpha-test cutoff`);
}
if (material.sourceTexture) {
const diffuse = (material.sourceTextureChannels ?? [])
.filter((channel) => String(channel?.name ?? "").toLowerCase() === "diffuse")
.map((channel) => normalizeResource(channel.texture));
if (!diffuse.includes(normalizeResource(material.sourceTexture))) {
errors.push(`${label} ${identity} diffuse channel does not match its source texture`);
}
}
}
return errors;
}
/** Fail closed when a resource cache predates exact ROS material-alpha provenance. */
export function validateNativeAlphaResourceCache({
manifest,
rawReport,
preparedReport,
expectedResource,
expectedSourceSha256,
expectedToolchain,
}) {
const errors = [];
if (!manifest || typeof manifest !== "object") {
errors.push("cache manifest is missing");
} else {
if (normalizeResource(manifest.source?.resource) !== normalizeResource(expectedResource)) {
errors.push("cache manifest source resource does not match");
}
if (!SHA256.test(String(expectedSourceSha256 ?? ""))
|| manifest.source?.sha256?.toLowerCase() !== expectedSourceSha256.toLowerCase()) {
errors.push("cache manifest source SHA-256 does not match");
}
for (const name of ["converterSha256", "exporterSha256", "nativeAlphaPolicySha256"]) {
const expected = expectedToolchain?.[name];
const actual = manifest.toolchain?.[name];
if (!SHA256.test(String(expected ?? "")) || actual?.toLowerCase() !== expected.toLowerCase()) {
errors.push(`cache toolchain ${name} is missing or stale`);
}
}
}
errors.push(...reportErrors(rawReport, "raw", expectedResource, expectedSourceSha256));
errors.push(...reportErrors(preparedReport, "prepared", expectedResource, expectedSourceSha256));
if (rawReport && preparedReport
&& JSON.stringify(rawReport.materials) !== JSON.stringify(preparedReport.materials)) {
errors.push("prepared material rows differ from the raw native exporter report");
}
return {
valid: errors.length === 0,
errors,
nativeMaterialCount: Array.isArray(rawReport?.materials) ? rawReport.materials.length : 0,
};
}