Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
101 lines
9.6 KiB
Python
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.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')
|