--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryMap.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryMap.c @@ -15,6 +15,7 @@ class ERR_DiscoveryMarker : SCR_MapMarkerBase { protected string m_Icon = "supplies"; + protected ref ERR_ObjectiveMapPresentation m_ERRPresentation; static string Icon(ERR_LocationKind kind, string id) { if (kind == ERR_LocationKind.EXTRACTION) return "helicopters"; @@ -29,6 +30,7 @@ m_Icon = "mark-question"; if (knowledge >= 2) m_Icon = Icon(kind, id); ApplyIcon(); + RefreshPresentation(); } protected void ApplyIcon() { @@ -46,10 +48,26 @@ } widget.SetColor(Color.FromSRGBA(35, 45, 50, 255)); } + // Production authored objectives use a separate map UI element. They never + // enter the player-annotation registry or change its content restrictions. + void StartPresentation() + { + if (!m_ERRPresentation) m_ERRPresentation = new ERR_ObjectiveMapPresentation(); + RefreshPresentation(); + m_ERRPresentation.Start(); + } + void RefreshPresentation() + { + if (!m_ERRPresentation) return; + int point[2]; + GetWorldPos(point); + m_ERRPresentation.SetData(Vector(point[0], 0, point[1]), GetCustomText(), m_Icon); + } void ERR_DiscoveryMarker() { SetCanBeRemovedByOwner(false); } void SetDiscoveryPosition(vector position, int knowledge) { SetWorldPos(ERR_DiscoveryMapRules.Coordinate(position[0], knowledge), ERR_DiscoveryMapRules.Coordinate(position[2], knowledge)); + RefreshPresentation(); } override void OnCreateMarker(bool skipProfanityFilter = false) { @@ -61,6 +79,8 @@ } override void OnDelete() { + if (m_ERRPresentation) m_ERRPresentation.Stop(); + m_ERRPresentation = null; SCR_MapEntity.GetOnMapClose().Remove(OnMapClosed); SCR_MapEntity.GetOnLayerChanged().Remove(OnMapLayerChanged); super.OnDelete(); @@ -70,6 +90,7 @@ override void SetCustomText(string text) { super.SetCustomText(text); + RefreshPresentation(); // Native SetCustomText updates data only; an already-open map needs its // existing widget updated too. Closed maps use the saved text on creation. SCR_MapMarkerWidgetComponent widget = GetMarkerComponent(); @@ -81,6 +102,10 @@ // must also leave the disabled list or map movement can resurrect it later. modded class SCR_MapMarkerManagerComponent { + void ERR_InsertDiscoveryMarker(ERR_DiscoveryMarker marker) + { + if (marker) marker.StartPresentation(); + } void ERR_RemoveDiscoveryMarker(ERR_DiscoveryMarker marker) { if (!marker || marker.GetMarkerID() != -1) return; @@ -115,6 +140,7 @@ m_ERRMarker = null; m_ERRShown = 0; m_ERRShownLabel = ""; return; } + if (m_ERRMarker) m_ERRMarker.RefreshPresentation(); if (Knowledge() == m_ERRShown && Label() == m_ERRShownLabel) return; SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance(); if (!manager || !manager.GetMarkerConfig()) return; @@ -136,15 +162,13 @@ m_ERRMarker.SetMarkerConfigID(0); m_ERRMarker.SetLocationIcon(Kind(), Id(), Knowledge()); m_ERRMarker.SetCustomText(label); - manager.InsertStaticMarker(m_ERRMarker, true); + manager.ERR_InsertDiscoveryMarker(m_ERRMarker); } m_ERRMarker.SetDiscoveryPosition(Position(), Knowledge()); m_ERRMarker.SetLocationIcon(Kind(), Id(), Knowledge()); m_ERRMarker.SetCustomText(label); - // Reactivate a previously culled rough marker if the exact point moves - // into the open map's view. Native update performs normal visibility checks. - SCR_MapEntity mapEntity = SCR_MapEntity.GetMapInstance(); - if (mapEntity && mapEntity.IsOpen()) manager.SetStaticMarkerDisabled(m_ERRMarker, false); + // The objective presentation performs its own normal viewport culling. + m_ERRMarker.RefreshPresentation(); m_ERRShown = Knowledge(); m_ERRShownLabel = Label(); } --- /dev/null +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_ObjectiveMapPresentation.c @@ -0,0 +1,134 @@ +// Authored objective UI, separate from user-created map annotations. The data +// owner supplies only its already-discovered, quantized map position. +class ERR_ObjectiveMapElement : SCR_MapUIElement +{ + protected vector m_ERRPosition; + protected ImageWidget m_ERRImage; + protected TextWidget m_ERRText; + protected bool m_ERRTextReady, m_ERRImageReady; + protected string m_ERRLastText, m_ERRLastIcon; + override void HandlerAttached(Widget w) + { + super.HandlerAttached(w); + m_ERRImage = ImageWidget.Cast(w.FindAnyWidget("Image")); + m_ERRText = TextWidget.Cast(w.FindAnyWidget("ObjectiveText")); + } + override vector GetPos() { return m_ERRPosition; } + void SetObjective(vector position, string text, string icon) + { + m_ERRPosition = position; + if (m_ERRText && (!m_ERRTextReady || m_ERRLastText != text)) + { + m_ERRText.SetText(text); m_ERRLastText = text; m_ERRTextReady = true; + } + if (!m_ERRImage || (m_ERRImageReady && m_ERRLastIcon == icon)) return; + ResourceName imageSet = "{3262679C50EF4F01}UI/Textures/Icons/icons_wrapperUI.imageset"; + if (icon == "mark-question") imageSet = "{E23427CAC80DA8B7}UI/Textures/Icons/icons_mapMarkersUI.imageset"; + m_ERRImage.LoadImageFromSet(0, imageSet, icon); + m_ERRLastIcon = icon; m_ERRImageReady = true; + } + // The layout ignores pointer input; objectives cannot select, drag or edit. + override void SelectIcon(bool invoke = true) {} +} + +class ERR_ObjectiveMapPresentation +{ + protected const ResourceName LAYOUT = "{B7DB6D50FA954684}UI/layouts/ERRMap/ObjectiveMapElement.layout"; + protected bool m_Started; + protected vector m_Position; + protected string m_Text, m_Icon; + protected SCR_MapEntity m_Map; + protected MapConfiguration m_Config; + protected Widget m_Canvas, m_Container, m_Root; + protected ERR_ObjectiveMapElement m_Element; + void SetData(vector position, string text, string icon) + { + m_Position = position; m_Text = text; m_Icon = icon; + if (m_Started) Refresh(); + } + void Start() + { + if (!m_Started) + { + m_Started = true; + SCR_MapEntity.GetOnMapOpenComplete().Insert(Opened); + SCR_MapEntity.GetOnMapClose().Insert(Closed); + SCR_MapEntity.GetOnMapPan().Insert(Panned); + SCR_MapEntity.GetOnMapZoom().Insert(Zoomed); + } + Refresh(); + } + protected void Opened(MapConfiguration config) { Refresh(); } + protected void Panned(float x, float y, bool adjusted) { Refresh(); } + protected void Zoomed(float zoom) { Refresh(); } + protected void Closed(MapConfiguration config) + { + if (config == m_Config) ClearWidget(); + } + protected void ClearWidget() + { + if (m_Root) m_Root.RemoveFromHierarchy(); + m_Root = null; m_Element = null; m_Map = null; m_Config = null; m_Canvas = null; m_Container = null; + } + static Widget Container(Widget mapRoot) + { + if (!mapRoot) return null; + Widget authored = mapRoot.FindAnyWidget("ERR_ObjectiveOverlay"); + if (authored) return authored; + Widget icons = mapRoot.FindAnyWidget("UIIconsContainer"); + // Vanilla deletes every child of its icons container on faction refresh. + // Use its supported parent instead, so our widget is an independent sibling. + if (icons) return icons.GetParent(); + return null; + } + protected bool OwnsSurface(SCR_MapEntity mapEntity, MapConfiguration config, Widget canvas, Widget container) + { + if (!m_Root || !m_Element) return false; + return m_Map == mapEntity && m_Config == config && m_Canvas == canvas && m_Container == container && m_Root.GetParent() == container; + } + void Refresh() + { + if (!m_Started || !GetGame()) return; + SCR_MapEntity mapEntity = SCR_MapEntity.GetMapInstance(); + if (!GetGame().GetPlayerController() || !mapEntity || !mapEntity.IsOpen()) { ClearWidget(); return; } + MapConfiguration config = mapEntity.GetMapConfig(); + Widget canvas = mapEntity.GetMapWidget(); + Widget container = Container(mapEntity.GetMapMenuRoot()); + if (!config || !canvas || !container || container == canvas || !GetGame().GetWorkspace()) { ClearWidget(); return; } + // The same native config object can be reused for another root/canvas. + if (!OwnsSurface(mapEntity, config, canvas, container)) + { + ClearWidget(); + m_Map = mapEntity; m_Config = config; m_Canvas = canvas; m_Container = container; + m_Root = GetGame().GetWorkspace().CreateWidgets(LAYOUT, container); + if (!m_Root) { ClearWidget(); return; } + m_Element = ERR_ObjectiveMapElement.Cast(m_Root.FindHandler(ERR_ObjectiveMapElement)); + if (!m_Element) { ClearWidget(); return; } + } + m_Element.SetObjective(m_Position, m_Text, m_Icon); + int x, y; + mapEntity.WorldToScreen(m_Position[0], m_Position[2], x, y, true); + // Use current pixel coordinates: the native cached world viewport is updated + // after pan/zoom callbacks and may still describe the preceding frame. + float width, height; + canvas.GetScreenSize(width, height); + bool visible = width > 0 && height > 0; + if (x < 0 || y < 0 || x > width || y > height) visible = false; + m_Root.SetVisible(visible); + if (!visible) return; + float canvasX, canvasY, containerX, containerY; + canvas.GetScreenPos(canvasX, canvasY); + container.GetScreenPos(containerX, containerY); + FrameSlot.SetPos(m_Root, GetGame().GetWorkspace().DPIUnscale(canvasX + x - containerX), GetGame().GetWorkspace().DPIUnscale(canvasY + y - containerY)); + } + void Stop() + { + m_Started = false; + SCR_MapEntity.GetOnMapOpenComplete().Remove(Opened); + SCR_MapEntity.GetOnMapClose().Remove(Closed); + SCR_MapEntity.GetOnMapPan().Remove(Panned); + SCR_MapEntity.GetOnMapZoom().Remove(Zoomed); + ClearWidget(); + } + void ~ERR_ObjectiveMapPresentation() { Stop(); } +} --- a/addons/core/Scripts/Game/ERR/20_Intel/ERR_MineIntel.c +++ b/addons/core/Scripts/Game/ERR/20_Intel/ERR_MineIntel.c @@ -144,7 +144,7 @@ marker.SetDiscoveryPosition(m_ERRMineAreas[i], 1); marker.SetLocationIcon(ERR_LocationKind.OUTPOST, "", 1); marker.SetCustomText("Reported mine area (500 m); may still be active"); - manager.InsertStaticMarker(marker, true); + manager.ERR_InsertDiscoveryMarker(marker); m_ERRMineMarkers.Insert(marker); } } --- /dev/null +++ b/addons/core/UI/layouts/ERRMap/ObjectiveMapElement.layout @@ -0,0 +1,54 @@ +FrameWidgetClass { + Name "ObjectiveMapElement" + Clipping False + Slot FrameWidgetSlot { + SizeX 360 + OffsetRight -360 + SizeY 64 + OffsetBottom -64 + Alignment 0.5 0.28125 + } + "Z Order" 70 + "Ignore Cursor" 1 + components { + ERR_ObjectiveMapElement { + m_bUseBackgroundGradient 0 + } + } + { + ImageWidgetClass { + Name "Image" + Slot FrameWidgetSlot { + Anchor 0.5 0 0.5 0 + SizeX 36 + OffsetRight -36 + SizeY 36 + OffsetBottom -36 + Alignment 0.5 0 + } + "Ignore Cursor" 1 + Color 0.016 0.026 0.032 1 + Texture "{E23427CAC80DA8B7}UI/Textures/Icons/icons_mapMarkersUI.imageset" + Image "mark-question" + } + TextWidgetClass { + Name "ObjectiveText" + Clipping False + Slot FrameWidgetSlot { + OffsetTop 38 + SizeX 360 + OffsetRight -360 + SizeY 26 + OffsetBottom -26 + } + "Ignore Cursor" 1 + Color 0.016 0.026 0.032 1 + Text "" + "Font Size" 14 + "Horizontal Alignment" Center + FontProperties FontProperties { + Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" + } + } + } +} --- /dev/null +++ b/addons/core/UI/layouts/ERRMap/ObjectiveMapElement.layout.meta @@ -0,0 +1,17 @@ +MetaFileClass { + Name "{B7DB6D50FA954684}UI/layouts/ERRMap/ObjectiveMapElement.layout" + Configurations { + LayoutResourceClass PC { + } + LayoutResourceClass XBOX_ONE : PC { + } + LayoutResourceClass XBOX_SERIES : PC { + } + LayoutResourceClass PS4 : PC { + } + LayoutResourceClass PS5 : PC { + } + LayoutResourceClass HEADLESS : PC { + } + } +} \ No newline at end of file --- a/addons/core/UI/layouts/ERRMap/SharedMapRenderTarget.layout +++ b/addons/core/UI/layouts/ERRMap/SharedMapRenderTarget.layout @@ -33,6 +33,18 @@ Clear 1 ClearColor 0.78 0.74 0.62 1 } + FrameWidgetClass { + Name "ERR_ObjectiveOverlay" + Slot FrameWidgetSlot { + Anchor 0 0 1 1 + SizeX -1024 + OffsetRight 0 + SizeY -1024 + OffsetBottom 0 + } + Clipping True + "Ignore Cursor" 1 + } } } } --- a/tests/test_mine_intel.py +++ b/tests/test_mine_intel.py @@ -53,7 +53,7 @@ struct Invoker {int removed=0;void Remove(int){removed++;}}; namespace ERR_LocationKind {int OUTPOST=3;}namespace SCR_EMapMarkerType {int PLACED_CUSTOM=1;} struct ERR_DiscoveryMarker {vector position;std::string label;int deleted=0;void SetType(int){}void SetMarkerConfigID(int){}void SetDiscoveryPosition(vector p,int){position=p;}void SetLocationIcon(int,std::string,int){}void SetCustomText(std::string s){label=s;}void OnDelete(){deleted++;}}; -struct SCR_MapMarkerManagerComponent {static inline SCR_MapMarkerManagerComponent*current=nullptr;bool config=true;Array inserted,removed;static SCR_MapMarkerManagerComponent*GetInstance(){return current;}bool GetMarkerConfig(){return config;}void InsertStaticMarker(ERR_DiscoveryMarker*m,bool){inserted.Insert(m);}void ERR_RemoveDiscoveryMarker(ERR_DiscoveryMarker*m){removed.Insert(m);m->OnDelete();}}; +struct SCR_MapMarkerManagerComponent {static inline SCR_MapMarkerManagerComponent*current=nullptr;bool config=true;Array inserted,removed;static SCR_MapMarkerManagerComponent*GetInstance(){return current;}bool GetMarkerConfig(){return config;}void ERR_InsertDiscoveryMarker(ERR_DiscoveryMarker*m){inserted.Insert(m);}void ERR_RemoveDiscoveryMarker(ERR_DiscoveryMarker*m){removed.Insert(m);m->OnDelete();}}; struct ERR_MineIntel; struct ERR_GameMode {static inline ERR_GameMode*current=nullptr;bool active=true;int enemy=1,player=2;DiscoveryService discovery;PlayersService players;Invoker stop; ERR_MineIntel*m_ERRMineIntel=nullptr;Arraym_ERRMineAreas;Arraym_ERRMineMarkers;bool m_ERRMineIntelStopped=false;SCR_MapMarkerManagerComponent*m_ERRMineMarkerOwner=nullptr; --- /dev/null +++ b/tests/test_objective_map_element.py @@ -0,0 +1,38 @@ +"""Actual authored map element updates avoid repeated text/texture loads while panning.""" +from pathlib import Path +import os +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' +# Native Enforce requires the inherited override argument name, not only its type. +assert 'override void HandlerAttached(Widget w)' in p.read_text() +assert 'super.HandlerAttached(w);' in body(p,'override void HandlerAttached(') +s=body(p,'void SetObjective(').replace('ResourceName','string') +for n in ['m_ERRText','m_ERRImage']:s=s.replace(n+'.',n+'->') +run(r''' +#include +#include +using string=std::string; +struct vector{int x=0,z=0;}; +struct TextWidget{string text;int sets=0;void SetText(string t){text=t;sets++;}}; +struct ImageWidget{string source,icon;int loads=0;void LoadImageFromSet(int,string s,string i){source=s;icon=i;loads++;}}; +struct Element{vector m_ERRPosition;TextWidget*m_ERRText=nullptr;ImageWidget*m_ERRImage=nullptr;bool m_ERRTextReady=false,m_ERRImageReady=false;string m_ERRLastText,m_ERRLastIcon; +'''+s+r''' +}; +int main(){ + TextWidget text;ImageWidget image;Element e;e.m_ERRText=&text;e.m_ERRImage=ℑ + e.SetObjective({250,750},"Unknown location (500 m area)","mark-question");assert(text.sets==1&&image.loads==1&&image.source.find("icons_mapMarkersUI")!=string::npos); + for(int i=0;i<500;i++)e.SetObjective({i,750},"Unknown location (500 m area)","mark-question"); + assert(text.sets==1&&image.loads==1&&e.m_ERRPosition.x==499); + e.SetObjective({612,734},"Radio station","radio");assert(text.sets==2&&image.loads==2&&image.icon=="radio"&&image.source.find("icons_wrapperUI")!=string::npos); + e.SetObjective({612,734},"Changed label","radio");assert(text.sets==3&&image.loads==2); + e.SetObjective({612,734},"Changed label","supplies");assert(text.sets==3&&image.loads==3); + // Each new widget initializes even for the same data; caches are instance-owned. + TextWidget nextText;ImageWidget nextImage;Element next;next.m_ERRText=&nextText;next.m_ERRImage=&nextImage; + next.SetObjective({612,734},"Changed label","supplies");assert(nextText.sets==1&&nextImage.loads==1&&text.sets==3); + Element delayed;delayed.SetObjective({1,2},"", "mark-question");assert(!delayed.m_ERRTextReady&&!delayed.m_ERRImageReady&&delayed.m_ERRPosition.z==2); + delayed.m_ERRText=&nextText;delayed.SetObjective({3,4},"", "mark-question");assert(nextText.text.empty()&&delayed.m_ERRTextReady&&!delayed.m_ERRImageReady); + int sets=nextText.sets;delayed.m_ERRImage=&nextImage;delayed.SetObjective({5,6},"", "mark-question");assert(nextText.sets==sets&&delayed.m_ERRImageReady&&nextImage.loads==2); +} +''') +print('PASS: actual map element initializes each widget, retries missing widgets, updates position and changes only stale text/icon data; no native image-load claim') --- /dev/null +++ b/tests/test_objective_map_presentation.py @@ -0,0 +1,100 @@ +"""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 +#include +#include +#include +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::vectorchildren;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::setsubscribers;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::vectorwidgets;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')