Spawners, population and reservations
Worlds fill themselves with creatures under provable caps, never in any
player’s view, and agents claim what they are about to use so two of them
never fetch the same drop. The design and its rationale are in
Talos E (docs/superpowers/specs/2026-09-27-talos-e-coordination-population-design.md)
§8 and §11; this page is the current contract for what phase E1 built (the
“hunt and fetch” slice: one spawner region with caps and the Ambient
policy, and the minimal reservation the pet uses).
Source: manifold_ai::coord (crates/manifold-ai/src/coord/) (wasm-safe
logic: registries, components, caps, the rate-limit queue, despawn bands,
traces, the reservation table, the spawn atoms) and the server wiring in
manifold_server_state::coord (crates/manifold-server-state/src/coord/)
(the three systems, visibility, persistence metadata, console commands).
Tests: the unit tests in manifold-ai, crates/manifold-ai/tests/determinism_guard.rs,
the coord::tests module of manifold-server-state and
game/flagship/tests/spawn_content.rs.
Data
Three server-side registries (the data stage):
| Registry | Directory | Entry |
|---|---|---|
manifold-ai:spawn-category | spawn_categories/ | SpawnCategory(id, per_player, per_region, counts_toward_hard_cap) |
manifold-ai:spawn-rule | spawn_rules/ | SpawnRule(id, archetype, category, weight, conditions, density: (per_region, category_per_region), distance: (min, max), persistence) |
manifold-ai:spawner | spawners/ | a content-placed spawner: Spawner(id, shape: Region(min, max), source: Rules([...]), policy, max_alive, cadence, attempts_per_eval, respawn: (delay, catch_up_max), leash, enabled) |
References (archetype, category, the rules) are checked with file and
line. Durations are ticks (160) or seconds ("4s"). conditions is a
gameplay condition in the spawn context set. E1 refuses the
Encounter and Persistent policies, and every shape and source but
Region and Rules, naming the phase that brings them.
Spawn atoms. A condition atom sees only its evaluation context, so the
spawner measures the cell first and passes what it measured as set-by-caller
values (SpawnFacts on A’s SpawnParams and SpawnContext); archetype
variant conditions read the same facts. Registered through
gameplay_extensions():
| Atom | Arguments | Holds when |
|---|---|---|
manifold:sky_light | at_least | the cell’s sky light is at least that (15 under open sky, 0 under an opaque block within 64 blocks: the server keeps no light maps) |
manifold:surface_block | block | the block under the creature’s feet has that key (checked against the world’s blocks at load) |
Spawners
A spawner is an entity with manifold-ai:spawner (Spawner) and
manifold-ai:spawner-state (SpawnerState: pending respawns as absolute
ticks), both server-only and persisted, with Persistence::Saved. The first
spawner pass after boot (after restore) creates one entity per content
placement that has none; Spawner.placement remembers the key, so a
restored spawner is never made twice.
Each creature carries SpawnedBy(spawner) (manifold-ai:spawned-by:
exclusive, Remove on delete, Keep on unload, persisted as a link), whose
reverse index is the spawner’s alive count; SpawnOrigin (policy, rule,
category, the attributed player, spawn tick; transient); and Home (anchor
cell, the spawner’s leash or the shape’s extent; persisted).
SpawnerSystem runs in the AI block (after ResolveActionHandles, before
navigation) every tick:
- content placements become spawner entities (once);
- deaths in last tick’s
CombatEvents.diedschedulenow + delayon their spawner (a creature-to-spawner map covers corpses despawned in the same tick); - every enabled spawner whose shape a client subscribes to asks navigation
to build around it (
ServerNav.focus_requests); - each due spawner (its cadence, staggered by durable id) evaluates: the
allowance (
max_aliveminus the living, the queued and respawns not yet due, at mostcatch_up_max); nothing if no client subscribes to the shape; a rule by weight (DerivedRandom, sitemanifold-ai:spawn.rule); C’sfind_spawn_cellsover the box, at leastdistance.minfrom every player and hidden from every subscribed player; then per cell, in order, the rule’s conditions and the caps; accepted cells join the queue; - the queue drains at each space’s rate limit through
spawn_archetype_batch, in request order (tick, spawner id, ordinal).
Spawns are placed at the tick phase’s flush, so an archetype’s anim-setup
hook gives the creature its ServerAnim in the flush that creates it.
Never in view. The eyes are every player whose client subscribes to
the shape’s interest regions, however far (entity scope is a cube of
64-block regions reaching about 192 blocks). A viewer sees a point inside
its 110° view cone (from its yaw and pitch) that one voxel ray from its eye
reaches. ViewerSight is C’s LineOfSight over the world’s blocks until D’s
line-of-sight service exists; /ai population shows the rays cast this tick.
A queued spawn that waited re-checks that its cell is still standable and
hidden, and is dropped otherwise (the next evaluation places again).
Caps and the rate limit
| Cap | Scope | Default |
|---|---|---|
| Local | creatures of a category in the 27 regions around a player; a candidate passes if any player that can draw it is under | category per_player (10) |
| Region | creatures of a rule, and of its category, in the candidate’s region | rule density, category per_region |
| Ambient | Ambient creatures in the space | 256 (PopulationConfig) |
| Fair share | above 80 % of the ambient cap, the attributed player (the nearest subscriber) keeps at most cap × 0.8 / players |
The census counts living ambient creatures and queued spawns when a spawner
is due. PopulationConfig holds each space’s settings: the ambient cap, the
encounter ceiling (1,000; directors spend it, E3) and the spawn rate limit
(16 per tick).
Despawning
DespawnSystem checks each Ambient creature once a second (staggered by
durable id); corpses are B’s.
| Situation | Rule |
|---|---|
| no client subscribes to its region (whether or not its chunk is resident) | despawn now |
| within 32 blocks of a player | never |
engaged: a non-empty Targets, OwnedBy or a held reservation | never |
| idle less than 30 s | not yet |
| visible to a subscribed player (the placement test) | not while seen |
| otherwise | 2.5 % per check (DerivedRandom, site manifold-ai:despawn) |
Ambient creatures are Persistence::Session: after a restart they are
gone, and the saved spawner spawns afresh by its rules. For the same reason
every band despawns as DespawnReason::Destroyed, including the one for a
chunk that is no longer resident: the creature never returns under its id,
so it is not Unloaded (entity core (docs/architecture/entity-core.md)).
Reservations
ReservationTable (a resource) holds claims keyed by target, slot
(Whole, Slot, Job, Cell) and layer. A task posts
ReservationRequests::claim(ClaimRequest::whole(holder, target)), renew
or release; ReservationSystem resolves them every tick in one batch:
releases, renewals, expiry (default 30 s unless renewed), then claims, the
best score winning each target and ties going to the lower durable id.
At most four claims per holder per tick (Throttled); a lost contest backs
off 2 s, doubling to 16 s. A target’s capacity is its Reservable
component, else 1. Outcomes (Granted, Renewed, Released,
Denied(..), Lost(Expired | TargetGone | HolderGone)) are in
ReservationOutcomes after the pass. The holder’s HeldReservations
mirrors the table; its remove hook releases everything when the holder
despawns, and the pass ends claims whose target despawned or whose holder
died. Reservations are not saved.
Traces and console
CoordTraces keeps the last 32 records per spawner (attempts with the rule
weights, each rejected cell and why, queue waits, spawns, despawns with
their band) and per reservation holder. Admin commands:
| Command | Shows or does |
|---|---|
/entity spawner list | every spawner, its shape, rules, alive count and queue |
/entity spawner info <#id or key> | its state and recent trace |
/entity spawner eval <spawner> | evaluates it now |
/entity spawner reset <spawner> | clears pending respawns and its queue |
/ai reservations [#id] | claims, optionally of one holder or target, with its trace |
/ai population | per space: ambient count and cap, category counts, queue, rate limit, rays |
Limits
PlayerAnchoredandPointsshapes, herds,EncounterandPersistentpolicies, promotion, directors anddoAmbientSpawningare later phases.- Visibility rays are counted but not charged to D’s AI budget, and the traces are always-on rings until D’s decision trace sink exists.
- No biome atom: the server has no biome source.