Files
EscapistsReintroduced/docs/evidence/objective-map-2026-10-02/producer-fix.patch
T
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

503 lines
26 KiB
Diff

--- 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<ERR_DiscoveryMarker*> 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<ERR_DiscoveryMarker*> 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;Array<vector>m_ERRMineAreas;Array<ERR_DiscoveryMarker*>m_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 <cassert>
+#include <string>
+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=&image;
+ 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 <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')