Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
--- 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')
|
||||
Reference in New Issue
Block a user