forked from phenom/healer-man
99 lines
4.3 KiB
JavaScript
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,
|
|
};
|
|
}
|