extends Object class_name RigRoles ## Canonical bone names -> this rig's actual bone indices. ## ## Pulled out of `ShooterPoseModifier._resolve` so the dance layer can use the ## same resolution instead of carrying a second copy of it. Two copies is how a ## rig ends up animating correctly under one modifier and not the other. ## ## Non-negotiable: never look a bone up by name. `tools/rig_map.py` resolves ## every rig to ROLES and writes them to the `.rig.json` sidecar, and this ## reads that. The name fallback below exists only for a model with no sidecar, ## and it must never be the first thing tried — four characters could not hold a ## gun because of exactly one hardcoded spelling. ## The library skeleton's spine, hips first. A rig that kept its own names maps ## onto this through the sidecar's `spine` chain. const SPINE := ["DEF-hips", "DEF-spine.001", "DEF-spine.002", "DEF-spine.003"] ## Everything either pose layer asks for. const CANONICAL := [ "DEF-hips", "DEF-spine.001", "DEF-spine.002", "DEF-spine.003", "DEF-neck", "DEF-head", "DEF-upper_arm.R", "DEF-forearm.R", "DEF-hand.R", "DEF-upper_arm.L", "DEF-forearm.L", "DEF-hand.L", "DEF-thigh.R", "DEF-shin.R", "DEF-thigh.L", "DEF-shin.L", "DEF-foot.R", "DEF-foot.L", ] ## Godot sanitises ':' to '_' while turning glTF nodes into scene names. The ## sidecar intentionally keeps the source bone spelling so Blender can verify ## it against the exported GLB, therefore every runtime consumer must resolve ## through this bridge instead of assuming those two representations match. static func find_imported_bone(skel: Skeleton3D, authored_name: String) -> int: if authored_name == "": return -1 var bone := skel.find_bone(authored_name) if bone < 0 and authored_name.contains(":"): bone = skel.find_bone(authored_name.replace(":", "_")) return bone ## Resolve `names` against a skeleton, given the sidecar's role table. ## ## The spine needs its own handling because a rig that kept its own skeleton ## names things differently AND has a different number of spine bones — Taila's ## hips are `DEF-spine` and her head is `DEF-spine.006`, and she has no bone with ## "neck" in its name at all. Unresolved, every lean and aim pitch silently did ## nothing. static func resolve(skel: Skeleton3D, roles: Dictionary, names: Array = CANONICAL) -> Dictionary: var alias := {} if not roles.is_empty(): var neck: String = roles.get("neck", "") var head: String = roles.get("head", "") var torso: Array = [] for n in roles.get("spine", []): if n != neck and n != head: torso.append(n) for i in mini(torso.size(), SPINE.size() - 1): alias[SPINE[i + 1]] = torso[i] var idx := {} for n in names: # Canonical names are the role keys with the DEF- prefix, so the limbs, # hips, neck and head all map straight through. var actual: String = alias.get(n, roles.get(String(n).trim_prefix("DEF-"), n)) var b := find_imported_bone(skel, actual) if b < 0: b = skel.find_bone(String(n)) idx[n] = b return idx