Combine Workshop addon and fix prison startup
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.
This commit is contained in:
@@ -0,0 +1,431 @@
|
||||
# Island occupation — agent brief
|
||||
|
||||
**Hand this file to a coding agent as-is.** It is self-contained: concept, rules,
|
||||
verified engine APIs, work breakdown, acceptance criteria and tests.
|
||||
|
||||
Companion files (same directory) hold the supporting infrastructure this design
|
||||
sits on: `README.md` (plan and rules), `T1-force-ledger.md`,
|
||||
`T2-presence-budget.md`, `T3-checkpoints.md`, `T4-smart-actions.md`,
|
||||
`T5-hq-integration.md`, `T6-ambient-signals.md`, `T7-performance.md`.
|
||||
If the ledger (T1) and presence budget (T2) are not yet implemented, build those
|
||||
first — this design assumes them.
|
||||
|
||||
---
|
||||
|
||||
## 1. The concept
|
||||
|
||||
The island does **not** start occupied. At mission start only the prison and its
|
||||
immediate surroundings hold troops. Everything else — the radio station, the
|
||||
helipad, the junctions, the depots — is empty ground with buildings on it.
|
||||
|
||||
Then the alarm goes off, and the enemy *moves in while the player watches*.
|
||||
Helicopters lift sections out to guard the objectives. Trucks drive to junctions
|
||||
and the soldiers get out and **build roadblocks over several minutes**. Patrols
|
||||
fan out along the roads. Over the next twenty to forty minutes the island closes
|
||||
around the player.
|
||||
|
||||
This gives three things at once:
|
||||
|
||||
1. **Atmosphere.** Watching a transport helicopter flare and unload eight
|
||||
soldiers onto the hill you were about to cross is worth more than finding
|
||||
those eight soldiers already standing there.
|
||||
2. **CPU.** Almost nothing exists at T+0. Deployments travel as ledger records
|
||||
and only become real AI near players. Start cost is near zero, and the
|
||||
steady-state cost is the same agent budget as before.
|
||||
3. **Counterplay and pressure.** Every deployment is interruptible, every loss
|
||||
is permanent, and an island left alone gets steadily harder to cross. Hiding
|
||||
is punished; moving fast is rewarded.
|
||||
|
||||
**The player must be able to change the outcome.** Shoot down the transport and
|
||||
those eight soldiers never arrive — that junction stays open for the rest of the
|
||||
round. That is the whole point of the design; protect it in every decision.
|
||||
|
||||
---
|
||||
|
||||
## 2. Non-negotiable rules
|
||||
|
||||
### Licensing
|
||||
|
||||
This project is an independent successor. The original Escapists mod and its
|
||||
`ScrtCore` / `ScrtSharedContent` dependencies are SCRT-ND licensed (an APL-ND
|
||||
variant) which **forbids derivative works**.
|
||||
|
||||
- Never copy code, configs, layouts, compositions or assets from the original
|
||||
Escapists mod, from ScrtCore/ScrtSharedContent, or from `/home/jakub/devel/escapists`.
|
||||
- Never introduce identifiers matching `ESCT*`, `SCRTC_*`, `SCRT_*`, `EAI_*`.
|
||||
`tools/check.py:39` fails the build on these.
|
||||
- Only vanilla Arma Reforger resources and resources owned by this repo may be
|
||||
referenced. Reimplementing a mechanic from scratch is fine; copying an
|
||||
implementation is not.
|
||||
|
||||
### Addon boundaries
|
||||
|
||||
Five addons only: `core`, `ai`, `intel`, `chat`, `map`. `ai`/`intel`/`map`
|
||||
depend on `core`; `chat` depends on all. **`core` must never reference `ERR_HQ`,
|
||||
`ERR_AircraftService`, or anything else from another addon.**
|
||||
|
||||
Occupation logic lives in **`core`**, so the base scenario occupies the island
|
||||
without the AI addon installed. Where it needs a companion capability it defines
|
||||
an interface in core and lets `ai` register an implementation — the existing
|
||||
`ERR_CommandService` and `ERR_AdminProvider` show the pattern. Without the AI
|
||||
addon, occupation still happens by road and on foot; only air lift is missing.
|
||||
|
||||
### Performance
|
||||
|
||||
1. No per-frame work. Every tick here is ≥ 1 s, staggered by id (`id % N`).
|
||||
2. No allocation inside loops. Reuse member arrays.
|
||||
3. Spatial bucketing (500 m cells) for proximity — copy `ERR_HQNetwork.Add/Build`.
|
||||
4. Hard caps everywhere, with a logged refusal when hit.
|
||||
5. Issue waypoints and let the engine drive. No custom per-frame steering.
|
||||
6. Hysteresis and dwell before any spawn/dormant flip.
|
||||
|
||||
### Style and validation
|
||||
|
||||
- Match surrounding indentation (older files use tabs). New lines under 120 chars.
|
||||
- Decision logic goes in **pure static rule classes** (see `ERR_MissionRules`,
|
||||
`ERR_DiscoveryMapRules`) so it is testable without the engine. Entity work
|
||||
stays in the service.
|
||||
- `python3 tools/check.py` must pass before you report done.
|
||||
- Add tests with `tests/policy_harness.py`: `body(path, signature)` extracts a
|
||||
real function body from the `.c` file and `run(code)` compiles and runs it as
|
||||
C++. Follow `tests/test_admin_session.py`.
|
||||
- Report honestly. Separate "compiles and passes scripted fixtures" from
|
||||
"verified in the real game". Do not write evidence claiming runtime
|
||||
verification you did not perform.
|
||||
|
||||
---
|
||||
|
||||
## 3. The occupation timeline
|
||||
|
||||
### T+0 — Dormant island
|
||||
|
||||
| Location | State at start |
|
||||
| --- | --- |
|
||||
| Prison | Full garrison, physical (players are there) |
|
||||
| Within 2 km of prison | 2 patrol records, dormant |
|
||||
| Staging areas (1–2 authored) | The entire remaining force pool, as records |
|
||||
| Everything else | Buildings and props only. **No troops.** |
|
||||
|
||||
Authored staging areas are the military sites the occupation flows out of — pick
|
||||
the existing barracks/airfield areas on Everon. They hold the pool and are where
|
||||
transport originates. A staging area is itself a site that can be attacked.
|
||||
|
||||
### T+alarm — Detection
|
||||
|
||||
The occupation plan starts when HQ learns something is wrong, through the
|
||||
existing systems, not a timer: an escape is witnessed, a body or breach is found,
|
||||
or a guard check-in is missed. If nothing is detected, occupation still begins
|
||||
on a long fallback (default 15 min, setting `occupationFallbackSeconds`) — a
|
||||
missing prisoner count is noticed eventually.
|
||||
|
||||
### T+alarm onward — Deployment
|
||||
|
||||
HQ walks a priority list and issues deployment orders while the pool lasts:
|
||||
|
||||
| Priority | Objective | Why |
|
||||
| --- | --- | --- |
|
||||
| 1 | Radio station(s) | Denies the player's first objective |
|
||||
| 2 | Helipad / extraction site | Denies the escape |
|
||||
| 3 | Road junctions and bridges on the ring road | Roadblocks, cordon |
|
||||
| 4 | Medical and depot sites | Denies resupply |
|
||||
| 5 | Overwatch positions near likely routes | Observation, HQ eyes |
|
||||
|
||||
Cadence: one order every 90–180 s early, slowing as the pool depletes
|
||||
(`occupationOrderSeconds`). At most **5 deployments in transit at once**.
|
||||
|
||||
### T+20 to T+40 — Entrenchment
|
||||
|
||||
Arrived sections build. Roadblocks go up over 6–10 minutes each. Checkpoints
|
||||
become manned (T3). Patrol routes establish between occupied sites. The island
|
||||
that was empty is now a network.
|
||||
|
||||
### Continuous — Adaptation
|
||||
|
||||
As HQ gains contacts (T5), it re-prioritises: the sector where players were seen
|
||||
gets the next deployments and a cordon on its exits; quiet sectors are thinned,
|
||||
not reinforced. With no contacts, occupation proceeds methodically down the
|
||||
priority list — so hiding tightens the noose.
|
||||
|
||||
---
|
||||
|
||||
## 4. Data model
|
||||
|
||||
Add to `core`, next to the ledger:
|
||||
|
||||
```
|
||||
class ERR_ForcePool // finite, permanent losses
|
||||
{
|
||||
int Sections; // remaining uncommitted sections
|
||||
int SectionStrength; // soldiers per section, default 4
|
||||
int TransportAircraft; // remaining flyable transports
|
||||
int TransportVehicles; // remaining trucks
|
||||
}
|
||||
|
||||
enum ERR_OccupationPriority { RADIO, EXTRACTION, JUNCTION, SUPPLY, OVERWATCH }
|
||||
|
||||
class ERR_OccupationObjective
|
||||
{
|
||||
string SiteId; ERR_OccupationPriority Priority;
|
||||
int RequiredSections; int AssignedSections;
|
||||
bool Held; bool Contested; bool Abandoned;
|
||||
}
|
||||
|
||||
enum ERR_DeployMethod { FOOT, CONVOY, AIRLIFT }
|
||||
enum ERR_DeployState { QUEUED, LOADING, IN_TRANSIT, UNLOADING, ARRIVED, LOST }
|
||||
|
||||
class ERR_DeploymentOrder
|
||||
{
|
||||
int Id; string ObjectiveId; ERR_DeployMethod Method; ERR_DeployState State;
|
||||
int LedgerRecordId; // the travelling force (T1 record)
|
||||
int TransportRecordId; // the aircraft/vehicle, -1 for foot
|
||||
vector Origin; vector LandingZone; float StartedAt; float EtaSeconds;
|
||||
}
|
||||
```
|
||||
|
||||
`ERR_OccupationPlan` owns the pool, the objective list and the live orders, and
|
||||
ticks every 5 s (staggered). It is created and ticked by `ERR_GameMode` on the
|
||||
existing tick path — **do not add a timer**.
|
||||
|
||||
---
|
||||
|
||||
## 5. Deployment methods
|
||||
|
||||
Method selection is a pure rule (`ERR_DeployRules.Select`): urgency, distance,
|
||||
and what transport remains. Roughly — under 1.5 km on foot, 1.5–6 km by convoy,
|
||||
over 6 km or urgent by air, falling back down the list when transport is gone or
|
||||
the AI addon is absent.
|
||||
|
||||
### Foot
|
||||
|
||||
The record's duty becomes `MOVE_TO`; it walks its route virtually and
|
||||
materialises near players like anything else. No transport consumed.
|
||||
|
||||
### Convoy
|
||||
|
||||
Consumes a truck. A `CONVOY` record carries the section along an authored road
|
||||
route. Slow (8–15 min) and ambushable — when physical, it is a real truck with
|
||||
real soldiers in it. If destroyed, the section and the truck are **permanently**
|
||||
lost from the pool.
|
||||
|
||||
### Air lift
|
||||
|
||||
Consumes an aircraft. Three rules make this affordable — read them before
|
||||
implementing, because the naive version blows the whole agent budget at T+0.
|
||||
|
||||
**Cargo is virtual in transit.** Passengers inside a fuselage are invisible.
|
||||
Never spawn them for the flight. A physical lift is **1 vehicle + 1 pilot**; the
|
||||
section spawns at the moment of unload. This takes a lift from ~10 agents to ~2
|
||||
with no visual difference, and it is the single most important optimisation in
|
||||
this document.
|
||||
|
||||
**Two aircraft in the air at once, not ten.** Queue as many lifts as the plan
|
||||
needs — the island-wide occupation is real — but execute at most
|
||||
`occupationConcurrentLifts` (default 2) physically. The rest fly as records with
|
||||
real routes and real timings, and materialise if a player comes near. The player
|
||||
is only ever in one place; ten simultaneous flights pay full price for drama
|
||||
nobody witnesses. The existing `ERR_AircraftService.MAX_AIRCRAFT` is 4, and the
|
||||
controller's demonstrated envelope is vertical flight plus a bounded 20 m route —
|
||||
ten concurrent cross-map flights with landings is far outside what is proven.
|
||||
Treat the concurrency as a dial: start at 2, measure with the T7 metrics, raise
|
||||
it only if the numbers allow.
|
||||
|
||||
**Aircraft get a far larger materialisation band than infantry.** A helicopter is
|
||||
visible and audible at 2–3 km, so lifts materialise at `presenceAirMetres`
|
||||
(default 2500 m) instead of the 600 m used for foot patrols. This is what
|
||||
actually delivers "helicopters moving around the island" — they are seen
|
||||
crossing the valley, not only overhead.
|
||||
|
||||
A fleet of ten airframes in the pool is fine; ten in the air simultaneously is
|
||||
not. Note also that ten AI helicopters converging on one staging area is where
|
||||
collisions, failed landings and wrecks blocking your own LZs come from.
|
||||
|
||||
Core cannot call the aircraft service directly, so:
|
||||
|
||||
```
|
||||
// core
|
||||
class ERR_TransportProvider // interface
|
||||
{
|
||||
bool CanLift(vector from, vector to);
|
||||
int Lift(int recordId, vector from, vector landingZone, float deadline);
|
||||
int LiftState(int orderId); // queued / flying / landed / unloaded / lost
|
||||
void CancelLift(int orderId);
|
||||
}
|
||||
```
|
||||
|
||||
`core` calls the registered provider; `ai` registers an implementation backed by
|
||||
the existing `ERR_AircraftService` (`addons/ai/Scripts/Game/ERR/ERR_AircraftService.c`),
|
||||
which already supports route flight, field flight, vertical flight, cancellation
|
||||
and a `MAX_AIRCRAFT` cap of 4. **Reuse it — do not write a second flight
|
||||
controller.** `{859B2A6A7DFA9EFB}` style GUIDs must come from
|
||||
`docs/vanilla-resources.json`; the transport helicopter
|
||||
`Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et` is already
|
||||
registered there.
|
||||
|
||||
Lift sequence — each stage is a state, none of it is per-frame:
|
||||
|
||||
1. `LOADING` — section boards at the staging area (virtual unless observed).
|
||||
2. `IN_TRANSIT` — aircraft flies the route. Physical when near observers; this
|
||||
is the moment the design exists for, so let it be seen and heard.
|
||||
3. `UNLOADING` — lands at the LZ, section disembarks, order is marked `ARRIVED`.
|
||||
4. Aircraft returns to staging and is returned to the pool.
|
||||
|
||||
**LZ selection:** flat ground within 150 m of the objective, not inside a
|
||||
building footprint, not on the road. Choose from authored LZ points per site
|
||||
rather than searching terrain at runtime — authoring is cheaper and more
|
||||
reliable than a runtime flatness search.
|
||||
|
||||
**Interruption:** if the aircraft is destroyed in any state before `ARRIVED`,
|
||||
the section and the aircraft are permanently removed from the pool, the
|
||||
objective returns to unassigned with a logged reason, and HQ must re-plan.
|
||||
Same for a convoy. This must be tested, not assumed.
|
||||
|
||||
---
|
||||
|
||||
## 6. Roadblock construction
|
||||
|
||||
A roadblock is built, not spawned. State machine, advancing on the 5 s plan tick:
|
||||
|
||||
| State | What exists | Duration |
|
||||
| --- | --- | --- |
|
||||
| `SURVEYED` | Nothing. Site reserved by an incoming order | — |
|
||||
| `MATERIEL` | Flat-packed props: stacked barriers, crates, a parked truck | on arrival |
|
||||
| `BUILDING` | Props appear in 3 stages as progress crosses 33/66/100% | 6–10 min |
|
||||
| `MANNED` | Complete; becomes a T3 checkpoint with its garrison | — |
|
||||
| `DAMAGED` | Partial props, reduced garrison | on attack |
|
||||
| `ABANDONED` | Props remain, no garrison, never repopulated | garrison wiped |
|
||||
|
||||
Rules:
|
||||
|
||||
- Progress advances **whether or not anyone is watching**, and props spawn on
|
||||
stage transitions regardless of observers. A player who leaves and returns
|
||||
finds a finished roadblock. Props are static and cost nothing.
|
||||
- Soldiers under construction use T4 smart actions so they look like they are
|
||||
working. Never all of them — keep a sentry.
|
||||
- Destroying props during `BUILDING` drops progress and can return the site to
|
||||
`MATERIEL`. Destroying it repeatedly must not loop forever: after two resets
|
||||
HQ abandons that junction and spends the section elsewhere.
|
||||
- Leave a drivable gap. A roadblock that fully seals a road can soft-lock a
|
||||
route; it should slow and expose the player, not wall them in.
|
||||
- At most **4 construction sites at once** (`occupationBuildSites`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Interaction with HQ and fairness
|
||||
|
||||
- Dormant records **never observe and never report**. Only physical units
|
||||
generate contacts. Enforce in code.
|
||||
- Deployment orders are HQ traffic: they travel the existing radio model
|
||||
(`ERR_HQNetwork`) with its latency and failure rules. Severing HQ's radio chain
|
||||
delays or loses deployment orders — killing radio operators slows the
|
||||
occupation, which is exactly the intended counterplay.
|
||||
- HQ's contact-driven re-prioritisation reuses the scored tasking from T5. The
|
||||
occupation plan proposes; HQ's priorities re-order the queue.
|
||||
|
||||
---
|
||||
|
||||
## 8. Caps (all settings, all enforced, all logged on refusal)
|
||||
|
||||
| Setting | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `activeAgentBudget` | 60 | Global active enemy agents (T2 owns enforcement) |
|
||||
| `occupationSections` | 16 | Total sections in the pool |
|
||||
| `occupationTransportAir` | 2 | Transport aircraft |
|
||||
| `occupationTransportRoad` | 3 | Trucks |
|
||||
| `occupationConcurrentOrders` | 5 | Deployments in transit at once |
|
||||
| `occupationConcurrentLifts` | 2 | Air lifts flown **physically** at once; others fly as records |
|
||||
| `presenceAirMetres` | 2500 | Materialisation band for aircraft (infantry use 600) |
|
||||
| `occupationBuildSites` | 4 | Simultaneous construction sites |
|
||||
| `occupationOrderSeconds` | 120 | Cadence between new orders |
|
||||
| `occupationFallbackSeconds` | 900 | Alarm fallback if nothing is detected |
|
||||
|
||||
---
|
||||
|
||||
## 9. Work breakdown
|
||||
|
||||
Implement in order. One agent per step is fine; steps O3 and O4 can run in
|
||||
parallel after O2.
|
||||
|
||||
- **O1 — Pool, objectives and plan skeleton.** `ERR_ForcePool`,
|
||||
`ERR_OccupationObjective`, `ERR_OccupationPlan` with the priority queue and the
|
||||
5 s tick. Authored staging areas and objective list in `EveronSites.conf`.
|
||||
Start state: prison garrison only, everything else empty.
|
||||
- **O2 — Deployment orders and transit.** `ERR_DeploymentOrder`,
|
||||
`ERR_DeployRules.Select`, foot and virtual transit, arrival handover to the
|
||||
objective. Permanent pool loss on destruction.
|
||||
- **O3 — Air lift.** The `ERR_TransportProvider` interface in core, the `ai`
|
||||
implementation over `ERR_AircraftService`, authored LZ points, load/fly/unload
|
||||
states, loss handling.
|
||||
- **O4 — Convoy.** Road routes, truck + section as a `CONVOY` record, ambush and
|
||||
loss handling, blocked-route fallback to foot.
|
||||
- **O5 — Roadblock construction.** The state machine, staged props, progress,
|
||||
damage and abandonment, handover to T3 checkpoints.
|
||||
- **O6 — Adaptation and proof.** Contact-driven re-prioritisation, cordon
|
||||
formation, plus the metrics and the measured comparison run from T7.
|
||||
|
||||
---
|
||||
|
||||
## 10. Acceptance criteria
|
||||
|
||||
1. At T+0 the island holds troops only at the prison and its two nearby patrols;
|
||||
measured active agents at mission start are under 20.
|
||||
2. Within 10 minutes of the alarm, the radio station and the helipad have
|
||||
garrisons that **arrived**, and the journal shows how each got there.
|
||||
3. A player positioned along a lift route sees a real helicopter fly, land,
|
||||
unload soldiers and depart.
|
||||
4. Destroying that helicopter before it unloads permanently removes those
|
||||
soldiers and that aircraft from the pool; the objective is logged as
|
||||
unassigned and is still empty ten minutes later.
|
||||
5. A player watching a junction sees a roadblock built in stages over minutes,
|
||||
not appear at once. Leaving and returning shows progress continued.
|
||||
6. Active agents never exceed `activeAgentBudget` at any sample during a
|
||||
20-minute occupation, and never exceed 20 in the first minute after the alarm
|
||||
despite the full deployment queue being issued.
|
||||
7. A lift in transit costs 2 agents, not 10 — verified in the metrics output.
|
||||
7. With the AI addon absent, occupation still completes by road and foot.
|
||||
8. Restarting the mission leaves no deployment, aircraft, convoy or prop behind.
|
||||
9. `python3 tools/check.py` passes.
|
||||
|
||||
## 11. Tests
|
||||
|
||||
`tests/test_occupation_plan.py`
|
||||
|
||||
- Priority order is respected while the pool lasts, and objectives are refused
|
||||
once the pool is empty rather than spawning free sections.
|
||||
- Pool accounting: a lost deployment permanently decrements sections and
|
||||
transport; totals never go negative and never regenerate.
|
||||
- Cadence and concurrency caps hold under a burst of eligible objectives.
|
||||
|
||||
`tests/test_deployment_rules.py`
|
||||
|
||||
- `Select` picks foot under 1.5 km, convoy in the middle band, air over 6 km, and
|
||||
falls back correctly when transport is exhausted or no provider is registered.
|
||||
- ETA scales with method speed and distance.
|
||||
- A destroyed transport moves the order to `LOST` and releases the objective.
|
||||
- A lift in transit reports zero passenger agents; the section's agents appear
|
||||
only on unload, and the count matches the section strength.
|
||||
- The physical-lift cap holds: with 6 queued lifts and a cap of 2, exactly 2 are
|
||||
flown physically and the other 4 remain records with valid ETAs.
|
||||
|
||||
`tests/test_roadblock_build.py`
|
||||
|
||||
- Progress crosses the three prop stages exactly once each, in order.
|
||||
- Progress advances with no observers present.
|
||||
- Two resets lead to abandonment, not an infinite rebuild loop.
|
||||
- The drivable gap is preserved in every completed variant.
|
||||
|
||||
---
|
||||
|
||||
## 12. Do not
|
||||
|
||||
- Do not spawn the island's garrisons at mission start "for now" — the staged
|
||||
arrival is the feature, not an optimisation to add later.
|
||||
- Do not write a second flight controller; use `ERR_AircraftService` through the
|
||||
provider interface.
|
||||
- Do not launch a fleet of helicopters at once. Ten physical lifts is ~90 agents
|
||||
before a single garrison exists, front-loads the whole occupation into five
|
||||
minutes, and stages nine flights where no player can see or stop them.
|
||||
- Do not spawn passengers for a flight. Cargo is virtual until unload.
|
||||
- Do not let core reference `ai` classes.
|
||||
- Do not make deployments invulnerable or teleport them on failure. If the player
|
||||
breaks a deployment, it stays broken.
|
||||
- Do not exceed the caps to make a demo look better.
|
||||
@@ -0,0 +1,164 @@
|
||||
# Living island — plan and agent rules
|
||||
|
||||
Goal: Everon should feel occupied — patrols that actually move, manned roadblocks,
|
||||
soldiers doing something other than standing — **without** raising the active AI
|
||||
count. The target is a constant, bounded agent budget (default 60 enemy agents)
|
||||
regardless of how much "activity" exists on the map.
|
||||
|
||||
## The core idea: simulate many, spawn few
|
||||
|
||||
Most of the island's forces exist as **records, not entities**. A record has a
|
||||
position, a route, a strength and a duty. A slow tick advances records along
|
||||
their routes with plain arithmetic — no AI, no navmesh, no entity. A record is
|
||||
materialised into a real `SCR_AIGroup` only when a player is close enough to
|
||||
observe it, or when HQ needs it in contact. When the player leaves, it goes back
|
||||
to being a record, keeping its position and losses.
|
||||
|
||||
This is what buys the "living island" feeling cheaply: twelve patrols can be
|
||||
crossing the map at once while only the two near players cost anything.
|
||||
|
||||
```
|
||||
ERR_ForceLedger records: id, kind, position, route, strength, duty, importance
|
||||
|
|
||||
|-- ERR_ForceSimulation advances dormant records along routes (2 s tick, pure math)
|
||||
|-- ERR_PresenceBudget decides which records are physical right now (3 s tick)
|
||||
| |-- spawns -> SCR_AIGroup.SpawnMembers()
|
||||
| |-- dormant -> SCR_AIGroup.DespawnMembers()
|
||||
|
|
||||
+-- consumed by ERR_HQ (ai addon) for tasking, ETA and availability
|
||||
```
|
||||
|
||||
**Hard invariant — fairness:** only *physical* units observe and report contacts.
|
||||
A dormant record never generates a sighting. This protects the HQ design, where
|
||||
enemy knowledge must be earned through line of sight and radio.
|
||||
|
||||
## Layer 2: presence without population
|
||||
|
||||
A site feels occupied because of evidence, not head count. Evidence is free —
|
||||
static props cost no CPU. So roadblocks, barriers, parked vehicles and lit
|
||||
windows are **permanent**; the soldiers manning them are **conditional**.
|
||||
|
||||
## Layer 3: soldiers doing something
|
||||
|
||||
Vanilla smart actions (`SCR_AISmartActionComponent`) let an existing soldier lean
|
||||
on a wall, smoke, or work on a vehicle instead of standing at a waypoint. This
|
||||
costs no extra agents — it changes what the agents you already pay for are doing.
|
||||
|
||||
## Task order
|
||||
|
||||
Do these in order; each builds on the previous one. One agent per task.
|
||||
|
||||
| Task | File | Depends on |
|
||||
| --- | --- | --- |
|
||||
| T1 | `T1-force-ledger.md` — records + route simulation | — |
|
||||
| T2 | `T2-presence-budget.md` — spawn/dormant arbitration under a cap | T1 |
|
||||
| T3 | `T3-checkpoints.md` — roadblocks and manned checkpoints | T1, T2 |
|
||||
| T4 | `T4-smart-actions.md` — idle activity at sites | T2 |
|
||||
| T5 | `T5-hq-integration.md` — HQ tasks ledger units, gets ETAs | T1, T2 |
|
||||
| T6 | `T6-ambient-signals.md` — cheap signs of life | T2 |
|
||||
| T7 | `T7-performance.md` — caps, metrics, thrash prevention | all |
|
||||
| T8 | `T8-hq-memory.md` — HQ belief: decaying contacts, searched ground | T5 |
|
||||
| T9 | `T9-pursuit.md` — alert states, hunting, air search, cordons | T5, T8 |
|
||||
| T10 | `T10-ai-migration.md` — port the old AI mod's proven behaviour under HQ | T5, T8, T9 |
|
||||
| O1–O6 | `OCCUPATION.md` — how the island gets occupied after the alarm | T1, T2 |
|
||||
|
||||
T3 and T4 can run in parallel once T2 lands. T6 can run in parallel with T5.
|
||||
T8 and T9 are what make the island *hunt* rather than merely be occupied; T9
|
||||
depends on T8, because HQ cannot search for what it does not remember.
|
||||
|
||||
---
|
||||
|
||||
# Rules every agent must follow
|
||||
|
||||
Paste this section into each agent along with its task file.
|
||||
|
||||
## Licensing — non-negotiable
|
||||
|
||||
This project is an independent successor. The original Escapists mod and its
|
||||
`ScrtCore` / `ScrtSharedContent` dependencies are under SCRT-ND (an APL-ND
|
||||
variant) which **forbids derivatives**. Therefore:
|
||||
|
||||
- Never copy code, config, layouts or assets from the original Escapists mod,
|
||||
from ScrtCore/ScrtSharedContent, or from `/home/jakub/devel/escapists`.
|
||||
- Never introduce identifiers matching `ESCT*`, `SCRTC_*`, `SCRT_*`, `EAI_*`.
|
||||
`tools/check.py` fails the build on these.
|
||||
- Only vanilla Arma Reforger resources and resources owned by this repository
|
||||
may be referenced. Every `{GUID}path` must either be in
|
||||
`docs/vanilla-resources.json` or be owned by a `.meta` file in this repo.
|
||||
- Reimplementing a *mechanic* from scratch is fine. Copying an implementation is not.
|
||||
|
||||
## Project constraints
|
||||
|
||||
- Five addons only: `core`, `ai`, `intel`, `chat`, `map`. Do not add addons or
|
||||
external dependencies.
|
||||
- Dependency direction: `ai`, `intel`, `map` depend on `core`; `chat` depends on
|
||||
all. **`core` must never reference `ERR_HQ` or anything from another addon.**
|
||||
Core defines interfaces (see `ERR_CommandService`); companions implement them.
|
||||
- The living-island ledger belongs in **`core`**, because the base scenario must
|
||||
work without the AI addon. HQ integration (T5) goes in **`ai`**.
|
||||
|
||||
## Performance rules — the whole point of this work
|
||||
|
||||
1. **No per-frame work.** Every tick in this subsystem is ≥ 1 s. Stagger work by
|
||||
unit id (`id % N`) so all units never update on the same frame.
|
||||
2. **No allocation inside loops.** Reuse member arrays; clear rather than
|
||||
reallocate. Do not allocate an `array<AIAgent>` per unit per tick.
|
||||
3. **Spatial bucketing for proximity.** 500 m cells, as already done in
|
||||
`ERR_HQNetwork.Add/Build`. Never do an all-pairs distance scan.
|
||||
4. **Hard caps everywhere,** with a logged refusal when hit. Never let a count
|
||||
grow unbounded.
|
||||
5. **Prefer vanilla behaviour over custom steering.** Issue a waypoint and let
|
||||
the engine drive; do not write per-frame movement code.
|
||||
6. **Hysteresis and dwell time.** Spawn and despawn radii must differ, and a unit
|
||||
must stay in its state a minimum time before flipping. Thrash is the main
|
||||
performance risk in this design.
|
||||
|
||||
## Code style
|
||||
|
||||
- Match the surrounding file's indentation (older files use tabs).
|
||||
- New lines stay under 120 characters. The repo already has 420 lines over 200
|
||||
characters; do not add more.
|
||||
- Put decision logic in **pure static rule classes** (see `ERR_DiscoveryMapRules`,
|
||||
`ERR_MissionRules`) so it can be tested without the engine. Entity manipulation
|
||||
stays in the service class.
|
||||
- Small classes. Do not extend `ERR_GameMode` or add another 100-field object.
|
||||
|
||||
## Validation — required before you report done
|
||||
|
||||
1. `python3 tools/check.py` must pass.
|
||||
2. Add Python tests using `tests/policy_harness.py`: `body(path, signature)`
|
||||
extracts a real function body from the `.c` file and `run(code)` compiles and
|
||||
executes it as C++. Test the pure rule classes this way. Follow the pattern in
|
||||
`tests/test_admin_session.py`.
|
||||
3. Report honestly. This repo separates "compiles and passes scripted fixtures"
|
||||
from "verified in the real game". If you did not run it in Workbench, say so.
|
||||
Do not write evidence files claiming runtime verification you did not perform.
|
||||
|
||||
## Verified engine APIs you will need
|
||||
|
||||
All confirmed present in the installed vanilla scripts (1.6.30):
|
||||
|
||||
| API | Where | Notes |
|
||||
| --- | --- | --- |
|
||||
| `SCR_AIGroup.SetLifecyclePolicy(policy, spawnDist, despawnDist, veryNearBlockDist)` | `Entities/SCR_AIGroup.c:2921` | `Manual` (default) or `ProximityDriven` |
|
||||
| `SCR_AIGroup.SpawnMembers()` / `DespawnMembers()` | `:2783` / `:2870` | Dormant group **keeps its entity** and records alive/dead counts, so a later `SpawnMembers()` re-materialises the right number |
|
||||
| `ChimeraAIGroup.SetImportance(int)` / `AIGroup.GetImportance()` | `generated/AI/ChimeraAIGroup.c:22` | Pass `SCR_EAISpawnImportance` values |
|
||||
| `SCR_EAISpawnImportance` | `AI/SCR_EAISpawnImportance.c` | LOW 0.50, NORMAL 0.70, HIGH 0.90, CRITICAL 1.00 — fraction of AIWorld's active-AI limit the tier may consume, enforced by `AIWorld::CanActivateGroup` |
|
||||
| `SCR_EAIGroupLifecyclePolicy` | `AI/SCR_EAIGroupLifecyclePolicy.c` | `Manual` = we drive it; `ProximityDriven` = group self-manages against `ObserversSystem` |
|
||||
| `ObserversSystem.GetObservers(out array<vector>)` | `generated/System/ObserversSystem.c:59` | Cheap observer positions; also `GetObserversMP` |
|
||||
| `SCR_AIGroup.SetEliminateWhenReached(true)` + `GetOnEliminatedWhenReached()` | used at `SCR_AmbientPatrolSpawnPointComponent.c:212` | If a player reaches a patrol that could not spawn (budget full), the area counts as cleared and the group retires instead of popping in later |
|
||||
| `SCR_AISmartActionComponent` (`ReserveAction`, `ReleaseAction`, `GetOnActionEnd`, `GetOnActionFailed`) | `AI/Components/SCR_AISmartActionComponent.c` | Idle activities |
|
||||
| `SCR_SmartActionWaypoint.SetSmartActionEntity(IEntity, string tag)` | `AI/Behavior/SCR_SmartActionWaypoint.c:31` | Sends a group to perform a tagged smart action |
|
||||
|
||||
Reference implementation worth reading (vanilla, not the original mod — safe to
|
||||
read and learn from): `Components/Locations/SCR_AmbientPatrolSpawnPointComponent.c`
|
||||
and `Systems/SCR_AmbientPatrolSystem.c`.
|
||||
|
||||
## Existing repo helpers — use these, do not reinvent
|
||||
|
||||
- `ERR_Actors.Spawn/DeleteSpawned/Ground/Players/Alive/CanFight/Group` — `addons/core/Scripts/Game/ERR/ERR_Actors.c`
|
||||
- `ERR_Resources.SearchWaypoint/MoveWaypoint/CycleWaypoint/DefendWaypoint` — waypoint prefab GUIDs
|
||||
- `ERR_SiteService` / `ERR_SiteDefinition` / `ERR_LocationComponent` — authored sites and their guards
|
||||
- `ERR_HQNetwork` — the 500 m spatial cell pattern to copy
|
||||
- `ERR_Settings.Get("key")` — integer settings; add new keys here
|
||||
- `ERR_HQJournal` via `ERR_HQ.Event(kind, message)` — **ai addon only**; core code logs with `PrintFormat("Reintroduced [ledger]: ...")`
|
||||
@@ -0,0 +1,93 @@
|
||||
# T1 — Force ledger and route simulation
|
||||
|
||||
Read `docs/living-island/README.md` first, including the rules section. Follow it.
|
||||
|
||||
## Goal
|
||||
|
||||
Introduce a server-owned record of every enemy formation on the island that does
|
||||
not require those formations to exist as entities. Dormant records move along
|
||||
routes using arithmetic on a slow tick. This is the foundation for everything
|
||||
else in the living-island work.
|
||||
|
||||
## What to build
|
||||
|
||||
Create `addons/core/Scripts/Game/ERR/ERR_ForceLedger.c`:
|
||||
|
||||
```
|
||||
enum ERR_ForceKind { GARRISON, PATROL, CHECKPOINT, CONVOY }
|
||||
enum ERR_ForceDuty { HOLD, PATROL_ROUTE, MOVE_TO, SEARCH, RETURN }
|
||||
|
||||
class ERR_ForceRoute // ordered waypoints + loop flag
|
||||
class ERR_ForceRecord // id, kind, position, heading, route, progress,
|
||||
// strength, casualties, duty, dutyTarget,
|
||||
// importance, radioCapability, group (null when dormant),
|
||||
// lastSimulatedAt, stateSince
|
||||
class ERR_ForceLedger // owns records; register/unregister/find/count/status
|
||||
```
|
||||
|
||||
Create `addons/core/Scripts/Game/ERR/ERR_ForceSimulation.c` with a **pure rule
|
||||
class** plus the tick that applies it:
|
||||
|
||||
```
|
||||
class ERR_ForceRules
|
||||
{
|
||||
// pure, engine-free, unit-testable
|
||||
static float Speed(ERR_ForceKind kind, ERR_ForceDuty duty); // m/s
|
||||
static int Advance(...); // returns new route index
|
||||
static vector Step(vector from, vector to, float speed, float seconds, out bool arrived);
|
||||
static bool Due(int id, float now, float lastSimulatedAt, float interval); // staggered
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Dormant records advance along their route polyline by `speed * elapsed`.
|
||||
Walking speed default 1.4 m/s, vehicle convoy 11 m/s, a holding garrison 0.
|
||||
- **Do not raycast, do not query the navmesh, do not touch the world** for
|
||||
dormant records. Straight-line interpolation between authored route points is
|
||||
the intended fidelity. Authored routes follow roads; that is the content's job,
|
||||
not the simulation's.
|
||||
- Y coordinate: keep the authored route point's Y. Only call `ERR_Actors.Ground()`
|
||||
at the moment a record is materialised (that is T2's job, not yours).
|
||||
- Tick interval default 2000 ms, staggered by `id % 8` so records do not all
|
||||
update on the same frame.
|
||||
- Cap the ledger at 128 records. Refuse beyond that and log once.
|
||||
|
||||
## Wiring
|
||||
|
||||
- `ERR_GameMode` owns the ledger, created next to the existing services
|
||||
(`m_Sites`, `m_Discovery`, …). Expose `ERR_ForceLedger Forces()`.
|
||||
- Tick it from the existing game-mode tick path. **Do not add a new timer or
|
||||
`EOnFrame`.**
|
||||
- `ERR_SiteService` registers a `GARRISON` record for each site guard group it
|
||||
already creates, so the ledger has real content from day one. The site's
|
||||
existing patrol cycle waypoints become the record's `ERR_ForceRoute`.
|
||||
- Records whose group dies must be marked eliminated and stop simulating.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. With five authored sites, the ledger holds five garrison records after
|
||||
preparation, and `Status()` reports them.
|
||||
2. A record with a 4-point looping route returns to its start after the expected
|
||||
time, within 5% — verified by the Python test, not by eye.
|
||||
3. Advancing 128 records for a simulated hour performs no world queries and no
|
||||
per-record allocation.
|
||||
4. `python3 tools/check.py` passes.
|
||||
|
||||
## Tests to write
|
||||
|
||||
`tests/test_force_ledger.py`, using `tests/policy_harness.py`:
|
||||
|
||||
- Extract `ERR_ForceRules.Step` and `Advance` with `body()` and run them as C++:
|
||||
a 4-leg loop advanced in 2 s steps arrives at each leg in the expected order
|
||||
and wraps correctly.
|
||||
- `Speed` returns 0 for `HOLD`, and a convoy outruns a foot patrol.
|
||||
- `Due` staggers: across ids 0..7 at the same `now`, not all are due.
|
||||
- Off-by-one on the final leg of a non-looping route ends at the last point and
|
||||
reports arrival exactly once.
|
||||
|
||||
## Do not
|
||||
|
||||
- Do not spawn or despawn anything in this task. T2 owns materialisation.
|
||||
- Do not reference `ERR_HQ` — this is core.
|
||||
- Do not add a `ref array` member that grows without a cap.
|
||||
@@ -0,0 +1,274 @@
|
||||
# T10 — Migrating the old AI mod's proven behaviour under HQ control
|
||||
|
||||
Read `docs/living-island/README.md` first, including the rules section.
|
||||
Depends on T1, T2, T5, T8, T9. Mostly **ai** addon.
|
||||
|
||||
The predecessor project at `/home/jakub/devel/escapists/escapists-ai` already
|
||||
implements much of what the living-island plan describes — hunting UAZ convoys, a
|
||||
timed reconnaissance helicopter, reactive roadblocks, guard check-ins, body
|
||||
discovery, mortar sites and supply convoys. That work is the maintainer's own
|
||||
design and is the reference for this task.
|
||||
|
||||
This brief says what to carry across, what the real parameters were, and — most
|
||||
importantly — how the architecture must change on the way.
|
||||
|
||||
---
|
||||
|
||||
## 1. Provenance and licensing — read before opening any source file
|
||||
|
||||
| Category | Files | What you may do |
|
||||
| --- | --- | --- |
|
||||
| Maintainer's own design | Most of `escapists-ai` | Carry the **design**: timings, distances, sequences, failure handling. Re-express the code against vanilla and ERR interfaces. |
|
||||
| Calls into protected API | 36 of 48 files reference `ESCT_*` | Those calls cannot come across — ERR has no Escapists dependency. Replace each with a vanilla or ERR equivalent. A call is not a copy, but the replacement must be written fresh. |
|
||||
| **Contains copied protected code** | `EAI_GroupLookup.c` | **Do not port, do not adapt, do not read as a model.** An independent comparison found 666 matching token sequences with the protected `ESCT_GroupLookup` / `ESCT_CharacterLookup`. Build group composition from vanilla entity catalogs from scratch, as `ERR_FactionCatalog` already does. |
|
||||
|
||||
Also binding:
|
||||
|
||||
- `tools/check.py:39` rejects any `EAI_*`, `ESCT*`, `SCRTC_*`, `SCRT_*` identifier.
|
||||
Everything is renamed to `ERR_*`, and no old prefix survives in a comment either.
|
||||
- Every `{GUID}path` must be in `docs/vanilla-resources.json` or owned here.
|
||||
The prefabs quoted below are vanilla and were verified against the installed
|
||||
`resourceDatabase.rdb`; add any that are missing from the manifest.
|
||||
- Do not copy the old files wholesale and rename the symbols. Each system below
|
||||
is re-implemented against the ledger, the belief layer and the presence budget.
|
||||
|
||||
---
|
||||
|
||||
## 2. The architectural change — this is the point of the task
|
||||
|
||||
In the old mod, each behaviour was an **independent subsystem with its own timer
|
||||
and its own spawning**: `EAI_AirReconSystem` ran a launch clock, the response
|
||||
system held a five-minute radio cooldown, the roadblock planner ran its own
|
||||
placement loop, mortars managed their own crews. They competed for the same world
|
||||
with no shared budget and no shared picture.
|
||||
|
||||
In this project every one of them becomes a **capability the HQ planner selects**,
|
||||
under one force pool, one agent budget and one belief:
|
||||
|
||||
```
|
||||
ERR_HQ ── belief (T8) ── what HQ thinks it knows
|
||||
│
|
||||
├── plans against objectives and beliefs (T5 scoring, T9 alert states)
|
||||
│
|
||||
└── chooses a capability and spends the pool:
|
||||
ground response · air search · roadblock · mortar fire ·
|
||||
supply convoy · garrison deployment (O1–O6)
|
||||
```
|
||||
|
||||
Concretely, for every system below:
|
||||
|
||||
- **No private timers.** A capability offers "available / not available, ETA, cost";
|
||||
the planner decides when. The old per-system cooldowns become availability
|
||||
rules the planner reads.
|
||||
- **No private spawning.** Units come from the ledger and the pool. A capability
|
||||
requests a record; `ERR_PresenceBudget` decides what is physical.
|
||||
- **No private world picture.** Capabilities act on beliefs (T8). Nothing queries
|
||||
player positions directly.
|
||||
- **Losses hit the shared pool.** A destroyed hunter UAZ is a section gone from
|
||||
the same pool that garrisons checkpoints.
|
||||
|
||||
Delete rather than port: `EAI_Performance.NearPlayer` / `ConvoyLOD`. That was an
|
||||
embryonic presence system; T2 replaces it, and two competing presence systems is
|
||||
worse than either alone.
|
||||
|
||||
---
|
||||
|
||||
## M1 — Detection: check-ins, losses and bodies
|
||||
|
||||
**Source:** `EAI_HQWatch.c`, `EAI_HQBodySearch.c`, `EAI_HQStatus.c`
|
||||
|
||||
This is the missing trigger for the occupation alarm, and it already exists.
|
||||
|
||||
- **Periodic check-ins.** Each unit owes HQ a status report every `CHECK_IN_MS`
|
||||
(180 s), with `REPLY_MS` (30 s) to answer. A unit that misses its check-in is
|
||||
not assumed dead — HQ opens an *investigation* and sends someone to look. That
|
||||
is the mechanism `OCCUPATION.md` referred to as "a guard check-in missed".
|
||||
- **Loss confirmation.** `OnControllableDestroyed` records a loss as unconfirmed;
|
||||
HQ only *knows* once a unit reports it. `SquadLimit(baseline, confirmedLosses)`
|
||||
shrinks what HQ can commit as its confirmed losses mount.
|
||||
- **Body discovery.** Bodies are found by units that can actually see them —
|
||||
`InView(distance, facing)` plus a visibility trace, scanned at most every 15 s
|
||||
per unit, capped at 16 tracked bodies. A found body queues radio evidence:
|
||||
*casualty evidence, cause unknown*. Quiet kills stay quiet until someone walks
|
||||
past the corpse, which is exactly the stealth counterplay worth keeping.
|
||||
|
||||
**Port as:** an ingest path into T8's belief and an alarm source for the
|
||||
occupation plan. A discovered body creates a low-confidence belief at that
|
||||
position; a missed check-in creates an investigation belief at the unit's last
|
||||
known location. Keep the trace budget (`m_Traces`) — it is what stops body
|
||||
scanning from costing a fortune.
|
||||
|
||||
---
|
||||
|
||||
## M2 — Ground response: the hunter convoy
|
||||
|
||||
**Source:** `EAI_AIResponseSystem.c`
|
||||
|
||||
The proven behaviour, with its real numbers:
|
||||
|
||||
- Two armed UAZs — `{0B4DEA8078B78A9B}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469_PKM.et`
|
||||
— spawned on roads `EAI_RESPONSE_DISTANCE` (1000 m, ±100 m tolerance) from the
|
||||
contact, on **opposite sides**: candidate bearings must be at least 30° from
|
||||
directly opposite (`dot ≤ -0.866`), up to 12 placement attempts.
|
||||
- Each vehicle is filled with spec-ops and exactly one medic, all seats occupied.
|
||||
- **One convoy is kept alive and re-tasked** when a later report gives a new
|
||||
contact, rather than spawning a fresh one per sighting. It is replaced only
|
||||
when a vehicle is lost.
|
||||
- A global radio cooldown of 300 s gates how often a response can be triggered.
|
||||
- Convoy gunners were given maximum AI skill. Keep a *deliberate* skill choice,
|
||||
but make it a setting rather than a constant — see the pacing rules in T9.
|
||||
|
||||
**Port as:** the ground hunting capability in T9. The 1000 m opposite-approach
|
||||
placement is exactly T9's "at most two bearings, escape vector left open" rule —
|
||||
implement it once, here, and have T9 use it. Respect T9's
|
||||
`minHunterSpawnMetres` (700 m): never place a response vehicle closer than that
|
||||
to any player, which the old `EAI_ResponseSafety` and `EAI_SettlementSafety`
|
||||
checks were already approximating.
|
||||
|
||||
---
|
||||
|
||||
## M3 — Air search: the reconnaissance helicopter
|
||||
|
||||
**Source:** `EAI_AirRecon.c` (1335 lines — the richest system in the old mod)
|
||||
|
||||
This *is* T9's hunter helicopter. Build one system, not two.
|
||||
|
||||
Behaviour worth carrying:
|
||||
|
||||
- **Launch timer.** `heliTimerMinutes` (default 15, range 1–1440) schedules the
|
||||
first departure and each subsequent one. Losing the aircraft schedules a
|
||||
replacement rather than ending air search permanently.
|
||||
- **Airport discovery, spread over time.** The island is scanned in 2000 m
|
||||
columns across successive updates — never in one frame — matching runway and
|
||||
control-tower prefabs, clustering structures within 1000 m into one airport and
|
||||
tracking the cluster centre. A named marker entity short-circuits the search.
|
||||
**Keep the incremental scan pattern**; it is a good model for any map-wide
|
||||
query in this project.
|
||||
- **Landing pad selection.** 12 attempts at 35–220 m from the centre, rejecting
|
||||
water, requiring flatness within 1.5 m across a 12 m cross, a 14 m radius
|
||||
cylinder clear of low obstacles, and a clear box up to 150 m overhead.
|
||||
- **Phased flight** with a landing retry: if a landing has not settled within
|
||||
180 s, search for a new pad (re-checked every 30 s within 150 m of home).
|
||||
- **Air observation feeds HQ** through the same report path as ground units.
|
||||
|
||||
**Change on the way across:**
|
||||
|
||||
- Fold into T9's air search: one airborne at a time, endurance-capped, observing
|
||||
under the same line-of-sight rules as infantry.
|
||||
- The launch timer becomes *availability*, not a trigger. The planner asks for
|
||||
air search when a belief justifies it; the timer only says whether an airframe
|
||||
is ready. A helicopter must not launch into an empty sky because a clock fired.
|
||||
- Draw the airframe from the shared pool, so searching competes with lifting.
|
||||
- Replace `ESCT_Misc.IsPositionInWater` and `ESCT_RoadNavigation` calls with
|
||||
vanilla equivalents (`SCR_TerrainHelper`, `SCR_WorldTools`, vanilla road API).
|
||||
|
||||
---
|
||||
|
||||
## M4 — Reactive roadblocks
|
||||
|
||||
**Source:** `EAI_HQRoadblocks.c`
|
||||
|
||||
Distinct from T3's authored checkpoints: these are placed *in response to a
|
||||
contact*, ahead of where HQ thinks the player is going.
|
||||
|
||||
- `Ahead(position, velocity, site)` rejects any site that is not in front of the
|
||||
contact's travel. `FreshDirection(distress, hasVelocity, age, speed)` decides
|
||||
whether a heading estimate is trustworthy enough to predict from at all.
|
||||
- Sites are snapped to real roads via a road-approach search, within 200 m of the
|
||||
chosen point, with a 25 m probe to orient the block along the road.
|
||||
- Placement requires ground clearance, distance from players, and being outside
|
||||
settlements.
|
||||
|
||||
**Port as:** the cordon mechanism in T9. T8's belief gives a better input than
|
||||
the old raw velocity: use the uncertainty circle and the objective-route
|
||||
inference to choose chokepoints between the belief and its likely destination.
|
||||
Keep `Ahead` and `FreshDirection` as pure rules — they are good, testable logic.
|
||||
|
||||
---
|
||||
|
||||
## M5 — Mortar sites
|
||||
|
||||
**Source:** `EAI_MortarSystem.c`, `EAI_MortarEngagement.c`, `EAI_MortarFiring.c`,
|
||||
`EAI_MortarPrediction.c`, `EAI_MortarSalvo.c`
|
||||
|
||||
Much of this is already migrated (`ERR_Mortar*`). What is missing is **site
|
||||
placement as an HQ capability**: the old system chose where a battery stands,
|
||||
spawned a 2B14 with crew and an RTO, equipped nine HE shells per crewman, pulled
|
||||
shells from nearby donors when the gunner ran dry, reserved airspace over the
|
||||
impact area, and audited site status (claimed / startup / operational / crew
|
||||
fighting / shells / radio connected / cooldown).
|
||||
|
||||
**Port as:** a `MORTAR_SITE` objective in the occupation plan (O1). HQ deploys a
|
||||
battery to a site like any other garrison — it costs a section from the pool,
|
||||
travels by the normal methods, and takes time to become operational. A mortar
|
||||
battery that is spent, overrun or out of shells stays spent. Keep the airspace
|
||||
reservation and the existing fairness: a battery fires at *beliefs*, delivered by
|
||||
radio, never at live player positions.
|
||||
|
||||
---
|
||||
|
||||
## M6 — Logistics and the threat extras
|
||||
|
||||
Lower priority; schedule after M1–M5.
|
||||
|
||||
| Source | What it gives | Port as |
|
||||
| --- | --- | --- |
|
||||
| `EAI_SupplyConvoys.c` (1463 lines) | Road convoy logistics with real routing | The convoy method in O4; reuse the road-following and slot logic, not the ESCT calls |
|
||||
| `EAI_Resupply.c`, `EAI_AmmoSupply.c`, `EAI_DepotLoading.c`, `EAI_LogisticsVehicle.c` | Ammunition and depot chains | A resupply capability that keeps garrisons and batteries fighting — finite, so cutting a depot matters |
|
||||
| `EAI_StingerEquipment.c` | A rare `Launcher_FIM92_Stinger` on enemy infantry, one in N | Anti-air threat against the player's stolen helicopter — the escape must not be trivially safe. Keep it rare and keep it a setting |
|
||||
| `EAI_ImprovisedPrisons.c` | Recapture rather than only killing | A strong escape-fiction feature: an unconscious escapist can be taken, not just killed. Worth its own task later |
|
||||
| `EAI_Mines.c`, `EAI_MineTask.c` | Mines laid on routes | Optional; only with clear warning cues, or it is pure frustration |
|
||||
| `EAI_AirAssault.c`, `EAI_AssaultParachute.c` | Airborne insertion | Already covered by O3 air lift; do not build a second insertion path |
|
||||
| `EAI_HelicopterRecovery.c`, `EAI_Landing.c`, `EAI_AircraftAmmo.c` | Aircraft servicing and recovery | Feed into the transport provider rather than a separate system |
|
||||
|
||||
---
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. No `EAI_*`, `ESCT_*` or `SCRTC_*` identifier appears anywhere in `addons/`;
|
||||
`python3 tools/check.py` passes.
|
||||
2. `EAI_GroupLookup.c` has not been ported in any form; group composition comes
|
||||
from vanilla catalogs through `ERR_FactionCatalog`.
|
||||
3. Every migrated capability draws units from the ledger and the shared pool.
|
||||
Grep proves it: no capability calls a spawn helper directly.
|
||||
4. No migrated system owns a private timer that triggers behaviour. Timers only
|
||||
report availability to the planner.
|
||||
5. A missed check-in produces an investigation; a discovered body produces a
|
||||
low-confidence belief; both are visible in the journal.
|
||||
6. A contact triggers **one** re-taskable hunter convoy, not one per sighting,
|
||||
and never places a vehicle within 700 m of a player.
|
||||
7. Air search launches because a belief justifies it, never because a clock
|
||||
fired into an empty sky.
|
||||
8. Destroying hunters, the helicopter or a mortar battery permanently reduces
|
||||
what HQ can do next — verified in the metrics.
|
||||
9. Active agents stay inside `activeAgentBudget` with every capability in play.
|
||||
|
||||
## Tests to write
|
||||
|
||||
`tests/test_detection.py` — check-in timing and reply windows; `SquadLimit`
|
||||
shrinks with confirmed losses; `InView` rejects a body behind the observer;
|
||||
the body-scan interval and tracking cap hold.
|
||||
|
||||
`tests/test_ground_response.py` — opposite-bearing placement satisfies the 30°
|
||||
rule and the 1000 m ± 100 m band; the 700 m minimum to players holds; a second
|
||||
contact re-tasks the existing convoy instead of spawning another; cooldown gates
|
||||
repeats.
|
||||
|
||||
`tests/test_air_search.py` — the launch timer reports availability but never
|
||||
triggers a launch on its own; pad selection rejects water, slopes over 1.5 m and
|
||||
obstructed overhead; landing retry fires after 180 s.
|
||||
|
||||
`tests/test_roadblock_siting.py` — `Ahead` rejects sites behind the contact;
|
||||
`FreshDirection` refuses a stale or speedless heading; chosen sites lie between
|
||||
the belief and its inferred objective.
|
||||
|
||||
## Do not
|
||||
|
||||
- Do not port a file by renaming its symbols. Re-implement against the ledger,
|
||||
the belief and the budget.
|
||||
- Do not recreate the old parallel-subsystem architecture. One planner, one pool,
|
||||
one budget, one picture.
|
||||
- Do not port `EAI_GroupLookup.c`, and do not use it as a reference while writing
|
||||
the replacement.
|
||||
- Do not let any migrated capability read player positions directly. Everything
|
||||
acts on beliefs earned through observation and radio.
|
||||
@@ -0,0 +1,97 @@
|
||||
# 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_AIGroup`s 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 from `ERR_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_aTargets` non-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. Hook
|
||||
`GetOnEliminatedWhenReached()` to retire the record.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. With a 60-agent budget and 30 records of 4 soldiers each, active agents never
|
||||
exceed 60, and `Status()` reports refusals.
|
||||
2. A player walking a straight line past three patrol routes sees each patrol
|
||||
materialise before it is visible and never sees one disappear.
|
||||
3. A patrol that loses two members while physical comes back with the reduced
|
||||
strength after a dormant period.
|
||||
4. No record flips state more than once per 30 s in a stationary-observer test.
|
||||
5. `python3 tools/check.py` passes.
|
||||
|
||||
## Tests to write
|
||||
|
||||
`tests/test_presence_budget.py`:
|
||||
|
||||
- `WantsPhysical` hysteresis: a record at 700 m stays in whichever state it is
|
||||
already in; crossing 600 m spawns; crossing 900 m dormants.
|
||||
- `DwellSatisfied` blocks a flip at 29 s and allows it at 31 s.
|
||||
- `WithinBudget` refuses 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.
|
||||
- `Score` ranks a pinned distant record above an unpinned near one, and ranks
|
||||
importance above distance for equal pinning.
|
||||
|
||||
## Do not
|
||||
|
||||
- Do not use `ProximityDriven` lifecycle 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.
|
||||
@@ -0,0 +1,73 @@
|
||||
# T3 — Roadblocks and manned checkpoints
|
||||
|
||||
Read `docs/living-island/README.md` first, including the rules section. Depends on T1, T2.
|
||||
|
||||
## Goal
|
||||
|
||||
Make the road network feel controlled. A checkpoint is **permanent evidence**
|
||||
(barriers, a parked truck, sandbags, a light at night) plus a **conditional
|
||||
garrison** (2–4 soldiers, materialised only when someone is near). The evidence
|
||||
costs no CPU and is always there; the soldiers cost agents and are not.
|
||||
|
||||
This is the single biggest "living island" win per CPU cycle: the player sees
|
||||
occupied terrain everywhere and pays for soldiers only where they are.
|
||||
|
||||
## What to build
|
||||
|
||||
1. **Site kind.** Extend `ERR_LocationKind` with `CHECKPOINT`, and
|
||||
`ERR_SiteDefinition` with the fields a checkpoint needs: prop layout choice,
|
||||
garrison size, whether it blocks the road, and the approach direction.
|
||||
Update `ERR_SiteDefinition` validation and its existing tests accordingly.
|
||||
|
||||
2. **Static layout.** `addons/core/Scripts/Game/ERR/ERR_CheckpointLayout.c`
|
||||
places vanilla props relative to the site origin and a road heading: barriers
|
||||
angled across one lane, sandbag positions, a parked vehicle, a barrel. Keep
|
||||
it to **one authored arrangement with a mirrored variant** — do not build a
|
||||
procedural composition system.
|
||||
- Use only vanilla prefabs. Every `{GUID}path` you add must be present in
|
||||
`docs/vanilla-resources.json`, or `tools/check.py` will reject it. Find real
|
||||
GUIDs by searching the installed vanilla data, not by guessing.
|
||||
- Props are spawned once during preparation and never ticked.
|
||||
|
||||
3. **Checkpoint records.** Each checkpoint registers a `CHECKPOINT` record with
|
||||
`ERR_ForceDuty.HOLD`, importance `NORMAL`, and a small garrison. The record's
|
||||
position is the checkpoint; it does not move.
|
||||
|
||||
4. **Authored content.** Add 6–10 checkpoints to
|
||||
`addons/core/Configs/ERR/EveronSites.conf` on the main road junctions between
|
||||
the prison, the radio station and the helicopter site — the routes players
|
||||
actually travel. Pick junctions, bridges and town entrances.
|
||||
|
||||
5. **Behaviour when manned.** The garrison gets a `DefendWaypoint` at the
|
||||
checkpoint. One soldier stands at the barrier, the rest hold nearby positions.
|
||||
Idle activity comes from T4 — leave hooks, do not implement smart actions here.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. Driving the prison → radio → helipad route, the player passes at least three
|
||||
checkpoints, each visibly built and manned when approached.
|
||||
2. With no players near any checkpoint, checkpoint garrisons contribute **zero**
|
||||
active agents.
|
||||
3. Props survive their garrison being wiped — a cleared checkpoint still looks
|
||||
like a checkpoint, and the record is marked eliminated so it does not repopulate.
|
||||
4. Checkpoint props never block the road so completely that a vanilla car cannot
|
||||
pass; leave a drivable gap or a moveable barrier.
|
||||
5. `python3 tools/check.py` passes, including the resource-GUID rules.
|
||||
|
||||
## Tests to write
|
||||
|
||||
`tests/test_checkpoint_layout.py`:
|
||||
|
||||
- The pure placement function returns positions that are within the expected
|
||||
radius of the site origin and correctly rotated for a given road heading.
|
||||
- The mirrored variant is the mirror image, and the drivable gap is preserved in
|
||||
both variants (assert a minimum clear width along the road axis).
|
||||
- `ERR_SiteDefinition` validation rejects a checkpoint with zero garrison or a
|
||||
garrison above the allowed maximum.
|
||||
|
||||
## Do not
|
||||
|
||||
- Do not import any composition, prefab or layout from the original mod.
|
||||
- Do not spawn checkpoint props per-approach; they are placed once at preparation.
|
||||
- Do not exceed 12 checkpoints on the first pass. Authoring more is cheap in
|
||||
props but adds up in records and in preparation time.
|
||||
@@ -0,0 +1,77 @@
|
||||
# T4 — Soldiers doing something
|
||||
|
||||
Read `docs/living-island/README.md` first, including the rules section. Depends on T2.
|
||||
|
||||
## Goal
|
||||
|
||||
The soldiers you already pay for should look busy rather than standing at a
|
||||
waypoint. This adds **zero** agents — it changes behaviour, not population, and
|
||||
is the cheapest immersion in the whole plan.
|
||||
|
||||
## What to build
|
||||
|
||||
`addons/core/Scripts/Game/ERR/ERR_SiteActivity.c`.
|
||||
|
||||
Use the vanilla smart-action system:
|
||||
|
||||
- `SCR_AISmartActionComponent` — sits on a world entity that offers an action;
|
||||
`ReserveAction(AIAgent)`, `ReleaseAction()`, `GetOnActionEnd()`,
|
||||
`GetOnActionFailed()`.
|
||||
- `SCR_SmartActionWaypoint.SetSmartActionEntity(IEntity entityWithSmartAction,
|
||||
string smartActionTag)` — the waypoint that sends a group member to perform it.
|
||||
|
||||
Per site, author a handful of **activity slots**: a position, a tag and which
|
||||
site kinds use it. When a garrison is physical and not alerted, assign idle
|
||||
members to free slots; when the site is alerted or the group is in combat,
|
||||
release the slots immediately and let normal AI take over.
|
||||
|
||||
Sensible slots to start with, by site kind:
|
||||
|
||||
- **Checkpoint:** one soldier at the barrier facing the road, one leaning
|
||||
against the parked vehicle, one seated.
|
||||
- **Outpost / depot:** a sentry at the gate, someone working at a crate, someone
|
||||
resting.
|
||||
- **Radio station:** an operator near the equipment, a sentry on the door.
|
||||
- **Prison:** a guard patrolling the walkway, a guard at the gate.
|
||||
|
||||
## Rules
|
||||
|
||||
- Activity assignment runs on the existing site tick, not a new one, and at most
|
||||
once every 5 s per site.
|
||||
- Never assign more than half a garrison to activities at once; the rest hold
|
||||
defensive positions. A checkpoint where everyone is smoking is worse than one
|
||||
where nobody is.
|
||||
- Alert state wins instantly: on perception contact, release every slot for that
|
||||
site in the same tick.
|
||||
- Slots are reserved so two soldiers never claim the same one; release on
|
||||
action end, action failure, death, and on going dormant.
|
||||
- If the vanilla smart action fails or is unavailable for a slot, fall back to a
|
||||
plain hold position and log once per site — never leave a soldier idle in a
|
||||
broken state.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. Approaching a manned checkpoint, at least one soldier is performing an
|
||||
activity rather than standing.
|
||||
2. Firing a shot near the site causes every activity to be released and the
|
||||
garrison to respond, with no soldier stuck in an animation.
|
||||
3. Active agent count is unchanged by this task — assert it in the admin status
|
||||
output before and after.
|
||||
4. A garrison that goes dormant and returns re-acquires activities without
|
||||
leaking reservations (reservation count returns to zero while dormant).
|
||||
5. `python3 tools/check.py` passes.
|
||||
|
||||
## Tests to write
|
||||
|
||||
`tests/test_site_activity.py`:
|
||||
|
||||
- The pure selection rule never assigns more than half the garrison.
|
||||
- A slot already reserved is not handed to a second agent.
|
||||
- Alert state returns an empty assignment set regardless of free slots.
|
||||
- Release-on-dormant leaves zero reservations.
|
||||
|
||||
## Do not
|
||||
|
||||
- Do not write custom animation code. Use the vanilla smart-action system only.
|
||||
- Do not add activity slots to the ledger records; slots are site content.
|
||||
- Do not let an activity survive an alert, a death, or a dormant transition.
|
||||
@@ -0,0 +1,96 @@
|
||||
# T5 — HQ commands the ledger
|
||||
|
||||
Read `docs/living-island/README.md` first, including the rules section. Depends on T1, T2.
|
||||
This task is in the **ai** addon. Core must not learn about HQ.
|
||||
|
||||
## Goal
|
||||
|
||||
Today `ERR_HQ.Assign()` can only task groups that physically exist near a
|
||||
contact. After this task, HQ tasks **ledger records** — including dormant ones
|
||||
far away — so a response can come from across the map, travel virtually, and
|
||||
materialise when it arrives. This is what makes the island feel coordinated
|
||||
rather than reactive, and it costs nothing while the responder is off-screen.
|
||||
|
||||
## What to build
|
||||
|
||||
1. **A core-side interface, implemented where the ledger lives.** Add to
|
||||
`ERR_ForceLedger` (core):
|
||||
|
||||
```
|
||||
int Available(vector near, float radius, out array<int> ids);
|
||||
float EstimateSeconds(int recordId, vector destination); // distance / Speed()
|
||||
bool Order(int recordId, ERR_ForceDuty duty, vector target);
|
||||
bool Pin(int recordId, bool pinned);
|
||||
ERR_ForceKind KindOf(int recordId);
|
||||
int StrengthOf(int recordId);
|
||||
```
|
||||
|
||||
These are generic force-management calls, not HQ concepts, so core owning
|
||||
them does not violate the dependency direction.
|
||||
|
||||
2. **HQ uses them.** In `ERR_HQ.Assign()`, replace first-fit-within-radius with a
|
||||
scored choice over ledger candidates:
|
||||
|
||||
```
|
||||
score = ContactPriority(report) / max(EstimateSeconds(id, target), 1)
|
||||
- CostOfLeaving(KindOf(id))
|
||||
```
|
||||
|
||||
`CostOfLeaving` is high for a `CHECKPOINT` or a garrison at an objective and
|
||||
low for a roving `PATROL`. Put this in a pure `ERR_HQTaskingRules` class so it
|
||||
is testable.
|
||||
|
||||
3. **Ordering a dormant record.** A dormant record receives the order through the
|
||||
ledger and changes duty to `MOVE_TO`/`SEARCH`; it travels virtually and
|
||||
materialises through T2's normal proximity rules when it nears players.
|
||||
HQ pins the record once it is within 1 km of the contact so it cannot go
|
||||
dormant mid-engagement.
|
||||
|
||||
4. **Radio realism is preserved.** The existing `ERR_HQNetwork` rules apply
|
||||
unchanged to **physical** units — a carrier that dies mid-transmission still
|
||||
kills the message. For a dormant record, evaluate the same relay geometry
|
||||
against ledger positions and the record's `radioCapability`, with the same
|
||||
hop limits and latency, but without entities. Log both paths to the journal
|
||||
so the two are distinguishable.
|
||||
|
||||
5. **Fairness invariant, unchanged:** dormant records never observe and never
|
||||
generate contacts. They only receive orders and move. Enforce it in code, not
|
||||
by convention — `Observe()` must refuse a record without a physical group.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. A contact reported near the radio station can be answered by a patrol 3 km
|
||||
away, which travels while dormant and appears as real AI on arrival.
|
||||
2. Checkpoint garrisons are not stripped to chase distant contacts — verify that
|
||||
`CostOfLeaving` keeps them in place unless nothing else is available.
|
||||
3. HQ's journal shows the ETA it used and the record it chose, so the decision is
|
||||
auditable after the fact.
|
||||
4. Killing the radio operator of a physical reporting unit still severs the
|
||||
report, exactly as today.
|
||||
5. `python3 tools/check.py` passes.
|
||||
|
||||
## Also fix while you are here
|
||||
|
||||
`ERR_HQUnit.Eligible()` allocates a fresh `array<AIAgent>` on every call, and
|
||||
`Assign()` calls it inside a contacts × units nested loop every 5 s. Cache the
|
||||
agent array on the unit and refresh it on the network tick instead. Likewise,
|
||||
replace the six linear scans over `m_Units` keyed by group (`ReserveGroup`,
|
||||
`LogisticsToken`, `ReserveLogistics`, `MortarConnected`, `Downlink`,
|
||||
`QueueSupplyStatus`) with a `map<SCR_AIGroup, ERR_HQUnit>` lookup.
|
||||
|
||||
## Tests to write
|
||||
|
||||
`tests/test_hq_tasking.py`:
|
||||
|
||||
- `EstimateSeconds` scales with distance and with kind speed.
|
||||
- The scored choice prefers a closer patrol over a distant one at equal priority,
|
||||
and prefers a distant patrol over a checkpoint garrison at equal distance.
|
||||
- With only checkpoint garrisons available and a high-priority contact, one is
|
||||
eventually released — the cost is a weight, not a veto.
|
||||
- A dormant record refuses to produce an observation.
|
||||
|
||||
## Do not
|
||||
|
||||
- Do not let HQ spawn units directly. It asks the ledger; the budget decides.
|
||||
- Do not add `ERR_HQ` references to core files.
|
||||
- Do not remove the existing radio latency or failure handling to simplify this.
|
||||
@@ -0,0 +1,87 @@
|
||||
# T6 — Cheap signs of life
|
||||
|
||||
Read `docs/living-island/README.md` first, including the rules section. Depends on T2.
|
||||
|
||||
## Goal
|
||||
|
||||
Sell an occupied island through things that are not soldiers. Every item here is
|
||||
either a static prop, a single entity, or a timed event — none of it adds a
|
||||
standing AI presence.
|
||||
|
||||
Budget for this whole task: **at most 4 active agents at any time, island-wide.**
|
||||
|
||||
## What to build
|
||||
|
||||
`addons/core/Scripts/Game/ERR/ERR_AmbientLife.c`.
|
||||
|
||||
### 1. Static occupation markers (zero CPU)
|
||||
|
||||
Placed once during preparation, never ticked:
|
||||
|
||||
- Parked military vehicles at sites and checkpoints.
|
||||
- Crates, barrels, tents, field tables at outposts.
|
||||
- At night, lit windows and lamps at occupied sites; dark at abandoned ones.
|
||||
Drive the light state from the site's eliminated flag, evaluated on the
|
||||
existing environment tick, not a new one.
|
||||
|
||||
This alone changes how the island reads at 300 m, for nothing.
|
||||
|
||||
### 2. Road traffic that exists only when watched
|
||||
|
||||
A `CONVOY` ledger record: a vehicle plus 2 crew travelling a long road route.
|
||||
It simulates virtually like anything else and materialises only near players.
|
||||
Two convoy records island-wide is enough — the player meets traffic occasionally
|
||||
and it is real when they do.
|
||||
|
||||
### 3. Distant transit
|
||||
|
||||
Occasionally (every 8–15 minutes, settings `ambientTransitSeconds`), a single
|
||||
helicopter flies a high-altitude route across the map, well above small-arms
|
||||
range, and despawns at the far edge. One vehicle, one pilot, bounded lifetime,
|
||||
hard cap of one at a time. It must never route over the extraction site while
|
||||
extraction is active.
|
||||
|
||||
### 4. Audio and radio texture
|
||||
|
||||
- Use only vanilla sound prefabs. **No audio from the original mod** — see the
|
||||
licensing rules.
|
||||
- Occasional distant, non-hostile gunfire near enemy sites (a training burst) is
|
||||
a strong cue. Gate it hard: never within 400 m of a player, never during an
|
||||
alert, at most once per 3 minutes.
|
||||
|
||||
## Rules
|
||||
|
||||
- Everything in this task is skippable: if the budget is full or the mission is
|
||||
in a terminal phase, ambient life is the first thing dropped.
|
||||
- One tick, ≥ 10 s, shared by all ambient features. No feature gets its own timer.
|
||||
- Every ambient entity is owned and tracked so `Stop()` removes all of it. Nothing
|
||||
here may survive a restart — the repo already had restart-cleanup work; do not
|
||||
regress it.
|
||||
- Ambient units are `SCR_EAISpawnImportance.LOW` so they lose the budget contest
|
||||
first.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. Standing still in open terrain for 10 minutes, a player sees or hears at least
|
||||
two distinct signs of occupation.
|
||||
2. Ambient features contribute at most 4 active agents at any moment, verified in
|
||||
the admin status output.
|
||||
3. Restarting the mission leaves no ambient entity behind.
|
||||
4. No ambient event fires during extraction over the extraction site.
|
||||
5. `python3 tools/check.py` passes.
|
||||
|
||||
## Tests to write
|
||||
|
||||
`tests/test_ambient_life.py`:
|
||||
|
||||
- The gating rule refuses a transit while one is already active, during a
|
||||
terminal phase, and when the agent budget is full.
|
||||
- The gunfire rule refuses within 400 m of a player and inside the cooldown.
|
||||
- The ownership list is emptied by `Stop()`.
|
||||
|
||||
## Do not
|
||||
|
||||
- Do not add civilians in this task. A civilian population is a separate, much
|
||||
larger piece of work with its own budget.
|
||||
- Do not spawn ambient AI that patrols indefinitely; ambient means bounded.
|
||||
- Do not reuse any audio, particle or composition asset from the original mod.
|
||||
@@ -0,0 +1,68 @@
|
||||
# T7 — Budget enforcement, metrics and anti-thrash
|
||||
|
||||
Read `docs/living-island/README.md` first, including the rules section. Depends on T1–T6.
|
||||
|
||||
## Goal
|
||||
|
||||
Prove the living island stays inside its CPU budget, and make it obvious when it
|
||||
does not. Without this task the rest is a plausible design with no evidence.
|
||||
|
||||
## What to build
|
||||
|
||||
1. **A metrics surface.** `addons/core/Scripts/Game/ERR/ERR_ForceMetrics.c`:
|
||||
|
||||
- active agents, physical records, dormant records, refused spawns
|
||||
- state flips in the last minute (the thrash indicator)
|
||||
- simulation tick duration, presence tick duration (min / mean / max)
|
||||
- budget headroom, and how long the budget has been saturated
|
||||
|
||||
Expose through the existing admin menu and `ERR_HQ.Event` journal (ai side).
|
||||
Reuse `ERR_AdminProvider`; do not build a new UI.
|
||||
|
||||
2. **Thrash guard.** If any record flips state more than twice per minute, widen
|
||||
that record's hysteresis bands automatically and log it. Repeated flipping is
|
||||
the failure mode that turns this design from a saving into a cost.
|
||||
|
||||
3. **Saturation policy.** When the budget stays full for more than 60 s, shed
|
||||
load in a defined order and log each step:
|
||||
`ambient (T6)` → `distant patrols` → `checkpoint garrisons beyond 1.5 km` →
|
||||
never shed: objective garrisons, HQ-pinned records, anything in combat.
|
||||
|
||||
4. **Tick cost discipline.** Measure the simulation and presence ticks. If either
|
||||
exceeds its allowance (2 ms simulation, 3 ms presence) for 5 consecutive
|
||||
ticks, halve the records processed per tick by increasing the stagger, and log.
|
||||
Degrade smoothly rather than stuttering.
|
||||
|
||||
5. **A measured comparison.** Extend the existing performance-comparison tooling
|
||||
(`docs/performance-comparison.md`) with a living-island run: same route, same
|
||||
duration, ledger enabled versus disabled. Record active agents and frame time.
|
||||
This is the number that decides whether the design worked.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. A 20-minute session with a player driving the full route keeps active agents
|
||||
at or under `activeAgentBudget` at every sample.
|
||||
2. Total state flips across all records stay under 2 per minute on average.
|
||||
3. Simulation and presence ticks stay inside their allowances at 128 records.
|
||||
4. The shed order is observable in the journal when the budget saturates.
|
||||
5. The comparison run is recorded with real numbers, and the write-up states
|
||||
plainly whether frame time improved, stayed flat, or regressed.
|
||||
6. `python3 tools/check.py` passes.
|
||||
|
||||
## Tests to write
|
||||
|
||||
`tests/test_force_metrics.py`:
|
||||
|
||||
- The thrash guard widens bands after the third flip inside a minute.
|
||||
- The shed order never selects a pinned, in-combat or objective record, even when
|
||||
everything else has already been shed.
|
||||
- The tick-cost governor increases stagger after 5 consecutive overruns and
|
||||
restores it after sustained headroom.
|
||||
|
||||
## Reporting rules
|
||||
|
||||
Follow the repo's evidence discipline. State which numbers came from a real
|
||||
Workbench or server run and which came from scripted fixtures. If the comparison
|
||||
was not run in the real game, say so in the write-up rather than implying it was.
|
||||
A regression that is reported honestly is more useful than an improvement that is
|
||||
not reproducible.
|
||||
@@ -0,0 +1,117 @@
|
||||
# T8 — HQ memory: belief, not a 2-minute cache
|
||||
|
||||
Read `docs/living-island/README.md` first, including the rules section.
|
||||
This task is in the **ai** addon. Depends on T5.
|
||||
|
||||
## The problem
|
||||
|
||||
`ERR_HQ.Tick()` deletes every contact older than `ERR_MissionRules.ContactFresh`
|
||||
(120 s), and `m_Contacts` is a flat array capped at 64. After two quiet minutes
|
||||
HQ knows *nothing*. There is no record that a break happened, no last known
|
||||
heading, no memory of which ground has already been searched. Velocity
|
||||
extrapolation between two reports under 30 s apart is the only inference in the
|
||||
system.
|
||||
|
||||
A commander's main asset is an accumulated picture. HQ currently has a sliding
|
||||
window. Everything that makes the enemy feel like a thinking opponent — sweeping
|
||||
outward from a last sighting, cutting off a route, giving up on cleared ground —
|
||||
is impossible without this task.
|
||||
|
||||
## What to build
|
||||
|
||||
`addons/ai/Scripts/Game/ERR/ERR_HQBelief.c`, with the usual split: a pure rule
|
||||
class and the service that applies it.
|
||||
|
||||
```
|
||||
class ERR_HQBeliefRules
|
||||
{
|
||||
static float Confidence(float ageSeconds, float halfLife); // decays, never deletes
|
||||
static float Radius(float ageSeconds, float growthPerSecond, float floor);
|
||||
static bool Supersedes(float newConfidence, float oldConfidence, float distance);
|
||||
static bool Actionable(float confidence, float radius); // worth tasking against
|
||||
static int SweepCell(...); // next unsearched cell, nearest-first outward
|
||||
}
|
||||
|
||||
class ERR_HQBelief { ... }
|
||||
```
|
||||
|
||||
### Contacts decay, they do not vanish
|
||||
|
||||
Replace deletion with decay. A belief holds position, observation time,
|
||||
confidence, an estimated heading, and the report that produced it.
|
||||
|
||||
- Confidence falls on a half-life (`beliefHalfLifeSeconds`, default 600).
|
||||
- **Uncertainty grows into a radius.** A contact 4 minutes old is not a point —
|
||||
it is a circle the target could have reached, expanding at
|
||||
`beliefRadiusGrowth` (default 1.2 m/s, roughly walking pace, faster if the last
|
||||
sighting was in a vehicle). This single value is what lets HQ search *an area*
|
||||
instead of chasing a stale pixel.
|
||||
- Beliefs become useless by growing wide and faint, not by being deleted. Retire
|
||||
one only when its radius exceeds a cap (default 2000 m) or a newer belief
|
||||
supersedes it.
|
||||
- Cap at 64 beliefs; merge rather than duplicate when a new report lands inside
|
||||
an existing belief's radius.
|
||||
|
||||
### Searched ground
|
||||
|
||||
A coarse coverage grid, 500 m cells, each holding "last swept at" and "swept by".
|
||||
A unit that completes a search waypoint marks its cells. HQ uses this to sweep
|
||||
*outward* from a last sighting instead of re-walking ground it already cleared.
|
||||
Cells decay back to unswept over `coverageDecaySeconds` (default 900) — ground
|
||||
cleared twenty minutes ago is not cleared now.
|
||||
|
||||
### Objective threat
|
||||
|
||||
Per objective, a standing threat score from nearby beliefs plus **route
|
||||
inference**: escapists need a radio and then a helicopter, so the radio station
|
||||
and the helipad carry standing weight even with no contacts at all. This is the
|
||||
plot of the mission, and HQ is allowed to know its own doctrine — it is not
|
||||
allowed to know where the players are.
|
||||
|
||||
### The fairness rule, enforced in code
|
||||
|
||||
Belief is fed **only** by reports delivered through the radio network. Never by
|
||||
player positions, never by a dormant record, never by a direct world query.
|
||||
Write the ingest path so there is exactly one entry point and it takes a
|
||||
delivered `ERR_HQReport`. If a report never arrives because its carrier was
|
||||
killed, HQ never learns it — that is the whole design.
|
||||
|
||||
## Wiring
|
||||
|
||||
- `ERR_HQ` owns the belief service and feeds it from `Accept()`.
|
||||
- `Assign()` (T5) tasks against beliefs rather than raw contacts.
|
||||
- The journal writes belief changes at a coarse level: new belief, superseded,
|
||||
retired, area swept. Not every decay tick.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. Ten minutes after the last sighting, HQ still knows roughly where to look and
|
||||
its search area has grown accordingly — it does not behave as though nothing
|
||||
ever happened.
|
||||
2. A unit that sweeps an area marks it; the next search order goes to adjacent
|
||||
unswept ground, not the same cells.
|
||||
3. With no contacts at all, the radio station and helipad still carry threat and
|
||||
still attract garrisons.
|
||||
4. Killing the reporting unit's radio operator prevents the belief from ever
|
||||
forming — verified, not assumed.
|
||||
5. Belief count stays capped and merges correctly under a burst of reports.
|
||||
6. `python3 tools/check.py` passes.
|
||||
|
||||
## Tests to write
|
||||
|
||||
`tests/test_hq_belief.py`:
|
||||
|
||||
- `Confidence` halves over the configured half-life and never returns negative.
|
||||
- `Radius` at t=0 equals the floor and grows linearly; a vehicle sighting grows faster.
|
||||
- `Supersedes` prefers a fresh nearby report over a stale wide one, and refuses
|
||||
to replace a high-confidence belief with a distant low-confidence one.
|
||||
- `SweepCell` returns nearest-first outward and never returns a cell swept
|
||||
within the decay window.
|
||||
- `Actionable` refuses a belief whose radius has outgrown the cap.
|
||||
|
||||
## Do not
|
||||
|
||||
- Do not delete beliefs on a timer. Decay is the mechanism.
|
||||
- Do not let anything except a delivered report write into belief.
|
||||
- Do not run belief decay per frame; recompute confidence and radius on read
|
||||
from the stored timestamp. There is no decay tick at all.
|
||||
@@ -0,0 +1,127 @@
|
||||
# T9 — Pursuit: alert states, hunting, air search and cordons
|
||||
|
||||
Read `docs/living-island/README.md` first, including the rules section.
|
||||
Depends on T5 and T8. Belief (T8) must exist — you cannot hunt what you do not
|
||||
remember.
|
||||
|
||||
## The problem
|
||||
|
||||
An occupied island is not a hunting island. Today HQ answers a sighting with one
|
||||
`AIWaypoint_SearchAndDestroy` at a predicted position, and releases the unit the
|
||||
moment it perceives anything. Nothing tracks a fleeing player, nothing searches
|
||||
from the air, nothing cuts a route. This task is where the pressure comes from.
|
||||
|
||||
## 1. Alert state
|
||||
|
||||
Per sector (reuse the 500 m coverage grid, grouped into ~2 km sectors):
|
||||
|
||||
| State | Trigger | Behaviour |
|
||||
| --- | --- | --- |
|
||||
| `CALM` | default | Garrisons hold, patrols run routes |
|
||||
| `ALERTED` | a belief forms in the sector | Patrols tighten toward it, checkpoints check harder |
|
||||
| `HUNTING` | actionable belief, confidence above threshold | Hunters dispatched, air search eligible |
|
||||
| `CORDON` | high confidence and a known direction of travel | Chokepoints blocked between the contact and its likely objective |
|
||||
|
||||
States decay downward on `alertDecaySeconds` (default 420) without new reports.
|
||||
Escalation is the pacing dial for the whole mission — it is what makes the island
|
||||
feel like it is closing in, and what lets it relax when the player breaks contact.
|
||||
|
||||
## 2. Hunting on the ground
|
||||
|
||||
Dispatch at most `huntMaxUnits` (default 3) against one belief. Hunters sweep the
|
||||
**uncertainty circle**, not a point: take the nearest unswept cells from T8's
|
||||
coverage grid and issue search waypoints outward from the last sighting.
|
||||
|
||||
- A hunter that completes its sweep reports back — "area searched, nothing found"
|
||||
— which marks the cells and lowers that belief's confidence. **HQ must be able
|
||||
to learn that a search failed.** Today it cannot, and that is a core defect.
|
||||
- Do not release the assignment the instant a unit perceives something. Combat
|
||||
suspends the sweep; when combat ends the sweep resumes or is re-tasked.
|
||||
- A belief that has been swept out returns the sector to `ALERTED`, then `CALM`.
|
||||
|
||||
## 3. Air search — the hunter helicopter
|
||||
|
||||
This is a **separate role from transport**. One at a time
|
||||
(`airSearchConcurrent`, default 1), drawn from the same aircraft pool, so a
|
||||
helicopter searching is a helicopter not lifting troops.
|
||||
|
||||
- Flies a search pattern (expanding box or lane search) over the belief's
|
||||
uncertainty circle at low altitude, using the existing `ERR_AircraftService`
|
||||
through the same provider interface as air lift.
|
||||
- It observes under the **normal fairness rules** — `ERR_HQObservation` line of
|
||||
sight, exposure and distance gating apply to its crew exactly as to a foot
|
||||
patrol. A helicopter sees more because it is high and moving, not because it
|
||||
cheats.
|
||||
- It reports by radio like any other unit. Shooting it down denies HQ its best
|
||||
sensor, and the loss is permanent from the pool.
|
||||
- Fuel/endurance cap (`airSearchSeconds`, default 420) then it returns to
|
||||
staging. An unlimited eye in the sky is oppressive and expensive.
|
||||
|
||||
## 4. Cordons
|
||||
|
||||
When a sector reaches `CORDON`, place up to `cordonChokepoints` (default 4)
|
||||
blocking positions **between the belief and the objective it is probably heading
|
||||
for** — bridges, road junctions, ridge saddles. Not on top of the player.
|
||||
|
||||
Cordon positions are authored chokepoints on the site map, selected by which ones
|
||||
lie on plausible routes from the belief to the radio station or helipad. This is
|
||||
the payoff of the staleness problem: HQ knows its picture is old, so it blocks
|
||||
where the player must pass rather than chasing where the player was.
|
||||
|
||||
## 5. Pacing — this decides whether the mission is fun or miserable
|
||||
|
||||
Players start with nothing. Unbounded pressure is not difficulty, it is a
|
||||
cutscene about dying. Enforce all of these:
|
||||
|
||||
- **Never materialise a hunter within `minHunterSpawnMetres`** (default 700) of
|
||||
any player. No unit ever appears close enough to feel teleported in.
|
||||
- **No convergence from all sides.** Hunters approach from at most two bearings;
|
||||
leave an escape vector open. A closing ring with no gap is a death sentence.
|
||||
- **Cooldown after contact is lost** (`huntCooldownSeconds`, default 180) before
|
||||
a fresh hunt on the same belief. Breaking contact must be rewarded.
|
||||
- **Hunting spends the pool.** Sections hunting are not garrisoning; if HQ
|
||||
commits hard to a hunt, its checkpoints thin and routes open elsewhere. Do not
|
||||
paper over this — it is the strategic trade that makes the system a game rather
|
||||
than a pressure dial.
|
||||
- Night reduces air search effectiveness and sighting range; it should be the
|
||||
player's window.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. Being seen and then breaking contact produces a visible search: units sweeping
|
||||
outward from where you were, a helicopter overhead, and then — if you stay
|
||||
hidden and still — the search moving away and the sector calming down.
|
||||
2. A failed sweep lowers confidence; HQ does not re-search the same cells.
|
||||
3. The hunter helicopter spots players only under the same LOS/exposure rules as
|
||||
infantry; destroying it measurably degrades HQ's picture.
|
||||
4. A cordon appears between the player and the objective, not around the player's
|
||||
exact position.
|
||||
5. No hunter ever materialises within 700 m of a player, and hunters never
|
||||
approach from more than two bearings at once.
|
||||
6. Heavy hunting visibly thins garrisons elsewhere — verify by checking a
|
||||
checkpoint's strength during a major hunt.
|
||||
7. Active agents stay inside `activeAgentBudget` throughout.
|
||||
8. `python3 tools/check.py` passes.
|
||||
|
||||
## Tests to write
|
||||
|
||||
`tests/test_pursuit.py`:
|
||||
|
||||
- Alert escalation and decay: state rises with confidence and falls after the
|
||||
decay window with no new reports.
|
||||
- Hunter dispatch respects `huntMaxUnits`, the cooldown, and the minimum spawn
|
||||
distance.
|
||||
- Bearing rule: given four candidate hunters around a belief, at most two
|
||||
distinct approach bearings are selected and an escape vector remains.
|
||||
- Cordon selection picks chokepoints on the belief→objective path, never the
|
||||
belief's own cell.
|
||||
- Air search respects concurrency and endurance, and returns to staging on
|
||||
timeout.
|
||||
|
||||
## Do not
|
||||
|
||||
- Do not give hunters or the search helicopter any knowledge the fairness rules
|
||||
would not produce. The pressure must come from good search, not from cheating.
|
||||
- Do not stack hunts on multiple beliefs simultaneously beyond the unit cap.
|
||||
- Do not let the cordon seal every route. There is always a way through; it
|
||||
should be slow, exposed or unpleasant, never absent.
|
||||
Reference in New Issue
Block a user