HumankindAssetFramework

Headless CLI & the mod build/deploy pipeline

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):

Setup

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.

Verbs

rebuild-model

-executeMethod HAF.Cli.RebuildModel -model <name> [-fresh] (or -all). Reuses the exact GUI path (ModelRegistry.LoadModelFactoryWindow.ConfigForUniversalBaker.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 FileVersionInfoGameVersion) 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 mod build→deploy mechanism (discovered)

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:

  1. Database checkDatabaseChecker.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.
  2. Apply versionTryApplyNextModificationVersion() 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).
  3. Build the versioned module — the asset bundle is built with the GUID+version baked into its AssetBundle name, so the versioned bundle’s CRC differs from a raw build (raw 3323885555 vs versioned 3379712144) — you cannot fake it by renaming. GUID from PlayerSettings.productGUID. Output: Assets/AssetBundles/StandaloneWindows64/ENCReload.<GUID>.<version>/.
  4. DeployCopyModification(...) 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.)

Database validation

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.

Deploy-only — Tools/haf-deploy.bat

A 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.

Implementation

ENCReload/Assets/Scripts/Editor/HafCli.csnamespace 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).

Verification record (2026-08-02)