-
Notifications
You must be signed in to change notification settings - Fork 0
District Visuals
Auto-generated from the repo docs by
tools/sync_wiki.sh— edit the source Markdown in the repo, not this wiki page.
Which district page do I want?
- A custom building model + texture on a district's tile, auto-leveled — the common case → District-Visuals.md (start here)
- …and its own strategic-zoom footprint, the scoped render path, multiple custom districts side by side → District-Dedicated-Visual.md
- How it was figured out (archived, not instructions) → Feasibility study · Footprint investigation
Status: SOLVED (2026-07-16; textured 2026-08-06) — a custom 3D model renders on a single district tile in-game, with its own baked texture. Render, fit, scope, grounding and color are all working: the plant mesh sits level on the reactor tile wearing its own albedo, and the rest of the city is untouched. This was long thought impossible (see the history at the end); it is not. This page documents the working recipe, the mechanism, and the constraints.
The second injection axis, alongside units. Goal: let a pack replace a district's on-map building with a custom static 3D model. It is far deeper than the unit path, but it works.
Three parts — a data edit, a runtime mesh-swap, and a lean bone-free bake:
1. Data (in the ENCReload Unity project): on the target district definition, set
ConstructibleVisualAffinity to a renderable affinity (e.g. DistrictVisualAffinity_MissileSilo) and clear its
Additional Visual Levels. The visual-levels list drives the district's DistrictState, which is a criterion in
the building lookup — a mismatched state resolves to no building (material 0,0,0,0 → empty tile). Clearing it falls
back to the default state, which resolves. (This — not the district class — is why foreign affinities kept coming up
empty.)
2. Runtime (the plugin, driven by the haf_districts.json registry — below): the rendered mesh lives deep inside
the resolved material:
channel-0 material = FxEvolverMaterialLevelBuildSelector (picks by CULTURE)
→ pairs[culture] = GUID → FxEvolverMaterialLevelBuildEmitter (emits a SET of parts; may nest)
→ levelBuildItems[].loadedEvolverMaterial → FxEvolverMaterialLevelBuildElement ← THE LEAF
.fxMesh (Guid) ← the mesh handle (field is `fxMesh`, not `mesh`)
.meshIndex (uint) ← the RESOLVED GPU slot (sentinel uint.MaxValue), set in the leaf's Load()
The plugin (Hk_DistrictRepoint → TickDistrictMeshSwap) walks this: loads each pairs sub-material with
FxEvolverMaterial.TryLoad(guid, synchrone:true), recurses levelBuildItems[].loadedEvolverMaterial to the leaf
Elements, sets each leaf's fxMesh to our FxMesh GUID, then calls the leaf's Load(fxManager, doublonIndex) so it
re-resolves meshIndex from our GUID. Because we reuse the game's own leaf (with its selector context/GPU data),
our mesh draws — where a foreign material handed in via SetChannel had no context and drew nothing.
3. Bake (the District Factory window — DistrictFactoryWindow.cs): Tools ▸ HAF ▸ District Factory does the
whole editor side in one Bake: pick the District (searchable dropdown over the project's district definitions), Browse a
model file, set Size / Rotation / Target-tris / Isolate, press Bake. It runs the same static bake core as the unit
Factory (UniversalBaker.Build — no dummy pawn needed; pawnDescription is registry-only there), wraps the result
as a district FxMesh via DistrictBaker.BakeFxMesh, and writes the registry entry. Two hard requirements the bake
handles for you —
-
Bone-free: a unit static-bake rigs the mesh (boneWeights/bindposes) for its Skeleton; the district's static
shader can't read a skinned vertex format, so it uploads but draws nothing. The bake writes a bone-free
_DistrictMeshcopy and wraps that. -
Lean: district building parts are tiny (hundreds of verts) and share the GPU buffer (below). Keep Target-tris
modest, or set
DistrictBufferHeadroom.
Then rebuild the mod (ships the FxMesh) and launch. The reactor renders.
The runtime side is data-driven: BepInEx/config/haf_districts.json holds any number of district models at once. Since
2026-08-20 it is a build artifact: the District Factory reads and writes the git-tracked
Assets/Databases/haf_districts.backup.json (the source — the name is historical) and regenerates the deployed copy on
every Save; a corrupt source is pinpointed and recoverable in one click from the window (see Factory-Manual.md,
the ONE-file registry section). The entries:
{ "districts": [
{ "district": "Extension_Base_BreederReactor",
"fxMeshGuid": "1457749632,1176062388,715769744,1624515593",
"atlasGuid": "260107174,1193535976,-95465828,-2065892038",
"isolate": true }
] }The plugin reads only district / fxMeshGuid / atlasGuid / isolate per entry (Newtonsoft — extra fields are
bake-time state for the window and are ignored). atlasGuid is the baked albedo the texture injection binds (below);
entries without it render untextured like before. Each entry gets its own leaf collection / private-leaf machinery, so several districts can
carry custom models simultaneously. The old single-model [District] keys (DistrictName + DistrictFxMeshGuid)
still work as a fallback only when the registry has no entries. DistrictRepoint = true remains the master enable.
District building meshes upload into a shared GPU mesh buffer — layer 'Visual', sized 3,000,000 verts, drawn by
FxComponentMeshContentManager.ContentLayer (GetFxMeshStructIndex → Register → FillMeshVertexAndBufferContent).
In a built-up late-game city it runs ~99.85% full (measured: 2,995,550 / 3,000,000). An oversized mesh gets a slot but
silently overflows the vertex buffer and is dropped — the exact "assigned index 4606, renders nothing" symptom.
Two levers:
- Lean bake (above) so the mesh fits the free space.
-
DistrictBufferHeadroom(config, opt-in, default 0): a Harmony prefix onContentLayer.LoadEncodingVertexAndBufferenlarges the Visual layer'sbaseVertexBufferSizeat creation. Setting2000000grows it 3M → 5M (~+96 MB VRAM); the shader reads the buffer size dynamically so it's transparent. Verified in-game ([District] enlarged 'Visual' mesh buffer: 3000000 -> 5000000).
Diagnostics: F8 window ▸ "Dump District" lists every registry entry (matched? leaf built? resolved meshIndex, our
FxMesh verts + bounds) and each mesh-manager layer's fill (verts used / size).
| Key | Meaning |
|---|---|
DistrictRepoint |
master enable (for the registry AND the legacy keys) |
DistrictBufferHeadroom |
extra verts for the Visual buffer at init (0 = off; 2000000 = +~96 MB VRAM) |
DistrictName |
LEGACY fallback — target ConstructibleDefinitionName; used only when haf_districts.json has no entries |
DistrictFxMeshGuid |
LEGACY fallback — the baked FxMesh GUID a,b,c,d for DistrictName
|
DistrictIsolate |
LEGACY fallback — scope the legacy entry to its own tiles (registry entries carry their own isolate) |
Moved to the [Debug] section on 2026-08-21 (investigation dials — off in normal play): DistrictDebug (the
repository dumps, ~40 ms/frame for the first seconds of a load), and the superseded proof modes
DistrictAffinityOverride / DistrictEvolverGuid. [Debug] also holds DumpPawnRig and AssetNameFilter. A key that
moves sections comes back at its default in an existing .cfg (BepInEx orphans the old entry) — re-set it under
[Debug] if you need it.
Gotcha: a new config key only appears in the .cfg after the new build runs once; adding it by hand must go
inside the right section. The FxMesh ships only after a mod rebuild/export.
The leaf Elements are shared across every district of a culture, so the raw swap turns every matching building on
the map into our mesh. DistrictIsolate fixes that by giving the target district a private leaf:
- On a target district instance's channel-0 selector,
CollectLeavesto a source leaf;UnityEngine.Object.Instantiateit (a private copy — mutating it can't touch the shared leaf). - Set the clone's
fxMeshto our GUID, reset itsloadingStatus, and callLoadIFN(fxManager)+Load(fxManager, doublon)so it gets a validMaterialIndexand re-resolvesmeshIndexfrom our mesh. - Per-frame: set each instance's
channels[layer].evolverMaterial= the private leaf (write the boxed struct back into the array) and call the publicRefreshChannel(int, EventNameEnum)so the Shuriken particle re-spawns andPatchParticlepicks up the private leaf'sMaterialIndex.
Multi-instance (2026-08-08, verified with a second reactor): a district can be built on many tiles — one
PresentationDistrict each. Each registry entry tracks a list of live instances (added by the UpdateLevelBuild
postfix, pruned via Unity fake-null when razed), and the tick repoints every instance's channel at the entry's
one shared private leaf — a leaf is just a material, and vanilla's shared selectors serve many channels the same
way. (The first implementation held a single instance slot per entry; a second copy of the district made ownership
ping-pong, and only the most recently updated tile showed the custom model.) Build lazily (sub-materials load async →
retry), re-apply every frame (the game reloads the shared selector into the channel on each UpdateLevelBuild).
Verified in-game: two reactors in different cities both show the custom plant; the rest of the map is untouched.
The District Factory has an embedded preview pane (2026-08-06): the baked mesh, textured, standing on a tile-sized ground square pinned at the true in-game surface level. The placement controls split by job:
-
Rotation offset — stand it up (baked into the mesh; the preview shows the result after each Bake). The bake
auto-aligns the longest axis, which can tip a near-cubic model onto its side around ANY axis (the plant needed
Z=-90). Dial it in the preview — no relaunch round-trips. - Facing on tile — turn it (0–360°, previewed LIVE). Always rotates the standing building about the vertical, so it can never tip the model; written into the FxMesh at Bake. The preview draws a NESW compass rose (2026-08-08) — lines to all four cardinals with letters reading North-up — because a district has no facing of its own: the rose shows how the tile sits on the map, and Facing 0° points along the North line. The preview hex is also corner-forward (the in-game district cell presents a corner toward North, measured on the reactor; unit previews stay edge-forward — units face their neighbors edge-on), and the bake's hex-clip planes match the same orientation. (The old three-field FxMesh import-angles control is retired from the UI — it overlapped Rotation offset and invited tipping; entries authored with it keep working.)
- Position offset — place it (previewed LIVE). Nudge across the tile in world units (a tile hex is ~7 across its flats; X/Z slide, Y lifts off the ground) — the same knob the Model Factory has for units, applied after the auto-level so the leveling can't cancel it.
- Grounding is automatic (2026-08-06). The game plants the mesh by its origin at the tile surface and nothing re-grounds it — a rotation offset that changed which axis is "up" used to sink the model (the plant surfaced only its containment domes). The district bake auto-levels: it computes where the vertices land with the entry's draw-time rotation applied and shifts them so the lowest point sits on the surface, footprint centered. Any rotation combination comes out standing level. (District paths only — props/projectiles keep their meaningful pivots.)
-
Scale. Baked-in (the
Sizeknob), so resizing needs a re-bake. A district tile hex is ~7 across its flats (~8 corner to corner) — the preview hex is true size, measured from the map's center-to-center tile spacing (6.93). A full site-plan model can carry Size 5–6, a single building ~2.5–5.
Health panel (2026-08-08): the District Factory validates on selection / after Bake / Re-check: shipped
GUIDs vs the assets on disk (drift = the silent "waiting for leaves" launch, now a red box), newest baked
asset vs the newest built Community assetbundle (STALE BUNDLE — re-bakes reshuffle atlas packing, the
mesh/atlas halves must ship from the same bake), and the definition's data prerequisites (non-empty
Additional Visual Levels = a guaranteed empty tile; missing ConstructibleVisualAffinity = nothing to swap).
A base-game target (named Extension_* — its definition lives in the game, not this project) is not a
project asset, so the panel can't inspect it and stays silent rather than false-warning "typo" (2026-08-09);
only a non-namespaced name that also isn't a project asset is flagged. Info-level notes never count as "issues".
A district on a coastal cliff or uneven tile overhangs into empty air — the reactor floated off the ledge.
The Foundation depth bake knob (registry foundationDepth, 0 = off) fixes it: it extrudes the building's
footprint straight down into the earth as a solid concrete plinth, so the building plants on a base that
runs down past the drop.
-
Geometry (
DistrictBaker.BakeFxMesh): after the auto-level/hex-clip, the footprint is measured in drawn space (post-rotation, so "down" is true world −Y regardless of the model's import angles), a box is built from the surface down to −depth, then inverse-rotated into stored space so the draw-time rotation lands it straight down. Four walls + a floor, wound so faces point outward/−Y; the cap is omitted (hidden under the building). -
Concrete texture (
AppendConcreteStrip): districts render one atlas, so the plinth needs concrete in it. The bake grows the atlas set by a fresh strip along the top — lightly-noised grey albedo, neutral (flat) normal, rough-concrete roughness — slides the existing content down and remaps the mesh UVs to match, so no existing texel is overwritten. The plinth faces sample that strip. - Purely bake-time: the runtime still receives one FxMesh + one atlas trio, so isolation / wonders / multi-instance are untouched. The plinth shows in the preview (below the model, going into the ground); the camera frames on the above-ground building so plinth depth doesn't shift the view center.
- Tuning: ~8–12 for a coastal cliff (the slider goes to 30). The footprint is the building's bounding box — a rectangular plinth; on a normal tile it's simply hidden underground.
- Known limit — a Z-fight at the seam on cliff tiles. Where the plinth walls meet the building's own perimeter walls, a shimmer can appear at map distance. It's depth-buffer precision, not geometry: the plinth and building share one mesh/transform and can't physically overlap (every building vertex sits at or above the plinth top), but a district sits far from the world origin under a huge far plane, so the depth step at that range is coarser than a few cm and the near-coplanar walls round to the same depth. A small separation (0.001) is invisible to the buffer; a large one (~0.15) clears it but shows as a visible slot between the building and its base — the two ends of the same trade-off (measured, both tried). Deferred with the shape intact. The path to solve it without a visible slot: inset the plinth a little behind the building's outer wall so a depth-beating gap stays hidden (at the cost of an invisible sub-decimeter overhang).
A district entry can carry parts: extra models composed onto the tile at bake, each with its own model file, Size, Rotation offset (stand it up), Facing (turn it) and Position offset (X/Z slide, Y lift). Each part bakes through the same core, auto-grounds to the base model's floor, and merges into one mesh + super albedo/normal/rough atlases sharing one set of pack rects — the runtime is untouched (still one FxMesh + one atlas trio per entry, so isolation, wonders, and multi-instance all just work). Parts are baked-in: placement shows in the preview after Bake.
-
Alpha-cutout foliage works in-game (verified on the beech tree — the district shader honors the atlas's
alpha). The bake keeps transparency end-to-end when a source carries it: the multi-material pack no longer
force-flattens alpha, transparent texels skip the black-repaint,
FinalizeAtlaspicks DXT5 over DXT1, and the preview material flips to cutout. Opaque models keep the exact old path (byte-identical re-bakes). - Surface maps ride along: the base's baked normal/rough atlases blit into same-rect super maps (area-average — a single tap aliases dense normals), neutral fill where a part ships none. (The v1 albedo-only shortcut let the donor's maps tint the whole composed model blue — falsified same evening.)
- Known cosmetic: the game's shadow pass does NOT alpha-test, so a dense leaf-card crown casts a merged solid shadow blob (the color pass cuts the leaves correctly; preview clean, measured in-game).
A district carves a raised terrain platform — the plinth you see the building stand on. It's resolved in
PresentationDistrict.UpdateHexagonSculpting (for wonders from the dedicated */District/ArtificialWonder/HexagonSculpting
database) → a HexagonSculptingDefinition index → ApplyHexagonSculptingDefinition. A custom wonder's cell is
empty → index 0 (None) → flat ground. The fourth empty-cell fix: Hk_DistrictHexSculpt postfixes it and
forces a chosen index. Per-entry Footprint (hex sculpting) field in the Factory (registry hexSculpt) +
global DistrictHexSculpt config; a live dial haf_hexsculpt.txt re-carves every sculpted district without a
relaunch (cycle the ~40 shapes fast, then ship the winner in the Factory).
Global vs per-entry (a footgun): the global DistrictHexSculpt / DistrictGroundMaterial configs apply to
every registry district at once (a district with a blank per-entry field falls back to the global). Handy for
a quick test, but it will raise/repaint districts you didn't mean to — e.g. a global platform floated the flat-based
Breeder Reactor. For shipping, leave the globals blank and set each district's Factory fields, so every
district configures itself and nothing bleeds onto its neighbours.
Which shape? Measured ([HexSculpt] NATIVE dump): most districts resolve to None — the city center,
administrative center, camp center, and cultivated tiles carve no platform. The districts with the raised plinth
are the emblematic quarters; e.g. Extension_Era1_OlmecCivilization → EmblematicAndCityCenter26. So to
match a real district's platform, use EmblematicAndCityCenter26 (the 01–33 variants are different footprint
shapes; POI_* are for natural/resource tiles). Verified in 3D: the Oracle carves the emblematic platform.
Two honest limits. (1) The preview can't show the platform — hex sculpting is a runtime terrain deformation the game applies with its own terrain engine + the shape's height data; the preview tile is a flat quad and the FxMesh carries no sculpt. Like final PBR shading, it's judged in-game, not in preview. (2) The raised 3D platform is NOT the top-down strategic-zoom footprint (the grey building silhouette on the strategic map) — measured with a full zoom-out: the platform appears in 3D but no strategic silhouette. That silhouette is a separate render-mode / strategic-representation path (very likely a fifth empty cell), still OPEN — its own focused spike.
A district also paints the terrain under it — PresentationDistrict.UpdateGroundMaterial resolves a
GroundMaterialDefinition from (Biome × ConstructibleVisualAffinity) and calls ApplyGroundMaterialDefinition.
A custom wonder's native affinity has no row → index 0 → bare terrain (the temple stood on raw desert). The
plugin postfixes UpdateGroundMaterial and forces a chosen ground index for our districts — the game's own
terrain paint, blended at the cell edges, not a flat mesh. Each entry carries its own Ground field in the
District Factory (a dropdown of the game's vocabulary: Prairie_* grass fields, Constructible_* paved
precincts, Sterile_* sparse); a global DistrictGroundMaterial config is the fallback default. Verified: the
Oracle on Prairie_Grassland (index 16) — a lush maintained field under the temple and its grove.
The Factory preview textures its tile with the real terrain image — the ground texture is a tile inside a
shared DefaultTextureAtlas, so the plugin resolves the authoring data → texture layer (Atlas + AtlasElement
GUIDs) → loads the atlas → GUIDToIndex(AtlasElement) → GetElementData(index) (the tile's min/max UV rect) →
OutputEntries[0].GetTexture (the 4096² page) → blit-crops that UV region → one PNG per material in
haf_ground_tex/ (the material's true Color is dumped alongside as a fallback). The tile hex gained planar
UVs so it maps; so a terrain-paint choice reads as real grass/pavement/sand in the preview before launch.
- Foliage toolkit (per part, verified on the beech): Leaf fullness = alpha gain + dilation rounds (needed because many leaf sheets have BINARY alpha — a plain gain is a no-op; measured), and Leaf size × = geometric scaling of the leaf cards, selected by characteristic (≤4-tri islands; a twig is a many-tri cylinder — size-only selection turned the tree into a spiked bush) and scaled around each card's stem (the vertex nearest the branch cloud — centroid scaling detached the leaves).
-
Per-mesh primitive ceiling — raised, not worked around (verified). A district mesh draws as sub-particles
(
count = ceil(primitives / outputLayer.PrimitivePerParticleCount)), and that count is an 8-bit field → hard-clamped at 255. A high-poly composed model (a temple + a grove) exceeds it and the excess is silently not drawn (the four-tree grove first showed temple + 1 tree, the rest dropped). Crucially the mesh is fully stored — the encoder ignores PPC — so only the render clamp bites. Since the private layer is ours to clone, the plugin multipliesPrimitivePerParticleCounton it (DistrictMeshDensityBoost, default 8): the ceiling (255 × PPC) rises for the same GPU work — fewer particles, each covering more primitives — and no re-bake is needed. Verified: PPC 64 → 512, ceiling ~130k primitives, the full grove renders. (A first guess of a 16-bit vertex limit was decompiled and disproved — the index buffer is 32-bit.) How far the ceiling goes (2026-09-13 investigation, IL ofGetEncodedMeshAndVisualParticleCountread fromAmplitude.Graphics.dll): the encoded field ismeshStartIndex | (particleCount << 24)— an 8-bit count (clamped at 255, with the game's own console warning above it) over a 24-bit start index. Unlike the pawn path, the district shader reads PPC dynamically fromperLayerDataCBand every HAF district route renders through a private layer clone, so the boost has no compiled-in wall: the render ceiling is simply255 × PPC × boost— and the boost now auto-sizes from the injected mesh's own triangle count (EffectiveDensityBoost: the FxMesh asset carries its Unity Mesh, so the plugin computesceil(tris / (255 × PPC))at layer-clone time; triangles safely overestimate encoded quads, and overshooting PPC is free).DistrictMeshDensityBoostremains as the FLOOR (default 8 ≈ 130k prims; the scoped path's low-PPC donor once needed a hand-set 32 — no longer). The REAL remaining walls are: (1) the shared Visual layer's vertex buffer — 3M verts and ~99% full in a late-game save before you add anything; give it room withDistrictBufferHeadroom/[Buffers] BufferOverrides(see Vertex-Budget); and (2) the 24-bit start index (~16.7M primitives into the layer) — and note it is unmasked in the encode, so overshooting it would corrupt the count byte rather than clamp; unreachable with shipped buffer sizes, worth remembering if buffers are ever pushed toward 16M+. - Grove copies (a part placed multiple times, verified): one bake, one atlas slot, geometry appended per copy, each auto-rotated by the golden angle. A per-part Target triangles budget keeps grove trees lean independently of the detailed base. Placement is literal and deterministic — an offset places the part's own footprint center measured from the tile center, and facing spins it in place. Three coordinate-shift bugs were hunted out to earn that: the golden-angle rotation orbited a model's arbitrary origin (→ rotate around the footprint center), the auto-level centered the base+parts union so a side-heavy grove shoved the base into a corner (→ base-anchored leveling), and parts placed vs the raw origin were then silently shifted by the base center (→ measure offsets from the tile center). The bake banner prints a receipt (parts/copies/atlas/center) and a per-placement log line, so a knob that didn't take effect is visible before launch — no tuning-into-a-gamble.
Districts rendered untextured for three weeks — the swap reused the game's own leaf material, and our atlas had no way in. Two measured facts cracked it:
-
The district building layer is a full-texture layer. It has no
FxComponentTextureAtlasManagerentry; every leaf resolvestextureIndexto the fixed full-texture slot (1) and the shader samples the layer material's bound sheet straight through the mesh UVs. (That's why an untextured custom mesh showed patches of the culture's building sheet — its 0..1 UVs swept a texture authored for baked-UV building parts.) An earlier design that painted a rect into the atlas-manager page was falsified by this trace and never shipped. - Texture is therefore a per-LAYER binding, shared by every building drawn through that layer — recoloring the shared material would repaint the whole culture.
The unlock is one step up from the leaf clone: clone the whole FxOutputLayer. BuildPrivateLeaf instantiates the
leaf's output layer alongside the leaf (Unity resets the non-serialized runtime state, so the clone is unregistered);
during the leaf's own Load, the game's renderer registers and loads the clone itself
(FxComponentRenderer.GetLayerIndexAddItIFN — a real registration API), creating private runtime materials and command
buffers. DistrictApplyTexture then registers a null atlas-info slot for the new layer (so the game's own resolve
returns full-texture for it forever), points the leaf at slot 1, and binds the baked albedo on the private runtime
materials (_MainTex when present, else the largest bound sheet — DistrictDebug dumps every property to catch a
wrong pick). The mesh's own 0..1 UVs sample the albedo exactly; no other building is touched.
Stability (2026-08-07, the Oracle arc): the private layer opts out of texture streaming (its mid/hi-res
material GUIDs are nulled at clone time, so the reduction system never loads a material over our binding — the cause
of a "perfect → brown → corrupt" degradation). District session state fully resets on new games and in-session
save-reloads (Sandbox.Load), so leaves and bindings always rebuild against the living world. All verified
in-game, incl. reload survival. Wonders ride the same machinery — see Wonder-Spike.md.
Surface maps are per-entry, not blanket (2026-08-08 regression + fix, verified on both districts): entries whose bake shipped normal/rough atlases bind them (plus neutral metallic/AO) — the temple's verified combo. Entries without baked maps keep the donor material's own vanilla maps under the injected albedo — the reactor's verified look. The stability pass briefly bound flat neutral maps on every entry; on the reactor's grey industrial palette that read as chrome domes and near-black walls ("texture got scrambled") while the temple, which had real maps, was unaffected — a reminder that a shared-code change verified on one district is not verified on the axis.
This was chased from ~8 angles that all looked like a wall before the recipe above cracked it:
- Inject a material via
SetChannel→ vanishes (no selector GPU context). - Swap the affinity (runtime or data) → null unless the visual-levels/DistrictState also matches (the key insight).
- Every district material type (culture selector, National-Project emitter, wonder scaffolding) is a context-gated composite — there is no standalone plain drawer to point at.
- Adding an affinity→material mapping (
AssetReferenceDatabaseContent) has no mod precedent (game-core).
The unlock was: don't hand the game a material — reuse its own leaf Element and swap only the fxMesh, with a
bone-free lean mesh that fits (or grow) the shared buffer.
Decompiled reference: C:\tmp\reactor\ — Selector.cs, Emitter.cs, Element.cs, DescElem.cs, GenDesc.cs,
MeshMgr.cs, FxMesh.cs.
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