Harden participant and map ownership and validate bounded physical flight

This commit is contained in:
2026-09-19 12:17:01 +02:00
parent dfd7cc49e2
commit 4125018d04
120 changed files with 20043 additions and 47 deletions
+13
View File
@@ -23,6 +23,19 @@ class FlightActionRunner(unittest.TestCase):
self.assertIn('flight-autohover requires Core powered-flight with flight-action-inputs',result.stderr)
self.assertEqual(list(root.iterdir()),[])
self.assertIn("if a.flight_autohover: command += ['-errFlightAutohover', '1']",(ROOT/'tools/headless_workbench.py').read_text())
def test_translation_requires_both_proven_control_paths(self):
for extra in [[],['--flight-action-inputs'],['--flight-autohover']]:
with self.subTest(extra=extra),tempfile.TemporaryDirectory() as directory:
root=Path(directory)
result=subprocess.run([sys.executable,str(ROOT/'tools/headless_workbench.py'),'--compat',str(root/'prefix'),'--profile',str(root/'profile'),'--project','core','--smoke','powered-translate','--rounds','1','--timeout','420',*extra],capture_output=True,text=True)
self.assertNotEqual(result.returncode,0)
self.assertIn('powered-translate requires Core with flight-action-inputs and flight-autohover',result.stderr)
self.assertEqual(list(root.iterdir()),[])
with tempfile.TemporaryDirectory() as directory:
root=Path(directory)
result=subprocess.run([sys.executable,str(ROOT/'tools/headless_workbench.py'),'--compat',str(root/'prefix'),'--profile',str(root/'profile'),'--project','core','--smoke','powered-translate','--rounds','1','--timeout','420','--flight-action-inputs','--flight-autohover'],capture_output=True,text=True)
self.assertIn('must be a disposable cloned prefix',result.stderr)
self.assertEqual(list(root.iterdir()),[])
def test_valid_scope_still_requires_disposable_prefix(self):
with tempfile.TemporaryDirectory() as directory:
root=Path(directory)
+118
View File
@@ -0,0 +1,118 @@
"""Actual Intel/HQ methods and readiness handoff; native expansion is an adapter."""
import re
from policy_harness import ROOT, body, run
intel=ROOT/'addons/intel/Scripts/Game/ERR/ERR_IntelService.c'
hq=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c'
ready=ROOT/'addons/ai/Scripts/Game/ERR/ERR_MortarReadiness.c'
system=ROOT/'addons/ai/Scripts/Game/ERR/ERR_MortarSystem.c'
def adapt(s):
s=s.replace('override ', '').replace('protected ', '').replace('null','nullptr')
s=re.sub(r'array<([^>]+)> (\w+) = \{\};',r'Array<\1> \2;',s)
s=re.sub(r'foreach \((\w+) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
for n in ('Replication','ERR_Actors','Math','ERR_Resources','ERR_DiscoveryService','SCR_InventoryStorageManagerComponent','SCR_AIGroupUtilityComponent','ERR_MortarSupplyState'):
s=s.replace(n+'.',n+'::')
for n in ('group','member','actor','request','inventory','known','unit','m_Mode','m_Battery','mode'):
s=s.replace(n+'.',n+'->')
s=s.replace('Actor.FindComponent(', 'Actor->FindComponent(').replace('SCR_InventoryStorageManagerComponent inventory', 'SCR_InventoryStorageManagerComponent* inventory').replace('SCR_AIGroupUtilityComponent utility', 'SCR_AIGroupUtilityComponent* utility')
s=s.replace('Settings().','Settings()->').replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->')
s=s.replace('FindComponent(SCR_InventoryStorageManagerComponent)','FindComponent()').replace('FindComponent(SCR_AIGroupUtilityComponent)','FindComponent()')
s=s.replace('purpose: EStoragePurpose.PURPOSE_ANY','EStoragePurpose::PURPOSE_ANY')
s=s.replace('ERR_IntelRequest request = new ERR_IntelRequest();','ERR_IntelRequest request = new Request();')
s=s.replace('ERR_HQUnit unit = new ERR_HQUnit();','ERR_HQUnit unit = new Unit();')
s=re.sub(r'^.*(?:Print|m_Journal.Write)\([^\n]*$', ';',s,flags=re.M)
return s
register=adapt(body(intel,'void RegisterGroup('));tick=adapt(body(intel,'void Tick('));hqregister=adapt(body(hq,'override void RegisterGroup('))
observation=adapt(body(intel,'bool ERR_ProbeWasRolled('))
poll=body(ready,'ERR_MortarSupplyState Poll(')
# Execute exact early stopped/ready guards and final qualified handoff. Native
# population/inventory callback validations between them are not simulated.
prefix=poll[poll.index('{')+1:poll.index(' SCR_AIGroup group')]
suffix=poll[poll.index(' if (m_Battery.CountShells() != 27)'):poll.index(' group.ActivateAI();')]
handoff=adapt(prefix+suffix)+'return ERR_MortarSupplyState::READY;'
assert poll.index('if (m_Ready) return') < poll.index('mode.RegisterGroup(group)')
for condition in ('if (!operation.Finished)', 'if (!operation.Succeeded)', 'if (population == ERR_MortarSupplyState.FAILED)', 'if (population == ERR_MortarSupplyState.PENDING)'):
assert poll.index(condition)<poll.index('mode.RegisterGroup(group)')
assert poll.count('mode.RegisterGroup(group)')==1
prepare=body(system,'bool Prepare(')
assert 'mode.RegisterGroup(group)' not in prepare
assert prepare.index('mode.Own(group)')<prepare.index('m_HQ.RegisterGroup(group)')<prepare.index('m_HQ.ReserveGroup(group)')
site=body(ROOT/'addons/core/Scripts/Game/ERR/ERR_SiteService.c','void Activate(')
assert site.index('m_State != ERR_PrepareResult.READY')<site.index('m_Mode.RegisterGroup(site.Group)')
run(r'''
#include <cassert>
#include <algorithm>
#include <string>
#include <vector>
#include <set>
using string=std::string;
template<class T> struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Insert(T a){this->push_back(a);}void Remove(int i){this->erase(this->begin()+i);}bool Contains(T a){return std::find(this->begin(),this->end(),a)!=this->end();}};
template<class T> struct Set:std::set<T>{bool Contains(T a){return this->count(a);}void Insert(T a){this->insert(a);}};
namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace Math{int rolls=0;int RandomInt(int,int){rolls++;return 0;}}
namespace EStoragePurpose{const int PURPOSE_ANY=0;}
struct Inventory {int requests=0;bool accept=true,folder=false;bool TrySpawnPrefabToStorage(int,int){requests++;return accept;}static Inventory* Cast(void* p){return static_cast<Inventory*>(p);}};
using SCR_InventoryStorageManagerComponent=Inventory;
struct Actor{bool alive=true,player=false;Inventory inventory;void* FindComponent(){return &inventory;}};
using IEntity=Actor*;
struct Agent{Actor* actor=nullptr;IEntity GetControlledEntity(){return actor;}};using AIAgent=Agent*;
namespace ERR_Actors{bool Alive(IEntity a){return a&&a->alive;}bool Player(IEntity a){return a&&a->player;}}
namespace ERR_Resources{int IntelFolder(){return 1;}}
namespace ERR_DiscoveryService{void Folders(Inventory* i,Array<IEntity>& a){a.clear();if(i->folder)a.Insert(nullptr);}}
struct Utility{static Utility* Cast(void* p){return static_cast<Utility*>(p);}};using SCR_AIGroupUtilityComponent=Utility;
struct Group{Array<AIAgent> members;Utility utility;int faction=1;void GetAgents(Array<AIAgent>& out){out=members;}void* FindComponent(){return &utility;}int GetFaction(){return faction;}};using SCR_AIGroup=Group*;
struct Settings{int chance=100;int Get(const char*){return chance;}};
struct Mode{struct Settings settings;int publications=0;struct Settings* Settings(){return &settings;}int EnemyFaction(){return 1;}void RegisterGroup(SCR_AIGroup);};
struct Request{IEntity Actor=nullptr;int Attempts=0;bool Accepted=false;};using ERR_IntelRequest=Request*;
struct Intel{Mode* m_Mode;Array<ERR_IntelRequest> m_Requests;Set<IEntity> m_Rolled;
'''+register+tick+observation+r'''
};
struct Clock{float GetWorldTime(){return 10;}Clock* GetWorld(){return this;}}clock_;Clock* GetGame(){return &clock_;}
struct Unit{int Id=0;SCR_AIGroup Group=nullptr;struct Utility* Utility=nullptr;float NextReport=0;};using ERR_HQUnit=Unit*;
struct HQ{Mode* m_Mode;int m_NextId=0;Array<ERR_HQUnit> m_Units;
'''+hqregister+r'''
};
Intel* intel_;HQ* hq_;
void Mode::RegisterGroup(SCR_AIGroup group){publications++;hq_->RegisterGroup(group);intel_->RegisterGroup(group);}
namespace ERR_MortarSupplyState{const int FAILED=-1,PENDING=0,READY=1;}
struct Battery{bool stopped=false;int shells=27;bool Stopped(){return stopped;}int CountShells(){return shells;}};
struct Ready{bool m_Stopped=false,m_Ready=false;Battery* m_Battery;int Finish(Mode* mode,SCR_AIGroup group){
'''+handoff+r'''
}};
int main(){
Mode mode;Intel intel{&mode,{},{} };HQ hq{&mode,0,{}};intel_=&intel;hq_=&hq;Group group;
hq.RegisterGroup(&group);assert(hq.m_Units.Count()==1&&mode.publications==0&&Math::rolls==0);
Actor actors[4];Agent agents[4];for(int i=0;i<4;i++){agents[i].actor=&actors[i];group.members.Insert(&agents[i]);}
Battery battery;Ready ready{false,false,&battery};battery.shells=26;
assert(ready.Finish(&mode,&group)==0&&mode.publications==0&&Math::rolls==0);
battery.shells=27;assert(ready.Finish(&mode,&group)==1&&mode.publications==1&&Math::rolls==4&&intel.m_Requests.Count()==4);
assert(hq.m_Units.Count()==1&&hq.m_NextId==1);
for(Actor& a:actors){assert(intel.ERR_ProbeWasRolled(&a));}assert(!intel.ERR_ProbeWasRolled(nullptr));Actor unrelated;assert(!intel.ERR_ProbeWasRolled(&unrelated));
Replication::server=false;assert(!intel.ERR_ProbeWasRolled(&actors[0]));Replication::server=true; // actual HQ method deduplicates readiness publication
for(int n=0;n<5;n++){assert(ready.Finish(&mode,&group)==1);intel.RegisterGroup(&group);hq.RegisterGroup(&group);}
assert(mode.publications==1&&Math::rolls==4&&hq.m_Units.Count()==1);
intel.Tick(1);for(Actor& a:actors)assert(a.inventory.requests==1);
for(int n=0;n<5;n++){intel.Tick(2+n);}for(Actor& a:actors)assert(a.inventory.requests==1); // accepted asynchronous spawn never reissued
for(Actor& a:actors){a.inventory.folder=true;}intel.Tick(10);assert(intel.m_Requests.Count()==0);
for(Actor& a:actors)a.inventory.folder=false; // real item subsequently picked up/dropped: assignment latch unchanged
intel.RegisterGroup(&group);intel.Tick(11);assert(Math::rolls==4&&intel.m_Requests.Count()==0);
actors[0].alive=false;intel.RegisterGroup(&group);intel.Tick(12);assert(Math::rolls==4);for(Actor& a:actors)assert(a.inventory.requests==1);
Ready stopped{true,false,&battery};assert(stopped.Finish(&mode,&group)==-1&&mode.publications==1);
battery.stopped=true;Ready cancelled{false,false,&battery};assert(cancelled.Finish(&mode,&group)==-1&&mode.publications==1);battery.stopped=false;
Actor killed;Agent killedAgent{&killed};Group deathGroup;deathGroup.members.Insert(&killedAgent);intel.RegisterGroup(&deathGroup);assert(Math::rolls==5);killed.alive=false;intel.Tick(13);assert(killed.inventory.requests==0&&intel.m_Requests.Count()==0);intel.RegisterGroup(&deathGroup);assert(Math::rolls==5);
Mode neverMode;neverMode.settings.chance=0;Intel never{&neverMode,{},{}};never.RegisterGroup(&group);int after=Math::rolls;neverMode.settings.chance=100;never.RegisterGroup(&group);assert(Math::rolls==after&&never.m_Requests.Count()==0); // no reroll of original negative decision
}
''')
print('PASS: actual Intel/HQ methods and one-shot qualified readiness handoff deliver late actors once; duplicate readiness, accepted spawn, pickup/drop, death and original negative rolls do not mint assignments')
# Observe only in Workbench, after readiness and before destructive fixture steps.
intel_source=intel.read_text()
observer_region=intel_source.split('#ifdef WORKBENCH',1)[1].split('#endif',1)[0]
assert 'bool ERR_ProbeWasRolled(IEntity actor)' in observer_region
assert 'ERR_Mortar' not in intel_source and 'ERR_HQ' not in intel_source
assert 'Insert(' not in observation and 'RandomInt' not in observation and 'TrySpawn' not in observation
smoke=(ROOT/'addons/chat/Scripts/Game/ERR/ERR_NativeSmoke.c').read_text()
step=smoke.split('else if (m_ERRSmokeStep == 120)',1)[1].split('else if (m_ERRSmokeStep == 121)',1)[0]
assert step.index('!battery.StartupActivated') < step.index('ERR_ProbeWasRolled(intelActor)') < step.index('DeleteEntityAndChildren(item)')
assert '!intelCrew.Contains(intelActor)' in step and 'intelCrew.Count() == 4' in step
assert 'RegisterGroup(' not in step and 'Settings().Set(' not in step and 'TrySpawn' not in step
print('PASS: Workbench-only read accessor rejects unknown/nonserver identities; Chat observes all distinct ready crew before existing shell-depletion mutation')
+123
View File
@@ -0,0 +1,123 @@
"""Execute fixture's actual ownership/cleanup and callback-evidence decisions.
Explicit native boundary doubles; this is not a native rendering/camera test.
"""
import re
from policy_harness import ROOT, body, run
SOURCE = ROOT / 'addons/map/Scripts/Game/ERRMap/ERRM_NativeMapOwnershipProbe.c'
def method(signature):
text = body(SOURCE, signature).replace('protected ', '')
text = re.sub(r'^ Print\(string\.Format[^\n]+\n', '', text, flags=re.M)
text = text.replace('SCR_MapEntity.GetMapInstance()', 'Map::current')
text = text.replace('SCR_MapEntity.GetOnMapClose().Remove(OnMapClosed)', 'closeObserver.Remove(this)')
text = text.replace('SCR_MapEntity.GetOnMapOpenComplete().Remove(OnMapReady)', 'readyObserver.Remove(this)')
text = text.replace('SCR_EntityHelper.DeleteEntityAndChildren(', 'DeleteEntityAndChildren(')
for field in ['m_Map', 'm_ReplacementConfig', 'm_ReplacementRoot', 'm_Capture', 'm_Spare']:
text = text.replace(field + '.', field + '->')
return re.sub(r'\bnull\b', 'nullptr', text)
methods = '\n'.join(method(x) for x in [
'protected bool ReplacementIsCurrent(', 'void Stop()',
'protected void SaveCounters(', 'protected bool VerifyStaleAndStop(',
'protected bool DeletionComplete(', 'protected void RequestOwnedDeletion(', 'void ObserveDeletion('])
# The first Stop in this file is the fixture method; adapters are named distinctly.
run(r'''
#include <cassert>
#include <vector>
#include <algorithm>
#include <string>
struct Probe;
struct Widget { int removes=0;void RemoveFromHierarchy(){removes++;} };
struct Config { Widget* RootWidgetRef=nullptr; };
struct Entity { int deletes=0; };
void DeleteEntityAndChildren(Entity*& value){value->deletes++;value=nullptr;}
struct Map { static Map* current;bool opened=true;Config* config=nullptr;Widget* root=nullptr;
int closes=0;float pan=0,zoom=0;bool IsOpen(){return opened;}Config* GetMapConfig(){return config;}
Widget* GetMapMenuRoot(){return root;}void CloseMap(){closes++;opened=false;}
float GetCurrentPan(){return pan;}float GetTargetZoomPPU(){return zoom;}
};Map* Map::current=nullptr;
struct Capture {int stops=0;void ERRM_StopOwnershipCapture(){stops++;}};
struct Observer {std::vector<Probe*> owners;void Remove(Probe* p){owners.erase(std::remove(owners.begin(),owners.end(),p),owners.end());}};
Observer closeObserver,readyObserver;
struct Probe {
Map* m_Map=nullptr;Widget* m_ReplacementRoot=nullptr;Config* m_ReplacementConfig=nullptr;Config* m_ReadyConfig=nullptr;
Capture *m_Capture=nullptr,*m_Spare=nullptr;Entity *m_CaptureEntity=nullptr,*m_SpareEntity=nullptr,*m_Holder=nullptr;
bool m_DeleteRequested=false,m_DeleteStateFailed=false;int m_CaptureDeleted=0,m_SpareDeleted=0;
bool m_Stopped=false;int m_CloseEvents=0,m_RestoreCalls=0,m_RestoreAuthorized=0,m_Completions=0,m_CenterCalls=0,m_FinishCalls=0;
int m_CloseBaseline=0,m_RestoreBaseline=0,m_AuthorizedBaseline=0,m_CompletionBaseline=0,m_CenterBaseline=0,m_FinishBaseline=0;
float m_PanBaseline=0,m_ZoomBaseline=0;
bool Check(bool result,const std::string&){if(!result)Stop();return result;}
'''+methods+r'''
void Bind(Map& map,Config& cfg,Widget& root,Capture& c,Capture& s,Entity& ce,Entity& se){
m_Map=&map;Map::current=&map;m_ReplacementConfig=&cfg;m_ReplacementRoot=&root;
map.opened=true;map.config=&cfg;map.root=&root;cfg.RootWidgetRef=&root;
m_Capture=&c;m_Spare=&s;m_CaptureEntity=&ce;m_SpareEntity=&se;
closeObserver.owners.push_back(this);readyObserver.owners.push_back(this);
}
};
int main(){
Map map;Config cfg,foreignCfg;Widget root,foreignRoot;Capture capture,spare;Entity ce,se;
Probe foreign;foreign.Bind(map,cfg,root,capture,spare,ce,se);
Probe other;closeObserver.owners.push_back(&other);readyObserver.owners.push_back(&other);
// Same object reused with another native root must not be closed in cleanup.
cfg.RootWidgetRef=&foreignRoot;map.root=&foreignRoot;
assert(!foreign.ReplacementIsCurrent());foreign.Stop();
assert(map.closes==0&&map.opened&&root.removes==1&&foreignRoot.removes==0);
assert(ce.deletes==1&&se.deletes==1&&capture.stops==1&&spare.stops==1);
assert(closeObserver.owners.size()==1&&closeObserver.owners[0]==&other);
assert(readyObserver.owners.size()==1&&readyObserver.owners[0]==&other);
foreign.Stop();assert(root.removes==1&&ce.deletes==1); // no duplicate cleanup
Probe changed;changed.Bind(map,cfg,root,capture,spare,ce,se);map.config=&foreignCfg;
changed.Stop();assert(map.closes==0); // distinct active config also preserved
Probe oldWorld;oldWorld.Bind(map,cfg,root,capture,spare,ce,se);Map replacementWorld;Map::current=&replacementWorld;
oldWorld.Stop();assert(map.closes==0); // old singleton cannot be closed
Probe exact;exact.Bind(map,cfg,root,capture,spare,ce,se);exact.Stop();assert(map.closes==1&&!map.opened);
// A correct view without callbacks running must not pass native evidence.
Probe missed;missed.Bind(map,cfg,root,capture,spare,ce,se);missed.SaveCounters();missed.m_RestoreCalls++;
assert(!missed.VerifyStaleAndStop()&&missed.m_Stopped);
Probe observed;observed.Bind(map,cfg,root,capture,spare,ce,se);observed.SaveCounters();
observed.m_CenterCalls+=2;observed.m_FinishCalls++;observed.m_RestoreCalls++;
assert(observed.VerifyStaleAndStop()&&!observed.m_Stopped&&map.opened);
observed.Stop();
Probe moved;moved.Bind(map,cfg,root,capture,spare,ce,se);moved.SaveCounters();
moved.m_CenterCalls+=2;moved.m_FinishCalls++;moved.m_RestoreCalls++;map.pan=1;
assert(!moved.VerifyStaleAndStop()); // actual changed map pan invalidates evidence
map.pan=0;
Probe closed;closed.Bind(map,cfg,root,capture,spare,ce,se);closed.SaveCounters();
closed.m_CenterCalls+=2;closed.m_FinishCalls++;closed.m_RestoreCalls++;closed.m_CloseEvents++;
assert(!closed.VerifyStaleAndStop()); // even a close/reopen to same view fails
// Request return is not deletion completion. Native refs and superclass
// completion are separate observations, all required, without timeout waiver.
Probe delayed;delayed.Bind(map,cfg,root,capture,spare,ce,se);
assert(!delayed.DeletionComplete());delayed.RequestOwnedDeletion();
int ceDeletes=ce.deletes,seDeletes=se.deletes;delayed.RequestOwnedDeletion();
assert(ce.deletes==ceDeletes&&se.deletes==seDeletes); // exactly one request
assert(!delayed.m_CaptureEntity&&!delayed.m_SpareEntity&&!delayed.DeletionComplete());
delayed.ObserveDeletion(1,true);delayed.ObserveDeletion(2,true);
assert(!delayed.DeletionComplete()); // callbacks alone don't invalidate native refs
delayed.m_Capture=nullptr;delayed.m_Spare=nullptr; // explicit native-boundary simulation
assert(delayed.DeletionComplete());delayed.ObserveDeletion(2,true);
assert(!delayed.DeletionComplete()); // duplicate cleanup is never accepted
Probe unreleased;unreleased.m_DeleteRequested=true;
unreleased.ObserveDeletion(1,false);unreleased.ObserveDeletion(2,true);
assert(!unreleased.DeletionComplete()); // a post-super leftover field fails
Probe premature;premature.ObserveDeletion(1,true);premature.m_DeleteRequested=true;
premature.ObserveDeletion(2,true);assert(!premature.DeletionComplete());
}
''')
text = SOURCE.read_text()
assert text.startswith('#ifdef WORKBENCH\n') and text.rstrip().endswith('#endif')
for name in ('CenterMapOnHolder', 'FinishMapCapture', 'RestoreCharacterCamera', 'CompleteMapModeSwitch'):
assert 'super.' + name + '();' in body(SOURCE, 'protected override void ' + name + '(')
deletion = body(SOURCE, 'protected override void OnDelete(')
assert deletion.index('super.OnDelete(owner)') < deletion.index('probe.ObserveDeletion(')
assert 'now >= m_DeleteDeadline' in text and 'm_DeleteDeadline = now + 3000' in text
assert 'SetCharacterCameraRenderActive(' not in text # no guessed getter or explicit write
assert 'GetOnMapOpenComplete().Insert(OnMapReady)' in text
assert 'm_ReadyConfig == m_ReplacementConfig' in text
assert 'CallLater(CenterMapOnHolder, 100, false)' in text
assert 'CallLater(FinishMapCapture, 200, false)' in text
print('PASS: actual native-map fixture ownership, cleanup and callback evidence fail closed; native execution remains separate')
+86
View File
@@ -0,0 +1,86 @@
"""Run actual roster, late-join selection and spectator server-selection methods.
Engine enumeration/geometry/streaming are adapters, not native/MP coverage.
"""
import re
from policy_harness import ROOT,body,run
core=ROOT/'addons/core/Scripts/Game/ERR'
def adapt(s):
s=s.replace('protected ','').replace('out vector position','vector& position').replace('null','nullptr')
s=re.sub(r'array<IEntity> (\w+) = \{\};',r'Array<IEntity> \1;',s)
s=s.replace('foreach (IEntity survivor : survivors)','for (IEntity survivor : survivors)')
for n in ('ERR_Phase','ERR_Actors','SCR_FactionManager','Replication','ERR_GameMode','ERR_ObserverReason','RplComponent','CompartmentAccessComponent'):
s=s.replace(n+'.',n+'::')
for n in ('mode','manager','life','component','target'):
s=s.replace(n+'.',n+'->')
s=s.replace('ERR_GameMode mode','ERR_GameMode* mode').replace('PlayerManager manager','PlayerManager* manager').replace('RplComponent component','RplComponent* component')
s=s.replace('GetGame().','GetGame()->').replace('Players().','Players()->')
s=s.replace('IEntity target;','IEntity target=nullptr;').replace('FindComponent(RplComponent)','FindComponent()')
return s
roster=adapt(body(core/'ERR_PlayerRoster.c','ERR_PlayerLife CurrentActor('))
spawn=adapt(body(core/'ERR_SpawnLogic.c','protected bool SpawnPosition('))
observe=adapt(body(core/'ERR_Observer.c','void ERR_UpdateObserverServer('))
run(r'''
#include <cassert>
#include <vector>
#include <map>
#include <algorithm>
using vector=int;using Faction=int;using RplId=int;
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}bool IsEmpty(){return this->empty();}void RemoveOrdered(int i){this->erase(this->begin()+i);}int Find(T a){auto p=std::find(this->begin(),this->end(),a);return p==this->end()?-1:p-this->begin();}};
struct RplComponent{int id=1;int Id(){return id;}static RplComponent* Cast(void* p){return static_cast<RplComponent*>(p);}};
struct Actor{bool alive=true,conscious=true;int faction=1,origin=123;Actor* vehicle=nullptr;RplComponent replication;int GetOrigin(){return origin;}void* FindComponent(){return &replication;}};using IEntity=Actor*;
struct Identity{int id=1;bool IsNull(){return id==0;}};
struct Life{Identity AccountId;bool Connected=true,UsedLife=true;int ConnectionId=1;IEntity Actor=nullptr;};using ERR_PlayerLife=Life*;
struct PlayerManager{std::map<int,IEntity> controlled;int GetPlayerIdFromControlledEntity(IEntity a){for(auto& p:controlled)if(p.second==a)return p.first;return 0;}IEntity GetPlayerControlledEntity(int id){return controlled[id];}};
struct Game{PlayerManager* manager;PlayerManager* GetPlayerManager(){return manager;}};Game* game_;Game* GetGame(){return game_;}
namespace Replication{bool server=true;int bumps=0;bool IsServer(){return server;}void BumpMe(){bumps++;}}
namespace ERR_Phase{const int PRISON=1;}
namespace ERR_ObserverReason{const int NONE=0;}
namespace ERR_Actors{bool Alive(IEntity a){return a&&a->alive;}void Players(Array<IEntity>& out,bool conscious=false){out.clear();for(auto& p:GetGame()->manager->controlled){IEntity a=p.second;if(Alive(a)&&(!conscious||a->conscious))out.push_back(a);}}}
namespace SCR_FactionManager{int SGetFaction(IEntity a){return a?a->faction:0;}}
namespace CompartmentAccessComponent{IEntity GetVehicleIn(IEntity a){return a?a->vehicle:nullptr;}}
struct Roster{std::map<int,ERR_PlayerLife> lives;ERR_PlayerLife Get(int id){auto p=lives.find(id);return p==lives.end()?nullptr:p->second;}
'''+roster+r'''
};
struct ERR_GameMode{static ERR_GameMode* current;Roster* roster;int phase=2,faction=1;bool active=true;static ERR_GameMode* Get(){return current;}bool Active(){return active;}int Phase(){return phase;}int PlayerFaction(){return faction;}int PrisonPosition(){return 0;}Roster* Players(){return roster;}};ERR_GameMode* ERR_GameMode::current=nullptr;
struct Placement{int nearCalls=0,prisonCalls=0;IEntity last=nullptr;bool clear=true;bool Near(IEntity a,int,vector& p){nearCalls++;last=a;p=a->origin;return clear;}bool Prison(int,int,vector& p){prisonCalls++;p=0;return true;}};
struct Spawn{Placement m_Placement;int m_AuthorizedPositions=0;
'''+spawn+r'''
};
struct Observer{int m_ERRObserverReason=1,m_ERRObserverAnchor=-1,m_ERRObservedId=-1,releases=0,streamRequests=0;IEntity m_ERRServerObserved=nullptr,m_ERRServerObservedVehicle=nullptr,own=nullptr;IEntity GetControlledEntity(){return own;}
void ERR_ReleaseObserved(){releases++;m_ERRServerObserved=nullptr;m_ERRServerObservedVehicle=nullptr;m_ERRObservedId=-1;}
void ERR_StreamObserved(IEntity,bool){streamRequests++;}
'''+observe+r'''
};
int main(){
PlayerManager manager;Game game{&manager};game_=&game;Roster roster;ERR_GameMode mode;mode.roster=&roster;ERR_GameMode::current=&mode;
Actor original,replacement,other;original.origin=100;replacement.origin=900;other.origin=200;
Life life;life.Actor=&original;roster.lives[1]=&life;manager.controlled[1]=&original;
auto accepted=[&](){Spawn s;vector p=-1;assert(s.SpawnPosition(&mode,p)&&p==original.origin&&s.m_Placement.last==&original&&s.m_Placement.prisonCalls==0);Observer o;o.ERR_UpdateObserverServer();assert(o.m_ERRServerObserved==&original&&o.m_ERRObserverAnchor==original.origin);};
auto rejected=[&](){Spawn s;vector p=-1;assert(s.SpawnPosition(&mode,p)&&p==0&&s.m_Placement.nearCalls==0&&s.m_Placement.prisonCalls==1);Observer o;o.m_ERRServerObserved=&original;o.ERR_UpdateObserverServer();assert(!o.m_ERRServerObserved&&o.releases==1);};
accepted();
roster.lives.clear();rejected();roster.lives[1]=&life; // unregistered controller
life.UsedLife=false;rejected();life.UsedLife=true; // pending initial assignment
life.Connected=false;rejected();life.Connected=true;
life.AccountId.id=0;rejected();life.AccountId.id=1;
life.ConnectionId=2;rejected();life.ConnectionId=1;
manager.controlled[1]=&replacement;rejected(); // recycled connection ID/controlled replacement cannot inherit original body record
manager.controlled[1]=&original;accepted(); // restored canonical handoff
original.alive=false;rejected();original.alive=true;
original.faction=2;rejected();original.faction=1;
mode.faction=0;rejected();mode.faction=1;
mode.roster=nullptr;rejected();mode.roster=&roster;
original.conscious=false;Spawn unconscious;vector p=-1;assert(unconscious.SpawnPosition(&mode,p)&&p==0&&unconscious.m_Placement.nearCalls==0);
Observer following;following.ERR_UpdateObserverServer();assert(following.m_ERRServerObserved==&original);original.conscious=true; // living unconscious prisoners still followable
// An invalid earlier candidate cannot shadow a later canonical survivor.
manager.controlled[1]=&replacement;Life second;second.Actor=&other;second.ConnectionId=2;second.AccountId.id=2;manager.controlled[2]=&other;roster.lives[2]=&second;
Spawn mixed;assert(mixed.SpawnPosition(&mode,p)&&p==other.origin&&mixed.m_Placement.last==&other);
Observer mixedFollow;mixedFollow.m_ERRServerObserved=&replacement;mixedFollow.ERR_UpdateObserverServer();assert(mixedFollow.m_ERRServerObserved==&other&&mixedFollow.releases==1);
manager.controlled[1]=&original;Observer cycling;cycling.ERR_UpdateObserverServer();assert(cycling.m_ERRServerObserved==&original);cycling.ERR_UpdateObserverServer(1);assert(cycling.m_ERRServerObserved==&other);cycling.ERR_UpdateObserverServer(1);assert(cycling.m_ERRServerObserved==&original);cycling.ERR_UpdateObserverServer(-1);assert(cycling.m_ERRServerObserved==&other);
// Existing terrain rejection and prison-phase placement stay delegated.
Spawn blocked;blocked.m_Placement.clear=false;assert(blocked.SpawnPosition(&mode,p)&&p==0&&blocked.m_Placement.nearCalls==2);
mode.phase=ERR_Phase::PRISON;Spawn prison;assert(prison.SpawnPosition(&mode,p)&&p==0&&prison.m_Placement.nearCalls==0);
Replication::server=false;Observer client;client.ERR_UpdateObserverServer();assert(!client.m_ERRServerObserved&&client.streamRequests==0);Replication::server=true;
}
''')
print('PASS: actual CurrentActor, SpawnPosition and observer-selection methods reject pending/unregistered/replaced/recycled/foreign/dead bodies; canonical selection/cycling, unconscious following and prison/geometry fallback preserved; native boundaries adapted')
+122
View File
@@ -294,4 +294,126 @@ controls(true,f,r);assert(f==0&&r==0);}
self.assertIn('m_Seat.GetOccupant() == m_Pilot',stop)
self.assertIn('CompartmentAccessComponent.GetVehicleIn(m_Pilot) == m_Aircraft',stop)
self.assertIn('RestoreAutohover(ownSeat)',stop)
def test_actual_translation_geometry_requests_and_state_gates(self):
signatures=['static bool Hover(', 'static bool TranslationInputs(', 'static bool TranslationActive(', 'static bool TranslationTarget(', 'static vector TranslationDemand(', 'static bool TranslationHeld(']
methods='\n'.join(body(SOURCE,k) for k in signatures)
methods=methods.replace('Math.', 'Math::').replace('vector.', 'vector::').replace('out vector target','vector& target')
callbacks='\n'.join(body(SOURCE,k) for k in ['protected bool BeginTranslation(', 'protected bool CheckTranslation('])
callbacks=callbacks.replace('protected ','').replace('ERR_PoweredFlightRules.','Rules::').replace('vector.','vector::')
callbacks=callbacks.replace('m_Controller.GetSignalsManager()', 'm_Controller.GetSignalsManager()').replace('SignalsManagerComponent signals','SignalsManagerComponent* signals').replace('signals.FindSignal','signals->FindSignal')
run(r'''#include <cassert>
#include <cmath>
#include <algorithm>
#include <initializer_list>
#include <string>
struct Math {static float AbsFloat(float x){return std::abs(x);}};
struct vector {
float x=0,y=0,z=0;
float& operator[](int i){return i==0?x:(i==1?y:z);}
vector operator+(vector v)const{return{x+v.x,y+v.y,z+v.z};}
vector operator-(vector v)const{return{x-v.x,y-v.y,z-v.z};}
vector operator*(float k)const{return{x*k,y*k,z*k};}
float Length()const{return std::sqrt(x*x+y*y+z*z);}
static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
static const vector Zero;
};
const vector vector::Zero={0,0,0};
template<class... T> void PrintFormat(T...){}
struct Rules {
'''+methods+r'''
};
struct SignalsManagerComponent {
bool present=true;
int FindSignal(const char*){return present?0:-1;}
};
struct Controller {SignalsManagerComponent signals; SignalsManagerComponent* GetSignalsManager(){return &signals;}};
struct Simulation {vector velocity; bool contact=false; vector GetVelocity(){return velocity;} bool HasAnyGroundContact(){return contact;}};
struct Probe {
bool m_TranslationMode=true,m_TranslationFrozen=false,m_TranslationProven=false,m_ActionInputs=true,m_AutohoverMode=true,hoverReady=true,stopped=false;
vector m_Home={100,25,200},m_TranslationOrigin,m_TranslationTarget;
int m_Stage=4,m_TranslationUpdates=0;
float m_StageDeadline=0,m_Stable=-1,m_NextLog=0,m_Deadline=240000;
Controller m_Controller; Simulation m_Simulation;
bool AutohoverReady(){return hoverReady;}
bool Check(bool ok,const char*){if(!ok)stopped=true;return ok;}
'''+callbacks+r'''
};
int main(){
const vector home={100,25,200},fallback={1,5,0};
for(float radius:{0.f,.49f,.5f,3.25f,7.99f})for(int angle=0;angle<360;angle+=5){
float radians=angle*3.14159265f/180;
vector start={home.x+radius*std::cos(radians),home.y+8,home.z+radius*std::sin(radians)},target;
assert(Rules::TranslationTarget(home,start,fallback,target));
assert(std::abs(vector::DistanceXZ(start,target)-4)<.0001f);
assert(target.y==start.y&&vector::DistanceXZ(home,target)<=5);
// Whole straight target segment lies inside the unchanged origin envelope.
for(int i=0;i<=20;i++)assert(vector::DistanceXZ(home,start+(target-start)*(i/20.f))<8);
}
vector target;
assert(!Rules::TranslationTarget(home,home,vector::Zero,target));
assert(!Rules::TranslationTarget(home,home+vector{8,0,0},fallback,target));
assert(!Rules::TranslationTarget(home,home+vector{NAN,0,0},fallback,target));
for(int mask=0;mask<16;mask++)for(int stage=0;stage<12;stage++){
bool selected=mask&1,frozen=mask&2,actions=mask&4,hover=mask&8;
assert(Rules::TranslationInputs(selected,actions,hover)==(!selected||(actions&&hover)));
assert(Rules::TranslationActive(selected,frozen,actions,hover,stage)==(selected&&frozen&&actions&&hover&&stage==5));
}
for(float x:{-100.f,-4.f,0.f,4.f,100.f})for(float z:{-10.f,0.f,10.f})for(float speed:{-30.f,-.5f,0.f,.5f,30.f}){
vector demand=Rules::TranslationDemand(home+vector{x,900,z},home,vector{speed,99,-speed});
assert(demand.Length()<=.030001f&&demand.y==0);
}
assert(Rules::TranslationDemand(home,home,vector::Zero).Length()==0);
assert(Rules::TranslationDemand(home+vector{4,0,0},home,vector::Zero).x>0);
assert(Rules::TranslationDemand(home+vector{4,0,0},home,vector{1,0,0}).x<0); // brake overspeed
assert(Rules::TranslationDemand(home,home,vector{0,0,-1}).z>0);
assert(Rules::TranslationHeld(false,8,.2f,0,1,4,.2f,.75f,3.25f));
assert(!Rules::TranslationHeld(true,8,.2f,0,1,4,.2f,0,4));
assert(!Rules::TranslationHeld(false,8,.2f,0,1,4,.25f,0,4));
assert(!Rules::TranslationHeld(false,8,.2f,0,1,4,.2f,.751f,4));
assert(!Rules::TranslationHeld(false,8,.2f,0,1,4,.2f,0,3.249f));
assert(!Rules::TranslationHeld(false,8,.2f,0,1,8,.2f,0,4));
// Execute real stage methods: frozen origin/target, distinct ticks, reset and bounded timeout.
Probe p;vector start=home+vector{3.25f,8,0};
assert(p.BeginTranslation(1000,start,fallback));
assert(p.m_Stage==5&&p.m_StageDeadline==46000&&p.m_Deadline==240000&&p.m_TranslationFrozen);
vector frozen=p.m_TranslationTarget;
assert(!p.BeginTranslation(2000,home,fallback)); // never retarget/rebase a running trip
assert(vector::DistanceXZ(frozen,p.m_TranslationTarget)==0&&p.m_StageDeadline==46000);
p.stopped=false;p.m_TranslationUpdates=3;
assert(p.CheckTranslation(2000,start,8,0,1,3.25));assert(p.m_Stable<0);
assert(p.CheckTranslation(3000,frozen,8,0,1,.75));assert(p.m_Stable==3000);
assert(p.CheckTranslation(7999,frozen,8,0,1,.75));assert(!p.m_TranslationProven);
p.m_Simulation.velocity={.3f,0,0};
assert(p.CheckTranslation(8000,frozen,8,.3f,1,.75));assert(p.m_Stable<0);
p.m_Simulation.velocity={0,0,0};
assert(p.CheckTranslation(9000,frozen,8,0,1,.75));
assert(p.CheckTranslation(13999,frozen,8,0,1,.75));assert(p.m_Stage==5);
assert(p.CheckTranslation(14000,frozen,8,0,1,.75));
assert(p.m_TranslationProven&&p.m_Stage==6&&p.m_StageDeadline==79000&&p.m_Deadline==240000);
Probe expired;assert(expired.BeginTranslation(1000,start,fallback));expired.m_TranslationUpdates=3;
assert(!expired.CheckTranslation(46000,expired.m_TranslationTarget,8,0,1,.75));assert(!expired.m_TranslationProven);
for(int invalid=0;invalid<7;invalid++){
Probe refused;
if(invalid==0)refused.m_TranslationMode=false;
if(invalid==1)refused.m_Stage=5;
if(invalid==2)refused.m_ActionInputs=false;
if(invalid==3)refused.m_AutohoverMode=false;
if(invalid==4)refused.hoverReady=false;
if(invalid==5)refused.m_Controller.signals.present=false;
if(invalid==6)refused.m_TranslationFrozen=true;
assert(!refused.BeginTranslation(1000,start,fallback)&&refused.stopped);
}
}
''')
tick=body(SOURCE,'int Tick(')
self.assertLess(tick.index('TranslationInputs('),tick.index('if (m_Stage == 0)'))
self.assertIn('height > 18 || height < -3 || drift >= 8 || transform[1][1] < 0.85',tick)
self.assertIn('now >= m_Deadline',tick)
self.assertIn('m_TranslationMode && !m_TranslationProven',tick)
demand=body(SOURCE,'protected bool ComputePilotDemand(')
self.assertLess(demand.index('if (m_AutohoverMode) { forward = 0; aside = 0; }'),demand.index('TranslationActive('))
self.assertEqual(SOURCE.read_text().count('m_TranslationTarget = target;'),1)
self.assertEqual(SOURCE.read_text().count('m_TranslationOrigin = position;'),1)
if __name__=='__main__': unittest.main()
+104
View File
@@ -0,0 +1,104 @@
"""Execute actual shared-map ownership/callback cleanup with explicit API doubles.
No Enforce compilation, rendering, remote client or multiplayer claim.
"""
import re
from policy_harness import ROOT, body, run
source = ROOT/'addons/map/Scripts/Game/ERRMap/ERRM_SharedMapComponent.c'
def adapt(signature):
text = body(source, signature).replace('protected ', '').replace('override ', '')
text = text.replace('IEntity owner', 'Entity* owner').replace('PlayerController playerController', 'PlayerController* playerController')
text = text.replace(' = null;', ' = nullptr;').replace('SCR_MapEntity.GetMapInstance()', 'Map::current')
text = text.replace('GetGame().', 'GetGame()->').replace('GetCallqueue().', 'GetCallqueue()->')
for field in ['m_ESMMapEntity', 'm_ESMMapConfig', 'm_ESMRootWidget', 'm_ESMRenderTarget', 'playerController']:
text = text.replace(field+'.', field+'->')
for callback in ['TryCaptureMapSurface', 'CenterMapOnHolder', 'FinishMapCapture']:
text = text.replace('.Remove('+callback+')', '.Remove(this, &Capture::'+callback+')')
text = text.replace('->Remove('+callback+')', '->Remove(this, &Capture::'+callback+')')
text = text.replace('CallLater('+callback+',', 'CallLater(this, &Capture::'+callback+',')
return text.replace('super.OnDelete(owner)', 'NativeDelete(owner)')
methods = '\n'.join(adapt(signature) for signature in [
'protected bool OwnsMapCapture(', 'protected void ReleaseMapCapture(',
'protected void FinishMapCapture(', 'protected void StopMapCapture(',
'protected void RestoreCharacterCamera(', 'protected override void OnDelete('])
run(r'''
#include <cassert>
#include <vector>
#include <algorithm>
struct Capture;struct Entity{};
struct Widget {bool enabled=true,removed=false;void SetEnabled(bool x){enabled=x;}void RemoveFromHierarchy(){removed=true;}};
struct Config {Widget* RootWidgetRef=nullptr;};
struct PlayerController {bool camera=false;int writes=0;void SetCharacterCameraRenderActive(bool x){camera=x;writes++;}};
struct Queue {
using Entry=std::pair<Capture*,void(Capture::*)()>;std::vector<Entry> pending;
void Remove(Capture* owner,void(Capture::*callback)()){pending.erase(std::remove(pending.begin(),pending.end(),Entry(owner,callback)),pending.end());}
void CallLater(Capture* owner,void(Capture::*callback)(),int,bool){pending.emplace_back(owner,callback);}
int Count(Capture* owner){return std::count_if(pending.begin(),pending.end(),[=](const Entry& e){return e.first==owner;});}
};
struct Game {Queue queue;PlayerController controller;Queue* GetCallqueue(){return &queue;}PlayerController* GetPlayerController(){return &controller;}};
Game game;Game* currentGame=&game;Game* GetGame(){return currentGame;}
struct Map {static Map* current;bool opened=true;Config* config=nullptr;int closes=0;
bool IsOpen(){return opened;}Config* GetMapConfig(){return config;}
void CloseMap(){closes++;opened=false;game.controller.SetCharacterCameraRenderActive(true);}
};Map* Map::current=nullptr;
struct Capture {
bool m_bESMInHand=true,m_bESMOwnsMapRenderer=false;
Map* m_ESMMapEntity=nullptr;Config* m_ESMMapConfig=nullptr;
Widget* m_ESMRootWidget=nullptr;Widget* m_ESMRenderTarget=nullptr;Entity* m_ESMCharacterOwner=nullptr;
static constexpr int MAP_RETRY_DELAY_MS=250;int completed=0,nativeDeletes=0;
void TryCaptureMapSurface(){}void CenterMapOnHolder(){}
void CompleteMapModeSwitch(){completed++;}void NativeDelete(Entity*){nativeDeletes++;}
'''+methods+r'''
void Bind(Map& map,Config& config,Widget& root,Widget& target){
m_ESMMapEntity=&map;m_ESMMapConfig=&config;m_ESMRootWidget=&root;m_ESMRenderTarget=&target;
config.RootWidgetRef=&root;map.config=&config;map.opened=true;Map::current=&map;m_bESMOwnsMapRenderer=true;
}
};
int main(){Map map;Config mine,foreign;Widget root,target,foreignRoot;foreign.RootWidgetRef=&foreignRoot;
Capture normal;normal.Bind(map,mine,root,target);assert(normal.OwnsMapCapture());
normal.RestoreCharacterCamera();assert(game.controller.camera&&game.controller.writes==1);
normal.FinishMapCapture();assert(map.closes==1&&!target.enabled&&!normal.m_bESMOwnsMapRenderer&&!normal.m_ESMMapConfig&&normal.completed==1);
normal.FinishMapCapture();assert(map.closes==1&&normal.completed==1); // late duplicate has no authority
Capture replaced;replaced.Bind(map,mine,root,target);map.config=&foreign;game.controller.camera=false;
int writes=game.controller.writes,closes=map.closes;
replaced.RestoreCharacterCamera();assert(game.controller.writes==writes&&!game.controller.camera);
replaced.FinishMapCapture();assert(map.closes==closes&&map.opened&&replaced.completed==0&&!target.enabled);
assert(game.queue.Count(&replaced)==1); // defer vanilla focus until foreign UI closes
replaced.StopMapCapture();assert(game.queue.Count(&replaced)==0&&map.opened&&game.controller.writes==writes);
Capture reused;reused.Bind(map,mine,root,target);mine.RootWidgetRef=&foreignRoot;
assert(map.config==reused.m_ESMMapConfig&&!reused.OwnsMapCapture()); // same native object was reused
reused.FinishMapCapture();assert(map.closes==closes&&reused.completed==0);reused.StopMapCapture();
Capture oldWorld;oldWorld.Bind(map,mine,root,target);Map newWorld;Map::current=&newWorld;
oldWorld.StopMapCapture();assert(map.closes==closes&&game.controller.writes==writes);
Capture spare;Widget spareRoot,spareTarget;spare.m_ESMRootWidget=&spareRoot;spare.m_ESMRenderTarget=&spareTarget;
spare.OnDelete(nullptr);assert(spare.nativeDeletes==1&&spareRoot.removed&&!spareTarget.enabled);
assert(game.controller.writes==writes&&!game.controller.camera&&map.closes==closes);
Capture owned,other;owned.Bind(map,mine,root,target);
game.queue.CallLater(&owned,&Capture::TryCaptureMapSurface,250,false);
game.queue.CallLater(&owned,&Capture::CenterMapOnHolder,100,false);
game.queue.CallLater(&owned,&Capture::FinishMapCapture,500,false);
game.queue.CallLater(&other,&Capture::FinishMapCapture,500,false);
owned.OnDelete(nullptr);assert(game.queue.Count(&owned)==0&&game.queue.Count(&other)==1);
assert(map.closes==closes+1&&!map.opened&&root.removed&&!target.enabled);
assert(owned.m_ESMMapEntity==nullptr&&owned.m_ESMMapConfig==nullptr&&owned.m_ESMRootWidget==nullptr&&owned.nativeDeletes==1);
owned.FinishMapCapture();assert(owned.completed==0&&map.closes==closes+1);
Capture shuttingDown;shuttingDown.Bind(map,mine,root,target);currentGame=nullptr;
writes=game.controller.writes;closes=map.closes;shuttingDown.OnDelete(nullptr);
assert(game.controller.writes==writes&&map.closes==closes&&shuttingDown.nativeDeletes==1);
currentGame=&game;
}
''')
center = body(source, 'protected void CenterMapOnHolder(')
assert center.index('!OwnsMapCapture()') < center.index('.ZoomPanSmooth(')
capture = body(source, 'protected void TryCaptureMapSurface(')
assert capture.index('m_ESMMapConfig = mapConfig;') < capture.index('.OpenMap(mapConfig)')
assert capture.index('if (!OwnsMapCapture())') < capture.index('RestoreCharacterCamera();')
print('PASS: actual shared-map callbacks bind exact map/config/root, preserve replacement UI/camera, defer focus, and remove only owned delayed work')