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, }; }