36 lines
7.5 KiB
Markdown
36 lines
7.5 KiB
Markdown
# Radio defence objective
|
||
|
||
Core's `ERR_RadioDefenceService` owns radio authorization, physical defender selection, continuous transmission time and completion. `ERR_GameMode` wires its events into mission progression; companion addons do not advance the objective. The service runs only on the server. Native tasks remain the replicated presentation of its progress.
|
||
|
||
A radio can be tuned once during `RADIO_SEARCH`, only if it is registered with the mission, is a radio site, has been identified, and a conscious prisoner is within five metres. The prisoner must be the current body of a connected, already-spawned round participant and belong to the configured player faction. An AI character, reserved disconnected body, spectator, hostile controlled character or replacement body outside the roster cannot authorize a transmission.
|
||
|
||
Once tuned, at least one eligible conscious prisoner must remain strictly inside the 60-metre radius. The service samples native player/character state on the existing one-second server tick. Multiple defenders share one timer: losing one defender does not reset progress while another remains. Losing the last eligible defender resets all accumulated time. Death, unconsciousness, distance, faction, current connection/roster identity and an empty server are checked on each sample. Backward or invalid sample times cannot manufacture completion.
|
||
|
||
The existing `/esc mission radio show|set N` command and 30–1800 second bounds are unchanged. Unlike the next-round mission clock settings, this duration remains live: increasing it preserves elapsed continuous occupancy and lengthens the remaining time; lowering it can complete a sufficiently long current transmission at the next server sample. A settings-save failure keeps the old value.
|
||
|
||
## Interface and events
|
||
|
||
The service exposes `State`, `Location`, `Occupied`, `DefenderCount`, `Since`, `DurationSeconds`, `SecondsRemaining(now)`, `DefenceRadius()` and `TuneRadius()`. GameMode retains its `TuneRadio`, `DefenceOccupied` and `DefenceSecondsRemaining` forwarding methods. These service/progress methods are server-side; clients consume the native replicated task description rather than reading the service.
|
||
|
||
- `OnStarted(location)` fires once when a validated radio begins. GameMode changes the phase to `RADIO_DEFENCE`; if that transition fails it stops the service.
|
||
- `OnProgressChanged(service)` fires when state, defender count, start timestamp or configured duration changes. Ordinary elapsed ticks do not emit redundant events. Consumers read the snapshot through the service getters.
|
||
- `OnCompleted(location)` fires once after the service latches completion. GameMode verifies the completed service and current phase, reveals the extraction site and changes to `EXTRACTION`.
|
||
|
||
The completion latch is set before either observer event runs. Nested ticks, repeated ticks or repeated tuning therefore cannot emit another completion. Mission stop and phase changes beyond radio defence cancel an incomplete service; a stopped service cannot restart. Completed state remains latched through cleanup.
|
||
|
||
## Validation scope
|
||
|
||
`tests/test_radio_defence_policy.py` runs the actual Enforce policy bodies in the existing C++ harness. It covers the phase/discovery/authorization matrix, exact five- and 60-metre boundaries, defender handover, interruption, zero eligible defenders, backward time and exact duration completion. These local checks and the existing repository checks pass; they are not Enforce compilation.
|
||
|
||
`ERR_NativeRadioDefenceProbe` is a WORKBENCH-only asynchronous fixture run before normal Core smoke tuning. Construct it with the current mode, real controlled prisoner and identified radio. `Tick(now)` returns `0` while pending, `1` only after all assertions and physical restoration pass, or `-1` on failure. The caller must invoke `Stop()` on cancellation, timeout and mission stop. Stop is idempotent: it detaches the fixture's event listeners, stops its isolated services, restores the saved radio duration and the real player's original transform/consciousness, and deletes its own second character. No fixture-owned recurring callqueue survives cancellation.
|
||
|
||
The fixture keeps native world advancement separate from its deterministic service samples. Teleports, consciousness changes, faction changes and death are followed by a minimum 750 ms wait before querying physical state. Recovery additionally polls the actual living/conscious controller state for at most twelve seconds per recovery step, without repeating the state-change command or changing health to force success. Those deadlines do not move when another poll is scheduled. Normal success waits for deferred player restoration to settle and verifies the restored setting, controller state and position.
|
||
|
||
One character is the actual local player's body. The second is a newly spawned physical character whose connection membership is simulated by a WORKBENCH-only defender source. Its sampled connection count is also simulated for the empty-server case. Both actors use native faction, controller, damage and position queries. They occupy separate positions to avoid overlapping bodies during knockdown and recovery. Diagnostics record actual radio/player positions and measured distance, controller life state and unconsciousness, plus the current phase, registered radio, roster binding, faction and authorization result. The five-metre tuning limit is checked against observed positions. Physical defence exclusion requests 60.5 metres and verifies at least 60 metres; the exact 60-metre policy boundary remains covered by the policy regression.
|
||
|
||
The isolated service tests ordinary AI and unregistered/hostile rejection, multiple defenders sharing one interval, unconscious/dead defender loss, empty-server reset, live duration changes, stop/terminal cancellation and duplicate or re-entrant completion. Its explicit timestamps test exact timer boundaries; they are not evidence of elapsed multiplayer or real-time transmission duration. The production service and the live mission phase are unchanged by the isolated probes. After successful cleanup, normal Core smoke tunes the live radio and performs the existing real delayed transmission, interruption and recovery sequence.
|
||
|
||
Recent native runs exposed fixture timing failures after physical mutation and during recovery. Those failures remain archived. The archived [050515](evidence/native-2026-09-19-050515/report.json) run completes the asynchronous radio fixture and ordinary escape twice, with 340 total assertions and no script/VM exceptions. Seven inherited resource errors still prevent a clean runtime pass. Scripted controller changes are not medical-treatment or revive validation. The fixture does not demonstrate two remote clients, real disconnection/reconnect timing, remote task synchronization or long-duration multiplayer behavior.
|
||
|
||
The initial native task now distinguishes `WAITING` from an actual interruption: tuning starts the service before its first occupancy sample, so that short interval says "Waiting for a conscious defender" and retains the configured required duration. A later loss of defenders still says "Transmission interrupted" and resets progress. The focused `test_radio_waiting_text.py` executes the actual copy methods across these states; the Core smoke branch also checks the real task description immediately after tuning, before its first occupancy sample. The [180610 Core regression](evidence/native-2026-09-19-180610-core-inventory-radio/report.json) passes that actual task assertion in both escape/restart rounds, with 614 total assertions and zero captured errors. Remote task synchronization remains unqualified.
|