5.3 KiB
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 and active 134044. 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.