Harden participant and map ownership and validate bounded physical flight
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
@@ -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::current=↦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')
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
|
||||
@@ -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=↦m_ESMMapConfig=&config;m_ESMRootWidget=&root;m_ESMRenderTarget=⌖
|
||||
config.RootWidgetRef=&root;map.config=&config;map.opened=true;Map::current=↦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')
|
||||
Reference in New Issue
Block a user