---
name: dayz-characters
description: >
  Import and author DayZ humanoids: infected/zombies, survivors and human
  NPCs on vanilla character bases. Covers retopo and rigging to
  OFP2_ManSkeleton, bind pose, weights, baked scale, UV/normal bake/textures,
  character LODs, config.cpp/model.cfg inheritance and in-game deformation.
  Use for custom zombie/infected/survivor/NPC, ragdoll, sandstorm PPE, "rig to OFP2_ManSkeleton",
  mesh explodes or stretches, spiky/pinched vertices, all weights on Pelvis,
  missing bone, giant infected, see-through body holes, foot sliding, wrong
  facing or animations not playing. Always invoke before rigging/configuring a
  DayZ humanoid. Compose with ai-3d-to-dayz, dayz-model-pipeline,
  dayz-animation-pipeline, dayz-pbo-build and dayz-test-ingame; use
  dayz-vehicles/dayz-aviation for those domains.
---

# DayZ Humanoid Character Modding

Custom humanoid characters in DayZ — infected (zombies), survivors, human NPCs — are built by
**reusing the vanilla character chain**, not from `EntityAI`. The hierarchy
(`DZ_LightAI → DayZInfected → ZombieBase → ZombieMaleBase / ZombieFemaleBase → variants`) gives you the
skeleton `OFP2_ManSkeleton`, every infected animation, and the AI driver **for free** through inherited
config — so the only thing you author is a new mesh. The real cost of the whole project is one step:
**rigging that custom mesh to `OFP2_ManSkeleton`**. Everything else is generic pipeline work delegated to
the skills below.

This skill is the character-specific layer on top of `dayz-model-pipeline` (generic geometry / LODs),
`dayz-texture-pipeline` (textures), `dayz-animation-pipeline` (the skeleton/anim deep-dive), and
`ai-3d-to-dayz` / `hunyuan3d-local` / `rodin3d-skill` (mesh generation). It is the humanoid counterpart
to `dayz-vehicles` (wheeled) and `dayz-aviation` (flight).

Scope: anything that walks on the player skeleton — infected, survivors, NPCs — generated by AI, modeled
from scratch, or imported from another game.
(desde 1.30 Exp: female military `ZmbF_SoldierNormal_Base` / `ZmbF_ArmyOfficer` / `ZmbF_eastSoldier_Base` and Nasdara `Zmb*_taki*` — `references/dayz-1-30-characters.md`.)

## PREFLIGHT

Gate real build work on `/dayz-preflight` (P:\ mounted, AddonBuilder, P:\Mods junction), per
`_shared/dayz-conventions.md`. Authoring config / rigging / baking offline does not need it; packing does.

**Before rigging a zombie/infected**: its canonical bind is the 95-bone `hermit_newbindpose.xob`, a SUBSET of
the player skeleton — NOT OFP2's full player bone list. Read THE CANONICAL-BIND INVARIANT (ZED bind) first.
(desde 1.30 Exp: that zed bind is unchanged; global bone indices are name-derived, not XML `index` values. [CHANGELOG] See `references/character-rigging.md`.)

## THE PIPELINE (who owns each stage)

| Stage | Owner | Notes |
|---|---|---|
| Generate / model the mesh | `ai-3d-to-dayz`, `hunyuan3d-local`, `rodin3d-skill` | multi-view + A/T-pose helps the rig; see CASE LOG |
| Retopo to low-poly manifold | `ai-3d-to-dayz` / `dayz-model-pipeline` | AI retopo leaves non-manifold — clean it |
| **RIG to OFP2_ManSkeleton** | **THIS skill** | the expensive, character-specific step → `references/character-rigging.md` |
| UV-unwrap + normal/AO bake (high→low→`_nohq`) | **THIS skill** + `dayz-texture-pipeline` | character-specific gotchas (conform misalignment, AI-high normals) → `references/character-uv-bake.md` |
| Texture `_co` / `_nohq` / `_smdi` | `dayz-texture-pipeline` | SubstanceToArma preset |
| Character LODs (Visual / Geometry / Fire / Memory / View) | THIS skill + `dayz-model-pipeline` | see CHARACTER LODs |
| config.cpp inheritance + model.cfg | **THIS skill** | inherit `ZombieMaleBase` → skeleton+anims+AI free |
| PBO / binarize | `dayz-pbo-build` | |
| In-game test | `dayz-test-ingame` (+ `dayz-mcp-verify`) | anims / foot-slide / collision / hit / spawn |

## RIGGING TO OFP2_ManSkeleton (the core — read `references/character-rigging.md`)

The single hardest step. Summary of the verified workflow ([LFInfectedBig ✓] 2026-06-24):

- **Skeleton** = `OFP2_ManSkeleton` (player + infected share it). The mesh MUST use the exact bone /
  selection names. In DayZ a "bone" in `model.cfg` is a **named vertex selection** weighted per-vertex;
  there is no shipped armature — `model.cfg` defines the skeleton, the `.p3d` carries selections+weights.
- **Armature source** = the official rig FBX `animation_rig_character.fbx` from
  [BohemiaInteractive/DayZ-Misc](https://github.com/BohemiaInteractive/DayZ-Misc) → "Rig and Animations".
  Blender-native FBX: armature with **114 bones (exact OFP2_ManSkeleton names)** + a weighted reference
  body `Male_body` in the **canonical bind A-pose**. Units are **centimetres**. Helpers (`*_Dummy`,
  `Weapon_*`, `EntityPosition`) import as **EMPTIES**, not bones → ignore them for body deform.
- **GOTCHA that silently ruins the bind**: before Blender bone-heat auto-weights, you MUST bake the scale
  into the armature (`transform_apply(scale=True)`). Scaling only the object/parent runs bone-heat at the
  rig's native scale against your differently-scaled mesh → **every vertex collapses onto `Pelvis`** and
  the rest of the bones get zero verts. Symptom: posing any limb does nothing.
- **Bind pose**: the mesh MUST ship in the canonical A-pose (anims are relative to that rest). If your
  mesh's limbs sit at a different angle, conform them to the rig (see reference) — a per-region rotation
  masked by the auto-weight value gives a crease-free shoulder.
- **Cleanup**: limit total influences to **≤4 per vertex** + normalize; target 0 zero-weight verts
  (zero-weight → pinched/spiky verts in-game). Disable deform on face/finger/eye bones for a body rig.
- **Blender deform-check = gross-motion sanity ONLY** (does a limb move at all; cross-midline weight
  bleed). It is a FALSE GATE for real deform — the engine skins against the real `OFP2_ManSkeleton`,
  not your rig-FBX armature (see the FALSE GATE section below). The deform gate is in-game/Buldozer.
  Bone-name mismatch → RPT logs `Bone X doesn't exist in skeleton OFP2_ManSkeleton` and that bone
  silently does not animate; a stray unnamed selection not in `skeletonBones[]` makes the mesh explode.

Bone catalog by zone lives in `dayz-animation-pipeline` → `references/player-skeleton.md`.

## THE CANONICAL-BIND INVARIANT — why a rig that looks fine at rest deforms under anims

[✓ in-game, LFInfectedBig S7 2026-06-24] The engine skins the mesh against the **canonical
`OFP2_ManSkeleton` bind** (the shared rig's bone positions), NOT a bind derived from your mesh. So the
inherited vanilla anims deform correctly only if your mesh matches that canonical bind in **three** ways:

- **Scale** — at ~vanilla size (the rig's `Male_body` ≈ 1.8 m). A mesh baked oversize (LFInfectedBig was
  2.277 m ≈ 1.27×) is bound against the 1.8 m canonical positions → limbs swing from the wrong pivots →
  gross deform under anims (not just foot-slide). Rescaling to ~1.8 m removed the catastrophic deform.
- **Orientation and handedness** — facing the same way as the rig, and not mirrored. A 180°-about-vertical
  error makes the character **face / walk backward** AND deform; a mirror makes limbs fling (next section).
  The `(x,z,−y)` transform shipped backward; `(−x,z,y)` turned it round but mirrored it. Both are det +1.
  The map that gets facing and side right is the det −1 `(x,z,y)` of dayz-model-pipeline Rule 12
  (corrected 2026-10-01; derivation in `references/character-rigging.md §6`).
- **Proportions / joint positions** — the mesh's anatomical elbow/knee/shoulder/wrist must sit at the
  canonical bone positions. An AI mesh has its OWN proportions; a UNIFORM-scale fit (`character-rigging.md
  §1`) preserves canonical proportions but cannot match a differently-proportioned mesh → bones land off the
  anatomy → limbs **overextend / stretch** under anims. Needs a **proportion conform** (§3), not just the
  arm-angle conform.

**The tell**: *fine at rest, breaks only when animating.* At the bind pose the skinned output equals the rest
mesh regardless of weights, so "looks right standing" proves nothing about the bind — only an anim (or a
deform-test posing real bone transforms, `character-rigging.md §5`) exposes a scale/orientation/proportion mismatch.

**Diagnostic** (offline, no in-game): per bone, distance from the bone head to the weighted centroid of its
verts; large distances (LFInfectedBig: LeftUpLeg 0.33 m vs 0.22 bone-len, hands 0.20 vs 0.06) = the bone
doesn't sit on the anatomy it drives → that limb will deform. (`_export/diag_bind_mismatch.py`.)

py3d transmits the per-vertex weights faithfully (the cross-side fix and the S7 fixes all propagated in-game)
→ Object Builder is not required for the export encoding itself.

### ⚠️ A ZED'S canonical bind is `hermit_newbindpose.xob` (95-bone SUBSET), NOT the player skeleton [✓ verified vanilla 2026-06-28]

Infected do NOT skin against the player rig. `ZombieBase` (and thus `ZombieMaleBase`, which your custom
infected inherits) carries an `enfanimsys` block — verified `DZ\characters\zombies\config.cpp:71-77`:
`graphname=…\infected\infected_main\infected.agr`, `defaultinstance=…infected_main.asi`,
`skeletonName="hermit_newbindpose.xob"`, `meshObject="…\z_hermit_m.xob"`. **`hermit_newbindpose.xob` is the
zed's bind pose, and it is a reduced subset.** Counting `<bone>` in `DZ\anims\cfg\skeletons.anim.xml`
(method: grep -c): **`hermit_newbindpose.xob` = 95 bones; `player_testing.xob` = 172 bones.** The 77-bone delta
the zed OMITS = the **entire face rig** (~40 `face_*`, `eyeleft/right`, lids, lips, tongue), the **finger**
bones (`pinky`/`ring`), **IK helpers** (`*handik`, `*forearmdirection*`, `righthandik_helper`), and all
**weapon** bones (`weapon_bone_01..06`, `magazine`, `trigger`, `bolt`, holsters, cameras).

- **Implication for a custom infected**: if your mesh carries weights on bones the zed bind lacks (face,
  fingers, IK), the engine resolves them wrong at runtime → deform, **including at idle** (the infected graph's
  default pose ≠ your A-pose). Conform/weight the mesh to the 95-bone `hermit_newbindpose` set, **or** override
  `enfanimsys` `skeletonName`/`meshObject` to `player_testing.xob` to restore the missing bones (the community
  fix for headgear/face stretch on zeds — Hoxdolum, DayZ Modders Discord 2024, mechanism confirmed vanilla).
- **Three bone catalogs — do NOT conflate**: the Enfusion `enfanimsys` `.xob` bind pose (zed 95 / player 172)
  is a DIFFERENT layer from the RV `model.cfg` `OFP2_ManSkeleton` (vanilla zombie ~159 bones, below). "Match
  the canonical bind" means the `.xob` the engine actually binds (`hermit_newbindpose`), not just the model.cfg
  skeleton. A mesh weighted to OFP2's full player bone list can still deform on a zed for this reason alone.
(desde 1.30 Exp: `infected.agr` remains the workspace index but is now Enfusion `AnimSrcGraph` text listing `.agf` subgraphs. Re-verified `enfanimsys` at `exp/characters_zombies/DZ/characters/zombies/config.cpp:50-57` (digest/skill `:71-77` was DeRap-shifted). Custom infected graphs: re-save the workspace in Workbench 1.30. Bone-index rule: `references/character-rigging.md`.)

### ⚠️⚠️ The Blender deform-test is a FALSE GATE — only in-game (or Buldozer) validates deform [LFInfectedBig S8 2026-06-24]

**The single most expensive lesson of this project.** Posing your imported rig-FBX armature in Blender and
measuring edge-stretch does NOT reproduce the engine's deform. S8 drove the Blender edge-stretch from **34× →
6×** across THREE rebuilds (re-weight, TPS conform, depth-preserving conform) while the in-game deform stayed
**unchanged and broken** (user: "sigue igual" — weird at idle AND animating) — a **3× false green**. Do NOT
trust the Blender armature pose-test as the deform gate; it cost ~12 offline iterations that produced zero
in-game improvement.

- **Why it lies**: the engine skins against the REAL `OFP2_ManSkeleton` from `DZ_Characters` (its bind /
  inverse-bind matrices + the skeleton your `model.cfg` declares), NOT the rig-FBX armature you posed in
  Blender. If your `model.cfg` skeleton hierarchy or the mesh's authored rest pose diverges from vanilla even
  slightly, the engine deforms wrong no matter how clean the Blender pose-test renders.
- **"Weird at REST / idle too" (not only walking) ⇒ bind / skeleton mismatch, NOT weights.** At the true bind
  pose LBS returns the rest mesh regardless of weights; if it looks wrong while barely moving, the engine's
  bind ≠ your mesh (model.cfg skeleton wrong, or rest pose ≠ engine bind). Re-weighting will NOT fix it.
- **Ground truth = a debinarized VANILLA character** (`DZ\characters\zombies\*.p3d` → external ODOL→MLOD converter).
  Its mesh IS authored in the exact engine bind pose, with the exact selection structure and a model.cfg that
  the engine accepts. **Copy its model.cfg skeleton verbatim** and conform/weight your mesh against ITS bind —
  do NOT hand-author a 114-bone hierarchy and assume it matches OFP2 (it didn't, here).
  (Scope: the debinarized vanilla is reliable for skeleton, named selections, model.cfg wiring and
  weights — NOT for visual-LOD vertex POSITIONS, which ODOL→MLOD conversion corrupts for character bodies;
  for positions use the `Male_body` of the rig FBX — see the addendum below / SP-034.)
- **Valid deform gates, in order**: (1) in-game spawn — the real one; (2) Buldozer (BI model viewer, real
  engine skinning). The Blender armature pose-test is good only for gross "does a limb move at all" sanity.
- **What DID hold up offline** (so the offline work isn't all waste): the winding gate `check_dayz_winding.py`
  (it held for the winding of the det +1 builds of that time, which in game turn out lit inverted; it fails
  a Rule 12 export that renders correctly, see OFFLINE GATE below and `references/character-rigging.md`
  Stage B), scale (f from
  armature span), orientation (T2), and **always LOOK at
  the REST SIDE/depth render** — a TPS conform to near-coplanar bone-midpoint targets flattened the mesh to
  paper (2D in-game); the flat side-view was in the render folder a whole build cycle before it was noticed.

### ⚠️ The mesh can be left/right MIRRORED vs the canonical skeleton — anims fling a limb back/up [LFInfectedBig S9 2026-06-25]

**The real root cause behind ~8 sessions of "arm bind" pain.** After making `model.cfg`'s `OFP2_ManSkeleton`
byte-match a vanilla zombie's (159 bones, `isDiscrete=0`, exact parents/order — which restored the leg
`leftupleg→propthighleft→leftleg` chain), the legs walked but the arms still flung back/up, equal at idle and
animating. The rest arms were a sane A-pose, so it was not pose/proportion — the whole mesh was **reflected in
X relative to the skeleton's left/right convention**: a `left*` selection's geometry sat where the canonical
`right*` bone lives, so the inherited anim drove it about a pivot ~2× a limb-width away → big lever → the limb
swings off even under tiny idle motion. AI/GLB meshes that went through a reflection (then had winding reversed
to render right-side-out) are prone to this, and a 180°-about-Y orientation fix can leave a residual reflection.
*Root cause, found 2026-10-01:* the reflection was the export map itself. Blender is right-handed and DayZ
left-handed, so every det +1 map (`(x,z,−y)`, `(−x,z,y)`) mirrors the mesh, and the face reversal it then
needs hides the mirror (dayz-model-pipeline Rule 12; `references/character-rigging.md §6`).

**The test (offline, decisive):** debinarize a vanilla male (external ODOL→MLOD converter), read the embedded
`Skeleton` bones + the visual LOD's per-vertex `vertex_bone_ref`, and compute each bone's vert-X relative to
the spine center. Vanilla puts `left*` on **+X**, `right*` on **−X**, consistent across shoulder/hip/arm (use
large-n bones; pelvis/hand centroids are noisy). Opposite side ⇒ mirrored. The conversion adds only
`boundingCenter` and reverses winding — it does NOT negate an axis — so ODOL-X == model-X and the comparison is
valid. (Gotcha: vanilla ODOL `named_selections` carry bone NAMES but EMPTY `selected_vertices`; real membership
is `vertex_bone_ref` pairs → `sub_skeletons_to_skeleton` → skeleton bone index.)

**The fix (two options; prefer the second):**
- **Swap L↔R selection names** in every LOD (`leftarm`↔`rightarm`, all sub-bones + fingers + the `l*`/`r*`
  memory points). Geometry/winding/normals stay byte-identical (confirmed-good rendering is untouched); only
  which bone each side binds to changes. Lowest risk, reversible. Leaves the mesh visually mirrored — fine for a
  symmetric humanoid; an off-center detail (a chest hole) lands on the mirror side (cosmetic).
- **Un-reflect the geometry**: negate X on all verts + reverse winding (and drop the swap). Makes the mesh
  properly handed. Bigger change (re-verify the winding gate + facing) but it can also clear a reflection side
  effect: single-sided faces culled from grazing angles the upright anim never reaches — e.g. a ragdoll corpse
  going invisible from one angle. Swap alone leaves the reflection, so that culling can persist; un-reflect
  fixes both at once. On a `(−x,z,y)` export with reversed faces, un-reflecting gives the Rule 12 positions
  and face order: `(x,z,y)` with the faces back in Blender order (Rule 12 also negates the normals). When the
  Blender source exists, re-export it that way instead of patching the p3d. The Rule 12 export this leads
  to was checked in game on a character (2026-10-02, `references/character-rigging.md §6`); the patch was not.

## UV + NORMAL/AO BAKE (read `references/character-uv-bake.md`)

Unwrapping the low and baking the high-poly detail into a tangent `_nohq` + AO is generic texture work,
but a rigged character adds gotchas that each cost an iteration ([LFInfectedBig ✓] 2026-06-25):

- **Bake from a PRE-conform proxy, not the conformed low.** The rig conforms the limbs (`character-rigging.md
  §3`); the high was never conformed → after rigging `low.dims ≠ high.dims` in those regions and a direct
  high→low bake is garbage there. Bake onto a proxy = shipped low's topology+UV with the pre-conform retopo
  positions (which align with the high); a tangent normal is pose-invariant so it applies to the conformed
  shipped low. Verify `proxy.dims == high.dims` first.
- **Do NOT `normals_make_consistent` the AI high-poly** (non-watertight → flips shells inward → black
  blotches). Use its original normals + `shade_smooth`.
- **Pre-fill the bake image with neutral `(0.5,0.5,1.0)` and `use_clear=False`** so misses stay flat, not
  black (a `(0,0,0)` texel decodes to an inward normal → renders black).
- **AO**: hide every mesh except the target (the coincident high self-occludes → dark AO) AND clear custom
  split normals + `normals_make_consistent` on the target.
- `_nohq` is DirectX **Y-** → green-invert the OpenGL bake (`dayz-texture-pipeline`). Triangulate before
  baking and ship triangulated so bake tangents == in-game tangents.
- Internal decorative geometry (ribcage): bone-heat fails on thin self-intersecting meshes → weight
  procedurally by height to the spine chain (or DATA_TRANSFER from the body, but strip arm/shoulder leakage).

The gate is a **lit preview of the low+normal vs the high**, plus numeric checks (UV stretch spread, %
black pixels = 0, AO surface mean ~200). Render and look (`blender-visual-review`).

## SCALING — runtime is broken, bake it into the mesh

"Make it bigger/smaller" has no config or script path: `SetScale`/`GetScale` are broken on entities
([T140705]) and the collision box does not follow; the Object-Spawner `scale` is static-objects-only.
The only route is to **bake the size into the mesh** on the same `OFP2_ManSkeleton`. Geometry + collision
LODs scale together. **BUT the engine binds against the canonical ~1.8 m bind (see THE CANONICAL-BIND
INVARIANT), so baked oversize scale does more than foot-slide — it DEFORMS the mesh under anims**:
LFInfectedBig at ~1.27× deformed grossly; rescaling to ~1.8 m removed the catastrophic deform [✓ in-game S7].
Small departures (≤~1.1×) may pass with only foot-slide / IK drift; a genuinely BIGGER animated character
that still deforms cleanly is **unsolved here** — it would need a custom skeleton with a scaled bind, not
baked mesh scale. Verify foot-contact / melee reach / door clipping / AI pathing in-game; if limbs distort
under anims, the size is fighting the bind.

## CONFIG INHERITANCE BY CHARACTER TYPE

Inherit the right base and the skeleton + anims + AI come for free via the inherited `enfanimsys` block.
Minimal infected pattern (verified vanilla, full detail in `dayz-custom-infected.md` §1):

```cpp
class CfgVehicles {
    class ZombieMaleBase;
    class MyZ_Base: ZombieMaleBase { scope=0; model="\MyMod\infected\myz.p3d"; hiddenSelectionsMaterials[]={...}; };
    class MyZ: MyZ_Base { scope=2; hiddenSelectionsTextures[]={"MyMod\data\myz_co.paa"}; };
};
```

| Character type | Inherit | Gives | Notes |
|---|---|---|---|
| Infected (male) | `ZombieMaleBase` | OFP2_ManSkeleton + infected anim graph + AI | [recipe ✓] community rule (PvZmoD): inherit `ZombieBase`, never `AnimalBase` |
| Infected (female) | `ZombieFemaleBase` | same, female proportions | |
| Survivor / human NPC | `SurvivorBase` / player chain | OFP2_ManSkeleton + player anims | `[TBD-verify]` — not yet built here; verify the base + clothing/proxy slots vs vanilla |
| Infected (female military, 1.30) | `ZmbF_SoldierNormal_Base` | female soldier AI | `ZmbF_SoldierNormal`, `ZmbF_ArmyOfficer`; `ZmbF_eastSoldier_Base` config-inherits `ZmbF_usSoldier_normal_Base` (`zombies/config.cpp:10376`) |

**THE WALL**: only **one** mod that modifies player/creature animations can be loaded at a time — loading
two crashes the client/server (Enfusion limit). A character that ships *new* animations conflicts with
every other anim mod on the server. A pure reskin that **inherits** the vanilla `enfanimsys` does NOT hit
this wall. Warn the user whenever a plan ships custom character anims. (`dayz-animation-pipeline` anchor 3.)

## CHARACTER LODs

- **Visual**: the rendered mesh (with the hole/detail).
- **Geometry (collision)**: must be **closed and convex** ([BI Validating Geometries]). A holed/concave
  torso is not convex → either convex-decompose into numbered `ComponentXX`, or (simple) keep collision
  **solid** and ignore the visual hole. Collision need not match Visual. Mass ≥ 10.
- **FireGeometry**: solid unless you want shots-through. With solid collision, bullets crossing a visual
  hole still hit — a design choice.
- **Memory / View / LandContact**: per `dayz-model-pipeline`. Use DayZ LOD resolutions, not Arma 3
  (`_shared/dayz-conventions.md` LOD table — ViewGeo `6e15`, FireGeo `7e15`).

## QUICK TRIAGE

| Symptom | Likely cause | Where |
|---|---|---|
| Posing the rig moves nothing / mesh won't deform | armature scale not applied before bone-heat → weights collapsed to `Pelvis` | `references/character-rigging.md` GOTCHA |
| Whole mesh explodes in-game | a named selection not in `skeletonBones[]`, or wrong skeleton name | RIGGING; check `model.cfg` skeleton |
| Spiky / pinched vertices | zero-weight verts or a selection with no weight | cleanup: normalize, fill weights |
| `Bone X doesn't exist in skeleton OFP2_ManSkeleton` (RPT) | bone-name typo (casing/underscore) | bone catalog in dayz-animation-pipeline |
| Limbs lag / float during anims | mesh not in canonical bind A-pose, or baked-scale drift | RIGGING bind-pose; SCALING |
| Anims don't play at all | not inheriting `ZombieMaleBase`/`enfanimsys`, or new-anim wall | CONFIG INHERITANCE |
| Bigger character, collision wrong | scaled at runtime instead of baked | SCALING |
| **Character faces / walks BACKWARD in-game** | det +1 export `(x,z,−y)`: it sends the rig front (−Y) to +Z; the bind faces −Z | re-export with `(x,z,y)` (Rule 12), not `(−x,z,y)`, which turns it round but mirrors it. `references/character-rigging.md §6`; THE CANONICAL-BIND INVARIANT |
| **Limbs overextend / stretch under anims, fine at rest** | mesh scale or proportions ≠ canonical bind | THE CANONICAL-BIND INVARIANT; proportion conform `character-rigging.md §3` |
| **Deforms wrong in-game but the Blender deform-test looked clean** | the Blender armature ≠ engine skeleton — the pose-test is a FALSE GATE | THE CANONICAL-BIND INVARIANT (FALSE GATE); debinarize vanilla + gate in-game/Buldozer |
| **Looks wrong at REST / idle, not only walking** | bind / skeleton mismatch (model.cfg ≠ vanilla, or rest pose ≠ engine bind) — NOT weights | debinarize a vanilla zombie, copy its model.cfg skeleton + match its bind |
| **Skeleton now matches vanilla but a limb flings back/up (idle AND animating), rest pose is a sane A-pose** | mesh is left/right **mirrored** vs the canonical skeleton — `left*` geometry sits where the `right*` bone is | debinarize a vanilla male, compare bone vert-X to spine center (vanilla `left*`=+X); cause = a det +1 export map; fix = re-export with `(x,z,y)` (Rule 12) or un-reflect (negate X + reverse winding, same result); swapping L↔R selection names only relabels the mirror. See THE CANONICAL-BIND INVARIANT (MIRROR) |
| **Custom infected/zombie deforms even at idle; mesh was rigged to the full player skeleton** | a zed binds to the 95-bone `hermit_newbindpose.xob` SUBSET (no face/finger/IK bones), not the player rig | conform/weight to the 95-bone set OR override `enfanimsys skeletonName`. See THE CANONICAL-BIND INVARIANT (ZED bind) |
| **Corpse/ragdoll goes invisible from a specific angle** | single-sided faces of a reflected mesh culled at grazing angles the upright anim never reaches | un-reflect the mesh (negate X + reverse winding; with the Blender source, re-export with `(x,z,y)`, Rule 12); a pure L/R selection swap leaves the reflection so the culling persists |
| **Whole mesh renders flat / paper / 2D in-game** | a conform/warp collapsed depth (Y) — e.g. TPS to near-coplanar bone-midpoint targets | conform in X/Z only, preserve Y; ALWAYS check the REST SIDE render, not just front |
| **In-game diffuse = "camo" / black-grey facet noise, persists without `_nohq`** | corrupt high→low bake (mis-aligned high/low) — the noise is in the `_co` DIFFUSE, not the normal map | open and LOOK at the baked `_co` PNG; `references/character-uv-bake.md` pre-conform proxy |
| **Green/grey/brown "camo" patches that SURVIVE replacing the `_co` entirely (uniform `_co` too) AND disabling `_nohq`** | an ENVIRONMENT/terrain texture in an rvmat stage with `uvSource="tex"` projects that texture onto the body UV. [LFInfectedBig S10] `dz\data\data\env_land_co.paa` (a landscape photo = DayZ env map) was in Stage7 with `uvSource="tex"` → painted the landscape over the mesh; survived 6 `_co` iterations because the fix is the rvmat, not the diffuse | **Audit EVERY rvmat stage** — an env/macro/detail map must be `uvSource="none"` (sampled by reflection), never `"tex"`. `ImageToPAA in.paa out.png` to LOOK at each stage's texture. The `_co`+normal Blender preview HIDES rvmat stages = FALSE GATE. First prove the mesh is clean with the CLAY-TEST (render `.p3d` geometry, NO textures) |
| **Solid, but dark on the sunlit side and lit in the shade (inverted lighting)** | first suspect: shading normals stored OUTWARD, as in the det +1 recipe (LFInfectedBig as shipped) or a Rule 12 export with its normals re-negated (base shading measured in game 2026-10-02 on an untextured client); rule out the material and `_nohq` too | check the stored sign with dayz-p3d-audit's absolute check (agreement near 0 % = normals against the winding); if they point outward, store them inward: Rule 12 / `py3d.blender_to_dayz()` negates them |
| **Textures on the INTERIOR / model see-through from outside (inside-out)** | the faces are not in the MLOD inward order: a det +1 map without face reversal (LFInfectedBig S6), or a det −1 map with one. Blender/Three.js previews are double-sided and HIDE this | export with Rule 12: `(x,z,y)`, faces in Blender order, normals negated. Reversing every face is right only under a det +1 map, and that map mirrors the mesh. See `references/character-rigging.md §6` |

## OFFLINE GATE — run before EVERY PBO (catches the inside-out bug without an in-game cycle)

**Rule 12 conflict (measured 2026-10-01).** `check_dayz_winding.py` encodes the LFInfectedBig build: det +1
map, faces reversed, normals left OUTWARD, so `cross·normal < 0`. Any build that stores its normals inward
with the cross product inward fails it: a Rule 12 export, and also a det +1 build with negated normals. On
the three MLODs of the Rule 12 in-game probe it exits 1 on all three. That includes the one that renders
solid and reads correctly in game (`cross.normal_positive=1.00`, `normals_outward=0.00`) and the mirrored one
that renders solid (det +1, faces reversed, normals negated); its fix hints would turn either inside-out.
The determinant does not say which convention a build follows; the stored normals do. Until the script is
updated, gate an export with inward normals with dayz-p3d-audit "Absolute winding check": normal agreement
≈ 100 % and a negative signed volume by winding. On the probe that check passes both solid variants and
fails the inside-out one. Measured on a skinned character too (LFInfectedBig, in game 2026-10-02, chiral
check in `references/character-rigging.md`): the Rule 12 build the script fails is solid and lit like
vanilla; the builds it passes, normals stored outward, are lit inverted (base shading, untextured client),
and the shipped one is also
mirrored. Store the normals inward; the absolute check passes the Rule 12 build and fails both others. (claim: CLAIM-CHAR-NORMALS-INWARD-SKILL)

```
python references/check_dayz_winding.py <source_mlod.p3d>   # legacy outward-normal builds only (see above)
```
A double-sided preview never shows DayZ's single-sided culling, so an inside-out model (textures on the
interior) only surfaces in-game — losing a test cycle. For a build that stores its normals outward (the
LFInfectedBig recipe; the script's `normals_outward` info line reads that sign) this detector encodes the
in-game-confirmed rule (LFInfectedBig S6): a correct SOURCE-MLOD visual LOD has
`cross(v1-v0,v2-v0)·stored_normal < 0`. Run it after building the `.p3d` and before AddonBuilder; if it
FAILs on such a build, reverse the visual winding and rebuild. A FAIL on a build with inward normals says
nothing about the winding: use the absolute check above, never the face reversal. S6 confirmed the
winding only: an outward-normal build renders solid but lit inverted (2026-10-02). Re-export it with
Rule 12 (a det +1 build is also mirrored); a Rule 12 build with outward normals only needs its normal
pool negated.
Detail + why (incl. why NOT to compare against a debinarized vanilla) in `references/character-rigging.md §6`.


**SP-290 (measured 2026-08-17):** exit 0 is not "safe to ship." The gate fails only on relative
winding (`cross.normal_positive` / `cn<=0.30`). It **measures** global inversion — it prints
`normals_outward[info]=0.00` and `NOTE normals look INWARD too (separate lighting issue): negate stored normals`
— but does not fail for it. Three fixtures from one source (`Crate\models\Crate_Wooden.p3d`,
9 LODs), gate run as shipped: intact (A) → exit 0, `cross.normal_positive=0.00`; winding inverted,
784 faces (B) → exit 1, `cross.normal_positive=1.00` (the named case — the gate DOES catch it);
winding AND stored normals inverted, 784 faces + 488 normals (C) → exit 0 `PASS`. That C state is
internally coherent, globally inside-out — the silent Blender Z-up → Y-up ship (`DZ-R` Rule 13;
WallLamp, Crate_Wooden). Do not ship on PASS; read the `[info]` line. Required of
`references/check_dayz_winding.py`: (1) `normals_outward == 0` (or below a threshold) MUST fail,
not annotate — combined with inverted winding this is the state the gate exists to block, not "a
separate lighting issue" *(2026-10-01: implemented as `NORMALS_OUTWARD_MIN = 0.35`, but it contradicts the
MLOD convention measured since: shading normals stored inward, dayz-p3d-audit "Absolute winding check"
(engine verdict 2026-09-07) and Rule 12 (in-game 2026-10-01). It fails every correct Rule 12 export; see
the conflict note above)*; (2) three exit codes, not two — today `exit 1` is both "defect found"
and "invalid input". An ODOL `.p3d` (`a6\LMGs\M249\m249_new.p3d`) raises a bare `AssertionError`
and exits 1, indistinguishable from inside-out. Need: `0` PASS, `1` defect, `2` invalid/non-MLOD
with an explicit "this file is ODOL, debinarize first"; (3) keep the three fixtures next to the
check (`f0_fixtures/crate_{A_intacto,B_solo_winding,C_winding_y_normales}.p3d`, regenerable with
`f0_winding_v2.py`). Fixture C inverts stored normals to reach the internally-coherent
globally-inside-out state; it does not byte-reproduce an exporter that also mirrors coordinates.

## CITE-THEN-VERIFY

Character config and bone names are easy to half-remember. Before writing a class name, base, property or
named selection, grep vanilla (`P:\dz\characters\`, `DZ\characters\zombies\`) or the cited reference, and
keep provenance labels (`[LFInfectedBig ✓]`, `[recipe ✓]`, `[TBD-verify]`). The bone names MUST match
`OFP2_ManSkeleton` exactly — verify against the rig FBX or `player-skeleton.md`, never from memory.

## REFERENCES

- `references/character-rigging.md` — the full verified rig workflow (armature source, scale gotcha,
  auto-weights, conform-to-bind, cleanup, deform-test) with the Blender-headless script patterns.
- `references/character-uv-bake.md` — UV-unwrap + high→low normal/AO bake: the pre-conform-proxy alignment
  fix, AI-high normal handling, neutral-prefill misses, AO occluder isolation, internal-mesh weighting, with
  Blender-headless script patterns.
- `dayz-custom-infected.md` (vault `20_Knowledge`) — the infected recipe: config inheritance, scaling,
  the see-through hole, LODs, with primary-source citations. Deep config detail lives here.
- `dayz-animation-pipeline` → `references/player-skeleton.md` — the OFP2_ManSkeleton bone catalog by zone.
- `references/character-ragdoll.md` — 1.30 `human.ragdoll` (`RagdollDef`) on `SurvivorBase`.
- `references/dayz-1-30-characters.md` — 1.30 Exp humanoid delta (variants, agr/agf, PPE, skinning, surrender).

## CASE LOG (append one entry per real import)

### LFInfectedBig — giant infected with a see-through torso hole (2026-06-23 → ongoing)
First character imported. ~1.2× infected, pass-through chest hole (ribs+spine), reskin of `ZombieMaleBase`.
- **Mesh**: AI-generated (Rodin v2.5 multi-view + `TAPose` recovers an A-pose from arms-down refs) →
  scripted ribcage cavity → retopo 14k manifold (hole open) → high-poly 1M kept for the bake.
- **Rig** [✓ 2026-06-24]: auto-weights from the BI DayZ-Misc FBX; the **armature-scale-apply GOTCHA**
  cost an intermediate failure (all weights → Pelvis) before it was caught; arms were 28°/34° narrower
  than canonical → conformed via weight-masked rotation; cleanup to max-4 influences, 0 zero-weight;
  deform-test passed (6961/7000 verts move, smooth, cavity follows the torso).
- **UV + bake** [✓ 2026-06-25]: Smart UV + rotating pack (uniform density, bbox 96.8%, triangulated 14k).
  Normal high 1M→low baked via a **pre-conform proxy** (the S4 conform had opened the arms ~8 cm →
  low≠high); the rainbow/black-blotch/dark-AO failures all traced to alignment, AI-high `make_consistent`
  flips, black-cleared misses, and coincident-high AO self-occlusion — all fixed (`references/character-uv-bake.md`).
  Maps in the NEW UV. Ribcage decimated 95k→15.8k + spine-height weights (bone-heat failed on the thin
  geometry); deform-test dot 0.823. `pose_position` ships REST=bind; set `'POSE'` for the deform-test.
- **Export rig→`.p3d` + texture + LODs + config + PBO** [✓ 2026-06-25]: gate R15 closed. Export = **py3d
  direct** (now verified in §6, no longer `[TBD-verify]`): fractional per-vertex bone-weight selections
  (lowercase), transform `(x,z,−y)` det+1 DERIVED from the armature matrix, validated against a debinarized
  vanilla zombie (ground truth for selection names + Y-up frame + character LOD anatomy). *(2026-10-01: that
  derivation missed the handedness change; the map is `(x,z,y)`, Rule 12. See `references/character-rigging.md §6`.)* `_co` baked DIFFUSE
  high→pre-conform-proxy onto the new UV; Super rvmat (Stage4 AS `uvSource="tex"`, not "none"). 6 LODs:
  Geometry hull `class='man'` (not skinned), FireGeo skinned-to-bones + `dmgzone_spine` + `meatbones.rvmat`,
  ViewGeo, LandContact, Memory (points from bone positions). model.cfg = full OFP2_ManSkeleton hierarchy,
  self-contained. binarize + CfgConvert + AddonBuilder → 6.4 MB PBO, all offline-verified.
- **Inside-out winding fix** [2026-06-25, deployed + offline-verified; in-game re-confirm pending]: first
  in-game build rendered inside-out (textures on interior) — the AI/GLB+retopo source ships glTF-CCW winding
  that DayZ back-faces despite the det+1 transform. Fix = reverse every visual face. Built
  `references/check_dayz_winding.py` (FAILs the inside-out build, PASSes the fixed one) and wired it as a
  pre-PBO gate so this never costs an in-game cycle again. Corrected §6 (it wrongly said "no flip").
- **Scale + orientation fixed, residual = proportions** [✓ in-game S7 2026-06-24]: the deep investigation
  diagnosed the S6 breakage offline. Texture "camo" = a corrupt high→low bake (noise in the `_co` DIFFUSE,
  not `_nohq`; body UV was clean) → replaced with a clean procedural flesh. Deform had two systemic causes,
  both confirmed in-game: (a) **scale** — the 2.277 m (1.27×) mesh fought the canonical ~1.8 m bind → uniform
  rescale f=0.7901; (b) **orientation** — the `(x,z,−y)` transform was 180° about vertical → faced/walked
  backward → fix `(−x,z,y)` (also det +1: it fixed the facing and kept the mirror that S9 found).
  Both landed (faces forward, vanilla-sized, camo gone). py3d weights transmit
  fine (Object Builder not needed). See THE CANONICAL-BIND INVARIANT.
  (`_export/{rescale_1x.py,build_full_p3d_1x_t2.py,diag_bind_mismatch.py}`.)
- **S8 — re-rig attempt + the FALSE-GATE wall** [2026-06-24, deform still broken in-game]: tried to fix the
  proportion deform offline. Diagnosed the shipped deform: asymmetric auto-weights (LeftUpLeg 37 vs RightUpLeg
  166 verts) + 19% cross-midline, and `clean_cross_side` AMPLIFIED it to 21–34× (it keeps the dominant side
  per vert; on bled asymmetric weights it reassigns whole regions to the OPPOSITE limb). Replaced the bind
  with **envelope weighting** (distance-to-bone-segment + sidedness fade + a Pelvis→crotch column) — robust
  for the see-through cavity (data-transfer from a solid `Male_body` mis-weighted the tunnel interior → 100×
  spikes). A TPS proportion conform to bone-midpoint targets **flattened the mesh to 2D** (targets near-
  coplanar in Y); fixed with an **X/Z-only conform preserving Y**. Blender edge-stretch fell 34×→6× over 3
  rebuilds. **BUT all 3 in-game builds deformed the same** (user: weird at idle AND animating) → the Blender
  deform-test is a FALSE GATE (see THE CANONICAL-BIND INVARIANT). Root cause now scoped to a **bind/skeleton
  mismatch** (model.cfg skeleton or authored rest ≠ vanilla OFP2). Confirmed in-game still-good: scale,
  orientation, winding, 3D volume (X/Z conform). Reusable: envelope weighting, the `_conform/` diagnostics.
- **Next (decided with user)**: STOP iterating against the rig FBX. **Debinarize a vanilla zombie, copy its
  model.cfg skeleton + match its bind/selection structure**, re-author the custom mesh against THAT, gate on
  in-game/Buldozer only. Brief `LFInfectedBig_dev\reviews\2026-06-24-prompt-vanilla-zombie-reproduce.md`.
- Detail: `30_Sessions\2026-06-24-LFInfectedBig-s8-offline-gate-false-green.md`,
  `2026-06-24-LFInfectedBig-scale-orient-fixed.md`, `LFInfectedBig_dev\HANDOFF.md` (live state).

[T140705]: https://feedback.bistudio.com/T140705
[BI Validating Geometries]: https://community.bohemia.net/wiki/Validating_Geometries


## (added 2026-06-28) Custom character deforms ONLY under animation = proportion, and the vanilla-body escape

Case LFInfectedBig (AI zombie ~1.2x): correct shape at rest, limbs over-extend when animated
("Vietnam flashback" at shoulders/arms). Verified diagnosis chain:

1. **A ZED's bind is `hermit_newbindpose.xob` (95 bones), not the player's** — `enfanimsys` block of
   `ZombieBase` (DZ\characters\zombies\config.cpp:71-78). Quick triage: diff the mesh's WEIGHTED
   bones against those 95 (parse `DZ\anims\cfg\skeletons.anim.xml`). BUT **0% weight outside the 95
   does NOT mean the deform is solved**: LFInfectedBig had all 42 weighted bones inside → the deform
   was NOT missing bones nor Hoxdolum's facial fix, it was **mesh-vs-skeleton proportion** (bone
   heads 0.10-0.22 m off the anatomy). diff script pattern: see _judge\diff_zed_skeleton.py.

2. **When the mesh doesn't match the skeleton, conform/re-weight has a ceiling** — it attenuates the
   deform (~30-45%) but never removes it; the geometry stays far from the pivots. **Robust exit =
   Camino C: rebuild on a VANILLA well-rigged body** (zero deform by construction) and graft only the
   distinctive bits (torso hole, look/texture, baked scale). Use this when conform/re-weight stalls.

3. **To get an EDITABLE vanilla body with positions AND weights: use the `Male_body` of the rig FBX
   `animation_rig_character.fbx` (Blender), NOT the ODOL->MLOD converter.** The conversion (ODOL->MLOD)
   CORRUPTS visual-LOD positions of character bodies (non-human bbox, span ~2.5x height); the ODOL
   `vertex_bone_ref` carries the weights but the positions are garbage. The FBX gives correct human
   positions + clean vertex groups (verified: Male_body 7499v, bbox 1.4x0.28x1.8, 0 unweighted,
   <=4 infl). Extract: import_scene.fbx -> mesh whose name contains 'body' -> matrix_world@v.co +
   v.groups; bone heads from the armature (`bone.head_local`). See _judge\extract_fbx_rig.py.

4. **py3d: to READ selection weights from an MLOD use the constructor `py3d.P3D(open(p,'rb'))`, NOT
   `m=P3D(); m.read(f)`** — only the constructor populates `selection.points = {Point: weight}`. An
   old debinarized MLOD may keep selection NAMES but with `.points` empty (weights lost).

5. **Offline deform judge (canonical LBS) — useful but with a hard limit.** A hierarchical FK with
   bind = bone-head positions reproduces a mis-proportioned mesh's deform (validated against in-game)
   and, with a negative control of capsules centered on the bones, discriminates good/bad. BUT it
   assumes **identity-rotation bind**; it is NOT faithful to a rig with real bone orientations in the
   bind (a vanilla body) -> the FK chain explodes (edge-stretch 400x+). To validate a well-rigged
   body offline: FK with FULL bind matrices, or deform in Blender with the native armature (reconcile
   the bind-matrix after FBX import, which otherwise explodes). The faithful deform gate is in-game.
   Cross-ref: SP-030 (judge method + silhouette 2nd axis), SP-034 (converter gap), LL-169.

## (added 2026-07-14) Offline deform judge = hierarchical LBS with CANONICAL bind, not the Blender armature (SP-030)

The deform-under-animation of a mesh rigged to `OFP2_ManSkeleton` CAN be judged OFFLINE - no in-game build, no real RTM - with a CANONICAL hierarchical LBS: `v' = sum(wi * Gi * Bi^-1 * v)`, where the bind `Bi = T(head_i)` uses the REAL bone heads (the ones the engine uses: Memory-LOD pivots / a `bones_*.json` derived from the rig) and `Gi` is FK over the REAL model.cfg hierarchy (`skeletonBones[]`), rotating each bone about ITS head and propagating to children. This reproduces the real over-extension (bones far from the anatomy -> long arcs when rotated) that an in-game build takes ~hours to reveal.

This does NOT contradict the FALSE GATE section above: the Blender armature / Blender deform-test / single-pivot rotation ARE false gates (they re-bind to non-canonical positions). Two reliable gates and one not:
- FALSE: Blender armature / Blender deform-test / single-pivot rotation (e.g. `inv_hunch_test.py`).
- OK: hierarchical LBS with bind = canonical heads + model.cfg hierarchy.
- OK: **mandatory synthetic negative control** - a capsule dummy centered EXACTLY on each bone segment, weighted 100% to that bone. Under the same pose it must bend clean; if the judge also breaks it, the judge over-reports and is useless. Turns "reproduces the deform" into "discriminates good vs bad".

Second axis (from the head-to-head round): a conform judge needs TWO axes reported together - (1) reduced over-extension under anim (edge-stretch), (2) rest-pose silhouette preservation (mean radius of a region's verts to its dominant bone's head->tail axis, original vs conform). Optimizing only (1) lets silhouette distortion pass: in a blind A/B the user's eye caught an arm-thickness diff (orig 0.140 m -> A 0.124 vs B 0.113) that all three offline metrics missed.

CAVEAT (why this is doc-applied, not "verified"): this is an offline PREFILTER validated to DISCRIMINATE (negative control + the S8 false-green case), NOT quantitatively validated against the engine; the final fidelity gate is still the user's eye in-game, and the LFInfectedBig case that produced it is not yet in-game-confirmed. Reference impl: `LFInfectedBig\_judge\judge_lbs.py` + `judge_control.py` + `judge_compare.py` / `judge_metric.py`. Origin: 2026-06-28 deform gate.

## (added 2026-08-03, ArmorHneck, IN-GAME VERIFIED) WORN CLOTHING binds via skeleton name "DayzTemporarySkeleton" — NOT OFP2_ManSkeleton

**The single fact that makes custom worn clothing (ClothingTypes m/f) work.** A worn p3d whose
model.cfg declares `skeletonName="OFP2_ManSkeleton"` renders as a RIGID block anchored above the
player (floating ~1 m, rotated, not animating) — the engine treats it as a model with its own
skeleton and never re-binds it to the wearer. ALL vanilla worn clothing (verified: armbend_dynamic_m,
chainmail_m via ODOL `model_info.skeleton.name`) compiles with the skeleton named
**`DayzTemporarySkeleton`** — same 159 bones, same hierarchy/order as OFP2, only the NAME differs
(a placeholder the engine substitutes with the wearer's skeleton). Fix = rename the CfgSkeletons
class + every `skeletonName=` in the clothing model.cfg to `DayzTemporarySkeleton` and rebinarize.
The community 114-bone OFP2 template fails TWICE for clothing: wrong bone count AND wrong name.

Worn-clothing canonical frame (measured on chainmail_m/hoodie_m, anatomical components — a global
Z histogram is TOO COARSE and false-negatives a 180° flip): **-Z = chest/front** (hoodie front
drawstrings at Z -0.14..-0.07; chainmail pelvis/spine centroids Z -0.04/-0.08), +X = anatomical
left, +Y up, origin at feet. A mesh built facing +Z disperses into "exploded" rigid pieces when
worn (each plate transformed by its bone from a flipped frame). Fix = R_y(180°) (det=+1, do NOT
touch winding) + swap left*/right* selection PAIRS in every LOD.

Handedness of that fix (added 2026-10-01). Not touching the winding is right: R_y(180°) is a rotation.
The swap it needs shows that the received mesh was a mirror, though: it faced +Z with `left*` on +X.
ArmorHneck's received p3d was the det +1 map `(x, z, −y)` of the artist's Blender coordinates, and the
shipped one is `(−x, z, y)` of them (`ArmorHneck_dev\CLAUDE.md`, artist alignment 2026-08-04).
Rotation + swap fixes the binding and keeps the mirror, which symmetric plates hide; text, logos, buckles
and one-sided straps come out reversed. With the Blender source, re-export it with `(x, z, y)`, Rule 12,
and no swap. With only the p3d, the reflection depends on its state: the received `(x, z, −y)` p3d takes
`z → −z`; the shipped `(−x, z, y)` one takes `x → −x` with the L/R swap undone, as "un-reflect" in THE
CANONICAL-BIND INVARIANT (`z → −z` on it would face +Z again). Either one moves every point (memory points
included), reverses every face and reflects the normals the same way. Decide on an asymmetric feature in
game: chiral check in `dayz-clothing`, CANONICAL WORN FRAME.

Diagnosis ladder that isolated this (reusable, each step in-game-cheap):
1. Cross-matrix bisection: {armband class + vanilla worn p3d} vs {vanilla top class + custom p3d}
   — separates config-side from p3d-side in ONE config-only rebuild.
2. Compare `ODOL.model_info.skeleton.name` custom vs vanilla worn (the tell: vanilla has NO
   "ofp2_manskeleton" string anywhere; a compliant clothing ODOL says DayzTemporarySkeleton).
3. Structure parity checklist custom-vs-vanilla worn: skeleton 159 bones, per-vertex weight sums
   (vanilla = raw bytes summing 255), sub_skeletons palette, sections (tops use `camomale` alone;
   ONLY armbands carry camoMale_big_a/b + small_a/b complexion variants), empty Geometry LOD with
   autocenter=0 is the vanilla worn pattern.
4. The registration gate is a live spawn (`unknown_type` = config not loaded — also caught here:
   config.bin from CfgConvert did not register at all; pack clothing PBOs with config.cpp as TEXT).

Origin: ArmorHneck 2026-08-03 (third-party RAR, ~6 in-game cycles to isolate; dual research
Claude+Codex on debinarized vanilla). Full case: `ArmorHneck_dev\CLAUDE.md` + research workspace
REPORT.md (armorhneck_research_ws).

## DayZ 1.30 Exp (build 1.30.164014)

What changes: `SurvivorBase` binds `ragdoll = "DZ/characters/bodies/human.ragdoll"`; infected `.agr` is an Enfusion index over `.agf`; new female military + Nasdara infected classes; `MINDSTATE_COWER`; human `Skinning` drops `Lard`; sandstorm PPE via `DEF_DUST_PARTICLE_BREATH` / `DEF_DUST_PARTICLE_EYES`.
What breaks: 1.29 custom infected animation workspaces; `EmoteManager.ForceSurrenderState` (use `PlayerBase.SetSurrenderState` / `IsSurrendered`); recipes expecting human `Lard`.
Migration: keep inheriting `ZombieMaleBase`/`ZombieFemaleBase` (or a 1.30 female-soldier base); re-save infected `.aw` in Workbench 1.30; if you replace a survivor body, keep/override the `ragdoll` path; do not author correlative bone `index` values in `skeletons.anim.xml`.
Checklist: [ ] workspace re-saved [ ] ragdoll path on survivors [ ] no `Lard` in human `Skinning` [ ] PPE constants if the character wears masks/eyewear [ ] `MINDSTATE_COWER` if you patch infected AI.
Detail: `references/character-ragdoll.md`, `references/dayz-1-30-characters.md`, `references/character-rigging.md`.
