Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
91 lines
7.4 KiB
Diff
91 lines
7.4 KiB
Diff
--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerGuide.c
|
|
+++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerGuide.c
|
|
@@ -30,6 +30,14 @@
|
|
}
|
|
protected void ERR_UpdateGuide()
|
|
{
|
|
+ // A retained controller may outlive its local ownership during replacement.
|
|
+ if (!GetGame()) return;
|
|
+ if (this != GetGame().GetPlayerController())
|
|
+ {
|
|
+ GetGame().GetCallqueue().Remove(ERR_UpdateGuide);
|
|
+ m_ERRGuideStarted = false;
|
|
+ return;
|
|
+ }
|
|
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;
|
|
--- a/tests/test_hint_delivery.py
|
|
+++ b/tests/test_hint_delivery.py
|
|
@@ -12,12 +12,14 @@
|
|
text=text.replace(typename+' ',typename+'* ')
|
|
for variable in ('mode','hints','resource','respawn','manager'):
|
|
text=text.replace(variable+'.',variable+'->')
|
|
+ text=text.replace('GetCallqueue().','GetCallqueue()->').replace('Remove(ERR_UpdateGuide)','Remove(this)').replace('CallLater(ERR_UpdateGuide, 1000, true)','CallLater(this, 1000, true)')
|
|
text=text.replace('GetGame().','GetGame()->').replace('EntitySpawnParams params = new EntitySpawnParams();','EntitySpawnParams params;')
|
|
return text
|
|
-methods=cpp(body(GUIDE,'override void OnControlledEntityChanged('))+'\n'+cpp(body(GUIDE,'protected void ERR_UpdateGuide('))+'\n'+cpp(body(OBSERVER,'protected void ERR_UpdateObserverClient('))
|
|
+methods=cpp(body(GUIDE,'protected void ERR_StartGuide(')).replace('ERR_StartGuide()', 'StartActual()')+'\n'+cpp(body(GUIDE,'override void OnControlledEntityChanged('))+'\n'+cpp(body(GUIDE,'protected void ERR_UpdateGuide('))+'\n'+cpp(body(OBSERVER,'protected void ERR_UpdateObserverClient('))
|
|
guidance=cpp(body(OBSERVER,'static string Message('))+'\n'+cpp(body(OBSERVER,'static bool Refresh('))
|
|
run(r'''#include <cassert>
|
|
#include <string>
|
|
+#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();}};
|
|
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION};}
|
|
namespace ERR_ObserverReason {enum {NONE,SPENT_LIFE,LATE_EXTRACTION,SPAWN_UNAVAILABLE,UNCONSCIOUS};}
|
|
@@ -44,11 +46,13 @@
|
|
struct SCR_RespawnSystemComponent {int destroys=0;static SCR_RespawnSystemComponent* current;static SCR_RespawnSystemComponent* GetInstance(){return current;}void DestroyLoadingPlaceholder(){destroys++;}} respawn;
|
|
SCR_RespawnSystemComponent* SCR_RespawnSystemComponent::current=&respawn;
|
|
struct RplId {static int Invalid(){return -1;}};
|
|
-struct Game {int spawns=0;SCR_ManualCamera* SpawnEntityPrefab(Resource*,int,EntitySpawnParams){spawns++;return &camera;}int GetWorld(){return 1;}CameraManager* GetCameraManager(){return &manager;}} game;
|
|
-Game* GetGame(){return &game;}
|
|
+struct Queue {std::set<void*> scheduled;int added=0;void CallLater(void* owner,int,bool){added++;scheduled.insert(owner);}void Remove(void* owner){scheduled.erase(owner);}} queue;
|
|
+struct Game {Queue* GetCallqueue(){return &queue;}void* local=nullptr;void* GetPlayerController(){return local;}int spawns=0;SCR_ManualCamera* SpawnEntityPrefab(Resource*,int,EntitySpawnParams){spawns++;return &camera;}int GetWorld(){return 1;}CameraManager* GetCameraManager(){return &manager;}} game;
|
|
+bool gameAvailable=true;Game* GetGame(){return gameAvailable?&game:nullptr;}
|
|
struct Controller {
|
|
+ Controller(){game.local=this;}
|
|
void ERR_UpdateRecoveryCamera(ERR_GameMode*){}
|
|
- int nativeChanges=0,starts=0;void ERR_StartGuide(){starts++;}
|
|
+ int nativeChanges=0,starts=0;bool m_ERRGuideStarted=false;void ERR_StartGuide(){starts++;StartActual();}
|
|
int m_ERRGuidePhase=-1;string m_ERRGuideMessage;
|
|
Actor* controlled=&actor;int m_ERRObserverReason=ERR_ObserverReason::NONE;
|
|
SCR_ManualCamera* m_ERRObserverCamera=nullptr;Vec m_ERRObserverAnchor;
|
|
@@ -58,7 +62,7 @@
|
|
void ERR_CloseObserverCamera(){closes++;m_ERRObserverReady=false;m_ERRObserverCamera=nullptr;}
|
|
'''+methods+r'''
|
|
};
|
|
-void reset(){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(){
|
|
reset();Controller g;
|
|
SCR_HintManagerComponent::current=nullptr;g.ERR_UpdateGuide();assert(g.m_ERRGuidePhase==-1&&hints.attempts==0);
|
|
@@ -95,6 +99,22 @@
|
|
Controller unconscious;actor.unconscious=true;unconscious.ERR_UpdateGuide();assert(unconscious.m_ERRGuidePhase==mode.phase);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
|
|
+ // recurring callback must not write global hints after ownership changed.
|
|
+ reset();Controller former;former.StartActual();former.ERR_UpdateGuide();Controller current;current.StartActual();
|
|
+ assert(queue.scheduled.count(&former)==1&&queue.scheduled.count(¤t)==1);
|
|
+ mode.phase=ERR_Phase::PREPARING;mode.message="new round preparing";
|
|
+ former.controlled=nullptr;int beforeStale=hints.attempts;
|
|
+ former.ERR_UpdateGuide();assert(hints.attempts==beforeStale);
|
|
+ assert(!former.m_ERRGuideStarted&&queue.scheduled.count(&former)==0&&queue.scheduled.count(¤t)==1);
|
|
+ assert(former.m_ERRGuidePhase==ERR_Phase::PRISON&&former.m_ERRGuideMessage=="prison");
|
|
+ current.ERR_UpdateGuide();assert(hints.attempts==beforeStale+1&¤t.m_ERRGuideMessage==mode.message);
|
|
+ current.ERR_UpdateGuide();assert(hints.attempts==beforeStale+1);
|
|
+ mode.phase=ERR_Phase::RADIO_SEARCH;mode.message="new radio clue";former.controlled=&actor;
|
|
+ former.ERR_UpdateGuide();assert(hints.attempts==beforeStale+1);
|
|
+ game.local=&former;former.StartActual();assert(former.m_ERRGuideStarted&&queue.scheduled.count(&former)==1);int added=queue.added;former.StartActual();assert(queue.added==added);hints.accepts=false;former.ERR_UpdateGuide();assert(former.m_ERRGuideMessage=="prison");
|
|
+ hints.accepts=true;former.ERR_UpdateGuide();assert(former.m_ERRGuideMessage==mode.message);
|
|
+ int beforeNoGame=hints.attempts;gameAvailable=false;former.ERR_UpdateGuide();assert(hints.attempts==beforeNoGame);gameAvailable=true;
|
|
reset();Controller s;s.controlled=nullptr;s.m_ERRObserverReason=ERR_ObserverReason::SPENT_LIFE;
|
|
// Actual camera setup executes; missing manager cannot consume delivery state.
|
|
SCR_HintManagerComponent::current=nullptr;s.ERR_UpdateObserverClient();assert(s.m_ERRObserverCamera==&camera&&!s.m_ERRObserverReady);
|
|
@@ -120,4 +140,4 @@
|
|
''')
|
|
for path,signature in ((GUIDE,'protected void ERR_UpdateGuide('),(OBSERVER,'protected void ERR_UpdateObserverClient(')):
|
|
assert 'CallLater' not in body(path,signature), 'Delivery retry uses the existing monitor only'
|
|
-print('PASS: actual guide/observer monitors retry native refusal, cache successful delivery, deduplicate and preserve living/preparing/spectator boundaries; native UI not exercised')
|
|
+print('PASS: actual guide ownership and scoped timer cancellation, current-controller preservation/reacquisition, hint retry/dedup and observer boundaries; native UI not exercised')
|