Update Healer Man server

This commit is contained in:
phenom committed 2026-10-06 22:25:34 -04:00
1 parent 6114a2295f
commit 69809f7228
593 files changed
+903643 -40774

No files matched your search

@@ -16,6 +16,8 @@ from pathlib import Path
import bpy
from mathutils import Vector
sys.path.insert(0, str(Path(__file__).resolve().parent))
from native_material_alpha import native_alpha_action, native_material_identity
COLLISION_CHUNK_TRIANGLE_LIMIT = 90_000
@@ -135,12 +137,14 @@ def image_alpha_role(image: bpy.types.Image | None) -> str:
return "cutout"
def preserve_material_alpha(material: bpy.types.Material) -> str:
image = material_image(material)
role = image_alpha_role(image)
if role == "opaque" or material.node_tree is None:
def apply_material_alpha_role(
material: bpy.types.Material,
image: bpy.types.Image | None,
role: str,
threshold: float = 0.5,
) -> str:
if role == "opaque" or image is None or material.node_tree is None:
return role
image_node = next(
node
for node in material.node_tree.nodes
@@ -167,32 +171,117 @@ def preserve_material_alpha(material: bpy.types.Material) -> str:
alpha_clip = material.node_tree.nodes.new("ShaderNodeMath")
alpha_clip.name = "RuneWaker Alpha Clip"
alpha_clip.operation = "GREATER_THAN"
alpha_clip.inputs[1].default_value = 0.5
alpha_clip.inputs[1].default_value = threshold
material.node_tree.links.new(alpha_output, alpha_clip.inputs[0])
material.node_tree.links.new(alpha_clip.outputs[0], alpha_input)
material.blend_method = "CLIP"
material.alpha_threshold = 0.5
material.alpha_threshold = threshold
return role
def deduplicate_materials(objects: list[bpy.types.Object]) -> dict[str, int]:
def preserve_material_alpha(
material: bpy.types.Material,
source_report: dict[str, object] | None,
) -> tuple[str, dict[str, object] | None, dict[str, object]]:
image = material_image(material)
role = image_alpha_role(image)
image_path = bpy.path.abspath(image.filepath) if image is not None else ""
identity = native_material_identity(material.name, image_path, source_report)
decision = native_alpha_action(identity, role)
action = str(decision.get("action", "unsupported"))
if action == "pixel-alpha-heuristic":
applied_role = apply_material_alpha_role(material, image, role)
decision = {
"action": applied_role,
"reason": decision.get("reason", "no-verified-native-material-match"),
}
elif action in {"blend", "mask", "additive"}:
applied_role = apply_material_alpha_role(
material,
image,
"cutout" if action == "mask" else "blend",
float(decision.get("threshold", 0.5)),
)
decision = {**decision, "applied": applied_role}
if identity is not None:
material["nativeSourceModel"] = identity["sourceModel"]
material["nativeSourceResourceSha256"] = identity["sourceResourceSha256"]
material["nativeMaterialIndex"] = identity["materialIndex"]
material["nativeSourceTexture"] = identity["sourceTexture"]
material["nativeAlphaPolicy"] = str(decision.get("reason", action))
lighting = identity.get("lightingType")
lighting_name = lighting.get("name") if isinstance(lighting, dict) else None
if isinstance(lighting_name, str) and lighting_name.strip():
material["nativeLightingType"] = lighting_name.strip().lower()
elif "nativeLightingType" in material:
del material["nativeLightingType"]
if action == "additive":
material["nativeBlendMode"] = "additive"
elif "nativeBlendMode" in material:
del material["nativeBlendMode"]
return role, identity, decision
def deduplicate_materials(
objects: list[bpy.types.Object],
source_report: dict[str, object] | None,
) -> dict[str, object]:
canonical: dict[str, bpy.types.Material] = {}
replacements = 0
native_alpha_rows: dict[int, dict[str, object]] = {}
for obj in objects:
for slot in obj.material_slots:
material = slot.material
if material is None:
continue
key = material_image_key(material)
alpha_role, identity, decision = preserve_material_alpha(material, source_report)
semantics_key = (
repr((identity["sourceModel"], identity["sourceResourceSha256"],
identity["materialIndex"], identity["sourceTexture"],
sorted(identity["alphaBlendParameters"].items())))
if identity is not None
else "pixel-alpha-heuristic:" + alpha_role
)
key = material_image_key(material) + "|" + semantics_key
if identity is not None:
native_alpha_rows[identity["materialIndex"]] = {
"material": identity["materialName"],
"materialIndex": identity["materialIndex"],
"sourceModel": identity["sourceModel"],
"sourceResourceSha256": identity["sourceResourceSha256"],
"sourceTexture": identity["sourceTexture"],
"stagedTexture": identity["stagedTexture"],
"materialType": identity["materialType"],
"lightingType": identity["lightingType"],
"alphaBlendParameters": identity["alphaBlendParameters"],
"sourceImageAlphaRole": alpha_role,
"conversionPolicy": decision,
}
if key in canonical:
if slot.material != canonical[key]:
slot.material = canonical[key]
replacements += 1
else:
canonical[key] = material
material["semanticRole"] = preserve_material_alpha(material)
material["semanticRole"] = decision.get("action", alpha_role)
material["sourceFormat"] = "RuneWaker DDS"
return {"unique": len(canonical), "replacements": replacements}
source_model = source_report.get("sourceModel") if isinstance(source_report, dict) else None
source_hash = source_report.get("sourceResourceSha256") if isinstance(source_report, dict) else None
native_alpha_materials = [native_alpha_rows[index] for index in sorted(native_alpha_rows)]
return {
"unique": len(canonical),
"replacements": replacements,
"nativeAlpha": {
"sourceModel": source_model,
"sourceResourceSha256": source_hash,
"matchedMaterialCount": len(native_alpha_materials),
"materials": native_alpha_materials,
"unsupportedPolicies": [
row for row in native_alpha_materials
if row["conversionPolicy"].get("action") == "unsupported"
],
},
}
def export_glb(
@@ -302,6 +391,12 @@ def render_topdown_preview(
def main() -> None:
source_obj, output_dir, slug, unit_scale = arguments()
output_dir.mkdir(parents=True, exist_ok=True)
source_report_file = source_obj.parent / "export-report.json"
source_report = (
json.loads(source_report_file.read_text(encoding="utf-8"))
if source_report_file.is_file()
else None
)
for stale in output_dir.glob(f"{slug}-collision*.glb"):
stale.unlink()
@@ -330,7 +425,7 @@ def main() -> None:
obj["sourceFormat"] = "RuneWaker ROS via OBJ bridge"
obj["sourceUnitScaleMeters"] = unit_scale
material_stats = deduplicate_materials(visual)
material_stats = deduplicate_materials(visual, source_report)
bounds_min, bounds_max = three_bounds(visual)
source_triangles = sum(triangles(obj) for obj in visual)
@@ -364,12 +459,6 @@ def main() -> None:
blend_file = output_dir / f"{slug}.source.blend"
bpy.ops.wm.save_as_mainfile(filepath=str(blend_file))
source_report_file = source_obj.parent / "export-report.json"
source_report = (
json.loads(source_report_file.read_text(encoding="utf-8"))
if source_report_file.is_file()
else None
)
report = {
"schemaVersion": 1,
"dungeonId": slug,
@@ -379,6 +468,7 @@ def main() -> None:
"obj": source_obj.name,
"report": source_report_file.name if source_report is not None else None,
"model": source_report.get("sourceModel") if source_report else None,
"resourceSha256": source_report.get("sourceResourceSha256") if source_report else None,
},
"coordinateTransform": {
"nativeBridge": "three(x,y,z)=(runewaker.x,runewaker.y,-runewaker.z)",
@@ -0,0 +1,115 @@
"""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"}
@@ -0,0 +1,105 @@
import unittest
from native_material_alpha import native_alpha_action, native_material_identity
class NativeMaterialAlphaTest(unittest.TestCase):
def setUp(self):
self.report = {
"sourceModel": r"model\dungeon\the_origin\act_precipice_02.ros",
"sourceResourceSha256": "a" * 64,
"materials": [
{
"name": "runewaker_material_2",
"sourceTexture": r"texture\dungeon\the_origin\gate_waterfall",
"stagedTexture": "textures/32435b59_gate_waterfall.dds",
"materialType": {"value": 0, "name": "standard"},
"lightingType": {"value": 0, "name": "standard"},
"sourceTextureChannels": [
{
"id": 1,
"name": "diffuse",
"texture": r"texture\dungeon\the_origin\gate_waterfall",
},
],
"alphaBlendParameters": [
{"name": "alphaTestMode", "value": 0},
{"name": "alphaTestValue", "value": 192},
{"name": "alphaMode", "value": 0},
{"name": "translucent", "value": True},
{"name": "alpha", "value": 1},
{"name": "alphaModifier", "value": 1},
],
},
],
}
def test_exact_resource_material_and_diffuse_identity_are_required(self):
identity = native_material_identity(
"runewaker_material_2",
r"C:\staging\textures\32435b59_gate_waterfall.png",
self.report,
)
self.assertIsNotNone(identity)
self.assertEqual(identity["materialIndex"], 2)
self.assertEqual(identity["sourceResourceSha256"], "a" * 64)
self.assertEqual(identity["sourceTexture"], r"texture\dungeon\the_origin\gate_waterfall")
self.assertEqual(native_material_identity(
"runewaker_material_3",
r"C:\staging\textures\32435b59_gate_waterfall.png",
self.report,
), None, "a name-only match is insufficient")
self.assertEqual(native_material_identity(
"runewaker_material_2",
r"C:\staging\textures\other.png",
self.report,
), None, "the staged diffuse basename must match")
invalid_provenance = {**self.report, "sourceResourceSha256": "not-a-hash"}
self.assertEqual(native_material_identity(
"runewaker_material_2",
r"C:\staging\textures\32435b59_gate_waterfall.png",
invalid_provenance,
), None, "source resource hash is mandatory")
def test_native_modes_distinguish_normal_blend_additive_and_opaque(self):
identity = native_material_identity(
"runewaker_material_2",
r"C:\staging\textures\32435b59_gate_waterfall.png",
self.report,
)
self.assertEqual(native_alpha_action(identity, "blend")["action"], "blend")
self.assertEqual(native_alpha_action(identity, "opaque")["action"], "opaque")
additive_parameters = dict(identity["alphaBlendParameters"], alphaMode=1)
additive = {**identity, "alphaBlendParameters": additive_parameters}
self.assertEqual(
native_alpha_action(additive, "blend"),
{"action": "additive", "reason": "native-srcalpha-one-additive"},
)
self.assertEqual(native_alpha_action(additive, "opaque")["action"], "additive")
additive_not_translucent = {**identity, "alphaBlendParameters": dict(additive_parameters, translucent=False)}
self.assertEqual(native_alpha_action(additive_not_translucent, "blend")["action"], "unsupported")
additive_alpha_tested = {**identity, "alphaBlendParameters": dict(additive_parameters, alphaTestMode=1)}
self.assertEqual(native_alpha_action(additive_alpha_tested, "blend")["action"], "unsupported")
additive_scaled = {**identity, "alphaBlendParameters": dict(additive_parameters, alpha=0.5)}
self.assertEqual(native_alpha_action(additive_scaled, "blend")["action"], "unsupported")
opaque_parameters = dict(identity["alphaBlendParameters"], translucent=False)
opaque_native = {**identity, "alphaBlendParameters": opaque_parameters}
self.assertEqual(native_alpha_action(opaque_native, "blend")["action"], "opaque")
def test_native_alpha_test_preserves_its_threshold(self):
identity = native_material_identity(
"runewaker_material_2",
r"C:\staging\textures\32435b59_gate_waterfall.png",
self.report,
)
parameters = dict(identity["alphaBlendParameters"], alphaTestMode=1, translucent=False, alphaTestValue=192)
decision = native_alpha_action({**identity, "alphaBlendParameters": parameters}, "cutout")
self.assertEqual(decision["action"], "mask")
self.assertAlmostEqual(decision["threshold"], 192 / 255)
if __name__ == "__main__":
unittest.main()