181 lines
17 KiB
Python
181 lines
17 KiB
Python
"""Actual fixture ownership/terminal cleanup plus explicit selector contracts."""
|
|
import os,re
|
|
from pathlib import Path
|
|
from policy_harness import ROOT,body,run
|
|
root=Path(os.environ.get('ERR_NATIVE_OBJECTIVE_MAP_ROOT',ROOT));p=root/'addons/core/Scripts/Game/ERR/00_Core/ERR_NativeObjectiveMapProbe.c'
|
|
def adapt(s):
|
|
s=s.replace('protected ','').replace('null','nullptr').replace('SCR_MapEntity.','SCR_MapEntity::')
|
|
for name in ['m_Map','m_Config','m_Marker','m_Presentation','m_SharedRoot','m_ReusedRoot']:
|
|
s=s.replace(name+'.',name+'->')
|
|
for name in ['GetGame','GetMenuManager','GetOnMapOpenComplete','GetOnMapClose']:
|
|
s=s.replace(name+'().',name+'()->')
|
|
return s.replace('Remove(Ready)','Remove(this)').replace('Remove(Closed)','Remove(this)').replace('MapConfiguration config','Config* config')
|
|
methods='\n'.join(adapt(body(p,s)) for s in ['protected bool OwnsMap(', 'protected bool CloseStandard(', 'protected int StandardClosed(', 'protected void Closed(', 'void Stop('])
|
|
run('''#include <cassert>
|
|
#include <set>
|
|
#include <string>
|
|
using string=std::string;
|
|
struct Widget{int deletes=0;void RemoveFromHierarchy(){deletes++;}};
|
|
struct Config{Widget*RootWidgetRef=nullptr;};
|
|
struct Event{std::set<void*>owners;void Remove(void*p){owners.erase(p);}};
|
|
struct SCR_MapEntity{static inline SCR_MapEntity*current=nullptr;static inline Event opened,closed;Config*config=nullptr;Widget*root=nullptr;bool open=true;int closes=0;static SCR_MapEntity*GetMapInstance(){return current;}static Event*GetOnMapOpenComplete(){return &opened;}static Event*GetOnMapClose(){return &closed;}bool IsOpen(){return open;}Config*GetMapConfig(){return config;}Widget*GetMapMenuRoot(){return root;}void CloseMap(){closes++;open=false;}};
|
|
struct Marker{int deletes=0;void OnDelete(){deletes++;}};struct Presentation{int stops=0;Widget*root=nullptr;Widget*ERR_ProbeRoot(){return root;}void Stop(){stops++;root=nullptr;}};struct Menu{};
|
|
struct Manager{int closes=0;bool asynchronous=false;bool CloseMenu(Menu*){closes++;if(!asynchronous&&SCR_MapEntity::current)SCR_MapEntity::current->CloseMap();return true;}};
|
|
struct Game{Manager manager;Manager*GetMenuManager(){return &manager;}}game;Game*GetGame(){return &game;}
|
|
struct Probe{bool m_Stopped=false,m_CloseRequested=false,m_CloseObserved=false;bool Check(bool ok,string){if(!ok)Stop();return ok;}SCR_MapEntity*m_Map=nullptr;Config*m_Config=nullptr,*m_Ready=nullptr;Widget*m_ExpectedRoot=nullptr,*m_SharedRoot=nullptr,*m_ReusedRoot=nullptr;Marker*m_Marker=nullptr;Presentation*m_Presentation=nullptr;Menu*m_Menu=nullptr;
|
|
'''+methods+'''
|
|
};
|
|
int main(){SCR_MapEntity map;SCR_MapEntity::current=↦Config cfg;Widget root,otherRoot,shared,reused;cfg.RootWidgetRef=&root;map.root=&root;map.config=&cfg;
|
|
Probe p;p.m_Map=↦p.m_Config=&cfg;p.m_ExpectedRoot=&root;Marker marker;Presentation presentation;Menu menu;p.m_Marker=▮p.m_Presentation=&presentation;p.m_Menu=&menu;p.m_SharedRoot=&shared;p.m_ReusedRoot=&reused;
|
|
assert(p.OwnsMap());cfg.RootWidgetRef=&otherRoot;assert(!p.OwnsMap());cfg.RootWidgetRef=&root;map.root=&otherRoot;assert(!p.OwnsMap());map.root=&root;
|
|
SCR_MapEntity replacement;SCR_MapEntity::current=&replacement;assert(!p.OwnsMap());SCR_MapEntity::current=↦
|
|
SCR_MapEntity::opened.owners.insert(&p);p.Stop();assert(p.m_Stopped&&marker.deletes==1&&presentation.stops==1&&game.manager.closes==1&&map.closes==1&&shared.deletes==1&&reused.deletes==1&&!SCR_MapEntity::opened.owners.count(&p));
|
|
p.Stop();assert(marker.deletes==1&&map.closes==1&&shared.deletes==1);
|
|
Probe interrupted;interrupted.m_Map=↦interrupted.m_Config=&cfg;interrupted.m_ExpectedRoot=&root;interrupted.m_Menu=&menu;map.open=true;map.root=&otherRoot;interrupted.Stop();assert(map.open&&map.closes==1&&game.manager.closes==1);
|
|
Probe partial;partial.Stop();assert(partial.m_Stopped&&game.manager.closes==1);
|
|
// Native CloseMenu can return while MapMenu.OnMenuClose has not happened.
|
|
map.root=&root;map.open=true;game.manager.asynchronous=true;
|
|
Probe pending;pending.m_Map=↦pending.m_Config=&cfg;pending.m_ExpectedRoot=&root;pending.m_Menu=&menu;pending.m_Presentation=&presentation;presentation.root=&shared;
|
|
assert(pending.CloseStandard()&&pending.m_Menu==&menu&&pending.m_CloseRequested&&map.open);assert(game.manager.closes==2);
|
|
assert(pending.StandardClosed()==0&&!pending.m_Stopped);assert(pending.StandardClosed()==0&&game.manager.closes==2);
|
|
Config foreign;pending.Closed(&foreign);assert(!pending.m_CloseObserved);
|
|
pending.Closed(&cfg);assert(pending.m_CloseObserved&&pending.StandardClosed()==0); // callback occurs before IsOpen clears
|
|
map.open=false;presentation.root=nullptr;assert(pending.StandardClosed()==1&&!pending.m_Menu&&!pending.m_CloseRequested);assert(game.manager.closes==2);
|
|
// Closed state alone cannot replace the observed exact native callback.
|
|
Probe noEvent;noEvent.m_Map=↦noEvent.m_Config=&cfg;noEvent.m_ExpectedRoot=&root;noEvent.m_Menu=&menu;noEvent.m_Presentation=&presentation;map.open=true;assert(noEvent.CloseStandard());map.open=false;assert(noEvent.StandardClosed()==0&&noEvent.m_Menu==&menu);noEvent.Stop();
|
|
// A close request is not repeated by Stop or after foreign map adoption.
|
|
Probe cancel;cancel.m_Map=↦cancel.m_Config=&cfg;cancel.m_ExpectedRoot=&root;cancel.m_Menu=&menu;map.open=true;assert(cancel.CloseStandard());int requests=game.manager.closes;cancel.Stop();assert(game.manager.closes==requests&&map.open);
|
|
Probe displaced;displaced.m_Map=↦displaced.m_Config=&cfg;displaced.m_ExpectedRoot=&root;displaced.m_Menu=&menu;displaced.m_Presentation=&presentation;assert(displaced.CloseStandard());requests=game.manager.closes;map.root=&otherRoot;assert(displaced.StandardClosed()==-1&&displaced.m_Stopped&&map.open&&game.manager.closes==requests);
|
|
// Exact close callback with a leaked production widget still fails.
|
|
map.root=&root;Probe leak;leak.m_Map=↦leak.m_Config=&cfg;leak.m_ExpectedRoot=&root;leak.m_Menu=&menu;leak.m_Presentation=&presentation;assert(leak.CloseStandard());leak.Closed(&cfg);map.open=false;presentation.root=&shared;assert(leak.StandardClosed()==-1&&leak.m_Stopped);
|
|
|
|
}
|
|
''')
|
|
s=p.read_text();assert s.startswith('#ifdef WORKBENCH')
|
|
assert 'System.GetTickCount() + 90000' in s and 'System.GetTickCount() + 180000' in s
|
|
assert s.count('System.MakeScreenshot(')==7
|
|
assert s.count('ERR OBJECTIVE CAPTURE READY: ')==7
|
|
assert s.count('m_Wait = now + 1500; return 0;')==7
|
|
assert 'm_Stage >= 16 && m_Stage <= 23 && !Check(ReadyToInspect()' in s
|
|
assert 'm_Stage == 6' in s and 'm_Stage == 15' in s and s.count('int closed = StandardClosed();')==2
|
|
assert 'SetBlocked(' not in s and 'SetPrivileged' not in s
|
|
base=(root/'addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c').read_text()
|
|
assert '"objective-map"' in base.split('ERR_NativeObjectiveProbe.BeginRound();')[0]
|
|
runner=(root/'tools/headless_workbench.py').read_text()
|
|
assert runner.index("objective-map requires --project core --rounds 1") < runner.index('steam, compat, profile =')
|
|
# Actual invalid CLI must refuse before nonexistent prefix/profile processing.
|
|
import subprocess,sys,tempfile
|
|
with tempfile.TemporaryDirectory(prefix='err-objective-selector-') as temporary:
|
|
sandbox=Path(temporary); profile=sandbox/'must-not-exist'; prefix=sandbox/'absent-prefix'
|
|
environment=os.environ.copy(); environment['PYTHONPATH']=str(ROOT/'tools')
|
|
for project,rounds in [('chat','1'),('core','2')]:
|
|
result=subprocess.run([sys.executable,str(root/'tools/headless_workbench.py'),'--smoke','objective-map','--project',project,'--rounds',rounds,'--compat',str(prefix),'--profile',str(profile)],capture_output=True,text=True,env=environment)
|
|
assert result.returncode==2 and 'objective-map requires --project core --rounds 1' in result.stderr,result.stderr
|
|
assert not profile.exists() and not prefix.exists()
|
|
print('PASS: actual native fixture root/config ownership and idempotent cleanup preserve foreign map, selector excludes ordinary route, fixed budgets/screenshots/UGC boundaries retained; native operations not executed')
|
|
|
|
# Execute the complete actual Inspect method: parent identity and frame projection
|
|
# are checked independently from native image-box coordinates. These are API
|
|
# doubles, not proof of engine widget parenting, pixels or pointer transparency.
|
|
inspect=body(p,'protected bool Inspect(').replace('protected ','')
|
|
for t in ['Widget','ERR_ObjectiveMapElement','TextWidget','ImageWidget']:
|
|
inspect=re.sub(r'\b'+t+r' (?=\w)',t+'* ',inspect)
|
|
for name in ['m_Presentation','m_Map','m_ExpectedRoot','m_Marker','root','element','container','nativeIcons','text','image']:
|
|
inspect=inspect.replace(name+'.',name+'->')
|
|
for name in ['GetGame','GetWorkspace','GetMapWidget']:
|
|
inspect=inspect.replace(name+'().',name+'()->')
|
|
for name in ['TextWidget','ImageWidget','ERR_DiscoveryMapRules','FrameSlot','Math']:
|
|
inspect=inspect.replace(name+'.',name+'::')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <string>
|
|
using string=std::string;using vector=std::array<float,3>;
|
|
struct Widget{Widget*parent=nullptr,*container=nullptr,*overlay=nullptr,*text=nullptr,*image=nullptr;vector placed={0,0,0};float sx=0,sy=0,width=36,height=36;bool visible=true;virtual ~Widget()=default;Widget*GetParent(){return parent;}Widget*FindAnyWidget(string n){if(n=="UIIconsContainer")return container;if(n=="ERR_ObjectiveOverlay")return overlay;if(n=="ObjectiveText")return text;if(n=="Image")return image;return nullptr;}int GetZOrder(){return 20;}bool IsVisible(){return visible;}void GetScreenPos(float&x,float&y){x=sx;y=sy;}void GetScreenSize(float&w,float&h){w=width;h=height;}};
|
|
struct TextWidget:Widget{string label;static TextWidget*Cast(Widget*w){return dynamic_cast<TextWidget*>(w);}string GetText(){return label;}};
|
|
struct ImageWidget:Widget{static ImageWidget*Cast(Widget*w){return dynamic_cast<ImageWidget*>(w);}void GetImageSize(int,int&w,int&h){w=64;h=64;}};
|
|
struct ERR_ObjectiveMapElement{vector pos;vector GetPos(){return pos;}};
|
|
struct Presentation{Widget*root=nullptr,*canvas=nullptr,*container=nullptr;ERR_ObjectiveMapElement*element=nullptr;Widget*ERR_ProbeRoot(){return root;}Widget*ERR_ProbeCanvas(){return canvas;}Widget*ERR_ProbeContainer(){return container;}ERR_ObjectiveMapElement*ERR_ProbeElement(){return element;}};
|
|
struct Map{Widget*canvas=nullptr;int x=240,y=180;Widget*GetMapWidget(){return canvas;}void WorldToScreen(int,int,int&ox,int&oy,bool pan){assert(pan);ox=x;oy=y;}};
|
|
struct Marker{int x=6123,y=7027;void GetWorldPos(int p[2]){p[0]=x;p[1]=y;}};
|
|
struct Workspace{float dpi=2;float DPIUnscale(float x){return x/dpi;}};
|
|
struct Game{Workspace workspace;Workspace*GetWorkspace(){return &workspace;}}game;Game*GetGame(){return &game;}
|
|
struct FrameSlot{static vector GetPos(Widget*w){return w->placed;}};
|
|
struct Math{static float AbsFloat(float x){return std::fabs(x);}};
|
|
struct ERR_DiscoveryMapRules{static int Coordinate(float x,int knowledge){return knowledge==1?int(std::floor(x/500))*500+250:int(x);}};
|
|
template<class...T>void PrintFormat(T...){}
|
|
struct Probe{Presentation*m_Presentation;Map*m_Map;Widget*m_ExpectedRoot;Marker*m_Marker;vector m_Point={6123,0,7027};int m_Stage=3;bool ready=true;bool ReadyToInspect(){return ready;}bool Check(bool b,string){return b;}
|
|
'''+inspect+r'''
|
|
};
|
|
int main(){Widget menu,container,nativeIcons,canvas,root,foreign;TextWidget text;ImageWidget image;ERR_ObjectiveMapElement element;Presentation presentation;Map map;Marker marker;
|
|
menu.container=&nativeIcons;nativeIcons.parent=&container;root.parent=&container;root.text=&text;root.image=ℑtext.label="Radio station";element.pos={6123,0,7027};
|
|
canvas.sx=100;canvas.sy=60;container.sx=24;container.sy=10;
|
|
presentation.root=&root;presentation.canvas=&canvas;presentation.container=&container;presentation.element=&element;map.canvas=&canvas;
|
|
// Canvas-relative projection240,180 -> screen340,240; supported parent has
|
|
// origin24,10, so slot(158,115) at DPI2, not slot(120,90).
|
|
root.placed={158,115,0};image.sx=322;image.sy=222;
|
|
Probe probe{&presentation,&map,&menu,&marker};assert(probe.Inspect(text.label,false));
|
|
root.placed={120,90,0};assert(!probe.Inspect(text.label,false));root.placed={158,115,0};
|
|
root.parent=&canvas;assert(!probe.Inspect(text.label,false));root.parent=&container;
|
|
nativeIcons.parent=&foreign;assert(!probe.Inspect(text.label,false));nativeIcons.parent=&container;
|
|
presentation.container=&foreign;assert(!probe.Inspect(text.label,false));presentation.container=&container;
|
|
presentation.canvas=&foreign;assert(!probe.Inspect(text.label,false));presentation.canvas=&canvas;
|
|
image.sx+=4;assert(!probe.Inspect(text.label,false));image.sx-=4;
|
|
element.pos[0]+=1;assert(!probe.Inspect(text.label,false));element.pos[0]-=1;
|
|
root.visible=false;assert(!probe.Inspect(text.label,false));root.visible=true;
|
|
probe.ready=false;assert(!probe.Inspect(text.label,false));probe.ready=true;
|
|
// Another valid parent origin and non-unit DPI cannot be compensated by
|
|
// dropping screen-origin offsets from the projection assertion.
|
|
container.sx=120;container.sy=110;game.workspace.dpi=1.25;root.placed={176,104,0};assert(probe.Inspect(text.label,false));
|
|
// Shared render roots choose their dedicated overlay instead of native icons.
|
|
menu.overlay=&container;nativeIcons.parent=&foreign;assert(probe.Inspect(text.label,false));
|
|
menu.overlay=&foreign;assert(!probe.Inspect(text.label,false));menu.overlay=&container;
|
|
marker.x=6250;marker.y=7250;element.pos={6250,0,7250};assert(probe.Inspect(text.label,true));
|
|
}
|
|
''')
|
|
print('PASS: actual full Inspect rejects canvas parenting, wrong active container/canvas, omitted origin offset, wrong rendered box and stale coordinates; differing origins and DPI accepted')
|
|
|
|
# Execute the added actual three phase blocks. Long-label capture must happen
|
|
# before reset/close, and the radio label must be restored for later reopening.
|
|
phases='\n'.join(body(p,'if (m_Stage == '+str(stage)+')') for stage in [5,22,23])
|
|
phases=phases.replace('m_Marker.','m_Marker->').replace('System.','System::')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
using string=std::string;
|
|
struct Marker{string label="Radio station";void SetCustomText(string text){label=text;}};
|
|
struct System{static inline int captures=0;static void MakeScreenshot(string path){assert(path=="$logs:err-objective-map-standard-long-warning");captures++;}};
|
|
int checkpoints=0;void Print(string text){assert(text=="ERR OBJECTIVE CAPTURE READY: standard-long-warning");checkpoints++;}
|
|
struct Probe{Marker*m_Marker;int m_Stage=5,closes=0,inspections=0;float m_Wait=0;bool Inspect(string text,bool rough){inspections++;return !rough&&m_Marker->label==text;}bool CloseStandard(){assert(m_Marker->label=="Radio station");closes++;return true;}
|
|
int Advance(float now){if(now<m_Wait)return 0;
|
|
'''+phases+r'''
|
|
return 0;}
|
|
};
|
|
int main(){Marker marker;Probe p{&marker};assert(p.Advance(100)==0&&p.m_Stage==22&&p.m_Wait==850&&p.closes==0);assert(marker.label=="Reported mine area (500 m); may still be active");
|
|
p.Advance(849);assert(System::captures==0&&p.inspections==1);
|
|
p.Advance(850);assert(p.m_Stage==23&&p.m_Wait==2350&&p.inspections==2&&System::captures==1&&checkpoints==1&&p.closes==0);
|
|
p.Advance(2349);assert(marker.label=="Reported mine area (500 m); may still be active"&&p.closes==0);
|
|
p.Advance(2350);assert(p.m_Stage==6&&p.closes==1&&marker.label=="Radio station"&&System::captures==1);
|
|
}
|
|
''')
|
|
assert 'GetOrigin() + "1037.25 0 1063.75"' in s
|
|
print('PASS: actual long-warning phases retain visible label through1500ms checkpoint then restore radio before close; synthetic marker separated from unmoved player')
|
|
|
|
# Actual Center method offsets the pan target, not the disclosed data or input.
|
|
center=body(p,'protected void Center(').replace('Math.Min','std::min').replace('GetMapWidget().','GetMapWidget()->').replace('Math.Min','std::min').replace('std::min(100, width * 0.25)', 'std::min(100.f, width * 0.25f)').replace('protected ','').replace('m_Marker.','m_Marker->').replace('m_Map.','m_Map->')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <initializer_list>
|
|
#include <algorithm>
|
|
struct Canvas{float width=500;void GetScreenSize(float&w,float&h){w=width;h=500;}};
|
|
struct Marker{int x=6250,y=7250;void GetWorldPos(int p[2]){p[0]=x;p[1]=y;}};
|
|
struct Map{Canvas canvas;Canvas*GetMapWidget(){return &canvas;}float zoom=0,x=0,y=0,duration=0;int calls=0;void ZoomPanSmooth(float z,float px,float py,float t){zoom=z;x=px;y=py;duration=t;calls++;}};
|
|
struct Probe{Marker*m_Marker;Map*m_Map;
|
|
'''+center+r'''
|
|
};
|
|
int main(){Marker marker;Map map;Probe p{&marker,&map};for(float zoom:{0.3f,0.6f}){p.Center(zoom);assert(map.zoom==zoom&&map.y==7250&&std::abs(map.duration-0.1f)<0.001f);assert(std::abs((marker.x-map.x)*zoom+100.0f)<0.01f);assert(marker.x==6250&&marker.y==7250);}assert(map.calls==2);map.canvas.width=124;p.Center(.3f);assert(std::abs((map.x-marker.x)*.3f-31)<.01f);}
|
|
''')
|
|
print('PASS: actual Center keeps marker offset within large and small native viewports at both fixture zooms without altering disclosed coordinates')
|