-
Notifications
You must be signed in to change notification settings - Fork 0
District Footprint Investigation
Auto-generated from the repo docs by
tools/sync_wiki.sh— edit the source Markdown in the repo, not this wiki page.
📁 ARCHIVED NOTE — frozen 2026-08-16, not maintained. The footprint spike, kept for the mechanism it maps. Current reference: District-Dedicated-Visual.md. Nothing below is current instructions.
Goal: a custom district (the breeder reactor) should show a persistent top-down footprint at strategic zoom, the way vanilla districts do, instead of shrinking away awkwardly.
What the footprint actually is: the vanilla city-map decal system — texture decals (Decal_CityMap_<biome>_*, a
1194-material global FxEvolverDescriptorLevelBuildDecal). For a district they live inside channel [0]'s
FxEvolverMaterialLevelBuildSelector, interleaved with the building geometry — not a separate channel. The plbc has
effectively one composited level-build content channel (mainLevelBuildComponantLayer), so building + footprint share it.
Why it was missing on the reactor: our isolate injection replaces channel [0]'s whole selector with one private leaf,
amputating the decal subtree. It was never a missing asset, an unreachable system, or a GPU gate (that conclusion came from
pre-swap dumps + the reactor's isolate=true registry flag silently overriding every "global" test — see the audit log below).
What renders the footprint: keeping the selector. Global mode keeps it → footprint renders (verified in-game + dump: 231 decals / 120 rendering, identical to native). But global swaps the shared Industry selector, so every Industry tile becomes a reactor.
Per-tile fix (built, on the spike branch): a private deep-clone of channel [0]'s selector (DeepCloneMat +
PointTileAtClonedSelector). It works for close-zoom + far-zoom: footprint renders on only the reactor tile, our
reactor mesh wears our albedo (one shared private FxOutputLayer), and the dome count is tunable. Every reusable building
block exists (BuildPrivateLeaf, CollectLeaves, LoadFxMaterial, ClonePrivateOutputLayer, the texture path).
The wall — mid-zoom LOD. A district renders different geometry per zoom band: close = detailed buildings (per-tile
cache/emitter-items — cloneable), mid = a lower-detail LOD set that resolves through the selector's pairs table into
SHARED materials (not cloneable), far = the decals. A post-swap dump proved it: of 483 visible elements only 75 were
ours, ~408 were shared donor. Those shared LOD elements can't be privatized — repointing is impossible (GUIDs), a per-frame
catch tanked perf and still missed them, and global-swapping them would reactor-ify every Industry district's mid-zoom. So
close-zoom is per-tile-controllable; mid-zoom LOD is shared and is not. That's the architectural limit of runtime
injection here.
A clean cross-zoom result cannot come from more runtime selector surgery (the mid-zoom LOD is shared). It needs the reactor to own its geometry at every LOD band via the content pipeline, not borrow Industry's shared selector:
-
Dedicated district visual for the reactor — its own selector/affinity so it is NOT sharing Industry's
pairs/LOD materials. This is new authoring capability the framework does not have yet (today the DistrictFactory injects a mesh into an existing selector; it does not author a new affinity/selector). Feasibility spike: can we clone an affinity's selector per district-type (shared by all reactors, not per-tile) and register it as the reactor's own visual, so a one-time global swap on it stays scoped to reactors AND covers the LOD bands? - Reactor LOD meshes — the game's LOD system needs our geometry at close/mid/far. Either bake real LOD levels, or make every LOD band resolve to our single mesh (acceptable for a wonder). This is where the mid-zoom donor is truly fixed — the LOD assets are ours, not shared donor GUIDs.
- Footprint via the affinity — keep the city-map decals from the borrowed affinity (they're per-hex, tile-agnostic), or author a decal set for the dedicated affinity.
-
Fallback that already ships: the plain isolate reactor (one clean building) +
ConstructibleVisualAffinity = Base_Industrymakes it disappear cleanly at strategic zoom — which resolved the original complaint (the awkward shrink). The footprint is the enhancement on top.
Current state: UseDeepClone=false → the shipped clean single reactor (playable, no footprint). The deep-clone is parked
on the spike branch with all knobs, for whoever builds the content-pipeline path above.
Status: ⭐ SOLVED (mechanism). The footprint is the city-map decal system living in channel [0]'s Selector alongside
the building. ISOLATE mode was deleting it — it replaces the whole channel-[0] selector with our single private leaf,
so the decal subtree (231 drawers, 120 rendering) is amputated. GLOBAL mode keeps the selector and the footprint
renders — verified in-game (zoom out → footprint visible) AND by post-swap dump (231 decals / 120 rendering preserved,
identical to native). The earlier "GPU selection gate" conclusion was WRONG: the reactor's registry entry is isolate=true,
which silently overrode every DistrictIsolate=false "global" test, so global was never actually tested on the reactor
until now. The fix is the original instinct — clone the selector (private to the tile), swap the building mesh inside
the clone, keep the decals — giving per-tile scope (not "all Industry became reactors") WITH the footprint.
Superseded below: the "CONCLUSIVE — gate is BELOW C#" and "ShaderDump next route" sections were built on pre-swap dumps
(the [Tree] dump fired before the per-frame swap) and the isolate override — both artifacts. The decals were removed, not
gated. Kept for the audit trail; do not act on them.
Branch: spike/district-footprint. Not on master — master carries only the shipped wins below.
The goal: a custom district (the breeder reactor) should show a persistent top-down footprint silhouette at strategic zoom / in battle, the way vanilla districts do — instead of just vanishing when the building demotes.
This was a long, branching hunt. This document records where the footprint actually lives, the complete list of falsified leads (so nobody re-walks them), the wins the hunt produced along the way, and the concrete next steps.
Enumerating FxManager components (GetFxComponent(int) / FxComponentCount, 21 components) surfaced a third
level-build material type, alongside the ones we already knew:
| Material | Role | How we relate to it |
|---|---|---|
FxEvolverMaterialLevelBuildSelector |
the close-up building | this is what our mesh injection replaces |
FxEvolverMaterialLevelBuildImpostor |
vegetation billboards (SpeedTree) | ruled out — 64 entries, all Veget_*/POI_*
|
FxEvolverMaterialLevelBuildDecal |
the city-map footprint | texture-based decal on the ground |
The decal:
- Is texture-based, not mesh-based:
decalMeshis empty; the footprint lives inlayer0/1/2(FxLevelBuildDecalTextureEntryProperty) — a projected decal texture. - Is not in the district's plbc channels. Dumping the reactor's plbc:
[0]building (our leaf),[1]emitter→FxEvolverMaterialLevelBuildMatching,[2]null,[3]emitter→Selectors. No decal. - Lives in a global singleton
FxEvolverDescriptorLevelBuildDecal(reach via its staticGetInstance(bool)), holding 1194 decal materials namedDecal_CityMap_<biome>_<category>_<style>andDecal_CityBricks_*— Industry / Money / Science / Garden / PublicOrder × Cold / Hot / Temperate. e.g.Decal_CityMap_Industry_Gravel_01,Decal_CityMap_Fims_Industry_SimpleShape_16,Decal_CityMap_Industry_Trash_04.
So the vanilla footprint assets exist and are named — the footprint is a strategic-map decal a district selects by biome × category and draws when it demotes.
Dumping every district's plbc material tree (DumpAnyDistrictTree — recurses emitter items + selector cache) settled
it beyond doubt:
-
Vanilla district (e.g.
Extension_Base_Industry) = 1 channel, whose topFxEvolverMaterialLevelBuildSelectorholds a deep tree of nested Selectors/Emitters that includes theFxEvolverMaterialLevelBuildDecaldrawers — the footprints. -
Reactor NATIVE (
DistrictRepoint=false, affinityBase_Industry) = identical to vanilla Industry: 1 channel, the same deep tree, 19 decal nodes present. The footprint was there all along. - Reactor WITH our isolate injection = 4 channels, ZERO decals.
PointTileAtPrivateLeaf does CollectLeaves(selector) → clone ONE building leaf → force that leaf onto the channel. It
replaces the entire selector — deep tree, decals and all — with a single building leaf, every frame. We were
amputating the footprint subtree ourselves. It was never a missing asset or an unreachable system.
-
Clone the selector (
Instantiate). ItsNonSerializedruntime cache resets, so onLoadit re-builds its full tree fresh and private — decals included. -
Swap only the building leaves'
fxMeshinside the clone. BuildingElementleaves carryfxMesh;Decalleaves carrydecalMesh—CollectLeavesalready keys onfxMesh, so decals are untouched by construction. - Force the cloned selector onto the channel (instead of a lone leaf).
Result: custom building up close, vanilla footprint decals survive and draw at strategic zoom, still scoped to our tile
(private clone). This is a change to the injection core (BuildPrivateLeaf/PointTileAtPrivateLeaf — clone one level up).
Re-verified global mode (DistrictIsolate=false, Industry affinity, zoomed out): still no footprint. This is a key
refinement: ApplyLeaves only swaps fxMesh on building leaves, and CollectLeaves skips decals (they carry
decalMesh), so our swap never touches the decal leaves — yet they stop rendering. So it's not merely the
selector-replacement; swapping the building mesh drops the decals indirectly. Prime suspects:
-
GPU mesh buffer eviction/collision — registering our large custom mesh (via the building leaf's
Load→GetMeshIndex) evicts or reindexes the decal meshes (the shared 100k-vert / 256-mesh per-layer buffer). - A presentation-state gate — the decal renders only when the building is in a native state our swap changes.
Refined again — the decals are PRESENT but GATED, and texture-based. In global mode (swap active) the reactor's tree
still has all 19 decal nodes (identical count to vanilla Industry) — so the swap doesn't remove or evict the decal
drawers; they're right there and still don't render. And every decal's decalMesh is Null — these are
texture-based decals (the footprint is the layer0 decal texture on a shared quad, not a per-decal mesh). So the
building-mesh swap gates off the decals' rendering while leaving them in the tree.
Open — the gating mechanism. Why do present, texture-based decals stop drawing when we swap the building mesh? Leading
suspects: (a) the decals share the building's output layer / render pass, and swapping the building mesh (+ Load)
disturbs the shared layer's state; (b) the decal draw is gated by the building's demotion/LOD state, which our custom
mesh (lodData=0, no LOD chain) never reaches; (c) our mesh's Load re-registers content in a way that invalidates the
decals' render data (FxComponentBlobDecalRenderDataBuffer). This is a deeper rendering interaction than a data edit and
warrants a focused implementation session — the clone-the-selector fix must preserve the building's native LOD/layer/
demotion state so the decal draw isn't gated off.
Bottom line: the footprint is definitively the city-map decal system, present in the reactor's native tree; our injection breaks its rendering (not its presence in global mode). The remaining work is understanding the render gate, not finding the footprint.
A three-way A/B settled it. In global mode (Industry selector swapped; Food/Science untouched), zoom out and compare
tiles: Food/Science show a footprint, the reactor does not — so the decals are the footprint, and the swap gates
only the swapped districts. Then we dumped each decal's full render-readiness state (visualOutput.OutputLayerIndex,
LoadedOutputLayer, its Atlas, the layerEntryCount/levelBuildDecalRenderDataEntryIndex it wrote). Result — the
reactor's gated decals are byte-identical to Food/Science's working ones:
reactor (GATED): outLayerIdx=792 loadedLayer=True atlas=True layerEntryCount=1 renderDataIdx=1044
Food (WORKS): outLayerIdx=792 loadedLayer=True atlas=True layerEntryCount=1 renderDataIdx=913
Science (WORKS): outLayerIdx=792 loadedLayer=True atlas=True layerEntryCount=1 renderDataIdx=1127
The reactor's decals are fully loaded, atlas-resolved, and have written their render-data entries — indistinguishable from a working district. So no C#-reachable state differs between footprint-renders and footprint-gated. The decal is render-ready; something below C# just doesn't place it at the reactor's hexagon.
That something is the GPU evolve compute shader. Per-hexagon content lives in FxComponentLevelBuildContent's
levelBuildContentCB (per-hexagon × layerOutputCount*2=16 layers); the selector's evolve dispatch
(FxComponentLevelBuildParticleAdder → AddFIMSParticle / FxComponentTerrain.AfterOneEvolveDelegate) reads each
element/selector's BBoxMin/BBoxMax (FxLevelBuildSelectorGPUData, written in WriteToGPUData ~41792) against the
camera to decide, per hexagon, whether to place the building element or demote to the sibling decal. Our custom
reactor mesh has a very different bounding box than the donor missile silo, so the size-driven element↔decal switch lands
differently — the building fades out (affinity opacity), but the selector still doesn't hand the hexagon to the decal.
This is the C#/GPU boundary. C# reflection is exhausted — every reachable field is identical between the two cases.
Tried the cheap version first — keep isolate's clean single reactor on the main channel and re-host a clone of the native
selector on a free channel so its decals still draw. Built (PreserveFootprintChannel + LoadFxMaterial), verified it
cloned the right source (CityMapSelector_Industry_00) and hosted it on channel 2. Result: renders nothing — close-up
shows no donor buildings from it either. Reason: the plbc has effectively one composited level-build content channel
(mainLevelBuildComponantLayer; the native reactor dump reads "1 channel(s)" — the extra channels seen post-swap are
not composited level-build layers). Decals cannot live on a separate channel; the building and the decals must share the
one main selector. Code left in place but disabled (call commented in TickDistrictMeshSwap); LoadFxMaterial is
reusable for the real fix. So the footprint genuinely needs the deep-clone/privatize path below — there is no side-channel
shortcut.
Implemented the private deep-clone (DeepCloneMat + PointTileAtClonedSelector, UseDeepClone). It worked for the
hard goals and is a real result:
- Footprint renders at strategic zoom, scoped to only the reactor tile (1657 nodes privatized, other Industry untouched).
-
Texture correct: all swapped reactor slots share ONE private
FxOutputLayer(e.deepLayer,ClonePrivateOutputLayer) with our albedo bound via the existingDistrictApplyTexture/BindAlbedopath — coherent reactor sheet, no donor garble. -
Count tunable: swap large slots (bbox ≥
DeepCloneBuildingMinSize), hide small props (size→0), thin the rest (DeepCloneKeepEvery, keep 1-in-N proportionally so the visible subset thins evenly, not a broken walk-order cap).
The wall — mid-zoom shows the donor. The district renders different geometry per zoom band (close = detailed buildings,
mid = a lower-detail LOD set, far = footprint decals). A post-swap element dump proved it: of 483 visible elements only 75
were ours (meshIdx=4607); ~408 were donor meshes we never swapped. Those mid-LOD elements resolve through the
selector's pairs variant table into SHARED materials, not the per-tile cache/emitter-items we clone. They cannot be
privatized: repointing them is impossible (pairs are GUIDs, not instances), a per-frame catch (EnsurePrivate) walking the
1657-node tree tanked performance and still missed them (they're not in the cache/items paths), and global-swapping the
shared LOD materials would turn every Industry district's mid-zoom into reactors. That's an architectural wall for the
runtime-injection approach: close-zoom is per-tile (cloneable), mid-zoom LOD is shared (not).
Status: parked (UseDeepClone=false, back to the shipped clean single reactor). The deep-clone code is committed for
whoever picks this up. A clean cross-zoom result likely needs a real content-pipeline district (custom mesh with its own
baked LODs + a dedicated affinity), not runtime selector surgery.
Verified structure: the reactor's plbc channel [0] is an FxEvolverMaterialLevelBuildSelector that holds both the
building elements and all 231 decals (the footprint). Isolate replaces that whole channel material with our one private
leaf → decals amputated. Global keeps the selector but swaps the shared leaves → footprint works but every Industry
tile becomes a reactor. The fix threads the needle:
-
Clone channel [0]'s selector (
UnityEngine.Object.Instantiate— private copy; NonSerialized runtime cache resets, rebuilds on Load). Its serializedpairs/defaultMaterialGUIDs and the decal subtree come along. -
Privatize the building leaves inside the clone. Cloning the selector alone is NOT enough — its element leaves resolve
by GUID through the shared
FxEvolverMaterial.TryLoadcache, so mutating them is still global. For each building Element leaf the clone resolves (viaCollectLeaves, which keys onfxMeshand already skipsdecalMeshdecals),Instantiatea private copy (exactlyBuildPrivateLeafline 407: "a private copy — mutating it won't touch the shared leaf"), set itsfxMeshto ours (+ the private-FxOutputLayertexture treatment), and repoint the clone's cache/pairs entry at our private leaf. Leave the decal leaves as-is — shared is fine, they're unmodified. -
Force the cloned selector onto this tile's channel [0] (like
PointTileAtPrivateLeafdoes with the leaf), re-assert per frame, RefreshChannel. Reset on save-reload (ResetDistrictSessionState).
Result: only the reactor's tile uses the private clone → our building mesh + the surviving footprint decals; other Industry
tiles untouched. This is a real implementation (clone + per-leaf privatize + texture + load + per-frame + reload), but every
building block already exists (BuildPrivateLeaf, CollectLeaves, ApplyLeaves, the private-layer texture path). Design
note: the reactor tile will show our mesh at each building slot the selector picks (the "reactor-ified district" look from
the global test, but scoped to one tile) — confirm that reads well, or restrict the swap to the primary element.
-
Confirm the BBox lever— DONE, ruled out (falsified lead 13). Forcing a small element bbox did not summon the footprint.lodData(lead 12) is out too. The C#-settable per-element levers are exhausted. -
Reverse-engineer the evolve/selection compute shader with the ShaderDump toolchain (
HumankindAssetFramework/tools/ShaderDump, see shaderdump-toolchain) — dumpAddFIMSParticle/ the level-build evolve kernels (FxComponentLevelBuildParticleAdder,FxComponentTerrain.AfterOneEvolveDelegate, theResolveElevationQueryMaterial/GeoDBQueryMaterialdispatches) and read exactly how a hexagon's element-vs-decal winner is chosen, and which per-entry field our swap perturbs. This is the only route left below the C# decompile — every reachable C# field is either identical between gated/working or has been falsified. This is where a fresh session must start. -
First, fix the probe so the next element diff is trustworthy: extend
DumpMatTreeto recurse the selector'spairsvariant table +defaultMaterial(mirrorCollectLeaves), then dump the reactor's building element native vs swapped (per-districtseen, no truncation) to see precisely which GPU-uploaded field ourLoad+swap changes on the actual building (not the shared props the current dump catches). - The clone-the-selector fix (preserve decals) is necessary but not sufficient — global mode already preserves the decals and still gates them. The GPU-selection fix is the missing half.
Every one of these was a plausible suspect, checked and ruled out in-game or by decompile:
-
HgFxAnchorComponent.RenderModeEnum—{Default, GhostOk, GhostNOk}= building-placement ghost, not zoom. StayedDefaultthrough the whole zoom. -
Unity cameras —
MainViewis inert (pinned at origin, matrix eye = 0, fov 60). Mercury renders through its own render context; UnityCameraproperties don't reflect the zoom. (ImpostorCamera,AvatarCamera,Camera,MainView,UIFxCamera— none usable.) - The per-frame private-leaf force — both isolate (private leaf) and global (shared-leaf swap) modes shrink identically. Not the injection method.
-
The impostor system — reached
FxComponentImpostorManager; its descriptor holds only vegetation/POI (64Veget_*/POI_*, SpeedTree atlas). District buildings don't use it. (Re-tested at full zoom-out with a live descriptor re-read to rule out a caching miss — still zero building impostors.) -
fadeInOutMode— passinginstantAppear:truemade no difference to the zoom transition; it only governs the on-load reveal ramp. -
IconicConstructibleTypeDB —MissileSilohas a non-null entry, shared with Military/NuclearTest. Not the differentiator. -
The entire
AssetReferenceRepository— dumped all 49 databases (1D + both 2D affinity DBs). ComparedMissileSilovsBase_Industryacross every affinity-keyed cell. The only gaps areConstructibleEvents(VFX) and*/District/MainLevel2(a build-evolution stage). No footprint cell. -
The
ConstructibleVisualAffinity(the affinity swap) — swapping toBase_Industrydid not add a footprint (and still shrank via the runtime swap). Affinity doesn't drive the strategic transition. -
The channel material swap at zoom — traced: the channel is only ever
selector → our-leafand never swaps at zoom-out. The game doesn't repoint the channel; it demotes in place. - Selector-vs-leaf — global mode keeps the selector intact (swaps only the mesh inside it) and still shows no footprint. Keeping the selector's strategic behavior is not enough.
-
FxMesh.lod—FxMeshhas a reallod(Guid to a lower LOD) +lodType. Baked a far-LOD FxMesh and set it (verified in the asset). Dead runtime path: the consumerfxMeshContentLodsis never assigned anywhere in the code — declared + read, zero writes — so it's null for everyone, and the LOD chain is never used (meshIndexLod0 = uint.MaxValue,lodData = 0on our leaf). Reverted. -
FxEvolverMaterialLevelBuildElement.lodData— our swap re-resolveslodDatafrom our LOD-less FxMesh (lodType=0) to 0. But a live dump showed working Food/Science building elements also runlodData=0(only a shared decorative prop, meshIdx 547, carrieslodData=3). SolodData=0is normal and renders footprints. Not the gate. -
Element
BBox(the "cheap shot") — the element uploadsBBoxMin/BBoxMaxto the GPU selection (FxLevelBuildElementGPUData,WriteToGPUData~43350) andbboxis a settable field (useCustomBBox+Bounds). Directly tested: forceduseCustomBBox=true+Bounds(0, 0.15³)on every swapped leaf inApplyLeaves, global mode, zoomed out — no footprint appeared. So the element→decal selection is not driven by the element bbox the way we hoped. Ruled out. (Reverted.)
Probe limitation discovered — and FIXED (commit ae5cf25): DumpMatTree only recursed levelBuildItems +
fxMaterialCacheEntries.Entries, but a selector's main building lives in its pairs variant table / defaultMaterial
(which CollectLeaves handles and the dump did not). Extended the recursion to mirror CollectLeaves (TryLoadMaterial
per pair guid — cached, no explosion), switched to GF (silences the AccessTools.Field warn-spam), raised depth 6→10.
Result — the real native composition (footprint working): the reactor's native Extension_Base_BreederReactor tree is
408 building element leaves (349 distinct meshes) + 231 decals (120 writing render data) — vs the 1 the old probe
caught. That is the full footprint machinery, present and rich in the native district.
SECOND methodology catch (blocks the swapped diff): the [Tree] dump fires from DistrictApplyEntries (on
UpdateLevelBuild), which only dumps + caches the tile — it does NOT swap. The swap runs per-frame in
TickDistrictMeshSwap. UpdateLevelBuild fires before the tick swap settles, so every dump so far captured the
PRE-swap (native) tree — which is why a "native vs global-swapped" reactor dump came back byte-identical (0 mesh
diffs, 231/231 common decals). That identical result is a timing artifact, not evidence about the swapped state. To
capture the true post-swap tree, the dump must be invoked from TickDistrictMeshSwap after ApplyLeaves, or dump
e.leaves/e.privateLeaf directly. Do this first next session — then the swapped decal/element counts are finally
trustworthy.
Note this doesn't change the verdict: the decals' render-readiness was already proven byte-identical gated-vs-working, and every per-element field is falsified. The swapped-tree diff would only tell us whether the swap drops or keeps the decal drawers — the selection gate remains in the GPU kernel regardless.
The footprint works for the donor mesh and not ours, and none of these reachable, settable differences reproduces it — because it was never in any of them. It's the decal.
The footprint chase kept dragging us down untested paths, and those paths paid off:
-
Foundation plinth (
03431aa,3d9af82) — a bake knob extrudes the building footprint straight down as a concrete plinth so a district on a cliff plants on a base instead of overhanging. Cliff-verified. -
Atlas-layer texture binding (
3b18185) —BindAlbedonow binds our sheet on an atlas-managed layer (falls back to the shader's albedo slot when nothing is bound), so a custom district can wear any city affinity and keep its texture — not just the missile silo's full-texture layer. Removes the missile-silo lock-in; enables graceful non-plugin fallback (a sensible vanilla building instead of a confusing missile silo). -
Clean zoom-out — setting a city-district
ConstructibleVisualAffinity(Base_Industry) in the district definition makes it fade/disappear cleanly like a vanilla building instead of the awkward national-project shrink. This resolved the original complaint. (A plugin "back-off guard"329ad6a/507e987was kept as a harmless defensive measure, but the trace proved the affinity is what fixes the shrink, not the guard.)
Design rule banked: pick each custom district's ConstructibleVisualAffinity for the best non-plugin fallback
(closest vanilla building), not just whatever renders.
A multi-hour hunt for a "rocky texture in the centre" that also twitched. Chased (and falsified) terrain
biome, district land-clearing, exploitation-tile rendering, the terrain matcher (no exploitation→rock rule
exists — 690 matchers scanned, only 3 POI ones, all Exploitation=ShouldNotBe), a solid model base-slab (fails:
ground conforms to terrain + cliffs), and the ground material (holds now, but only paints the perimeter; a DEPOSIT
tile reverts a postfix-only override — fixed with an ApplyGroundMaterialDefinition prefix that rewrites+skips).
Actual root cause (user's instinct was right — "it came along with the footprint"): the MissileSilo footprint
donor grafts Decal_CityBricks_Industry_Gravel_* and Decal_Destroyed/Dammaged_Battlement_* (rubble on
PointOfInterest_Curiosities_OutputLayer). The rubble/POI layer draws at close 3D zoom and toggles at the
strategic↔3D boundary = the twitch. Fix: GraftFootprint filters out Gravel/CityBricks/Battlement/Destroyed/Damaged,
keeping only SchematicView. Full write-up in District-Dedicated-Visual.md.
Lesson: on close-zoom ground artifacts, dump the grafted decals ([DecalBind]) BEFORE theorising about terrain.
-
DumpDecalDescriptor()— reachesFxEvolverDescriptorLevelBuildDecalvia staticGetInstance(bool)and dumps all 1194 decal materials (name +decalMesh). -
DumpDistrictChannels()/DumpMatTree()— recurse a district's plbc channels (emitter items + selector cache) and log every material type + any nestedDecal. - Reusable, deadlock-free reach patterns:
distFxManager.GetFxComponent<T>()for Fx components; a descriptor's staticGetInstance(bool)for the global singletons.
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