HumankindAssetFramework

Formations — custom unit formations & pawn counts (the fifth data axis)

Change how many soldier models a unit fields and how they’re arranged on the world map, with zero baked assets — a runtime override driven by haf_formations.json. Link a PresentationUnitDefinition to a formation whose dummy count and layout you author in the Unity SDK, and the plugin injects it into the live database and repoints the unit at load. Fully reversible: delete the link and the unit is vanilla next launch.

Status: VERIFIED IN-GAME 2026-07-28 — 12-, 16-, 19- and 32-model units render correctly (all models on the hex, banner centered). The fix is count-agnostic; the vanilla 9/10 ceiling is gone. See The >9 story. Model scale is built with two selectable modes — solved for spacing and non-human models, WIP on vanilla humans; see Model scale before using it.


What it controls

Two entry kinds: unit links and MACRO replacements

An haf_formations.json entry works in one of two modes, decided by whether Unit is set:

Precedence: macro replacements rewrite the shared formation; unit links repoint their unit at a different formation and therefore overrule the replacement for that unit. A handful of macro entries + a few unit links for showcases covers the entire roster without forgetting anyone.

Why “macro”: the entry expresses a rule, not a single override — and the rule vocabulary is meant to grow. Planned discriminators (reserved, not yet implemented): era (e.g. replace Formation_Scatter_Spaced_9 with 19 dummies only for Era 4+ units), unit class, and land/naval — so one registry can express “denser formations as eras progress” without touching every unit definition.

User workflow (no mod rebuild)

  1. Extract a vanilla formation asset into the project (Assets/Databases/UnitFormation/…) — or duplicate one — so you have a PresentationFormationDefinition you can edit. Its Inspector shows a live hex preview with numbered dummies + XYZ fields.
  2. Author it: add/remove dummies (each needs 6 CoordinatePerDirection entries), set positions, keep the six ColumnsCountPerRow arrays consistent (cell counts must equal the dummy count). Inconsistent grids make the game throw at load — see Troubleshooting.
  3. Link it: open Tools ▸ HAF ▸ Formation Override, Pick the unit (PresentationUnitDefinition name, e.g. PresentationLandUnit_Era1_Common_Warriors_Default), Pick the formation asset, Save link.
  4. Launch — no rebuild. The plugin reads haf_formations.json from BepInEx/config, rebuilds the formation as a runtime ScriptableObject, Database.Adds it, and repoints the unit.

Save always re-reads the asset. The window used to cache the formation data when you Picked it; if you then edited the asset in the Inspector and hit Save, it silently shipped the stale Pick-time copy (“the save had no effect”). Save now re-extracts the asset first, so a plain Save link always captures your current edits. There’s also a manual Re-read button. If in doubt, delete + recreate the link.

Engine laws (decompiled, durable)

The >9 story (the vanilla dummy-pool ceiling)

Vanilla’s biggest formation is 9–10 dummies, and Formation3DPrefab (the template every Formation3D is cloned from) ships with that many dummy child objects. SetDummyCount never reallocates — it only GameObject.SetActive(i < count) over the existing children — so the prefab’s child count is the real ceiling. That’s the “magic number 9/10.”

The plugin grows the prefab past it (Hk_FormationPrefabExtend clones the last dummy child before the pool is built). But that surfaced a subtle bug: on a pooled Formation3D, the extra Dummies[] slots still referenced the prefab’s dummies (a runtime-added child isn’t remapped on pool-clone the way a native child is). Those prefab dummies sit at world origin, so the game’s Dummies[i].Transform.localPosition = Position write moved a prefab dummy and the instance’s pawn was stranded at (~0,0,0) — 3 of a 12-unit’s models teleported to the map origin (which projected to “3 warriors lost far to the east”), while the army banner drifted toward them. Their dummyLocal was correct; the unit’s world offset was simply never applied because the dummy wasn’t a child of the unit’s formation.

The fix (EnsureInstanceCapacity, a prefix that runs before the positioning loop): for each Dummies[i] whose transform isn’t a child of this instance, replace it with a fresh clone of a genuine instance-child dummy, parented under the instance. Now every slot is a real child and inherits the unit’s world position. Verified: 12/12 on the hex, log [Formation] replaced N prefab-bound dummy slot(s) …, zero pawns at origin. Battles already render 12+ models per unit — same engine — so this was always achievable; it was a binding bug, not a hard limit.

The load-race safety net

The override applies a few frames into load (it waits for the databases). Units that spawn before it lands keep the old formation/count until re-formed. FormationReinstantiate (default on) walks the live armies after the override applies and re-runs the game’s own UpdatePawns on any repointed unit that’s under its target count, so it catches up (a one-time re-form). In practice the repoint usually wins the race and this rarely fires; turn it off to keep whatever count a unit had when it first rendered.

Config

Key ([Formations]) Default Effect
FormationOverride true Master switch. Reads haf_formations.json, injects + repoints. Inert if the file is absent/empty.
FormationReinstantiate true After apply, re-form already-spawned under-count units (load-race catch-up). Costs a one-time visible re-form pop.

Troubleshooting (read BepInEx/LogOutput.log)

Model scale: two modes and their limits

For non-human units, prefer the Resize Lab instead (Unit-Size.md). That axis scales the unit’s vertex data in the live Fx buffer plus its per-pawn placement — verified in-game, free on the vertex budget, and immune to both failure modes below, because it never asks a transform to grow geometry (the shaders don’t do that; see Unit-Size § Why it must be done this way). The two modes here remain the way to scale models and spacing together as one formation, and the data mode’s engine notes stay valuable — but for “make this ship bigger”, use a unitScales rule.

The formation link carries a per-unit scale (window: Formation scale, 0.2–2.0). By default it scales the models and the dummy spacing together; Footprint override (layoutScale) decouples the spacing. Two implementations exist, selectable per link (window dropdown, registry scaleMode) — a hard-won field campaign (2026-07-28) established exactly what each can and cannot do:

transform (default — “Transform (simple)”)

Sets each pawn root’s localScale at PresentationPawn.InstantiatePawn (Hk_FormationPawnScale). One line of mechanism, and bodies + spacing look right immediately.

Known limits (field-proven, unfixable in this mode): the engine applies a root scale inconsistently across three GPU subsystems

  1. Body skinning follows it (bodies look right scaling down; scaling up distorts limbs — shriveled arms at 1.25);
  2. Rigid equipment fragments (helmet/shield/weapon — bone-glued meshes) receive it twice on their vertices and once on their anchor: at 0.8 a helmet buries inside the skull (“bald legionaries”) and shields hug the hand;
  3. Procedural weapon-slot / look-at bones ignore it entirely.

Verdict: usable for vehicles/creatures/custom models (single skinned mesh, no fragments) and for quick experiments; NOT shippable on vanilla humans.

data (“Skeleton data (deep, WIP)”)

Puts the scale into the data and leaves every transform at 1: clones the definition’s Skeleton, multiplies all bone BindPose/Local translations by s (rotations untouched — the engine’s clips are rotation-only, so vanilla animations replay correctly on a scaled bind by construction), scales every hosted body mesh and every EQ fragment collection’s pre-encoded vertices by s, then swaps the addon onto the clones (the custom-model repoint idiom) with a FragmentEntry rebuild + surgical GPU-descriptor repoint.

State: bodies and gear meshes verified correct; one subsystem still defeats it on humans — the procedural bone layers (head look-at, RLUDS weapon slots) write bone poses each frame in authored vanilla proportions, so helmets anchor at vanilla head height above a scaled body and heads tilt. Next attack documented: decompile the BoneRotation0-3/slot layer writer (the plugin already owns aim-layer levers from the barrel-twist work). Untested but promising on vehicles (no fragments, no slots, no look-at).

Engine internals the data mode ran into (reusable knowledge)

Capability summary

Target transform data
Vanilla humans bodies OK (down only), gear breaks bodies + gear meshes OK, gear anchors break (procedural layers) — WIP
Vehicles / ships / planes expected clean (untested) expected clean (untested)
Custom HAF models expected clean expected clean (or bake at the right size instead)

Baking a scaled unit as a custom model (gear merged into the mesh, Model Factory pipeline) sidesteps every runtime subsystem at the cost of per-pawn equipment variation — the pragmatic route if a scaled human unit is needed before the procedural-layer work lands.

Formation by size (era ageing) — VERIFIED IN-GAME 2026-07-30

Pairs the formation axis with the Unit-Size axis (Unit-Size.md): as the Global Era Lab shrinks an aged unit, its formation can swap so a tiny lone hull becomes a squadron of small hulls (field-proven: an aged Bireme re-formed into three wedge-formation ships, live, when the era anchor crossed the threshold).

R.E.D.-style: the count + scale pair

This axis is one half of a R.E.D.-style rebalance (after the classic Civ 5 R.E.D. Modpack by Gedemon): model count (the formation axis above — solved, verified to 32/unit) + model scale (above — solved for the spacing half; model-size half usable on non-humans, WIP on vanilla humans). The eventual goal is an optional “R.E.D. Patch” pack — a curated set of formation + size overrides across the roster (smaller, more-numerous infantry; big-but-sparse tanks; smaller planes), opt-in and fully reversible. Practical counts: a hero/showcase unit can go 30–50; a whole-roster rebalance wants ~12–20 per unit (cost scales with total on-screen pawns, not per-unit), so ~18 is a good roster default.

Files