Status: SOLVED (2026-07-16) — a custom prop renders in a pawn’s hand in-game. The third injection axis, after units and districts. Proven end-to-end the same night the district pipeline shipped: Humankind’s Slingers — bare-handed since release — now carry an actual sling (a free Sketchfab model), positioned in the hand.
Extended (2026-07-19): HAND PROPS ON CUSTOM SKELETONS — the Combine soldier carries an M60. A second, fully runtime attachment route for models injected by the unit axis (whose donors — vehicles — have no weapon slots, and whose skeletons are ours, not the game’s): see §”Hand props” below. Weapon in the soldier’s hand, correct grip through idle/run/stance/fire, in its own gunmetal skin.
Unlike the unit and district axes, this one rides the game’s own data path: a pawn definition’s attachment slots already reference equipment assets by GUID, and pawn definitions are ordinary moddable data. The plugin only has to cross one gate (below).
C:\tmp\props\)PresentationPawnDefinition.Attachements[] = { SlotName ("Weapon_RightHand_0"), Fragment.Guid }
→ PresentationPawnFragmentMesh (ScriptableObject — the EQ_* assets)
.ModelPrefab (GameObjectReference) → matched against a REGISTERED MeshCollection.SourcePrefab
.ModelName → GetFxMeshIndex(name) inside that collection
.MaterialRef (Guid) → must have an OutputLayerEntry (borrow a vanilla weapon's)
.CastShadow / .tags → tags only flag Bow/Shield animation variants; default is fine
→ a RIGID mesh glued to the slot's bone (no skinning), GPU-encoded at pawn spawn
MeshCollection is itself trivial: { prefab (the SourcePrefab key), skeleton (null for a rigid prop),
skinnedMeshInfos[] = { MeshName, FxMeshContent { Guid → an FxMesh, ImportAngles } } } — the FxMeshContent encoding
fields fill themselves at GetMeshIndex time, so authoring only needs the FxMesh GUID. Skeleton derives from
MeshCollection, which is why the same registration API serves both.
Proof shortcut (zero code): point a vanilla pawn’s weapon slot at an existing EQ fragment (Slingers +
EQ_DLC_04_Weapon_Boomerang_01) in the SDK, rebuild the mod — the slot honors mod data entirely.
PropBaker.cs)Tools ▸ HAF ▸ Prop Lab authors the whole chain from a model file:
{a,b,c,d}; the
same encoding as image references). The dump lists every field; its MaterialRef is the one you’ll borrow
(the Default button fills the shared EQ_DLC04_Weapons material, verified working).UniversalBaker.Build — pawnDescription is registry-only, so no dummy pawn)
→ bone-free FxMesh (DistrictBaker.BakeFxMesh with mergeSubMeshes: true) → <name>_Collection.asset +
EQ_<name>_Fragment.asset, GUIDs printed (collection GUID → clipboard).Settings persist in EditorPrefs; the embedded preview shows the baked prop (decimation damage is visible before a relaunch).
[Props] config| Key | Meaning |
|---|---|
PropRegister |
master enable |
PropCollectionGuids |
semicolon-separated a,b,c,d GUIDs of our MeshCollections |
PropCollectionNames |
matching asset names (vestigial — see trap 3’s correction) |
Hk_PropRegister (Harmony postfix on AnimationManager.AnimationLoad) registers our collections via the public
RegisterMeshCollection right after the game registers its own — before any pawn definition resolves its fragments —
and re-arms per session. An Update-tick registration survives as a late-repair safety net only — since 2026-07-21 it
is armed by that first AnimationLoad (before the mod bundle mounts, every catalog request is a guaranteed miss
that LogErrors into the Amplitude diagnostics — it used to paint 64+ red lines per boot) and paced to ~1 attempt/s.
AnimationLoad postfix.ParseGuidCsv splitting on '-' and silently stripping the sign off negative GUID components — the catalog
was always being asked for a corrupted GUID. The by-name fallback (PropCollectionNames, via
AssetBundle.GetAllLoadedAssetBundles) never actually worked either — Amplitude mounts community bundles through
its own asset provider, invisible to Unity’s loaded-bundle list — and survives only as dead weight (removal
candidate). Lesson: when a workaround’s comment claims an engine gap, re-verify the input before believing it.BakeFxMesh(mergeSubMeshes: true) flattens them (safe: the packed atlas already
unified the UVs).PropCollectionGuids cfg entry is NOT harmless (corrected 2026-07-21: the name fallback never worked) —
update the cfg GUID after any re-bake that changed it, or the collection silently fails to register (= trap 1).<name>_FxMesh asset’s Import Angles
does NOT work — the baked angle field doesn’t survive the mod bundle (in-game the class default (-90,0,0)
applies; see §Hand props). Author orientation with the Prop Lab’s Rotation offset (baked into vertices,
preview-visible) and position with Position offset; test one axis at a time with a deliberately large value
to learn the frame mapping, then dial in. The handPropAngles registry override is the relaunch-only knob.<name>_Atlas on a private
clone of the borrowed layer, repainted per tick (see §Hand props). Vanilla-slot fragments still borrow.haf_props.json (Edit existing / New / Remove in the Prop
Lab); the vanilla-slot runtime remains config-key driven (PropCollectionGuids/Names).The vanilla slot path above needs an attachment slot in the pawn description (GetSlotIndex −1 silently drops
the attachment) — and a unit-axis model’s donor is typically a vehicle with no weapon slots, while its rendered
skeleton is OURS (renamed b###_ bones). The hand-prop route sidesteps all of it: the plugin constructs the
FragmentEntry itself at repoint time and glues the Prop-Lab mesh to a bone of the injected skeleton.
Authoring (all in the two Labs):
M60) — Size relative to the pawn (a size-2 soldier carries a ~1.25 gun),
Target-tris ~2000, the shared weapon material (Default). Orientation is authored with Rotation offset
(baked into the vertices, visible in the preview) and placement with Position offset — both re-bake knobs.R_Hand, matched case-insensitively against the renamed bones). Save (no
bake) → rebuild the mod.Registry (per model): handPropName, handPropGuid, handPropMat (“” = the shared EQ_DLC04_Weapons
material), handPropBone, and handPropAngles — a hand-edited “x,y,z” runtime rotation override stamped onto
the FxMesh asset before encoding (“” stamps zero; see the third discovery below). Override iteration =
change + relaunch only.
The three engine discoveries that made it work (all decompiled + field-verified):
FragmentEntries after that leaves
the renderer drawing the old fragment count (glued-yet-invisible). The game’s own full rebuild is UNSAFE mid-load
(it skips not-yet-loaded definitions WITHOUT reserving their slot — every later pawn type shifts onto the wrong
fragments/bones; in the field this scattered the recon drones and the howitzer). The plugin instead patches ONLY
its own definition: existing fragment slots copied to the buffer tail + the prop appended there +
descriptor[defId] repointed at the new contiguous block._Proxy textures; Amplitude can
reset the material after a one-shot paint (the skin flip-flopped between sessions). The prop atlas is painted on
a private clone of the layer (unit-retexture isolation — real DLC weapons untouched) and repainted every
tick with a ReferenceEquals fast-path, exactly like the unit retexture recovery.(-90,0,0), silently tipping every prop over vs. the preview. The plugin therefore ALWAYS stamps the angles at
load (the registry override, else zero), making in-game orientation equal the baked vertices.Limits: the fragment record has no scale channel — the runtime scale knob moves the glue position but cannot
resize the mesh (match sizes at bake). One hand prop per model entry for now.