Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
159 lines
13 KiB
Diff
159 lines
13 KiB
Diff
--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c
|
|
+++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c
|
|
@@ -349,6 +349,17 @@
|
|
protected void ERR_OnObserverMonitorStarted() {}
|
|
protected void ERR_UpdateObserverClient()
|
|
{
|
|
+ if (!GetGame()) return;
|
|
+ if (this != GetGame().GetPlayerController())
|
|
+ {
|
|
+ // A replaced controller may retain its timer until native destruction.
|
|
+ // Release only its presentation; never restore its old body camera.
|
|
+ GetGame().GetCallqueue().Remove(ERR_UpdateObserverClient);
|
|
+ m_ERRObserverMonitor = false;
|
|
+ m_ERRRecoveryBody = null; m_ERRRecoveryDeadline = -1;
|
|
+ ERR_CloseObserverCamera();
|
|
+ return;
|
|
+ }
|
|
ERR_GameMode mode = ERR_GameMode.Get();
|
|
ERR_UpdateRecoveryCamera(mode);
|
|
bool observe = mode && ERR_ObserverRules.View(m_ERRObserverReason, mode.Active(), ERR_Actors.Alive(GetControlledEntity()), ERR_Actors.CanFight(GetControlledEntity()));
|
|
--- a/tests/test_hint_delivery.py
|
|
+++ b/tests/test_hint_delivery.py
|
|
@@ -3,16 +3,16 @@
|
|
from pathlib import Path
|
|
import os
|
|
GUIDE=Path(os.environ.get('ERR_GUIDE_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerGuide.c'))
|
|
-OBSERVER=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c'
|
|
+OBSERVER=Path(os.environ.get('ERR_OBSERVER_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c'))
|
|
def cpp(text):
|
|
- text=text.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','RplId'):
|
|
text=text.replace(name+'.',name+'::')
|
|
for typename in ('ERR_GameMode','SCR_HintManagerComponent','Resource','SCR_RespawnSystemComponent','CameraManager'):
|
|
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('GetCallqueue().','GetCallqueue()->').replace('Remove(ERR_UpdateGuide)','Remove(this)').replace('Remove(ERR_UpdateObserverClient)','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,'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('))
|
|
@@ -53,7 +53,7 @@
|
|
Controller(){game.local=this;}
|
|
void ERR_UpdateRecoveryCamera(ERR_GameMode*){}
|
|
int nativeChanges=0,starts=0;bool m_ERRGuideStarted=false;void ERR_StartGuide(){starts++;StartActual();}
|
|
- int m_ERRGuidePhase=-1;string m_ERRGuideMessage;
|
|
+ int m_ERRGuidePhase=-1;string m_ERRGuideMessage;bool m_ERRObserverMonitor=false;Actor* m_ERRRecoveryBody=nullptr;float m_ERRRecoveryDeadline=-1;
|
|
Actor* controlled=&actor;int m_ERRObserverReason=ERR_ObserverReason::NONE;
|
|
SCR_ManualCamera* m_ERRObserverCamera=nullptr;Vec m_ERRObserverAnchor;
|
|
int m_ERRObservedId=1;bool m_ERRObserverReady=false;
|
|
--- /dev/null
|
|
+++ b/tests/test_observer_ownership.py
|
|
@@ -0,0 +1,104 @@
|
|
+"""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'))
|
|
+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 <cassert>
|
|
+#include <set>
|
|
+#include <string>
|
|
+using string=std::string;
|
|
+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 {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::set<void*>owners;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*SpawnEntityPrefab(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{
|
|
+ IEntity actor=nullptr,m_ERRRecoveryBody=nullptr;float m_ERRRecoveryDeadline=-1;
|
|
+ bool m_ERRObserverMonitor=false,m_ERRObserverReady=false,m_ERRShownObserverAvailable=false;
|
|
+ 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'''
|
|
+};
|
|
+int main(){
|
|
+ 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);
|
|
+ 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')
|