-
Notifications
You must be signed in to change notification settings - Fork 0
Formations
Auto-generated from the repo docs by
tools/sync_wiki.sh— edit the source Markdown in the repo, not this wiki page.
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.
-
Pawn count — how many models the unit shows. On the map this is
ceil(healthRatio × Formation.DummyCount), so a full-health unit shows exactlyDummyCountmodels; a damaged one shows proportionally fewer. There is no hidden cap —DummyCountis simply the length of the formation'sDummies[]array. -
Layout — each dummy's local
Positionplaces its model relative to the unit's tile (plus a small random jitter, the unit'sCoordinationValues.DummyOffsetPosition). The six per-orientationCoordinatePerDirectiongrids + the hiddenColumnsCountPerRow0..5arrays drive the logical row/column grid used for facing and attack targeting. -
Packing — the random jitter makes formations read loose. The window's Override packing jitter toggle
(registry
dummyOffset, runtime-only) lets you tighten it:0sits models perfectly on the dummy grid, a small value (e.g.0.05) packs them tightly with a touch of variation, unticked (-1) keeps the vanilla scatter. On repoint the plugin sets the unit'sDummyOffsetPositionto that value — no rebuild. -
Scale — the window's Formation scale toggle (registry
scale) resizes the unit's models AND their spacing together (scaling a formation means the whole formation); Footprint override (layoutScale) decouples the spacing when you want small men on a wide line or vice versa. Two implementations selectable per link (scaleMode) — see Model scale for what works and what breaks.
An haf_formations.json entry works in one of two modes, decided by whether Unit is set:
-
Unit link (
unitset) — repoints ONEPresentationUnitDefinitionat the named formation. Precise, per-unit. -
Macro replacement (
unitEMPTY) — overwrites a named formation in the live database with the entry's data. Every unit of every era — including units from other mod packs that reference vanilla formation names and never rescaled anything — inherits the new layout with ONE entry. Example: replaceFormation_Scatter_Spaced_9with a 19-dummy layout and the whole roster's scatter infantry fields 19 models. The window has an explicit entry-type toolbar: Replace a formation (macro) shows two fields — Replace formation (the target name, with a picker over the known vanilla formations) and With layout (Pick the project asset that carries the new layout; its data is used, its name isn't). Per-unit knobs (jitter/scale) don't apply — they live on unit definitions; use a unit link for those.
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.
-
Extract a vanilla formation asset into the project (
Assets/Databases/UnitFormation/…) — or duplicate one — so you have aPresentationFormationDefinitionyou can edit. Its Inspector shows a live hex preview with numbered dummies + XYZ fields. -
Author it: add/remove dummies (each needs 6
CoordinatePerDirectionentries), set positions, keep the sixColumnsCountPerRowarrays consistent (cell counts must equal the dummy count). Inconsistent grids make the game throw at load — see Troubleshooting. -
Link it: open Tools ▸ HAF ▸ Formation Override, Pick the unit (
PresentationUnitDefinitionname, e.g.PresentationLandUnit_Era1_Common_Warriors_Default), Pick the formation asset, Save link. -
Launch — no rebuild. The plugin reads
haf_formations.jsonfromBepInEx/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.
-
Count =
Mathf.CeilToInt(healthRatio × Formation.DummyCount)(PresentationUnit.InstantiatePawns/CheckPawnCountValidity).DummyCount=Dummies.Length. The game hard-errors "Invalid pawn count" if the actual count ever disagrees — so if you see fewer models than expected and there's no such error, the unit is simply at less than full health (or it's a different unit — see below). -
Per-unit via
PresentationUnitDefinition.PresentationFormationDefinition, resolved lazily by name at spawn throughDatatableElementReference. Repointing must install a fresh reference struct (never mutate the cached one, which caches its resolved element + revision). -
Layout is set in
FormationHelper.InitializeFormation3DForDefinition:Dummies[i].Transform.localPosition = definition.Dummies[i].Position, thenInitialize()captures that as the resting position.BuildDummiesGrid()only builds the lookup grid fromCoordinatePerDirection— it does not move transforms. -
Watch out — same "unit", different definitions. Each cultural/independent variant is its own
PresentationUnitDefinition:…Warriors_Default(your trained unit) vs…Warriors_Rogue(independent/barbarian, spawned by the animals faction) are separate and carry their own formations. A link to_Defaultdoes not touch_Rogue. If a "Warriors" unit shows the wrong count, confirm which definition it is. -
Overwriting a vanilla formation name affects every unit using it. The plugin overwrites the named formation's
data in place (the registry is the source of truth), so if you reuse a vanilla name like
Formation_Scatter_Spaced_12, every unit referencing that name gets your layout. Use a unique name to scope it to one unit. The log warns loudly.
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 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.
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. Covers both inject/overwrite entries and pure-repoint links (a unit pointed at a formation already in the DB) — the catch-up targets the resolved target formation's dummy count, so a repoint-only link's pre-override units are re-formed too. |
-
[Formation] registry: N link(s)— the file was read. -
[Formation] '<formation>' … OVERWRITTEN in place (N dummies)/injected …— the formation data is live. -
[Formation] '<unit>' now uses formation '<formation>' (N pawns at full health)— the repoint took. -
[Formation] MACRO replacement live: every unit referencing '<formation>' now fields N pawns …— a macro replacement entry applied (the OVERWRITTEN-in-place warning above it is expected and is the mechanism). -
[Formation] Formation3DPrefab dummy pool extended 9 -> N— the >9 growth ran. -
[Formation] replaced N prefab-bound dummy slot(s) …— the origin-stranding fix ran (expected for any formation once the prefab is grown past vanilla). -
[Formation] re-instantiated '<unit>': pawns A -> B …— the load-race catch-up fired. -
[Formation] '<unit>' dummy jitter -> V (tighter packing).— the packing override was applied. -
[Formation] '<unit>' pawns scaled xS (Transform mode: root localScale).—transformscale mode applied. -
[Formation] '<def>': skeleton '<name>_HAFsS' — N bone binds ×S, M hosted mesh(es) scaled.—datamode built the scaled skeleton clone. -
[Formation] '<def>': SCALED xS in data (skeleton + k fragment(s) this pass); descriptor[id] repointed …—datamode fully applied to the definition. -
[Formation] '<def>': descriptor not yet populated — …— normal on early Loads indatamode; a later pass (or the game's own fill from the already-replaced entries) completes it. -
Gear floats above scaled bodies / heads tilt (
datamode, humans) → the known procedural-bone-layer limit — see Model scale. -
Fewer models than expected, no "Invalid pawn count" error → the unit isn't full health, or it's a different
definition (e.g.
_Rogue). -
A stray unit icon / models far away → pre-fix origin stranding; make sure the plugin build has the
replaced … prefab-boundfix. -
"Mismatched mods" / crash at load → inconsistent
ColumnsCountPerRowvs dummy coords; the Formation Override window validates this before it lets you save, so re-save from the window.
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
datamode's engine notes stay valuable — but for "make this ship bigger", use aunitScalesrule.
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:
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 —
- Body skinning follows it (bodies look right scaling down; scaling up distorts limbs — shriveled arms at 1.25);
- 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;
- 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.
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).
- Vanilla fragment geometry ships pre-encoded in
FxMeshContent.verticesBytesinside the MeshCollection — there is no loadable FxMesh asset behind those guids. Positions are the first 3 floats of each vertex record (stride =bytes/(vertexCount·4)floats). - Modified bytes are rejected as "Mesh content is corrupted: checksum failed" — zero
verticesBytesCrcto skip the guard (0 disables validation by design). - The encoder caches mesh slots per guid — a mutated content needs a fresh guid or the cached original wins.
-
RegisterMeshCollectiononly encodes when the fx pipeline reports Loaded — callLoadIFNexplicitly after it. - The addon's
Loadruns more than once; vanillaReloadFragmentsrebuildsFragmentEntriesfrom the definition each time (clobbering replacements) — re-apply on every Load, tag clones (_HAFsname suffix) so they are never re-scaled, and expect the GPU descriptor slot to be empty (count 0) on early passes (the game fills it later from the addon's — by then replaced — array).
| 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.
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).
-
Authored PER UNIT in the Formation Override window: a unit link's "Formation by size" rows are
{scale up to, formation}— the first row whose threshold is >= the unit's effective scale (Resize-Lab rule × era-grid cell) wins; above every threshold the unit keeps its configured/own formation. Rows are sorted on Save; stored assizeFormationson the link inhaf_formations.json. - Only fires for units with a Resize Lab rule — the thresholds compare against the effective scale the resize engine computes, so an unruled unit never swaps.
- Live: the check rides the same per-frame path as the era re-scaling — when the era anchor moves a unit across a threshold mid-game, the definition is repointed and every live unit re-forms in place (no reload). Rising back above all thresholds restores the unit's original formation.
- Guard: the target formation must exist in the live database (vanilla, or injected by any saved Formation Override entry) — otherwise the swap is skipped with a loud log.
-
Legacy fallback: thresholds saved by the old Global Era Lab table (in
haf_models.json) still apply to units without per-unit rows; the Era Lab shows them with a Clear button. Per-unit rows always win. - Log:
[Resize] formation-by-size: '<unit>' at effective x0.3 -> 'Formation_Wedge_3' (N live unit(s) re-formed).
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.
- Editor:
Assets/Scripts/Editor/FormationOverrideWindow.cs,FormationRegistry.cs→haf_formations.json. - Plugin:
Patches/FormationOverridePatch.cs(FormationOverride+ theHk_FormationPrefabExtend/Hk_FormationInstanceCapacity/Hk_FormationSpawnDiag/Hk_FormationPawnScalehooks; thedatascale mode also ridesUniRepointHook's AddOn.Load postfix viaMaybeScaleFragments). Registered inPlugin.cs. - Registry: the editor reads and writes the git-tracked
Assets/Databases/haf_formations.backup.json(the source — historical name) and regenerates the game'sBepInEx/config/haf_formations.jsonon every Save (a build artifact, since the 2026-08-20 collapse; pinpointed corruption + one-click recovery in the window).
Get started
- Getting Started
- Installation
- Troubleshooting
- Authoring State and Deployment
- Mod Editor version.xml Recovery
- Building
- Backup
Author models and behavior
- Editor Tools
- Factory Manual
- Vehicle Lab Quickstart
- Animated Models
- Animation Pitfalls
- Textures
- Unit Size
- Unit Combat Behavior
- Formations
- Pawn Props
- Projectiles
- Game Sound Lab
- Firing on Attack
- Turn Ease
- Facing Persistence
- Donor Clip Flight
Districts and wonders
Ship and operate
Internals and project