Status: designed, not built. Scoped for the package-release phase. Raised by an external review (2026-08-02); the pack structure half already exists (see below). This note fixes what to validate, where the checks live, and the message format, so the eventual build is a straight implementation.
Today a HAF pack fails in two very different ways depending on the mistake:
modId, missing
dependsOn, ordering cycles, and same-pawn conflicts all produce plain-language warnings in the BepInEx log and the
haf_load_report.txt (implemented in the 07-14 / 07-19 multi-mod work). Bad input fails soft — never a crash.muzzleBone: "Turrret"), an unresolvable asset GUID, or a
missing texture/WAV path doesn’t warn — the feature just doesn’t happen. For someone authoring a pack on their own
machine, “the flash is in the wrong place and I don’t know why” is exactly the friction that stops third-party
adoption.The validator turns silent content failures into named, actionable messages before (or at) load.
Grounded in the fields a ModelEntry / district / prop / projectile entry actually references:
| Class | Fields | Check | Where it’s checkable |
|---|---|---|---|
| Asset GUIDs | skeleton, atlas, clip, moveClip/idleClip/… , district FxMesh, prop MeshCollection |
the GUID resolves to a loaded asset of the right kind | boot-time (needs the game’s asset DB) |
| Bone names | muzzleBone, turretBone, animateBones, hand-prop bone |
the named bone exists in the entry’s skeleton | editor (baker knows the model’s bones) and boot-time (against the loaded skeleton) |
| File paths | soundFile, soundStart/Stop/Idle/Attack/Death/Battle, PNG skins |
the file exists in the pack folder | editor and boot-time (plain file existence) |
| Target unit | pawnDescription |
matches a real game unit descriptor | editor (pawn dropdown) and boot-time (unit catalog) |
| Schema | field names, types, enums (materialMode), numeric ranges |
keys are known, types/enums valid, ranges sane | editor (pre-ship) |
Note the split: file/bone/pawn/schema checks are doable in the editor (before the pack ever ships — the best DX), while GUID resolution and bone-against-loaded-skeleton need the running game, so they belong to a boot-time pass on the end user’s machine.
Factor the rules into one pure ValidateEntry(entry, context) returning a list of (severity, message) — mirrors the
existing FormationOverridePatch.Validate(Entry) pattern and stays unit-testable (it’s pure logic over data + a small
lookup interface). Two thin callers:
## Pre-flight section to haf_load_report.txt plus a summary log line.One line per problem, always naming pack + entry + the specific fault + (where cheap) the valid options:
[Preflight] pack 'coolmod' entry 'PanzerIV' (pawn 'AttackHelicopter'):
bone 'Turrret' not found in skeleton — available: Root, Body, Turret, Barrel, Muzzle
texture 'skins/panzer.png' not found in pack folder
clip GUID 3f2a… did not resolve to a ClipCollection (was it baked?)
warning = the feature degrades but the pack still loads (today’s fail-soft behaviour, now explained);
error = the entry is unusable and skipped. Default to warning — never regress the “bad input never crashes” rule.[Preflight] pack 'coolmod': 2 warning(s), 0 error(s) — see haf_load_report.txt.check_schema_parity.sh — that’s a dev-side guard against ModelDef↔ModelEntry source drift across the
two repos. This validator is author-side, against a concrete pack’s data.