Architecture Overview¶
This page illustrates how Civillis decides whether a hostile mob is allowed to spawn, and how the major systems — civilization scoring, decay, Podium of Spawning, and monster heads — interact.
Spawn Decision Workflow¶
Every time Minecraft's natural spawn cycle tries to place a hostile mob, Civillis intercepts and runs the following decision pipeline:
flowchart TD
SpawnAttempt["Hostile Mob Spawn Attempt"]
NaturalCheck{Natural spawn?}
ShrineQuery["Query active spawning podiums"]
HasNearby{Inside podium pocket?}
HeadNearby["SHRINE_NEARBY: Allow spawn"]
ConvertCheck{3+ skulls?}
ConvertMob["Roll conversion"]
HeadAttract{Distant podium attraction?}
HeadSuppress["SHRINE_SUPPRESS: Block spawn"]
Whitelist{Spawn-gate whitelist?}
ZonePolicy{Structure allows spawn?}
ScoreQuery["Query civilization score"]
ApplyDecay["Apply decay factor"]
ThresholdCheck{Score vs thresholds}
Allow["Allow spawn"]
Probabilistic["Probabilistic block"]
Block["Block spawn"]
SpawnAttempt --> NaturalCheck
NaturalCheck -->|No: spawner / egg / summon| Allow
NaturalCheck -->|Yes| ShrineQuery
ShrineQuery --> HasNearby
HasNearby -->|Yes| HeadNearby
HeadNearby --> ConvertCheck
ConvertCheck -->|Yes| ConvertMob
ConvertCheck -->|No| Allow
ConvertMob --> Allow
HasNearby -->|No| HeadAttract
HeadAttract -->|Suppressed by distant podiums| HeadSuppress
HeadAttract -->|Not suppressed| Whitelist
Whitelist -->|Yes| Allow
Whitelist -->|No| ZonePolicy
ZonePolicy -->|Yes| Allow
ZonePolicy -->|No| ScoreQuery
ScoreQuery --> ApplyDecay
ApplyDecay --> ThresholdCheck
ThresholdCheck -->|Below low threshold| Allow
ThresholdCheck -->|Between thresholds| Probabilistic
ThresholdCheck -->|Above mid threshold| Block
Non-natural spawns (spawn eggs, spawners, /summon, reinforcements) bypass the pipeline entirely and always succeed.
Datapack hooks: Before civilization scoring runs, dimension policies (civil_dimension_policies) can disable civilization logic (and optionally head / podium stages) per dimension. Spawn gate entity lists (civil_spawn_gate_entities) can force extra entity types through the gate or whitelist selected types. Zone policies (civil_zone_policies) can allow hostile spawns inside matched vanilla structures regardless of nearby score. See Structure spawn rules, Dimension rules, and Data-Driven Registries.
Persistence (NBT storage)¶
Civillis does not use an embedded SQL database. Since the 1.2.0 storage rewrite, civilization L1 shard scores, ServerClock, decay presence data, mob head positions, Podium of Undying anchors, and Podium of Spawning anchors are persisted through a dedicated NBT-based storage layer with asynchronous I/O. Dirty data is staged in memory and written in a unified flush about every 30 seconds (600 ticks), not as a synchronous follow-up to every cache mutation. On cold paths, bulk region load pulls an entire on-disk region into L1 once; that region is then marked activated so repeated work does not re-read the same NBT bulk until the next flush deactivates it after persisting. Result shards remain derived cache in memory and are rebuilt from L1 when needed.
World switches keep each world's data isolated; see the changelog for storage-related upgrade notes.
Decay scheduling and prefetch¶
Outer-zone decay is advanced by a player-aware prefetch / round-robin system so work tracks where players actually patrol. Wilderness is cheap: the engine avoids spending decay budget on areas that are not meaningfully tied to player presence, while civilized frontiers stay consistent with the decay model described below.
Civilization Score¶
The score represents how "civilized" an area is. It is computed once per detection area and then kept up to date incrementally:
flowchart TD
Blocks["Recognized blocks in world"]
Palette{"Target blocks in chunk palette?"}
SkipChunk["Skip: score = 0"]
Weight["Scan section, look up weight per block"]
VCScore["Per-voxel-chunk score (capped at 1.0)"]
Aggregate["Distance-weighted aggregation<br/>across all chunks in detection range"]
Score["Cached civilization score"]
Change["Block placed or broken"]
Recompute["Recompute affected voxel chunk"]
Delta["Delta = new score - old score"]
DeltaZero{Delta = 0?}
Propagate["Apply delta × distance coefficient<br/>to each overlapping cached result"]
Blocks --> Palette
Palette -->|No| SkipChunk --> VCScore
Palette -->|Yes| Weight --> VCScore
VCScore --> Aggregate --> Score
Change --> Recompute --> Delta --> DeltaZero
DeltaZero -->|Yes| Score
DeltaZero -->|No| Propagate --> Score
- Each 16³ voxel chunk scores the weighted sum of recognized blocks inside it, capped at 1.0
- The detection range (default 240×240×48 blocks) defines how many chunks are aggregated
- After the initial computation, block changes only recompute the single affected chunk and propagate the difference — the cached score is never fully recomputed
Decay Integration¶
Civilization protection is not permanent. The decay system modulates the outer-zone contribution of the civilization score based on player presence:
flowchart TD
ScoreComputed["Raw score computed"]
SplitZones["Split: core zone + outer zone"]
CoreScore["Core: always full strength"]
OuterScore["Outer: modulated by decay"]
PresenceCheck["When was this area last visited?"]
GracePeriod{Within grace period?}
FullOuter["Outer: full strength"]
DecayApply["Outer: exponential decay applied"]
FloorCheck["Clamped to decay floor"]
CombinedScore["Effective score = core + decayed outer"]
ScoreComputed --> SplitZones
SplitZones --> CoreScore
SplitZones --> OuterScore
OuterScore --> PresenceCheck
PresenceCheck --> GracePeriod
GracePeriod -->|Yes| FullOuter
GracePeriod -->|No| DecayApply
DecayApply --> FloorCheck
FloorCheck --> CombinedScore
FullOuter --> CombinedScore
CoreScore --> CombinedScore
When a player returns, their presence gradually advances the recorded visit time, restoring the outer zone contribution step by step.
Podium of Spawning / Monster Head Interaction¶
Active Podiums of Spawning operate on a separate pathway that runs before the civilization score is even consulted. Mob heads inside the podium pocket provide the type pool and scaling inputs:
flowchart TD
SpawnPos["Spawn position"]
ScanHeads["Query indexed podium buckets near spawn"]
DimFilter["Filter by dimension / head mechanics"]
LocalZone{Inside active podium pocket?}
AllowSpawn["SHRINE_NEARBY: Allow spawn"]
CountHeads{"3+ eligible heads in pocket?"}
NoConvert["Spawn original mob"]
RollConvert["Roll conversion probability"]
ConvertSuccess{Roll succeeds?}
SpawnConverted["Spawn as converted type"]
SpawnOriginal["Spawn original mob"]
DistantCalc["Evaluate podiums within attraction window"]
SuppressRoll{Suppression roll succeeds?}
BlockSpawn["SHRINE_SUPPRESS: Block spawn"]
ContinueToCiv["Continue to civilization scoring"]
SpawnPos --> ScanHeads --> DimFilter --> LocalZone
LocalZone -->|Yes| AllowSpawn --> CountHeads
CountHeads -->|No| NoConvert
CountHeads -->|Yes| RollConvert --> ConvertSuccess
ConvertSuccess -->|Yes| SpawnConverted
ConvertSuccess -->|No| SpawnOriginal
LocalZone -->|No| DistantCalc --> SuppressRoll
SuppressRoll -->|Yes| BlockSpawn
SuppressRoll -->|No| ContinueToCiv
- Skulls restricted to specific dimensions (e.g., wither skeleton skulls -> Nether only) are filtered out before they affect conversion or attraction
- Podium pockets bypass civilization locally; heads inside the pocket shape the mob type pool
- Conversion probability scales with skull count; converted mobs bypass this pipeline on their own spawn to prevent recursion
- Distant suppression is range-bounded and index-backed: only nearby active podiums are considered
Mob Flee Behavior Layer¶
Mob Flee AI runs as a post-spawn behavior layer for existing hostile mobs. It does not replace spawn gating; it complements it.
- Spawn gating decides whether a new hostile mob is allowed to appear
- Mob Flee AI decides whether an already-existing hostile mob should retreat from civilization pressure
High-pressure cores get panic-like retreat (including possible combat disengagement); lighter pressure produces outward drift.
flowchart TD
Tick["Periodic evaluation per mob"]
Enabled{"Mob Flee AI enabled?"}
HeadZone{"Inside local head force-allow zone?"}
Score["Read local civilization score"]
GreenLine["greenLine = spawnThresholdMid"]
AboveGreen{score >= greenLine?}
CombatLine["combatLine = greenLine + (1-greenLine) * combatFleeRatio"]
ModeCheck{"Current state"}
IdleBranch["IDLE mode (no target)"]
CombatBranch["COMBAT_PANIC mode (has target)"]
IdleZone{score < combatLine?}
IdleProb["P_idle = (score - greenLine) / (combatLine - greenLine)"]
IdleRoll{"Roll < P_idle ?"}
IdleForce["score >= combatLine -> guaranteed idle flee start"]
CombatGate{score >= combatLine?}
CombatProb["P_panic = ((score - combatLine)/(1-combatLine)) * (1-greenLine)"]
CombatRoll{"Roll < P_panic ?"}
FindTargetIdle["Choose flee target (idle)<br/>1) nearby head-zone target<br/>2) 8-direction gradient to lower score"]
FindTargetPanic["Choose flee target (panic)<br/>1) nearby head-zone target<br/>2) 8-direction gradient to lower score"]
StartIdle["Start IDLE flee"]
StartPanic["Start COMBAT panic<br/>clear target, panic burst"]
ContinueIdle["Continue normal AI (idle branch)"]
ContinueCombat["Continue normal AI (combat branch)"]
Continue["Continue normal AI"]
Tick --> Enabled
Enabled -->|No| Continue
Enabled -->|Yes| HeadZone
HeadZone -->|Yes| Continue
HeadZone -->|No| Score --> GreenLine --> AboveGreen
AboveGreen -->|No| Continue
AboveGreen -->|Yes| CombatLine --> ModeCheck
ModeCheck -->|No attack target| IdleBranch --> IdleZone
IdleZone -->|Yes| IdleProb --> IdleRoll
IdleRoll -->|No| ContinueIdle
IdleRoll -->|Yes| FindTargetIdle --> StartIdle
IdleZone -->|No| IdleForce --> FindTargetIdle
ModeCheck -->|Has attack target| CombatBranch --> CombatGate
CombatGate -->|No| ContinueCombat
CombatGate -->|Yes| CombatProb --> CombatRoll
CombatRoll -->|No| ContinueCombat
CombatRoll -->|Yes| FindTargetPanic --> StartPanic
ContinueIdle --> Continue
ContinueCombat --> Continue
Registry Loading & Injection¶
The mod's rules are largely data-driven. JSON registries under data/<namespace>/… load at startup and on /reload:
flowchart TD
subgraph Load [Datapack Loading]
BW["civil_blocks"]
HT["civil_heads"]
ZP["civil_zone_policies"]
DP["civil_dimension_policies"]
Merge["Merge and resolve overrides"]
BW --> Merge
HT --> Merge
ZP --> Merge
DP --> Merge
end
Merge -->|block weights| ScoreEngine["Civilization Score Engine"]
Merge -->|head types| HeadTracker["Head Tracker"]
Merge -->|structure rules| ZonePolicies["Zone policy eval"]
Merge -->|per dimension| DimPolicies["Dimension policy eval"]
DimPolicies --> ScoreEngine
ZonePolicies --> SpawnDecision["Spawn Decision"]
ScoreEngine -->|"O(1) score lookup"| SpawnDecision
HeadTracker -->|"proximity + conversion"| SpawnDecision
SpawnDecision --> Outcome["allow / block / convert"]
- civil_blocks — recognized blocks and weights for scoring
- civil_heads — skull types, dimensions, conversion flags
- civil_zone_policies — structure-tagged spawn exceptions (e.g. monuments)
- civil_dimension_policies — per-dimension toggles for civilization and head mechanics
Modpacks can add, modify, or replace entries without touching mod code. Details: Data-Driven Registries.