Makes HAF operable without the GUI: the editor’s authoring functions run from the command line via Unity batch
mode, so an agent, a script, or CI can do what a human does in Tools ▸ HAF — including the full mod build + deploy.
Rationale/scoping in Headless-CLI-Design.md.
Status (2026-08-02):
rebuild-model — re-bake a model headless (Unity batch). Verified (AttackHelicopter: assets written, registry updated, exit 0).clean-export — remove the deployed mod (Unity batch). Verified.build-mod — full build + deploy, headless, end-to-end. Runs the game’s own Mod Editor build steps past the batch-hostile database gate, stamps the mod + game version, and deploys the versioned module to Community. Verified in-game (2026-08-02: built 1.3201, GameVersion 1.30.4814, loads with no compatibility warning).Tools/haf-deploy.bat — deploy-only, a pure file copy (no Unity). Verified. Fallback for when you build in the editor and just want to (re)deploy.C:\Program Files\Unity 2021.3.1f1\Editor\Unity.exe
(Unity Hub ▸ Installs, or the Start-menu shortcut target).Wrapper: Tools/haf.bat (in the ENCReload repo, where the CLI code lives):
haf rebuild <resourceName> [-fresh] re-bake one model (-fresh forces a full re-slim)
haf rebuild-all re-bake every model with a source file
haf build FULL mod build + deploy (the Mercury Mod Editor build, headless)
haf clean remove the deployed ENCReload Community export
Deploy-only (no Unity): Tools\haf-deploy.bat. Each Unity verb prints one [HAF-CLI] {…} JSON line; exit 0 ok, 2 bad-arg/not-found, 3 failed.
rebuild-model ✅-executeMethod HAF.Cli.RebuildModel -model <name> [-fresh] (or -all). Reuses the exact GUI path
(ModelRegistry.Load → ModelFactoryWindow.ConfigFor → UniversalBaker.Build/BuildAnimated → copy BakeResult GUIDs →
ModelRegistry.Upsert) — can’t drift from the Bake button. Writes pack.json + Assets/Resources/<name>_{ModelMesh,Skeleton,Atlas}.asset.
clean-export ✅-executeMethod HAF.Cli.CleanExport. Deletes Community\ENCReload.<GUID>.* (the “move your mod … is denied” fix,
scoped to GUID cd3480e932114f8084db755ddd65f2d8). It removes the live deployed mod — a pre-build cleanup.
build-mod ✅ (full build + deploy)-executeMethod HAF.Cli.BuildMod. Reproduces, via reflection, exactly what clicking Build in the Mod Editor does —
but headless. It does not call the top-level BuildModification wrapper (that method’s first act is a database gate
that hard-aborts in batch mode — see Database validation). Instead it calls the three private
build steps that sit past the gate, in order:
TryBuildModification(RuntimeModule, StandaloneWindows64, out msg) → DistributeModification(…, false) →
CopyModification(…, false) — the last copies the versioned module into the game’s Community folder.
Version stamping (critical). TryBuildModification stamps runtimeModule.Version and runtimeModule.GameVersion
from two statics that the editor’s version panel normally pre-loads — and a batch run never draws that panel, so both
default to 0.0. A GameVersion 0.0 makes the game reject the mod as “built using another game version.” So BuildMod
runs the same prep first: LoadTargetMercuryApplicationVersionIFN() (reads the game exe’s FileVersionInfo →
GameVersion) and TryResetNextModificationVersion() (current mod Version + 1 → next). The mod version is read from the
module asset (Assets/Runtime/ENCReload.asset) each run, so it self-increments across builds just like the GUI. The exit
line reports the stamped version + gameVersion. Reflection keeps the editor compile-check independent of the Mercury SDK DLL.
The user’s “build the mod” is the menu Mercury ▸ Mod Editor (ModuleEditor). The top-level
ModuleEditor.BuildModification (in ModuleEditor.Distribution.cs) wraps a pipeline whose meaningful steps are:
DatabaseChecker.CheckDatabases(), first thing in the wrapper. In batch mode a DB error
aborts the build with no dialog; in the editor it shows a “database has errors, build anyway?” prompt you click
past. BuildMod skips this gate — see Database validation.TryApplyNextModificationVersion() stamps Version (next mod version) and GameVersion (game exe
version) onto the module. Both come from statics the version panel pre-loads, so a headless build must load them
itself (see build-mod above).3323885555 vs versioned 3379712144) — you cannot
fake it by renaming. GUID from PlayerSettings.productGUID. Output: Assets/AssetBundles/StandaloneWindows64/ENCReload.<GUID>.<version>/.CopyModification(...) copies the module into the game’s Community folder, whose path is computed, not
hardcoded: GetCommunityFolderPath() = Path.GetFullPath(Application.GameDirectory + "/../Humankind/Community").BuildMod calls steps 3–4 directly (plus the step-2 prep) and skips step 1, so it inherits correct versioning and the
config-derived deploy target with no re-implementation. (Earlier dead end: AssetBundleBuildSettings.Build alone only
produces the raw un-versioned bundle; the versioning + deploy live in ModuleEditor, one level up.)
The wrapper runs DatabaseChecker before building. On ENC it flags
NullReferenceException: Null class reference for AirUnit_Era5_Common_Biplanes — but this is a spurious pre-build
validation error, not a data bug: the check can’t resolve UnitClass_FighterAircraft yet (Biplanes’ class block is
byte-identical to the working MonoplaneFighters), and the real build resolves it fine. In the editor you click
“Build anyway” and it works — which is why the mod has always built despite the console error. BuildMod does the
headless equivalent: it skips the DB gate and runs the build steps past it (the editor’s “Build anyway” path), rather
than letting a false positive hard-abort the batch run. If you ever want the gate enforced, run the check separately —
don’t route the build through the wrapper.
Tools/haf-deploy.batA pure file copy (no Unity) for when the versioned module already exists (e.g. built in the editor) and you just want
it in Community. Finds the newest Assets/AssetBundles/StandaloneWindows64/ENCReload.<GUID>.<version>/, cleans old
Community exports, and copies the 4 core files — stripping the .meta files and the .assetbundle.txt (matches
the editor’s own deploy exactly; verified by diffing). Useful as a fallback and for understanding the deploy contract.
ENCReload/Assets/Scripts/Editor/HafCli.cs — namespace HAF { static class Cli }, verbs RebuildModel / CleanExport /
BuildMod. Batch-mode arg parse via Environment.GetCommandLineArgs; JSON result via Debug.Log("[HAF-CLI] …"); exit via
EditorApplication.Exit(code). SDK types (ModuleEditor) resolved by name via reflection so the editor compile-check
stays independent of the Mercury DLL. Compile-checked (bash Tools/editor_compile_check.sh).
clean → {"ok":true,"removed":1}, exit 0.rebuild AttackHelicopter → [Factory] AttackHelicopter DONE …, assets rewritten, pack.json updated, {"ok":true,"rebuilt":1}, exit 0.haf-deploy.bat → deployed the 4 core files; in-game: [GameBinding] OK — 31 type(s), 22 model(s), 0 conflict(s).build → full end-to-end success. Skipped the spurious Biplanes DB gate, stamped Version 1.3201 +
GameVersion 1.30.4814, built the ~230 MB versioned bundle, and deployed the 4 core files to Community.
Verified in-game: loads with no “built using another game version” warning, rebaked TankDestroyer present.
(First bypass attempt shipped 0.0/GameVersion 0.0 and was rejected — root cause was the un-loaded version statics,
now fixed by the version prep.)