Files
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

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(&current)==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(&current)==1);
+ assert(former.m_ERRGuidePhase==ERR_Phase::PRISON&&former.m_ERRGuideMessage=="prison");
+ current.ERR_UpdateGuide();assert(hints.attempts==beforeStale+1&&current.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')