From 681d5e5eae880c5aae692d51b9cf526469409d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BD=C3=A1=C4=8Dek?= Date: Fri, 30 Oct 2026 15:20:21 +0100 Subject: [PATCH] Extend results countdown and stabilize spectator hints; record Workshop 0.0.128 --- CHANGELOG.md | 7 ++- addons/core/Prefabs/ERR/GameMode.et | 2 +- .../Scripts/Game/ERR/00_Core/ERR_Observer.c | 19 +++++- .../Game/ERR/00_Core/ERR_PlayerGuide.c | 4 +- .../ERR/00_Core/ERR_UnconsciousCameraProbe.c | 3 +- docs/configuration.md | 3 + .../native-fixture.c | 1 + .../readable-end-results.json | 9 +++ .../readable-end.log | 20 ++++++ tests/test_hint_delivery.py | 62 ++++++++++++------- 10 files changed, 100 insertions(+), 30 deletions(-) create mode 100644 docs/evidence/community-statistics-2026-10-30/readable-end-results.json create mode 100644 docs/evidence/community-statistics-2026-10-30/readable-end.log diff --git a/CHANGELOG.md b/CHANGELOG.md index 432781f..1d1f69c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog -Latest published Workshop version: **0.0.127** +Latest published Workshop version: **0.0.128** + +## 2026-10-30 — Results reading time and spectator hints + +- Kept mission results open for two minutes before automatic restart. +- Prevented objective hints from interrupting unconscious spectators and stabilized recovery messages to reduce repeated switching. ## 2026-10-30 — Results panel visibility diff --git a/addons/core/Prefabs/ERR/GameMode.et b/addons/core/Prefabs/ERR/GameMode.et index 67809f2..c26bd57 100644 --- a/addons/core/Prefabs/ERR/GameMode.et +++ b/addons/core/Prefabs/ERR/GameMode.et @@ -11,6 +11,6 @@ ERR_GameMode : "{0F307326459A1395}Prefabs/MP/Modes/GameMode_Base.et" { m_SpawnLogic ERR_SpawnLogic "{79AD722B2EFD595F}" {} } } - m_fAutoReloadTime 10 + m_fAutoReloadTime 120 m_bUseSpawnPreload 1 } diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c index d85bac3..475108a 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c @@ -284,6 +284,8 @@ modded class SCR_PlayerController protected ERR_ObserverReason m_ERRShownObserverReason; protected bool m_ERRShownObserverAvailable; protected string m_ERRShownRecoveryStatus; + protected string m_ERRPendingRecoveryStatus; + protected int m_ERRRecoveryStatusSince; protected float m_ERRLastCycle = -1000; protected IEntity m_ERRRecoveryBody; protected float m_ERRRecoveryDeadline = -1; @@ -468,13 +470,24 @@ modded class SCR_PlayerController string recovery = mode.RecoveryStatus(); string selfEpiHint = ERR_SelfEpinephrineHint(); if (!selfEpiHint.IsEmpty()) recovery += "\n" + selfEpiHint; - if (ERR_ObserverGuidance.Refresh(m_ERRObserverReady, m_ERRShownObserverReason, m_ERRObserverReason, m_ERRShownObserverAvailable, survivorAvailable) || m_ERRShownRecoveryStatus != recovery) + bool contextChanged = ERR_ObserverGuidance.Refresh(m_ERRObserverReady, m_ERRShownObserverReason, m_ERRObserverReason, m_ERRShownObserverAvailable, survivorAvailable); + int now = System.GetTickCount(); + // Medical samples can alternate between improving and stalled. Keep the + // accepted hint readable until the new status has remained stable for 3s. + // This affects presentation only, never the authoritative defeat timer. + if (m_ERRPendingRecoveryStatus != recovery || now < m_ERRRecoveryStatusSince) + { + m_ERRPendingRecoveryStatus = recovery; + m_ERRRecoveryStatusSince = now; + } + if (!contextChanged && m_ERRShownRecoveryStatus != recovery && now - m_ERRRecoveryStatusSince < 3000) return; + if (contextChanged || m_ERRShownRecoveryStatus != recovery) { SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance(); if (!hints) return; // retry on the existing monitor; do not lose the first hint string message = ERR_ObserverGuidance.Message(m_ERRObserverReason, survivorAvailable); if (!recovery.IsEmpty()) message += "\n" + recovery; - if (!hints.ShowCustom(message, "Spectating", 12)) return; + if (!hints.ShowCustom(message, "Spectating", 12, m_ERRObserverReady)) return; m_ERRObserverReady = true; m_ERRShownObserverReason = m_ERRObserverReason; m_ERRShownObserverAvailable = survivorAvailable; @@ -510,6 +523,8 @@ modded class SCR_PlayerController { m_ERRObserverReady = false; m_ERRShownRecoveryStatus = ""; + m_ERRPendingRecoveryStatus = ""; + m_ERRRecoveryStatusSince = 0; if (!m_ERRObserverCamera) return; if (GetGame().GetPlayerController() == this) m_ERRObserverCamera.TrySwitchToControlledEntityCamera(); m_ERRObserverCamera.Terminate(); diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerGuide.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerGuide.c index ce65d3b..1b2d86b 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerGuide.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerGuide.c @@ -40,7 +40,9 @@ modded class SCR_PlayerController } ERR_GameMode mode = ERR_GameMode.Get(); if (!mode) return; - if (mode.Phase() != ERR_Phase.PREPARING && (ERR_ObserverState() != ERR_ObserverReason.NONE || !ERR_Actors.Alive(GetControlledEntity()))) return; + // Local unconsciousness can arrive before the replicated observer reason. + // Do not let objective hints compete with casualty/spectator guidance then. + if (mode.Phase() != ERR_Phase.PREPARING && (ERR_ObserverState() != ERR_ObserverReason.NONE || !ERR_Actors.CanFight(GetControlledEntity()))) return; string message = ERR_ObjectiveText.Description(mode, false); if (message.IsEmpty()) return; if (m_ERRGuidePhase == mode.Phase() && m_ERRGuideMessage == message) return; diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_UnconsciousCameraProbe.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_UnconsciousCameraProbe.c index ce75fb4..1bf2129 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_UnconsciousCameraProbe.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_UnconsciousCameraProbe.c @@ -208,7 +208,8 @@ modded class ERR_GameMode m_ERRCameraBody = pc.GetControlledEntity(); m_ERRCameraDamage = SCR_CharacterDamageManagerComponent.Cast(m_ERRCameraBody.FindComponent(SCR_CharacterDamageManagerComponent)); if (!ERR_CameraCheck(m_ERRCameraDamage && m_ERRCameraDamage.ERR_IsRevivablePlayer(), "admitted player retains treatable body")) return; - ERR_CameraKnockout(); m_ERRCameraStage = 1; m_ERRCameraWait = now + 3000; return; + // Allow the server medical sample plus the 3s spectator hint settling window. + ERR_CameraKnockout(); m_ERRCameraStage = 1; m_ERRCameraWait = now + 6000; return; } if (m_ERRCameraStage == 1) { diff --git a/docs/configuration.md b/docs/configuration.md index 87dfea7..232e5ff 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -384,6 +384,9 @@ The list permits up to 32 item/destination entries, 48 per entry and 64 total it ## After-mission statistics +The results screen remains open for 120 seconds before automatic restart. A +server's explicit `-autoReload` command-line setting still takes precedence. + Victory and defeat screens include a scrollable mission summary: duration, participant and escape counts, squad totals, and each participant's kills, friendly kills, deaths, times downed, recoveries and final escape status. diff --git a/docs/evidence/community-statistics-2026-10-30/native-fixture.c b/docs/evidence/community-statistics-2026-10-30/native-fixture.c index ecadc62..7d1700b 100644 --- a/docs/evidence/community-statistics-2026-10-30/native-fixture.c +++ b/docs/evidence/community-statistics-2026-10-30/native-fixture.c @@ -25,6 +25,7 @@ modded class ERR_GameMode if (!Active()) return; SCR_PlayerController pc = SCR_PlayerController.Cast(GetGame().GetPlayerController()); if (!pc || !pc.GetControlledEntity()) return; + if (!ERR_CommunityCheck(GetAutoReloadDelay() == 120, "results remain open for two minutes before automatic restart")) return; string account = UUID.GenV4(); string first; string second; if (!ERR_CommunityCheck(ERR_TelemetryCredentials.Resolve("https://fixture.invalid/api/mission-events", account, first), "private credential saved")) return; if (!ERR_CommunityCheck(ERR_TelemetryCredentials.Resolve("https://fixture.invalid/api/mission-events", account, second) && first == second && first.Length() == 72, "native credential serialization preserves installation secret")) return; diff --git a/docs/evidence/community-statistics-2026-10-30/readable-end-results.json b/docs/evidence/community-statistics-2026-10-30/readable-end-results.json new file mode 100644 index 0000000..7f815cf --- /dev/null +++ b/docs/evidence/community-statistics-2026-10-30/readable-end-results.json @@ -0,0 +1,9 @@ +{ + "default_restart_delay_seconds": 120, + "native_assertions_passed": 13, + "native_completed": true, + "hint_tests": "Production monitor methods tested for transient recovery alternation, three-second settling, silent updates, local unconsciousness before replicated observer state, hint-manager rejection, stale controller and camera cleanup.", + "limits": "Exact reported pair of messages not identified by user yet. No live multiplayer reproduction. Native run checks compiled scripts, configured delay, results geometry and text, not two minutes of elapsed waiting.", + "published": true, + "publication": "User confirmed Workshop 0.0.128; local publishing manifest version recorded in CHANGELOG.md." +} diff --git a/docs/evidence/community-statistics-2026-10-30/readable-end.log b/docs/evidence/community-statistics-2026-10-30/readable-end.log new file mode 100644 index 0000000..bd24d55 --- /dev/null +++ b/docs/evidence/community-statistics-2026-10-30/readable-end.log @@ -0,0 +1,20 @@ +15:13:55.260 SCRIPT : ERR SMOKE PASS: community results remain open for two minutes before automatic restart +15:13:55.263 SCRIPT : ERR SMOKE PASS: community private credential saved +15:13:55.268 SCRIPT : ERR SMOKE PASS: community native credential serialization preserves installation secret +15:13:55.268 SCRIPT : ERR SMOKE PASS: community native JSON roundtrip and current map/result validation +15:13:55.268 SCRIPT : ERR SMOKE PASS: community another map is rejected +15:13:55.429 SCRIPT : ERR SMOKE PASS: community native defeat screen requested +15:13:58.906 SCRIPT : ERR RESULT GEOMETRY: Base_ContentHolder x=0 y=0 width=501 height=225 visible=1 opacity=0.723378 +15:13:58.906 SCRIPT : ERR SMOKE PASS: community Base_ContentHolder has positive rendered dimensions +15:13:58.906 SCRIPT : ERR RESULT GEOMETRY: ERR_RoundResults x=135.917 y=47.9167 width=229.167 height=129.167 visible=1 opacity=1 +15:13:58.906 SCRIPT : ERR SMOKE PASS: community ERR_RoundResults has positive rendered dimensions +15:13:58.906 SCRIPT : ERR RESULT GEOMETRY: ResultFrame x=135.917 y=47.9167 width=229.167 height=129.167 visible=1 opacity=1 +15:13:58.907 SCRIPT : ERR SMOKE PASS: community ResultFrame has positive rendered dimensions +15:13:58.907 SCRIPT : ERR RESULT GEOMETRY: GameOver_State x=140.083 y=47.9167 width=220.833 height=10.8333 visible=1 opacity=1 +15:13:58.907 SCRIPT : ERR SMOKE PASS: community GameOver_State has positive rendered dimensions +15:13:58.907 SCRIPT : ERR RESULT GEOMETRY: StatisticsScroll x=140.083 y=85 width=220.833 height=89.5833 visible=1 opacity=1 +15:13:58.907 SCRIPT : ERR SMOKE PASS: community StatisticsScroll has positive rendered dimensions +15:13:58.907 SCRIPT : ERR RESULT GEOMETRY: RoundStatistics x=140.083 y=85 width=215 height=52.1875 visible=1 opacity=1 +15:13:58.908 SCRIPT : ERR SMOKE PASS: community RoundStatistics has positive rendered dimensions +15:13:58.908 SCRIPT : ERR SMOKE PASS: community open defeat screen refreshes after late community result +15:14:04.041 SCRIPT : ERR SMOKE COMPLETE: community defeat native credentials, JSON validation and result screen refresh diff --git a/tests/test_hint_delivery.py b/tests/test_hint_delivery.py index 8bde4e8..c14067c 100644 --- a/tests/test_hint_delivery.py +++ b/tests/test_hint_delivery.py @@ -7,7 +7,7 @@ OBSERVER=Path(os.environ.get('ERR_OBSERVER_SOURCE', ROOT/'addons/core/Scripts/Ga PROBE=Path(os.environ.get('ERR_OBSERVER_PROBE_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_UnconsciousCameraProbe.c')) def cpp(text): text=text.replace(' = null;', ' = nullptr;').replace('protected ', '').replace('override ', '').replace('super.OnControlledEntityChanged(from, to);', 'nativeChanges++;') - for name in ('ERR_GameMode','ERR_Phase','ERR_ObserverReason','ERR_Actors','ERR_ObjectiveText','SCR_HintManagerComponent','ERR_ObserverGuidance','ERR_ObserverRules','Resource','ETransformMode','SCR_ManualCamera','SCR_RespawnSystemComponent','SCR_EditorManagerEntity','RplId'): + for name in ('System','ERR_GameMode','ERR_Phase','ERR_ObserverReason','ERR_Actors','ERR_ObjectiveText','SCR_HintManagerComponent','ERR_ObserverGuidance','ERR_ObserverRules','Resource','ETransformMode','SCR_ManualCamera','SCR_RespawnSystemComponent','SCR_EditorManagerEntity','RplId'): text=text.replace(name+'.',name+'::') for typename in ('ERR_GameMode','SCR_HintManagerComponent','Resource','SCR_RespawnSystemComponent','SCR_EditorManagerEntity','CameraManager'): text=text.replace(typename+' ',typename+'* ') @@ -23,6 +23,7 @@ run(r'''#include #include #include struct string:std::string {using std::string::string;using std::string::operator=;string(std::string s):std::string(s){}bool IsEmpty()const{return empty();}}; +namespace System {int now=0;int GetTickCount(){return now;}} namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION};} namespace ERR_ObserverReason {enum {NONE,SPENT_LIFE,LATE_EXTRACTION,SPAWN_UNAVAILABLE,UNCONSCIOUS};} struct Actor {bool alive=true;bool unconscious=false;} actor;using IEntity=Actor*; @@ -30,7 +31,7 @@ struct ERR_Actors {static bool Alive(Actor* a){return a&&a->alive;}static bool C struct ERR_GameMode {string recovery;string RecoveryStatus(){return recovery;}int phase=ERR_Phase::PRISON;bool active=true;string message="prison";static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}int Phase(){return phase;}bool Active(){return active;}} mode; ERR_GameMode* ERR_GameMode::current=&mode; struct ERR_ObjectiveText {static string Description(ERR_GameMode* m,bool){return m->message;}static string Title(int){return "objective";}}; -struct SCR_HintManagerComponent {static SCR_HintManagerComponent* current;bool accepts=true;int attempts=0,shown=0;string last;static SCR_HintManagerComponent* GetInstance(){return current;}bool ShowCustom(string text,string,int){attempts++;if(!accepts)return false;shown++;last=text;return true;}} hints; +struct SCR_HintManagerComponent {static SCR_HintManagerComponent* current;bool accepts=true,silent=false;int attempts=0,shown=0;string last;static SCR_HintManagerComponent* GetInstance(){return current;}bool ShowCustom(string text,string,int,bool quiet=false){attempts++;if(!accepts)return false;shown++;last=text;silent=quiet;return true;}} hints; SCR_HintManagerComponent* SCR_HintManagerComponent::current=&hints; struct ERR_ObserverRules { '''+cpp(body(OBSERVER,'static bool View('))+r''' @@ -62,11 +63,13 @@ struct Controller { SCR_ManualCamera* m_ERRObserverCamera=nullptr;Vec m_ERRObserverAnchor; int m_ERRObservedId=1;bool m_ERRObserverReady=false; string m_ERRShownRecoveryStatus; + string m_ERRPendingRecoveryStatus;int m_ERRRecoveryStatusSince=0; int m_ERRShownObserverReason=0;bool m_ERRShownObserverAvailable=false;int closes=0; Actor* GetControlledEntity(){return controlled;}int ERR_ObserverState(){return m_ERRObserverReason;} - void ERR_CloseObserverCamera(){closes++;m_ERRObserverReady=false;m_ERRShownRecoveryStatus="";m_ERRObserverCamera=nullptr;} + void ERR_CloseObserverCamera(){closes++;m_ERRObserverReady=false;m_ERRShownRecoveryStatus="";m_ERRPendingRecoveryStatus="";m_ERRRecoveryStatusSince=0;m_ERRObserverCamera=nullptr;} '''+methods+r''' }; +void settled(Controller& c){c.ERR_UpdateObserverClient();System::now+=3000;c.ERR_UpdateObserverClient();} void reset(){queue=Queue();gameAvailable=true;mode=ERR_GameMode();ERR_GameMode::current=&mode;hints=SCR_HintManagerComponent();SCR_HintManagerComponent::current=&hints;actor=Actor();Resource::current=&resource;manager.current=&camera;} int main(){ reset();Controller g; @@ -100,8 +103,8 @@ int main(){ int attempts=hints.attempts;c.ERR_UpdateGuide();assert(hints.attempts==attempts&&c.m_ERRGuidePhase==-1); actor.alive=true;c.controlled=&actor;c.m_ERRObserverReason=ERR_ObserverReason::NONE;c.ERR_UpdateGuide();assert(hints.attempts==attempts+1); } - // Unconscious living prisoners retain recovery guidance. Preparing needs no body. - Controller unconscious;actor.unconscious=true;unconscious.ERR_UpdateGuide();assert(unconscious.m_ERRGuidePhase==mode.phase);actor.unconscious=false; + // Local unconsciousness suppresses objectives even before observer metadata arrives. + Controller unconscious;actor.unconscious=true;int beforeUnconscious=hints.attempts;unconscious.ERR_UpdateGuide();assert(unconscious.m_ERRGuidePhase==-1&&hints.attempts==beforeUnconscious);actor.unconscious=false; Controller preparing;preparing.controlled=nullptr;mode.phase=ERR_Phase::PREPARING;mode.message="preparing";preparing.ERR_UpdateGuide();assert(preparing.m_ERRGuideMessage=="preparing"); ERR_GameMode::current=nullptr;int attempts=hints.attempts;preparing.ERR_UpdateGuide();assert(hints.attempts==attempts); // A prior local controller can remain alive until native teardown. Its @@ -142,36 +145,47 @@ int main(){ // Unconscious observation preserves the original body, delivers recovery wording, // and closes as soon as the local body wakes even before server metadata clears. reset();Controller casualty;actor.unconscious=true;casualty.m_ERRObserverReason=ERR_ObserverReason::UNCONSCIOUS; - casualty.ERR_UpdateObserverClient();assert(casualty.m_ERRObserverCamera==&camera&&casualty.controlled==&actor&&hints.last.find("You are unconscious")!=string::npos); - casualty.ERR_UpdateObserverClient();assert(hints.shown==1); + settled(casualty);assert(casualty.m_ERRObserverCamera==&camera&&casualty.controlled==&actor&&hints.last.find("You are unconscious")!=string::npos); + settled(casualty);assert(hints.shown==1); // The phase-guide explicitly excludes this server-designated unconscious // observer; the existing observer monitor must deliver recovery/defeat facts. int attemptsBeforeGuide=hints.attempts;casualty.ERR_UpdateGuide();assert(hints.attempts==attemptsBeforeGuide); mode.recovery="Everyone is unconscious with no treatment or recovery progressing. The mission fails after 30 uninterrupted seconds in this state."; - casualty.ERR_UpdateObserverClient();assert(hints.shown==2&&hints.last.find(mode.recovery)!=string::npos&&casualty.ERR_ProbeAcceptedRecoveryStatus()==mode.recovery); - casualty.ERR_UpdateObserverClient();assert(hints.shown==2); // stable status never repeats + settled(casualty);assert(hints.shown==2&&hints.last.find(mode.recovery)!=string::npos&&casualty.ERR_ProbeAcceptedRecoveryStatus()==mode.recovery); + settled(casualty);assert(hints.shown==2); // stable status never repeats mode.recovery="Treatment or natural recovery is possible; the round continues.";hints.accepts=false; - casualty.ERR_UpdateObserverClient();assert(hints.shown==2&&casualty.m_ERRShownRecoveryStatus.find("30 uninterrupted")!=string::npos); - hints.accepts=true;casualty.ERR_UpdateObserverClient();assert(hints.shown==3&&hints.last.find(mode.recovery)!=string::npos); + settled(casualty);assert(hints.shown==2&&casualty.m_ERRShownRecoveryStatus.find("30 uninterrupted")!=string::npos); + hints.accepts=true;settled(casualty);assert(hints.shown==3&&hints.last.find(mode.recovery)!=string::npos); mode.recovery="A conscious prisoner's reconnection may provide help.";SCR_HintManagerComponent::current=nullptr; - casualty.ERR_UpdateObserverClient();assert(hints.shown==3&&casualty.m_ERRShownRecoveryStatus.find("natural recovery")!=string::npos); - SCR_HintManagerComponent::current=&hints;casualty.ERR_UpdateObserverClient();assert(hints.shown==4&&hints.last.find(mode.recovery)!=string::npos); - mode.recovery="";casualty.ERR_UpdateObserverClient();assert(hints.shown==5&&casualty.m_ERRShownRecoveryStatus.empty()&&hints.last.find("reconnection")==string::npos); - casualty.ERR_UpdateObserverClient();assert(hints.shown==5); + settled(casualty);assert(hints.shown==3&&casualty.m_ERRShownRecoveryStatus.find("natural recovery")!=string::npos); + SCR_HintManagerComponent::current=&hints;settled(casualty);assert(hints.shown==4&&hints.last.find(mode.recovery)!=string::npos); + mode.recovery="";settled(casualty);assert(hints.shown==5&&casualty.m_ERRShownRecoveryStatus.empty()&&hints.last.find("reconnection")==string::npos); + settled(casualty);assert(hints.shown==5); + // Rapid alternating medical samples must not replace the accepted message. + int stableShows=hints.shown; + for(int i=0;i<20;i++){mode.recovery=i%2?"recovering":"stalled";System::now+=250;casualty.ERR_UpdateObserverClient();} + assert(hints.shown==stableShows&&casualty.m_ERRShownRecoveryStatus.empty()); + mode.recovery="stable medical warning";casualty.ERR_UpdateObserverClient(); + System::now+=2999;casualty.ERR_UpdateObserverClient();assert(hints.shown==stableShows); + System::now+=1;casualty.ERR_UpdateObserverClient();assert(hints.shown==stableShows+1&&hints.silent&&casualty.m_ERRShownRecoveryStatus==mode.recovery); + // A regressed clock restarts the settling interval rather than accepting stale time. + mode.recovery="after clock reset";System::now=0;casualty.ERR_UpdateObserverClient();assert(hints.shown==stableShows+1); + System::now=3000;casualty.ERR_UpdateObserverClient();assert(hints.shown==stableShows+2); + int afterStableUpdates=hints.shown; // Waking closes the view; a new status cannot keep a recovered observer hint alive. - mode.recovery="remaining casualty warning";actor.unconscious=false;casualty.ERR_UpdateObserverClient();assert(!casualty.m_ERRObserverCamera&&casualty.closes==1&&hints.shown==5&&casualty.ERR_ProbeAcceptedRecoveryStatus().empty()); + mode.recovery="remaining casualty warning";actor.unconscious=false;casualty.ERR_UpdateObserverClient();assert(!casualty.m_ERRObserverCamera&&casualty.closes==1&&hints.shown==afterStableUpdates&&casualty.ERR_ProbeAcceptedRecoveryStatus().empty()); // An old controller cannot consume the replacement's recovery-status update. - reset();Controller oldCasualty;actor.unconscious=true;oldCasualty.m_ERRObserverReason=ERR_ObserverReason::UNCONSCIOUS;oldCasualty.ERR_UpdateObserverClient(); + reset();Controller oldCasualty;actor.unconscious=true;oldCasualty.m_ERRObserverReason=ERR_ObserverReason::UNCONSCIOUS;settled(oldCasualty); Controller newCasualty;newCasualty.m_ERRObserverReason=ERR_ObserverReason::UNCONSCIOUS;mode.recovery="recovery warning after reconnect";int beforeOld=hints.shown; - oldCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld&&oldCasualty.m_ERRShownRecoveryStatus.empty()); - newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+1&&hints.last.find(mode.recovery)!=string::npos); - mode.active=false;mode.recovery="old round recovery";newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+1&&!newCasualty.m_ERRObserverReady&&newCasualty.m_ERRShownRecoveryStatus.empty()); + settled(oldCasualty);assert(hints.shown==beforeOld&&oldCasualty.m_ERRShownRecoveryStatus.empty()); + settled(newCasualty);assert(hints.shown==beforeOld+1&&hints.last.find(mode.recovery)!=string::npos); + mode.active=false;mode.recovery="old round recovery";settled(newCasualty);assert(hints.shown==beforeOld+1&&!newCasualty.m_ERRObserverReady&&newCasualty.m_ERRShownRecoveryStatus.empty()); // A later active round's first observer hint contains only its current fact. - mode.active=true;mode.recovery="new round recovery";newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+2&&hints.last.find("new round recovery")!=string::npos&&hints.last.find("old round recovery")==string::npos); + mode.active=true;mode.recovery="new round recovery";settled(newCasualty);assert(hints.shown==beforeOld+2&&hints.last.find("new round recovery")!=string::npos&&hints.last.find("old round recovery")==string::npos); // An admin-enabled self-epi hint refreshes once, and disappears when disabled. - newCasualty.epiHint="Press Space to use epinephrine";newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+3&&hints.last.find(newCasualty.epiHint)!=string::npos); - newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+3); - newCasualty.epiHint="";newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+4&&hints.last.find("epinephrine")==string::npos); + newCasualty.epiHint="Press Space to use epinephrine";settled(newCasualty);assert(hints.shown==beforeOld+3&&hints.last.find(newCasualty.epiHint)!=string::npos); + settled(newCasualty);assert(hints.shown==beforeOld+3); + newCasualty.epiHint="";settled(newCasualty);assert(hints.shown==beforeOld+4&&hints.last.find("epinephrine")==string::npos); } ''') for path,signature in ((GUIDE,'protected void ERR_UpdateGuide('),(OBSERVER,'protected void ERR_UpdateObserverClient(')):