Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
503 lines
26 KiB
Diff
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=ℑ
|
|
+ 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.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')
|