Stop successor services on native game-over and preserve escape results

This commit is contained in:
2026-09-19 13:56:15 +02:00
parent a880e0485c
commit 6f0e4c5978
70 changed files with 11457 additions and 23 deletions
+21
View File
@@ -0,0 +1,21 @@
# Native game-over closes successor services
The successor previously stopped through its own terminal `Transition`, preparation failure or destruction. Vanilla's independent `EndGameMode` path could therefore enter native POSTGAME and show the end screen while the successor phase, preparation callback, HQ tick and admissions remained active until reload. The installed vanilla editor game-over attribute reaches this path through `SCR_GameOverScreenManagerComponent.SetEditorGameOverType` and its delayed `EndGameMode` call. This was a source-evidenced lifecycle defect; no remote-client timing assumption is needed.
The successor now overrides `OnGameModeEnd` with a per-instance guard. It latches native termination on every machine, removes pending local observer binding, and on authority converts an unfinished successor phase to DEFEAT without creating an escape snapshot. It publishes only existing identity-derived results, emits the successor phase event and stops services once. An already terminal successor victory or defeat keeps its phase, reason and extraction snapshot; it is not republished or rewritten. The original native `endData` passes unchanged to `super.OnGameModeEnd`, which remains the sole owner of restart scheduling. The hook never calls `Transition` or `EndGameMode`, and a reentrant/duplicate notification cannot schedule another restart through it.
Installed vanilla ordering matters: `SCR_BaseGameMode.EndGameMode` stores `endData` and sets POSTGAME before `OnGameStateChanged` calls `OnGameModeEnd`; component end callbacks run afterward. The base implementation may restart synchronously when auto-reload is zero. Successor cleanup therefore happens before delegation. Client `Active()` also observes the local native-end latch, so independent phase replication cannot briefly leave the observer UI treating POSTGAME as active.
`StopServices` now removes queued `StartScenario`, `ServerTick` and `BindLocalObserver`. Preparation and local binding also guard an already dequeued callback against stopped/native-ended state. Existing observer monitors remain mode-state driven, and normal admission cleanup still runs through the existing OnStop event. No native clock setters, result identities, permission rules or unrelated mission transition rules changed.
Run `python3 tests/test_native_end_lifecycle.py`. It executes the actual native-end hook, shutdown and active bodies with native boundaries substituted, plus the actual preparation entry guard. It checks every unfinished phase, pre-existing victory/defeat snapshots, unchanged native data, exactly-once services/restart delegation, reentrant listeners, client phase separation and stale preparation callbacks. It does not prove engine callback timing or multiplayer.
## Native two-round checkpoints
`ERR_NativeEndProbe.c` is entirely WORKBENCH-only. The Core runner must select either `-errBaseSmoke 1 -errSmokeOutcome native-end-preparing` or `native-end-active`, exactly two rounds, a bounded timeout of at least 120 seconds, and vanilla auto-reload of at least two seconds. Existing BaseSmoke must return early for these two outcomes after its `super.EOnInit` and native opt-in gate, before registering its normal fixture work. The integrated BaseSmoke early return and runner restrictions prevent the ordinary fixture from also running.
The preparation case requires the actual initial `StartScenario` still queued; it does not remove or defer that callback to manufacture the precondition. The active case waits for the actual authored mission to reach PRISON. Both invoke the real inherited `EndGameMode` once with native defeat data, observe unchanged end-data object identity, one phase/stop event, no fabricated escaped accounts, and absent preparation/server/local-binding queues. After more than a second they invoke the actual stale preparation/tick bodies plus idempotent shutdown and verify no new work, then leave restart entirely to vanilla.
The private marker is `$profile:erri-native-end-round.json`. The runner must refuse pre-existing marker, `.tmp` or `.bak` paths before launching. The first completed observation writes `completedRound=1`; a fresh second mode requires that marker and checks fresh phase/extraction/native-end state, then removes only its marker after success. The probe emits exactly one `ERR SMOKE COMPLETE` per completed mode, with `round` and `preparing` diagnostics. Its own 45-second per-mode deadline and queued observation callback are independently bounded/cleaned up. Both native cases now pass two rounds with 21 assertions and zero captured errors each: [preparation 133847](evidence/native-2026-09-19-133847-native-end-preparing/report.json) and [active 134044](evidence/native-2026-09-19-134044-native-end-active/report.json). Vanilla owns the observed restart between fresh instances. The fixtures verify local shutdown of the active mission and queues; they do not arrange an in-flight native preload or prove remote client timing. Current normal Core and companion escape/restart regressions pass separately on the same addon sources.
Unknown personal results now display “Round ended”; only a recorded escaped/missed extraction result uses helicopter-specific wording. The existing native result widget regression verifies unknown, delayed known result and reset refresh; native winner connection IDs never manufacture personal escape.