forked from phenom/healer-man
116 lines
5.0 KiB
Python
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"}
|