Extend results countdown and stabilize spectator hints; record Workshop 0.0.128
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-10-30 15:20:21 +01:00
parent 213c31dfb2
commit 681d5e5eae
10 changed files with 100 additions and 30 deletions
+6 -1
View File
@@ -1,6 +1,11 @@
# Changelog # 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 ## 2026-10-30 — Results panel visibility
+1 -1
View File
@@ -11,6 +11,6 @@ ERR_GameMode : "{0F307326459A1395}Prefabs/MP/Modes/GameMode_Base.et" {
m_SpawnLogic ERR_SpawnLogic "{79AD722B2EFD595F}" {} m_SpawnLogic ERR_SpawnLogic "{79AD722B2EFD595F}" {}
} }
} }
m_fAutoReloadTime 10 m_fAutoReloadTime 120
m_bUseSpawnPreload 1 m_bUseSpawnPreload 1
} }
@@ -284,6 +284,8 @@ modded class SCR_PlayerController
protected ERR_ObserverReason m_ERRShownObserverReason; protected ERR_ObserverReason m_ERRShownObserverReason;
protected bool m_ERRShownObserverAvailable; protected bool m_ERRShownObserverAvailable;
protected string m_ERRShownRecoveryStatus; protected string m_ERRShownRecoveryStatus;
protected string m_ERRPendingRecoveryStatus;
protected int m_ERRRecoveryStatusSince;
protected float m_ERRLastCycle = -1000; protected float m_ERRLastCycle = -1000;
protected IEntity m_ERRRecoveryBody; protected IEntity m_ERRRecoveryBody;
protected float m_ERRRecoveryDeadline = -1; protected float m_ERRRecoveryDeadline = -1;
@@ -468,13 +470,24 @@ modded class SCR_PlayerController
string recovery = mode.RecoveryStatus(); string recovery = mode.RecoveryStatus();
string selfEpiHint = ERR_SelfEpinephrineHint(); string selfEpiHint = ERR_SelfEpinephrineHint();
if (!selfEpiHint.IsEmpty()) recovery += "\n" + selfEpiHint; 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(); SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
if (!hints) return; // retry on the existing monitor; do not lose the first hint if (!hints) return; // retry on the existing monitor; do not lose the first hint
string message = ERR_ObserverGuidance.Message(m_ERRObserverReason, survivorAvailable); string message = ERR_ObserverGuidance.Message(m_ERRObserverReason, survivorAvailable);
if (!recovery.IsEmpty()) message += "\n" + recovery; 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_ERRObserverReady = true;
m_ERRShownObserverReason = m_ERRObserverReason; m_ERRShownObserverReason = m_ERRObserverReason;
m_ERRShownObserverAvailable = survivorAvailable; m_ERRShownObserverAvailable = survivorAvailable;
@@ -510,6 +523,8 @@ modded class SCR_PlayerController
{ {
m_ERRObserverReady = false; m_ERRObserverReady = false;
m_ERRShownRecoveryStatus = ""; m_ERRShownRecoveryStatus = "";
m_ERRPendingRecoveryStatus = "";
m_ERRRecoveryStatusSince = 0;
if (!m_ERRObserverCamera) return; if (!m_ERRObserverCamera) return;
if (GetGame().GetPlayerController() == this) m_ERRObserverCamera.TrySwitchToControlledEntityCamera(); if (GetGame().GetPlayerController() == this) m_ERRObserverCamera.TrySwitchToControlledEntityCamera();
m_ERRObserverCamera.Terminate(); m_ERRObserverCamera.Terminate();
@@ -40,7 +40,9 @@ modded class SCR_PlayerController
} }
ERR_GameMode mode = ERR_GameMode.Get(); ERR_GameMode mode = ERR_GameMode.Get();
if (!mode) return; 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); string message = ERR_ObjectiveText.Description(mode, false);
if (message.IsEmpty()) return; if (message.IsEmpty()) return;
if (m_ERRGuidePhase == mode.Phase() && m_ERRGuideMessage == message) return; if (m_ERRGuidePhase == mode.Phase() && m_ERRGuideMessage == message) return;
@@ -208,7 +208,8 @@ modded class ERR_GameMode
m_ERRCameraBody = pc.GetControlledEntity(); m_ERRCameraBody = pc.GetControlledEntity();
m_ERRCameraDamage = SCR_CharacterDamageManagerComponent.Cast(m_ERRCameraBody.FindComponent(SCR_CharacterDamageManagerComponent)); 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; 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) if (m_ERRCameraStage == 1)
{ {
+3
View File
@@ -384,6 +384,9 @@ The list permits up to 32 item/destination entries, 48 per entry and 64 total it
## After-mission statistics ## 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, Victory and defeat screens include a scrollable mission summary: duration,
participant and escape counts, squad totals, and each participant's kills, participant and escape counts, squad totals, and each participant's kills,
friendly kills, deaths, times downed, recoveries and final escape status. friendly kills, deaths, times downed, recoveries and final escape status.
@@ -25,6 +25,7 @@ modded class ERR_GameMode
if (!Active()) return; if (!Active()) return;
SCR_PlayerController pc = SCR_PlayerController.Cast(GetGame().GetPlayerController()); SCR_PlayerController pc = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!pc || !pc.GetControlledEntity()) return; 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; 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, 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; 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;
@@ -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."
}
@@ -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
+38 -24
View File
@@ -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')) PROBE=Path(os.environ.get('ERR_OBSERVER_PROBE_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_UnconsciousCameraProbe.c'))
def cpp(text): def cpp(text):
text=text.replace(' = null;', ' = nullptr;').replace('protected ', '').replace('override ', '').replace('super.OnControlledEntityChanged(from, to);', 'nativeChanges++;') 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+'::') text=text.replace(name+'.',name+'::')
for typename in ('ERR_GameMode','SCR_HintManagerComponent','Resource','SCR_RespawnSystemComponent','SCR_EditorManagerEntity','CameraManager'): for typename in ('ERR_GameMode','SCR_HintManagerComponent','Resource','SCR_RespawnSystemComponent','SCR_EditorManagerEntity','CameraManager'):
text=text.replace(typename+' ',typename+'* ') text=text.replace(typename+' ',typename+'* ')
@@ -23,6 +23,7 @@ run(r'''#include <cassert>
#include <string> #include <string>
#include <set> #include <set>
struct string:std::string {using std::string::string;using std::string::operator=;string(std::string s):std::string(s){}bool IsEmpty()const{return empty();}}; 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_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION};}
namespace ERR_ObserverReason {enum {NONE,SPENT_LIFE,LATE_EXTRACTION,SPAWN_UNAVAILABLE,UNCONSCIOUS};} namespace ERR_ObserverReason {enum {NONE,SPENT_LIFE,LATE_EXTRACTION,SPAWN_UNAVAILABLE,UNCONSCIOUS};}
struct Actor {bool alive=true;bool unconscious=false;} actor;using IEntity=Actor*; 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; 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; 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 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; SCR_HintManagerComponent* SCR_HintManagerComponent::current=&hints;
struct ERR_ObserverRules { struct ERR_ObserverRules {
'''+cpp(body(OBSERVER,'static bool View('))+r''' '''+cpp(body(OBSERVER,'static bool View('))+r'''
@@ -62,11 +63,13 @@ struct Controller {
SCR_ManualCamera* m_ERRObserverCamera=nullptr;Vec m_ERRObserverAnchor; SCR_ManualCamera* m_ERRObserverCamera=nullptr;Vec m_ERRObserverAnchor;
int m_ERRObservedId=1;bool m_ERRObserverReady=false; int m_ERRObservedId=1;bool m_ERRObserverReady=false;
string m_ERRShownRecoveryStatus; string m_ERRShownRecoveryStatus;
string m_ERRPendingRecoveryStatus;int m_ERRRecoveryStatusSince=0;
int m_ERRShownObserverReason=0;bool m_ERRShownObserverAvailable=false;int closes=0; int m_ERRShownObserverReason=0;bool m_ERRShownObserverAvailable=false;int closes=0;
Actor* GetControlledEntity(){return controlled;}int ERR_ObserverState(){return m_ERRObserverReason;} 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''' '''+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;} 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(){ int main(){
reset();Controller g; reset();Controller g;
@@ -100,8 +103,8 @@ int main(){
int attempts=hints.attempts;c.ERR_UpdateGuide();assert(hints.attempts==attempts&&c.m_ERRGuidePhase==-1); 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); 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. // Local unconsciousness suppresses objectives even before observer metadata arrives.
Controller unconscious;actor.unconscious=true;unconscious.ERR_UpdateGuide();assert(unconscious.m_ERRGuidePhase==mode.phase);actor.unconscious=false; 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"); 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); 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 // 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, // Unconscious observation preserves the original body, delivers recovery wording,
// and closes as soon as the local body wakes even before server metadata clears. // 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; 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); settled(casualty);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(hints.shown==1);
// The phase-guide explicitly excludes this server-designated unconscious // The phase-guide explicitly excludes this server-designated unconscious
// observer; the existing observer monitor must deliver recovery/defeat facts. // observer; the existing observer monitor must deliver recovery/defeat facts.
int attemptsBeforeGuide=hints.attempts;casualty.ERR_UpdateGuide();assert(hints.attempts==attemptsBeforeGuide); 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."; 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); settled(casualty);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); // stable status never repeats
mode.recovery="Treatment or natural recovery is possible; the round continues.";hints.accepts=false; 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); settled(casualty);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); 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; 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); settled(casualty);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); SCR_HintManagerComponent::current=&hints;settled(casualty);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); mode.recovery="";settled(casualty);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==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. // 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. // 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; 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()); settled(oldCasualty);assert(hints.shown==beforeOld&&oldCasualty.m_ERRShownRecoveryStatus.empty());
newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+1&&hints.last.find(mode.recovery)!=string::npos); settled(newCasualty);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()); 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. // 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. // 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.epiHint="Press Space to use epinephrine";settled(newCasualty);assert(hints.shown==beforeOld+3&&hints.last.find(newCasualty.epiHint)!=string::npos);
newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+3); settled(newCasualty);assert(hints.shown==beforeOld+3);
newCasualty.epiHint="";newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+4&&hints.last.find("epinephrine")==string::npos); 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(')): for path,signature in ((GUIDE,'protected void ERR_UpdateGuide('),(OBSERVER,'protected void ERR_UpdateObserverClient(')):