Files
jakub cab998fb64 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.
2026-09-20 16:42:52 +02:00

432 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.