Files
2026-10-06 22:25:34 -04:00

116 lines
5.0 KiB
Python

"""Fail-closed matching and policy for native Runewaker material alpha flags."""
from __future__ import annotations
import os
import re
from pathlib import PurePosixPath
from typing import Any
_MATERIAL_NAME = re.compile(r"^runewaker_material_(\d+)$", re.IGNORECASE)
_SHA256 = re.compile(r"^[0-9a-f]{64}$", re.IGNORECASE)
def _resource_path(value: Any) -> str:
return str(value or "").replace("/", "\\").strip("\\").casefold()
def _image_basename(value: Any) -> str:
normalized = str(value or "").replace("\\", "/")
basename = normalized.rsplit("/", 1)[-1].casefold()
if basename.endswith(".dds"):
return basename[:-4] + ".png"
if not os.path.splitext(basename)[1]:
return basename + ".png"
return basename
def native_material_identity(
material_name: str,
image_path: str,
source_report: dict[str, Any] | None,
) -> dict[str, Any] | None:
"""Join one imported material only when source, native index and diffuse image agree."""
if not isinstance(source_report, dict):
return None
source_model = str(source_report.get("sourceModel") or "")
source_hash = str(source_report.get("sourceResourceSha256") or "")
if not source_model or not _SHA256.fullmatch(source_hash):
return None
match = _MATERIAL_NAME.fullmatch(str(material_name or ""))
if not match:
return None
material_index = int(match.group(1))
rows = [row for row in source_report.get("materials", [])
if isinstance(row, dict) and row.get("name") == material_name]
if len(rows) != 1:
return None
row = rows[0]
if _image_basename(image_path) != _image_basename(row.get("stagedTexture")):
return None
source_texture = _resource_path(row.get("sourceTexture"))
diffuse_textures = {
_resource_path(channel.get("texture"))
for channel in row.get("sourceTextureChannels", [])
if isinstance(channel, dict) and str(channel.get("name", "")).casefold() == "diffuse"
}
if not source_texture or source_texture not in diffuse_textures:
return None
parameters = {
str(parameter.get("name")): parameter.get("value")
for parameter in row.get("alphaBlendParameters", [])
if isinstance(parameter, dict) and parameter.get("name")
}
return {
"sourceModel": source_model,
"sourceResourceSha256": source_hash.casefold(),
"materialName": material_name,
"materialIndex": material_index,
"sourceTexture": source_texture,
"stagedTexture": _image_basename(row.get("stagedTexture")),
"materialType": row.get("materialType"),
"lightingType": row.get("lightingType"),
"alphaBlendParameters": parameters,
}
def native_alpha_action(identity: dict[str, Any] | None, image_alpha_role: str) -> dict[str, Any]:
"""Translate only proven, supported native modes; report unknown modes explicitly."""
if identity is None:
return {"action": "pixel-alpha-heuristic", "reason": "no-verified-native-material-match"}
parameters = identity.get("alphaBlendParameters", {})
required = ("alphaTestMode", "alphaMode", "translucent", "alpha", "alphaModifier")
if any(name not in parameters for name in required):
return {"action": "unsupported", "reason": "native-alpha-parameters-incomplete"}
try:
test_mode = int(parameters["alphaTestMode"])
alpha_mode = int(parameters["alphaMode"])
translucent = bool(parameters["translucent"])
alpha = float(parameters["alpha"])
alpha_modifier = float(parameters["alphaModifier"])
except (TypeError, ValueError, OverflowError):
return {"action": "unsupported", "reason": "native-alpha-parameters-invalid"}
if abs(alpha - 1.0) > 1e-6 or abs(alpha_modifier - 1.0) > 1e-6:
return {"action": "unsupported", "reason": "native-global-alpha-factor-not-one"}
if test_mode == 0 and alpha_mode == 0 and translucent:
return {
"action": "blend" if image_alpha_role != "opaque" else "opaque",
"reason": "native-normal-translucent-alpha" if image_alpha_role != "opaque"
else "native-normal-translucent-texture-alpha-opaque",
}
if test_mode == 0 and alpha_mode == 0 and not translucent:
return {"action": "opaque", "reason": "native-normal-opaque-material"}
if test_mode == 0 and alpha_mode == 1 and translucent:
return {"action": "additive", "reason": "native-srcalpha-one-additive"}
if test_mode == 1 and alpha_mode == 0 and not translucent:
try:
threshold = max(0, min(255, int(parameters["alphaTestValue"]))) / 255.0
except (KeyError, TypeError, ValueError, OverflowError):
return {"action": "unsupported", "reason": "native-alpha-test-threshold-missing"}
return {
"action": "mask" if image_alpha_role != "opaque" else "opaque",
"reason": "native-greater-alpha-test",
"threshold": threshold,
}
return {"action": "unsupported", "reason": "native-alpha-mode-combination-unsupported"}