The runtime (this plugin) is a Humankind Asset Framework host: it loads not just ENC’s registry but any number of packs, so a modder augments their own units with a custom 3D model, texture, and sound without touching ENC’s files or code. You ship a config file plus your assets; the runtime discovers, merges, and reports.
This is the loader contract. For how to bake a model into the assets a pack references, see Factory-Manual.md.
A pack is one JSON file with the schema wrapper around the familiar models array:
{
"schemaVersion": 1,
"modId": "yourmod",
"dependsOn": [],
"loadAfter": ["enc"],
"overrides": [],
"models": [ /* the same model entries the Factory writes — see Factory-Manual.md */ ]
}
A copy-ready starting point is haf-pack.example.json.
| Key | Meaning |
|---|---|
schemaVersion |
The HAF schema version this file targets. Currently 1. Evolves additively — new keys are added, old files keep loading. |
modId |
Your pack’s unique id. Keep it stable; it’s how you’re named in the load report and how others depend on you. |
models |
Your model entries — identical to what the Factory bakes. Runtime-only entries (a retexture/tint/sound with no baked mesh) need no GUIDs. |
dependsOn |
modIds your pack requires. A missing dependency means your pack is skipped (loudly, in the log + report). Also orders you after them. |
loadAfter |
modIds your pack must load after (soft: an absent modId is ignored, your pack still loads). |
overrides |
explicit {modId, pawnDescription} replacements: your entry replaces that pack’s entry on that pawn. Declared = consensual; without it, the clash is a conflict and the first-loaded entry wins. |
Backward compatible: a legacy bare { "models": [...] } with no wrapper still loads — it just gets default metadata
(modId = the filename, schemaVersion = 0). ENC’s own haf_models.json is treated as the base pack enc.
Naming — framework vs packs (deliberate): everything FRAMEWORK-level is
haf_*(haf_packs/,haf_load_report.txt); everythinghaf_*(haf_models.json,haf_sounds/,haf_skins/) belongs to ENC the pack — the reference pack, branded like any pack should be. Your pack’s files carry your name (your folder, your modId); you never touch anhaf_*path. The framework identity is fully neutral since 2026-07-19:HumankindAssetFramework.dll, GUIDcommunity.humankind.haf, menu rootTools ▸ HAF.
BepInEx/config/haf_models.json (loaded first, as modId enc).Your pack, self-contained (recommended, since 2026-07-19): one directory in haf_packs/:
BepInEx/config/haf_packs/
mymod/
pack.json ← your registry (default modId = the folder name)
sounds/ ← your custom WAVs (soundFile / soundStartFile / soundStopFile)
skins/ ← your retexture PNGs (textureFile)
haf_packs/mymod.json — file-based assets then resolve from a sibling folder
haf_packs/mymod/ (sounds/, skins/), if present.Assets: baked mesh/skeleton/atlas resolve by Amplitude GUID, so they work from any mod’s bundle the game loads — the runtime doesn’t care which mod shipped them. File-based assets (custom WAVs, PNG skins) resolve relative to the owning pack first (
sounds//skins/in your pack folder), falling back to the legacy sharedhaf_sounds/andhaf_skins/folders — so old packs keep working, and a new pack ships as one self-contained directory that never collides with another mod’s filenames.
haf_packs/*.json (filename order).modIds — the first file keeps the id; later same-id packs are skipped (log + report).dependsOn validation — a pack whose dependency isn’t loaded is skipped (iterated: skipping one pack can
invalidate a pack that depended on it).dependsOn + loadAfter: with no declared constraints the
order is exactly the old base-first + filename order; a dependency cycle is broken loudly by falling back to
file order for its members.models are combined. A model’s identity is its pawnDescription (the physical pawn slot — two skins
can’t ride one pawn).{modId, pawnDescription} for the current owner replaces
that entry (logged + reported as an override, not a conflict).overrides if the replacement is intentional.Not built (deliberately): a patches concept — field-level modification of another pack’s entry, as opposed to
overrides’ whole-entry replacement. It would let compatibility packs tweak one knob without duplicating a full model
definition; queued until a real compatibility pack needs it, so its shape is driven by a real use case.
Every load writes BepInEx/config/haf_load_report.txt — the first thing to check after adding your pack:
HAF load report (regenerated every load)
packs=2 models=14 conflicts=0 overrides applied=0
[enc] schemaVersion=1 models=13 file=haf_models.json
[yourmod] schemaVersion=1 models=1 file=yourmod.json
loadAfter: enc
If your pack isn’t listed, it wasn’t discovered (wrong folder, a parse error — check the BepInEx log), or it was
skipped by resolution (duplicate modId, missing dependsOn — the RESOLUTION section says which and why). An
OVERRIDES APPLIED section lists declared replacements that took effect; a CONFLICTS section means two packs are
fighting over a pawn undeclared.