-
Notifications
You must be signed in to change notification settings - Fork 0
Facing Persistence
Auto-generated from the repo docs by
tools/sync_wiki.sh— edit the source Markdown in the repo, not this wiki page.
Humankind's own save does not store which way a unit is facing. The simulation Unit/Army have no
orientation field — facing lives only on the presentation (PresentationUnit.FormationAngle, an int world
heading) and is recomputed from movement/actions on load, so a reloaded unit resets its heading to neutral.
This feature restores it, in a HAF-owned side-file that never touches the game save.
BepInEx/config/haf_state/facing/<saveName>.facing
One file per save, named after the save (quick-save.facing, auto-save 3.facing, your manual name…). Plain
text, one line per army:
<armyGUID>,<angleDegrees>
armyGUID = the army's serialized SimulationEntityGUID (the same id survives the reload — that's why it's the
key, not a tile position); angleDegrees = its FormationAngle.
Patches/FacingPersistPatch.cs, all reflection (the game types aren't referenced), fail-soft throughout:
-
Capture (main thread,
Plugin.Update→FacingPersist.Tick): every ~¼ s in steady state, walkPresentation.PresentationEntityFactoryController.PresentationArmyEntitiesand refresh an in-memory{guid → angle}map from each loaded army'sFormationAngle. Reads the presentation only from the main thread. -
Save (
Sandbox.Savepostfix,Hk_SandboxSave): the save may run off the main thread, so we write the pre-captured map (not a fresh presentation read) to<StorageContainerInfo.Name>.facing. -
Load (
Sandbox.Loadpostfix,Hk_SandboxLoad): arm the matching file; the tick applies it once pawns exist. -
Restore (main thread): while a file is armed, the tick makes a pass over the armies and restores each saved unit's heading exactly once — the instant its pawn loads (no neutral flash) — via
PresentationUnit.FlipPawnsGrid(angle, FormationMoveBehaviour.Teleport), then marks it handled and never touches it again. A unit already in motion when the pass reaches it (IsAnyPawnMoving) is left alone — its heading is the game's. The restore stops the moment every saved unit has been handled (one cycle); a ~5 s frame cap only backstops saved units that never load this session.Why single-shot (2026-08-01): the original design re-applied on any heading drift for a ~5 s window to catch a
respawnAfterLoadrebuild. But it couldn't tell a load/respawn reset from the player moving the unit, so it snapped the heading back every frame and the unit crab-walked sideways for the first ~5 s after a load. Single-shot restore + skip-if-moving removes that entirely (and drops the per-frame walk + a per-tick dictionary allocation — it was a real perf cost too).Respawn re-arm (2026-08-16): the "trade-off" above turned out to bite every
respawnAfterLoadunit — the helicopters. TheirUpdatePawnsrebuild fires a few frames post-load and recomputesFormationAngleto neutral after the single-shot restore already closed, so they lost their heading while the organ gun (no respawn) kept it. Fixed by coordinating the two systems instead of re-widening the window:MaybeRespawnPostLoadcallsFacingPersist.OnArmyRespawned(army)right after each respawnUpdatePawns, which queues that army to have its saved angle re-applied once it's loaded + stationary (the respawn runs before the facing tick in the sameUpdate, so there's no neutral flash). A session-longsavedFacingmap keeps the angles available after the initial window closes, and the same skip-if-moving guard means a unit the player is moving is still left alone — so respawn units get their heading back without reintroducing the crab-walk.
Factory / PersistUnitFacing (BepInEx config), default on. Applies to all armies (vanilla + custom),
keyed by GUID, per-save file. Turn off to disable capture and restore entirely.
The save/load choke points found here are useful for any HAF state that must survive a reload:
-
Save:
Amplitude.Mercury.Sandbox.Sandbox.Save(StorageContainerInfo, SerializationFormat, GameSaveDescriptor)(or the static eventSandbox.OnSaveStateChange).StorageContainerInfo.Name= the per-file identity. -
Load:
Sandbox.Load(StorageContainerInfo)(orSandbox.OnLoadStateChange) — fires when the simulation is deserialized; the presentation is not built yet, so apply presentation changes in the post-load poll, not in this hook. -
Stable army key:
PresentationArmy.ArmyInfo.SimulationEntityGUID(ulong), serialized → survives load;PresentationEntityFactoryController.GetArmy(ulong)bridges a saved GUID back to itsPresentationArmy.
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