Files
EscapistsReintroduced/tests/test_objective_map_presentation.py
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

101 lines
9.6 KiB
Python

"""Actual objective overlay lifecycle with explicit native widget/map doubles.
No claim of native rendering, clipping, layout, remote or platform validation.
"""
import os,re
from pathlib import Path
from policy_harness import ROOT,body,run
root=Path(os.environ.get('ERR_OBJECTIVE_MAP_ROOT',ROOT))
p=root/'addons/core/Scripts/Game/ERR/00_Core/ERR_ObjectiveMapPresentation.c'
def cpp(s):
s=s.replace('protected ','').replace('null','nullptr')
s=s.replace('SCR_MapEntity.','SCR_MapEntity::').replace('ERR_ObjectiveMapElement.Cast','ERR_ObjectiveMapElement::Cast').replace('FrameSlot.','FrameSlot::')
for t in ['SCR_MapEntity','MapConfiguration','Widget']:
s=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',s)
for n in ['mapEntity','canvas','container','mapRoot','authored','icons','m_Root','m_Element']:
s=re.sub(r'\b'+n+r'\.',n+'->',s)
for n in ['GetGame','GetWorkspace','GetOnMapOpenComplete','GetOnMapClose','GetOnMapPan','GetOnMapZoom']:
s=s.replace(n+'().',n+'()->')
s=s.replace('FindHandler(ERR_ObjectiveMapElement)','FindHandler()')
for n in ['Opened','Closed','Panned','Zoomed']:
s=s.replace('Insert('+n+')','Insert(this)').replace('Remove('+n+')','Remove(this)')
return s
methods='\n'.join(cpp(body(p,s)) for s in ['void SetData(', 'void Start(', 'protected void Opened(', 'protected void Panned(', 'protected void Zoomed(', 'protected void Closed(', 'protected void ClearWidget(', 'static Widget Container(', 'protected bool OwnsSurface(', 'void Refresh(', 'void Stop('])
run(r'''
#include <cassert>
#include <string>
#include <set>
#include <vector>
using string=std::string;
struct vector{float v[3]={0,0,0};float&operator[](int i){return v[i];}};
struct MapConfiguration{};
struct ERR_ObjectiveMapElement{vector pos;string text,icon;int sets=0;static ERR_ObjectiveMapElement*Cast(ERR_ObjectiveMapElement*p){return p;}void SetObjective(vector p,string t,string i){pos=p;text=t;icon=i;sets++;}};
struct Widget{bool visible=false,removed=false,handler=true;int removals=0;float x=0,y=0,sx=0,sy=0;bool isCanvas=false;Widget*parent=nullptr,*authored=nullptr,*icons=nullptr;std::vector<Widget*>children;Widget*GetParent(){return parent;}Widget*FindAnyWidget(string n){return n=="ERR_ObjectiveOverlay"?authored:icons;}void GetScreenPos(float&a,float&b){a=sx;b=sy;}void ClearChildren(){for(auto*w:children)w->RemoveFromHierarchy();children.clear();}void GetScreenSize(float&w,float&h){w=4000;h=4000;}ERR_ObjectiveMapElement element;ERR_ObjectiveMapElement*FindHandler(){return handler?&element:nullptr;}void RemoveFromHierarchy(){removed=true;removals++;}void SetVisible(bool v){visible=v;}};
struct Event{std::set<void*>subscribers;int inserts=0;void Insert(void*p){subscribers.insert(p);inserts++;}void Remove(void*p){subscribers.erase(p);}};
struct SCR_MapEntity{
static inline SCR_MapEntity*current=nullptr;static inline Event opened,closed,panned,zoomed;
bool open=true;MapConfiguration config;Widget canvas,mapRoot,icons,frame;Widget*activeCanvas=&canvas;
SCR_MapEntity(){canvas.isCanvas=true;mapRoot.icons=&icons;icons.parent=&frame;}Widget*GetMapMenuRoot(){return &mapRoot;}
vector minimum{{0,0,0}},maximum{{1000,0,1000}};int transforms=0;float zoom=2,pan=0;
static SCR_MapEntity*GetMapInstance(){return current;}static Event*GetOnMapOpenComplete(){return &opened;}static Event*GetOnMapClose(){return &closed;}static Event*GetOnMapPan(){return &panned;}static Event*GetOnMapZoom(){return &zoomed;}
bool IsOpen(){return open;}MapConfiguration*GetMapConfig(){return &config;}Widget*GetMapWidget(){return activeCanvas;}
void GetMapVisibleFrame(vector&a,vector&b){a=minimum;b=maximum;}
void WorldToScreen(float x,float y,int&sx,int&sy,bool){transforms++;sx=x*zoom+pan;sy=y*zoom+pan;}
};
struct Workspace{bool fail=false,missingHandler=false;int creates=0;std::vector<Widget*>widgets;Widget*CreateWidgets(const char*,Widget*parent){creates++;if(fail||parent->isCanvas)return nullptr;auto*w=new Widget();w->parent=parent;parent->children.push_back(w);w->handler=!missingHandler;widgets.push_back(w);return w;}float DPIUnscale(float x){return x/2;}~Workspace(){for(auto*w:widgets)delete w;}};
struct Game{bool controller=true,workspaceReady=true;Workspace workspace;bool GetPlayerController(){return controller;}Workspace*GetWorkspace(){return workspaceReady?&workspace:nullptr;}}game;Game*GetGame(){return &game;}
namespace FrameSlot{void SetPos(Widget*w,float x,float y){w->x=x;w->y=y;}}
struct Presentation{
static constexpr auto LAYOUT="authored-layout";bool m_Started=false;vector m_Position;string m_Text,m_Icon;SCR_MapEntity*m_Map=nullptr;MapConfiguration*m_Config=nullptr;Widget*m_Canvas=nullptr,*m_Container=nullptr,*m_Root=nullptr;ERR_ObjectiveMapElement*m_Element=nullptr;
'''+methods+r'''
};
int main(){
SCR_MapEntity map;SCR_MapEntity::current=&map;map.canvas.sx=120;map.canvas.sy=60;map.frame.sx=20;map.frame.sy=10;
Presentation a,b;a.SetData(vector{{250,0,750}},"Unknown location (500 m area)","mark-question");
game.controller=false;a.Start();assert(!a.m_Root&&SCR_MapEntity::opened.subscribers.count(&a));
game.controller=true;a.Refresh();Widget*first=a.m_Root;assert(first&&first->visible&&first->x==300&&first->y==775&&first->parent==&map.frame);
assert(first->element.pos[0]==250&&first->element.text=="Unknown location (500 m area)");
int created=game.workspace.creates;a.Start();a.Opened(&map.config);assert(game.workspace.creates==created&&SCR_MapEntity::opened.inserts==1);
// Native user-annotation privacy has no state/input in this objective registry.
// Ordinary annotations remain outside these methods and are not mutated.
bool userAnnotationBlocked=true;
a.SetData(vector{{612,0,734}},"Radio station","radio");assert(a.m_Root==first&&first->element.icon=="radio"&&userAnnotationBlocked);
map.zoom=4;map.pan=10;a.Zoomed(4);assert(first->x==1279&&first->y==1498);
map.pan=-3000;a.Panned(0,0,false);assert(!first->visible);
map.pan=10;a.Panned(0,0,false);assert(first->visible);
MapConfiguration unrelated;a.Closed(&unrelated);assert(a.m_Root==first);
a.Closed(&map.config);assert(first->removed&&!a.m_Root&&SCR_MapEntity::opened.subscribers.count(&a));
a.Opened(&map.config);assert(a.m_Root&&a.m_Root!=first&&a.m_Root->element.text=="Radio station");
// Same native config but another canvas; only our old widget is removed.
Widget otherCanvas;otherCanvas.isCanvas=true;Widget*second=a.m_Root;map.activeCanvas=&otherCanvas;a.Refresh();assert(second->removed&&a.m_Canvas==&otherCanvas&&!otherCanvas.removed);
// Native task refresh deletes only its own icon children, not our sibling.
Widget nativeTask;map.icons.children.push_back(&nativeTask);map.icons.ClearChildren();assert(nativeTask.removed&&!a.m_Root->removed&&a.m_Root->parent==&map.frame);
// Reusing canvas/config but changing the supported parent must rebuild only ours.
Widget replacementFrame;Widget*oldParentWidget=a.m_Root;map.icons.parent=&replacementFrame;a.Refresh();assert(oldParentWidget->removed&&a.m_Container==&replacementFrame&&a.m_Root->parent==&replacementFrame);
// Dedicated authored shared-map frame has priority over native task layout.
Widget sharedFrame;sharedFrame.sx=70;sharedFrame.sy=90;map.mapRoot.authored=&sharedFrame;oldParentWidget=a.m_Root;a.Refresh();assert(oldParentWidget->removed&&a.m_Container==&sharedFrame&&a.m_Root->parent==&sharedFrame);
// Parent absence or a malformed canvas parent cannot reach CreateWidgets.
map.mapRoot.authored=nullptr;map.icons.parent=map.activeCanvas;int beforeInvalid=game.workspace.creates;a.Refresh();assert(!a.m_Root&&game.workspace.creates==beforeInvalid);
map.mapRoot.icons=nullptr;a.Refresh();assert(!a.m_Root&&game.workspace.creates==beforeInvalid);
map.mapRoot.authored=&sharedFrame;a.Refresh();assert(a.m_Root&&a.m_Container==&sharedFrame);
b.Start();Widget*bRoot=b.m_Root;assert(bRoot&&bRoot!=a.m_Root);a.Stop();assert(!a.m_Root&&!bRoot->removed&&!SCR_MapEntity::opened.subscribers.count(&a)&&SCR_MapEntity::opened.subscribers.count(&b));
a.Opened(&map.config);assert(!a.m_Root);a.Stop();assert(!bRoot->removed);
// Missing creation/configuration retries without registering duplicate listeners.
b.Closed(&map.config);game.workspace.fail=true;b.Refresh();assert(!b.m_Root);game.workspace.fail=false;game.workspace.missingHandler=true;b.Refresh();assert(!b.m_Root);game.workspace.missingHandler=false;b.Refresh();assert(b.m_Root);
map.open=false;b.Refresh();assert(!b.m_Root);map.open=true;b.Opened(&map.config);assert(b.m_Root);
SCR_MapEntity replacement;SCR_MapEntity::current=&replacement;Widget*old=b.m_Root;b.Refresh();assert(old->removed&&b.m_Map==&replacement);
b.Stop();assert(SCR_MapEntity::opened.subscribers.empty()&&SCR_MapEntity::closed.subscribers.empty()&&SCR_MapEntity::panned.subscribers.empty()&&SCR_MapEntity::zoomed.subscribers.empty());
}
''')
# Production callers must never insert authored objective data into the annotation lists.
discovery=(root/'addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryMap.c').read_text()
mine=(root/'addons/core/Scripts/Game/ERR/20_Intel/ERR_MineIntel.c').read_text()
assert 'manager.ERR_InsertDiscoveryMarker(m_ERRMarker);' in discovery
assert 'manager.ERR_InsertDiscoveryMarker(marker);' in mine
assert 'manager.InsertStaticMarker(m_ERRMarker' not in discovery
assert 'manager.InsertStaticMarker(marker' not in mine
assert 'override void SetBlocked' not in discovery
shared=root/'addons/core/UI/layouts/ERRMap/SharedMapRenderTarget.layout'
assert 'Name "ERR_ObjectiveOverlay"' in body(shared,'RTTextureWidgetClass')
assert 'Name "ERR_ObjectiveOverlay"' not in body(shared,'MapWidgetClass')
print('PASS: actual objective overlay lifecycle preserves separate ownership, late readiness/reopen, data refresh, pan/zoom/culling, supported sibling/shared-frame parents, coordinate offsets, native task-clear isolation and terminal cleanup; native rendering remains unverified')