-
Notifications
You must be signed in to change notification settings - Fork 0
Code Map
Auto-generated from the repo docs by
tools/sync_wiki.sh— edit the source Markdown in the repo, not this wiki page.
Where things live in HAF. The repository contains both the BepInEx runtime and the installable Unity editor package; ENCReload is a consumer and reference pack, not the editor source.
| File | Role |
|---|---|
Plugin.cs |
BepInEx entry point. Config binds, the Harmony hook registration list (every Hk_* / Uni*Hook must be listed here or it silently never patches), the per-frame Update() pump, and the F8 diagnostic window. |
Prober.cs |
Standalone reflection prober / dev spelunking. |
Haf.Schema/HafModelSchema.cs |
The shared model schema (netstandard2.0 DLL): the fields stored identically by the editor's ModelDef and the plugin's ModelEntry, defined ONCE. Both classes inherit it (de-duplicated, no hot-path churn); ships next to the plugin (ProjectReference) — deploy both via tools/deploy-plugin.sh. GUID/bake/runtime-state fields stay on the two classes (divergent by design). |
editor/ |
The authoritative installable Unity editor package: Model Factory, Labs, registry writer, packaged helpers under Tools~, and its compile/hand-list guards. |
Patches/GameBinding.cs |
Startup compatibility report + the one place each game-type NAME lives (call sites bind via GameBinding.<Type>, not a scattered Type.GetType). Resolves the catalogued game types/members HAF binds to — including hot-path structs derived from anchor members rather than guessed by name — logs [GameBinding] OK or a specific NOT FOUND, and writes haf_bindings_report.txt every launch. Plain System.Reflection, unit-tested. |
Patches/DialConfig.cs |
The pure parse half of the four live haf_*.txt dials (rotor trim, turn ease, terrain hug, battle turn). No Unity, no file I/O, no reflection: text in, typed config + a list of human-readable problems out. The Poll* methods keep the I/O and log the problems, so an unrecognised line names its own line number instead of being silently dropped. Unit-tested (Tests/DialParseTests.cs) with a legacy-parity oracle (Tests/DialLegacyParityTests.cs). Adding a dial key = one case + one word in that dial's Known list. |
Patches/PoseMath.cs |
The pure per-frame pose decisions: which clip a pawn plays and where in it. The proximity-weighted state vote (PickState), the attack/after/pre-move windows, the nearest-fire match, the deploy ramp and the recoil sweep. No reflection, no Time.time, no locking — the Poll/Pose callers still take the lock and pass the list in. The three match radii are NOT the same (state 4u, fire 4u, deploy 3u); read the constants before touching a call site. Unit-tested (Tests/PoseMathTests.cs) with a legacy-parity oracle (Tests/PoseMathLegacyParityTests.cs). |
Patches/ |
The injection engine + Harmony patches (below). |
Tests/ |
xUnit suite over the pure registry/parse/era layer. See docs/Testing.md. |
baker/ |
Source/build inputs for pipeline helpers such as glbconv/ and reactor_silhouette.py; packaged runnable copies are under editor/Tools~. The old editor-script snapshot was deleted 2026-08-21. |
UniversalInject is a single internal static partial class spread across files (Phase-1 split, 2026-08-02).
All partials share one field set and one member namespace — the file boundaries are for navigation only, they
carry no access or lifetime meaning. To find a method, pick the concern:
| File | What's in it |
|---|---|
UniversalInjectPatch.cs |
The "home" file: registry load/parse/resolve/conflict-merge (LoadRegistry, ParsePack, ParseModels, ResolvePacks), unit↔entry matching (LongestMatch, FindEntryForUnitDefinition), RearmModelRegistration. Also the standalone types: ModelEntry, StateSample, FireInstance, DeploySample. |
UniversalInject.Inject.cs |
Skeleton registration + repoint onto our mesh (EnsureRegistered, RepointMatch, EnsureUploaded), body-mesh discovery/rename, ReloadFragments, InjectHandProp, and the donor-dump diagnostics. |
UniversalInject.Retexture.cs |
Texture-only reskin (keep vanilla mesh): ApplyTexture, ApplyTextureOnly, GreyIsolate (private output-layer clone so the reskin can't bleed onto the emblematic original). |
UniversalInject.Clips.cs |
Animated-clip plumbing: LoadClipCollection, InjectClipCollections, ResolveAnimId / ResolveCollAnimId. |
UniversalInject.PackTuning.cs |
PackTuning.Parse — the pure parse of the three pack tuning tables (unitScales, eraGrid, formationThresholds) over the resolved packs in load order, with a note per cross-pack interaction for the load report. |
UniversalInject.ScaleEra.cs |
Unit size + era aging: CurrentEra/era resolution, ScaleDescriptorMeshes (the mesh-scale engine), formation-swap-by-size, ResizeStatusLines. |
UniversalInject.Pose.cs |
Pawn creation + per-frame pose: OnPawnAdded, ForceOurSkeleton, ApplyFreeze, ApplyAnimatedPose, PhaseFor, ComputePoseTime, StatePose (idle/move/after/attack state machine), DeployPoseTime, FireOncePoseTime. Nested PawnCtx. |
UniversalInject.Muzzle.cs |
Turret aim + muzzle: TurretizeAimLayer, MuzzleRedirect/CompensateDonorOffset, SanitizeAimLayer/ClearAimLayer, ApplyPositionOffset, ApplyScale, LogPoseHookOnce. |
UniversalInject.Combat.cs |
Combat + post-load: MaybeRespawnPostLoad (first-instance rotor race), ProcessFireQueues, TryEarlyAttackSound (the FaceEnemy roar seam), OnPawnDeath/OnBattleStarted/ProcessBattleCries, ProcessAnimStates, ProcessDeployState, TickOne. |
UniversalInject.Audio.cs |
Engine/move audio: ProcessEngineAudio, emitter helpers, PlaySoundTest, DumpSoundCatalog. Also the Game Sound Lab plumbing: ShouldSilenceEvent/EnsureSoundOverrides (silence vanilla events from haf_sounds.json) and PlayEventByName/StopEventAudition (F8 audition). See docs/Game-Sound-Lab.md. |
UniversalInject.PropsBudget.cs |
What stayed behind when the district axis moved out (2026-08-21): pawn-prop registration (RegisterPropCollections, TickPropRegister), projectile overrides, LoadAmpliAsset/ParseGuid4 (shared asset-by-GUID loading), the live GPU mesh budget (ReadMeshBudget, DumpMeshBudget) and the atlas dump. |
DistrictInject.cs |
The district axis — its own static class since 2026-08-21 (was the UniversalInject.Districts partial). Registry (distModels, nested DistrictModel), the ISOLATE path (private leaf, per-tile channel repoint, footprint preservation, deep-clone), texture binding, the mesh strategic footprint (B&W / flatten), DistrictApplyEntries, the per-session reset + main-thread destroy queue. Reaches the rest of the plugin only through using static UniversalInject (reflection + asset helpers). |
DistrictInject.Scoped.cs |
The SCOPED path (was UniversalInject.RepoDump.cs): PollDistrictSelectorTile / PollDistrictMainRows, the reactor footprint graft, wonder cell registration, ground-material + hexagon-sculpting apply, and the DistrictDebug-gated repository dumps. Per-district ScopedState. |
UniversalInject.Reflection.cs |
The one member reader/writer the whole plugin funnels through: GetMember/SetMember/MakeGuid + the (type,name) member cache — a ConcurrentDictionary, because the sim-thread hooks read through it too (2026-08-21). |
ThreadDiscipline.cs |
The declared thread discipline for ModelEntry's mutable fields (2026-08-22): [MainThread] / [Locked] / [Concurrent] + the reflection helpers ModelEntryThreadTests uses. The per-instance companion to SessionState.cs's per-static lifetimes. |
SessionState.cs |
The declared session-state registry (2026-08-21): [SessionScoped] / [SessionScoped(Manual=…)] / [ProcessLived(…)] attributes + SessionState.Reset(scope), which clears every registry-managed static collection of a scope. RearmModelRegistration calls the Model reset, ResetDistrictSessionState the District one. Tests/SessionStateTests.cs fails the build on any undeclared static collection. |
FrameCost.cs |
The per-frame cost meter (2026-08-21): every Plugin.Update call group and the pose hook (vanilla / ours, with sub-buckets) timed into named buckets, 5 s window, F8 line + [FrameCost] log line per minute. Pure Format is unit-tested. |
FastMember.cs |
Compiled DynamicMethod accessors for boxed-struct fields/properties with nested paths ("ObjectSpace.Translation"), numeric conversion, null = "not available, use reflection". Unit-tested on the CLR; the game's Mono is the second runtime. |
UniversalInject.PawnFast.cs |
The PawnEntry accessor set built once from the first entry (ids, translation, rotation, scale, the nine poses, the four aim slots) + the wrappers the pose path calls (TryGetTranslation, SetPose, SetBoneRotation …), each with its reflection fallback. |
UniversalInject.SubPawnScan.cs |
ONE source for "the live sub-pawns of OUR units": a targeted presentation walk (armies, squadrons, air formations, battle units), self-verified against a full scene scan once per session, 2 s cadence; the scan is the fallback. |
ClipRoles.cs |
The clip-role table (2026-08-21): ClipRole enum (Primary + eight state roles), ClipBinding (guid / collection / animId / duration), the name / log-tag / pack-JSON-key per role, NewTable(). ModelEntry.Roles[9] is built from it; every "all roles" site loops ClipRoles.All. Pure, unit-tested. |
UniversalInject.SmokeTest.cs |
The in-game smoke harness: RunSmokeTest (gathers live binding/registry/injection counts and logs a single PASS/FAIL line, echoed to the F8 panel), the pure SmokeVerdict (unit-tested), the per-district facts (GatherDistrictFacts — both tile ledgers + DistrictTexState texture health), and the InjectionErrors counter the four injection paths bump. See docs/Testing.md. |
UniversalInject.Hooks.cs |
The Harmony patch classes that call into the above: UniRegisterHook, UniRepointHook, UniPawnPoseHook, Hk_MuzzleRelocate, Hk_AudioTrace, Hk_DistrictRepoint, Hk_AnimatedBonePoolHeadroom, Hk_DistrictBufferHeadroom, Hk_PropRegister, Hk_ProjectileOverride. |
Other patch files (already separate, not part of UniversalInject):
| File | Role |
|---|---|
Patches/CombatEventPatch.cs |
Sim/presentation combat hooks: Hk_EarlyAttackSound, death cue, battle-start, VFX suppression, projectile stash, FireProbe. |
Patches/PivotHoldPatch.cs |
Pivot in place: Hk_PivotMoveHold prefix on PresentationArmy.UpdateWaitForReadyToMove defers the whole vanilla move start while the eased yaw turns to the next history tile (UniversalInject.ShouldHoldArmyMove, per unit). docs/Turn-Ease.md. |
Patches/FormationOverridePatch.cs |
Formation axis: pawn-count + layout override, prefab/instance pool extension. |
Patches/FacingPersistPatch.cs |
Save/load facing restore (the standard save has no facing field). |
All reflection member access funnels through UniversalInject.GetMember/SetMember (cached, property-first,
finds non-public), which live — with MakeGuid and the member cache — in UniversalInject.Reflection.cs.
FormationOverride.Mem and FireProbe.Member are thin forwarding aliases to GetMember. (FacingPersist keeps
its own small field-only cache, CachedField, for its self-contained use.)
The cache is shared across threads. GetMember is called from the sim-thread hooks as well as the main thread
(FireProbe.Member on ArtilleryStrikeStarted, OnBattleStarted's contender walk, FacingPersist.OnSave/OnLoad),
and the members those paths touch are touched by nothing on the main thread — so the first use is always an insert.
The cache is therefore a ConcurrentDictionary (2026-08-21; it was a plain Dictionary annotated "main-thread
only", a freeze-class race). Do not replace it with a Dictionary + "main thread only" comment again.
-
A new Harmony hook only fires if it's added to the
hooks[]list inPlugin.cs— a missed registration is silent. The load line reportspatched/totalcounting the methods actually patched (not "didn't throw"). - Adding a registry field: the fields stored identically by both classes are the shared
Haf.Schema.HafModelSchema— add it there ONCE (default = the field initializer) and bothModelDef(editor) andModelEntry(plugin) inherit it (compiler-enforced, can't drift). The plugin's primary parse is generic (m.ToObject<ModelEntry>()inParseModels), so a name-matching field maps automatically; only the GUID arrays (skel[]→sa/sb/..) andposition(Vector3 — Newtonsoft chokes on it, stripped + re-pinned by hand) are explicit. What still hand-syncs: the regex fallback (malformed-JSON path) hand-lists every field — add the matchingRegex.Matchesline there. Then run the drift guard:bash tools/check_schema_parity.sh— it fails loudly if the fallback lags the shared schema or the GUID hand-lists, or reads a key the baker never writes. -
ModelEntryinheritsHaf.Schema.HafModelSchemafor the shared fields (de-duplicated via inheritance, no hot-path churn); the POCO decomposition split is still declined. Keep runtime-state / divergent-GUID fields onModelEntryitself.
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