"""Actual observer monitor/recovery/cleanup; native camera lifecycle is doubled.""" import os,re from pathlib import Path from policy_harness import ROOT,body,run p=Path(os.environ.get('ERR_OBSERVER_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c')) game_mode=Path(os.environ.get('ERR_GAME_MODE_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c')) binder=body(game_mode,'protected void BindLocalObserver(') binder=binder.replace('protected ', '').replace('SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());','Controller* controller = static_cast(GetGame()->GetPlayerController());') binder=binder.replace('GetGame().','GetGame()->').replace('GetCallqueue().','GetCallqueue()->').replace('Remove(BindLocalObserver)','Remove(this)').replace('controller.','controller->') def cpp(s): s=s.replace('protected ','').replace('null','nullptr') for n in ('ERR_GameMode','ERR_Actors','ERR_ObserverRules','ERR_ObserverGuidance','SCR_HintManagerComponent','SCR_RespawnSystemComponent','SCR_EditorManagerEntity','Resource','SCR_ManualCamera','RplId','ETransformMode','LogLevel','System'): s=s.replace(n+'.',n+'::') for n in ('ERR_GameMode','SCR_HintManagerComponent','SCR_RespawnSystemComponent','SCR_EditorManagerEntity','Resource','CameraManager','CameraBase'): s=re.sub(r'\b'+n+r' (\w+)',n+r'* \1',s) for n in ('mode','hints','respawn','editor','resource','manager','m_ERRObserverCamera'): s=s.replace(n+'.',n+'->') s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('GetCallqueue().','GetCallqueue()->') s=s.replace('Remove(ERR_UpdateObserverClient)','Remove(this)').replace('CallLater(ERR_UpdateObserverClient, 250, true)','CallLater(this, 250, true)') s=s.replace('EntitySpawnParams params = new EntitySpawnParams();','EntitySpawnParams params;') return s methods='\n'.join(cpp(body(p,n)) for n in ('void ERR_StartObserverMonitor(', 'protected void ERR_UpdateObserverClient(', 'protected void ERR_UpdateRecoveryCamera(', 'protected void ERR_CloseObserverCamera(')) 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();}}; struct Actor {bool alive=true,conscious=true;};using IEntity=Actor*; struct ERR_Actors {static bool Alive(IEntity a){return a&&a->alive;}static bool CanFight(IEntity a){return Alive(a)&&a->conscious;}}; struct ERR_GameMode {string RecoveryStatus(){return "";}bool active=true;static ERR_GameMode*current;static ERR_GameMode*Get(){return current;}bool Active(){return active;}} mode; ERR_GameMode*ERR_GameMode::current=&mode; struct ERR_ObserverRules {static bool View(int reason,bool active,bool alive,bool conscious){return active&&((reason==4&&alive&&!conscious)||(reason!=0&&!alive));}}; struct ERR_ObserverGuidance {static bool Refresh(bool ready,int oldReason,int reason,bool oldAvailable,bool available){return !ready||oldReason!=reason||oldAvailable!=available;}static string Message(int,bool){return "spectating";}}; struct Vec{Vec operator+(const char*)const{return {};}}; struct EntitySpawnParams{int TransformMode=0;Vec Transform[4];};namespace ETransformMode{enum{WORLD};} struct Resource{static Resource*Load(const char*){static Resource r;return &r;}bool IsValid(){return true;}}; struct CameraBase{virtual ~CameraBase()=default;}; struct CameraManager{CameraBase*current=nullptr;int sets=0;CameraBase*CurrentCamera(){return current;}void SetCamera(CameraBase*c){current=c;sets++;}} manager; int terminated=0,deleted=0,explicitSwitch=0; struct SCR_ManualCamera:CameraBase{ CameraBase*previous=nullptr;bool ended=false; static SCR_ManualCamera*Cast(SCR_ManualCamera*c){return c;} void TrySwitchToControlledEntityCamera(){explicitSwitch++;} void Terminate(){if(!ended){ended=true;terminated++;}} // SDK SCR_ManualCamera destructor invokes SwitchToPreviousCamera, which // changes only its own currently selected camera, never a foreign current one. ~SCR_ManualCamera(){Terminate();if(manager.current==this&&previous)manager.SetCamera(previous);deleted++;} }; struct SCR_HintManagerComponent{int attempts=0;static SCR_HintManagerComponent*GetInstance(){static SCR_HintManagerComponent h;return &h;}bool ShowCustom(string,string,int){attempts++;return true;}}; struct SCR_RespawnSystemComponent{int clears=0;static SCR_RespawnSystemComponent*GetInstance(){static SCR_RespawnSystemComponent r;return &r;}void DestroyLoadingPlaceholder(){clears++;}}; struct SCR_EditorManagerEntity{bool opened=false;int closes=0;static SCR_EditorManagerEntity*GetInstance(){static SCR_EditorManagerEntity e;return &e;}bool IsOpened(){return opened;}void Close(bool){opened=false;closes++;}}; struct RplId{static int Invalid(){return -1;}};namespace LogLevel{enum{WARNING};}void Print(const char*,int){} namespace System{bool IsConsoleApp(){return false;}} struct Queue{std::setowners;int added=0;void CallLater(void*owner,int,bool){owners.insert(owner);added++;}void Remove(void*owner){owners.erase(owner);}} queue; struct World{float now=1;float GetWorldTime(){return now;}} world; struct Game{void*local=nullptr;int spawns=0;void*GetPlayerController(){return local;}Queue*GetCallqueue(){return &queue;}World*GetWorld(){return &world;}CameraManager*GetCameraManager(){return &manager;} SCR_ManualCamera*SpawnEntityPrefabLocal(Resource*,World*,EntitySpawnParams){spawns++;auto*c=new SCR_ManualCamera();c->previous=manager.current;manager.SetCamera(c);return c;}} game; bool gameAvailable=true;Game*GetGame(){return gameAvailable?&game:nullptr;} struct Controller{ string ERR_SelfEpinephrineHint(){return "";} IEntity actor=nullptr,m_ERRRecoveryBody=nullptr;float m_ERRRecoveryDeadline=-1; bool m_ERRObserverMonitor=false,m_ERRObserverReady=false,m_ERRShownObserverAvailable=false; string m_ERRShownRecoveryStatus; int m_ERRObserverReason=0,m_ERRShownObserverReason=0,m_ERRObservedId=1,starts=0; Vec m_ERRObserverAnchor;SCR_ManualCamera*m_ERRObserverCamera=nullptr;CameraBase*playerCamera=nullptr; IEntity GetControlledEntity(){return actor;}CameraBase*GetPlayerCamera(){return playerCamera;} void ERR_OnObserverMonitorStarted(){starts++;} '''+methods+r''' }; struct Binder {bool m_Stopped=false,m_NativeEndHandled=false; '''+binder+r''' }; int main(){ // The real repeating binder must survive the first controller and gaps. Binder binder;Controller first,replacement;queue.owners.insert(&binder); game.local=nullptr;binder.BindLocalObserver();assert(queue.owners.count(&binder)&&queue.added==0); game.local=&first;binder.BindLocalObserver(); assert(first.m_ERRObserverMonitor&&first.starts==1&&queue.owners.count(&first)); assert(queue.owners.count(&binder)); binder.BindLocalObserver();assert(first.starts==1&&queue.added==1); game.local=nullptr;binder.BindLocalObserver();assert(queue.owners.count(&binder)); game.local=&replacement;binder.BindLocalObserver(); assert(replacement.m_ERRObserverMonitor&&replacement.starts==1&&queue.owners.count(&replacement)); first.ERR_UpdateObserverClient(); assert(!first.m_ERRObserverMonitor&&!queue.owners.count(&first)&&queue.owners.count(&replacement)); binder.BindLocalObserver();assert(replacement.starts==1&&queue.added==2); binder.m_Stopped=true;binder.BindLocalObserver();assert(!queue.owners.count(&binder)&&queue.owners.count(&replacement)); binder.m_Stopped=false;binder.m_NativeEndHandled=true;queue.owners.insert(&binder);binder.BindLocalObserver(); assert(!queue.owners.count(&binder)&&queue.owners.count(&replacement)&&queue.added==2); queue.owners.clear();queue.added=0;game.local=nullptr; Actor body;CameraBase formerBodyCamera,newBodyCamera; Controller former,current;former.actor=&body;former.playerCamera=&formerBodyCamera; game.local=&former;manager.current=&formerBodyCamera;former.ERR_StartObserverMonitor(); body.conscious=false;former.m_ERRObserverReason=4;former.ERR_UpdateObserverClient(); assert(former.m_ERRRecoveryBody==&body&&former.m_ERRObserverCamera&&queue.owners.count(&former)); SCR_ManualCamera*oldOwned=former.m_ERRObserverCamera; // New controller owns a separate spectator camera; old body wakes meanwhile. current.playerCamera=&newBodyCamera;current.m_ERRObserverReason=1;game.local=¤t;current.ERR_StartObserverMonitor();current.ERR_UpdateObserverClient(); SCR_ManualCamera*newOwned=current.m_ERRObserverCamera;assert(newOwned&&newOwned!=oldOwned&&manager.current==newOwned); auto*editor=SCR_EditorManagerEntity::GetInstance();editor->opened=true;body.conscious=true; int beforeClears=SCR_RespawnSystemComponent::GetInstance()->clears; int beforeSets=manager.sets,beforeHints=SCR_HintManagerComponent::GetInstance()->attempts,beforeSpawns=game.spawns,beforeDeleted=deleted; former.ERR_UpdateObserverClient(); assert(manager.current==newOwned&&manager.sets==beforeSets&&editor->opened&&editor->closes==0); assert(!former.m_ERRRecoveryBody&&former.m_ERRRecoveryDeadline==-1&&!former.m_ERRObserverMonitor&&!former.m_ERRObserverCamera); assert(deleted==beforeDeleted+1&&terminated==deleted&&explicitSwitch==0); assert(current.m_ERRObserverCamera==newOwned&&queue.owners.count(¤t)==1&&queue.owners.count(&former)==0); assert(SCR_HintManagerComponent::GetInstance()->attempts==beforeHints&&game.spawns==beforeSpawns&&SCR_RespawnSystemComponent::GetInstance()->clears==beforeClears); // Even a queued stale spectator callback cannot create a camera or a hint. former.actor=nullptr;former.m_ERRObserverReason=1;former.ERR_UpdateObserverClient(); assert(game.spawns==beforeSpawns&&manager.current==newOwned&&SCR_HintManagerComponent::GetInstance()->attempts==beforeHints); // Start is still local-only; reacquisition restarts exactly one owned timer. int added=queue.added;former.ERR_StartObserverMonitor();assert(queue.added==added); game.local=&former;former.actor=&body;former.m_ERRObserverReason=0;former.ERR_StartObserverMonitor();former.ERR_StartObserverMonitor(); assert(queue.added==added+1&&former.starts==2&&queue.owners.count(¤t)==1); // Normal recovery still closes the editor and returns this controller's body. body.conscious=false;former.ERR_UpdateObserverClient();assert(former.m_ERRRecoveryBody==&body); body.conscious=true;former.ERR_UpdateObserverClient();assert(manager.current==&formerBodyCamera&&!editor->opened&&editor->closes==1&&!former.m_ERRRecoveryBody); // Foreign current camera survives this controller's retained camera cleanup. game.local=¤t;manager.current=&newBodyCamera;current.ERR_CloseObserverCamera();assert(manager.current==&newBodyCamera&&deleted==beforeDeleted+2); // If the old owned camera is still current at replacement, its native // destructor may restore its own previous camera; no new-controller camera is deleted. Controller abandoned;abandoned.m_ERRObserverReason=1;game.local=&abandoned;manager.current=&newBodyCamera; abandoned.ERR_StartObserverMonitor();abandoned.ERR_UpdateObserverClient();assert(manager.current==abandoned.m_ERRObserverCamera); game.local=¤t;beforeSets=manager.sets;beforeDeleted=deleted;abandoned.ERR_UpdateObserverClient(); assert(!abandoned.m_ERRObserverCamera&&manager.current==&newBodyCamera&&manager.sets==beforeSets+1&&deleted==beforeDeleted+1); assert(queue.owners.count(&abandoned)==0&&queue.owners.count(¤t)==1); // Solo death may leave the controlled corpse assigned; restore the orbit // camera if the native death camera replaces it on a later tick. Controller solo;Actor corpse;corpse.alive=false;corpse.conscious=false; solo.actor=&corpse;solo.m_ERRObserverReason=1;game.local=&solo;editor->opened=false; solo.ERR_UpdateObserverClient();auto*corpseCamera=solo.m_ERRObserverCamera; assert(corpseCamera&&manager.current==corpseCamera); manager.current=&newBodyCamera;beforeClears=SCR_RespawnSystemComponent::GetInstance()->clears;beforeSpawns=game.spawns; solo.ERR_UpdateObserverClient(); assert(manager.current==corpseCamera&&game.spawns==beforeSpawns&&SCR_RespawnSystemComponent::GetInstance()->clears==beforeClears+1); // Game Master remains usable during spectating, including before first camera creation. editor->opened=true;manager.current=&newBodyCamera;solo.ERR_UpdateObserverClient(); assert(manager.current==&newBodyCamera&&game.spawns==beforeSpawns); solo.ERR_CloseObserverCamera();solo.ERR_UpdateObserverClient(); assert(!solo.m_ERRObserverCamera&&manager.current==&newBodyCamera&&game.spawns==beforeSpawns); editor->opened=false;solo.ERR_UpdateObserverClient();assert(solo.m_ERRObserverCamera&&manager.current==solo.m_ERRObserverCamera); solo.ERR_CloseObserverCamera(); gameAvailable=false;former.ERR_UpdateObserverClient();gameAvailable=true; } ''') print('PASS: actual observer monitor/recovery/cleanup rejects retained old controller, preserves new camera/editor/hints/timer, restarts once and retains normal recovery; native effects modeled from SDK, no runtime claim')