"""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"}