Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
4.5 KiB
4.5 KiB
T2 — Presence budget: materialise few, simulate many
Read docs/living-island/README.md first, including the rules section. Depends on T1.
Goal
Decide, on a slow tick, which ledger records are physical SCR_AIGroups right
now — under a hard agent budget, without thrashing, and without the player ever
seeing a unit appear or vanish in view.
What to build
addons/core/Scripts/Game/ERR/ERR_PresenceBudget.c, again split into a pure
rule class and a service:
class ERR_PresenceRules
{
static bool WantsPhysical(float nearestObserver, bool currentlyPhysical,
float spawnBand, float despawnBand); // hysteresis
static bool DwellSatisfied(float now, float stateSince, float minimumDwell);
static int Score(ERR_ForceKind kind, int importance, float nearestObserver, bool pinned);
static bool WithinBudget(int activeAgents, int requestedAgents, int budget);
}
class ERR_PresenceBudget { ... }
Required behaviour
- Observers: use
ObserversSystem.GetObservers(out array<vector>). Bucket observers into 500 m cells (copy the pattern fromERR_HQNetwork.Add/Build); never scan all records against all observers. - Hysteresis: spawn band 600 m, despawn band 900 m (settings
presenceSpawnMetres,presenceDespawnMetres). A record inside 600 m wants to be physical; it only goes dormant past 900 m. - Dwell: minimum 30 s in a state before it may flip (
presenceDwellSeconds). - Budget:
activeAgentBudget, default 60. Count real agents, not records. When a spawn would exceed it, refuse and log — do not spawn and hope. - Eviction: when over budget, dormant the lowest-scoring physical records
first (farthest, least important, not pinned). Never evict a record that is in
combat (
SCR_AIGroupUtilityComponent.m_Perception.m_aTargetsnon-empty), that contains a player, or that is pinned by HQ. - Pinning: expose
Pin(int recordId, bool pinned)so T5 can force a record physical while HQ has it tasked near players. - Never despawn in view: if any observer is within the despawn band, the record stays physical, full stop. The dwell timer does not override this.
Materialisation
- Physical: create or re-use the group,
SetImportance(...)from the record's tier,SetLifecyclePolicy(SCR_EAIGroupLifecyclePolicy.Manual, ...)— we drive spawn/despawn, not the engine's proximity policy, because the ledger is the source of truth. - Place the group at
ERR_Actors.Ground(record.Position)— this is the only place ground height is resolved. - Dormant:
DespawnMembers(). The group entity stays alive and remembers alive and dead counts, so re-materialising restores the right strength. Write the surviving strength back into the record. - Casualties taken while physical must persist into the record, so a mauled patrol stays mauled after going dormant.
- Set
SetEliminateWhenReached(true)on ambient patrol records so that if a player walks onto a record that could not spawn because the budget was full, the area counts as cleared instead of the patrol popping in later. HookGetOnEliminatedWhenReached()to retire the record.
Acceptance criteria
- With a 60-agent budget and 30 records of 4 soldiers each, active agents never
exceed 60, and
Status()reports refusals. - A player walking a straight line past three patrol routes sees each patrol materialise before it is visible and never sees one disappear.
- A patrol that loses two members while physical comes back with the reduced strength after a dormant period.
- No record flips state more than once per 30 s in a stationary-observer test.
python3 tools/check.pypasses.
Tests to write
tests/test_presence_budget.py:
WantsPhysicalhysteresis: a record at 700 m stays in whichever state it is already in; crossing 600 m spawns; crossing 900 m dormants.DwellSatisfiedblocks a flip at 29 s and allows it at 31 s.WithinBudgetrefuses the spawn that would cross the cap, and a sequence of 20 spawn requests against a 60 budget with 4-man groups accepts exactly 15.Scoreranks a pinned distant record above an unpinned near one, and ranks importance above distance for equal pinning.
Do not
- Do not use
ProximityDrivenlifecycle for ledger records — it would let the engine fight the ledger over who owns the decision. - Do not delete group entities to go dormant;
DespawnMembers()is the mechanism that preserves strength. - Do not add a per-frame check for "is the player looking at it". Distance bands plus dwell are sufficient and cheap.