--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c @@ -54,7 +54,7 @@ string enabled; if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return; string nativeEndCase; - array independentFixtures = {"optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "native-end-active", "native-end-preparing", "generated-sites", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"}; + array independentFixtures = {"optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "objective-map", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "native-end-active", "native-end-preparing", "generated-sites", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"}; if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && independentFixtures.Contains(nativeEndCase)) return; ERR_NativeObjectiveProbe.BeginRound(); ERR_NativeProgressionIdentityProbe.BeginRound(); --- /dev/null +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeObjectiveMapProbe.c @@ -0,0 +1,361 @@ +#ifdef WORKBENCH +// Explicit UI mechanism fixture. It creates no intel, tasks, actors or inventory +// and never changes annotation restrictions. Shared-root evidence is NOT a +// physical held-texture, manual-input, restricted-platform or multiplayer test. +modded class ERR_ObjectiveMapPresentation +{ + Widget ERR_ProbeRoot() { return m_Root; } + ERR_ObjectiveMapElement ERR_ProbeElement() { return m_Element; } + Widget ERR_ProbeCanvas() { return m_Canvas; } + Widget ERR_ProbeContainer() { return m_Container; } + bool ERR_ProbeStopped() { return !m_Started && !m_Root && !m_Element; } +} +modded class ERR_DiscoveryMarker +{ + ERR_ObjectiveMapPresentation ERR_ProbePresentation() { return m_ERRPresentation; } +} +modded class SCR_MapMarkerManagerComponent +{ + int ERR_ProbeAnnotationCount() { return m_aStaticMarkers.Count() + m_aDisabledMarkers.Count(); } +} +class ERR_NativeObjectiveMapProbe +{ + protected ERR_GameMode m_Mode; + protected SCR_MapEntity m_Map; + protected SCR_MapMarkerManagerComponent m_Annotations; + protected ref ERR_DiscoveryMarker m_Marker; + protected ref ERR_ObjectiveMapPresentation m_Presentation; + protected SCR_MapMenuUI m_Menu; + protected Widget m_SharedRoot, m_ReusedRoot, m_ExpectedRoot, m_PreviousWidget; + protected ref MapConfiguration m_Config; + protected MapConfiguration m_Ready; + protected vector m_Point; + protected int m_Stage, m_AnnotationCount; + protected int m_Deadline; + protected float m_Wait; + protected bool m_Stopped, m_Done, m_CloseRequested, m_CloseObserved; + static bool Enabled() + { + string enabled, outcome; + return System.GetCLIParam("errBaseSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "objective-map"; + } + void ERR_NativeObjectiveMapProbe(ERR_GameMode mode) + { + m_Mode = mode; + m_Deadline = System.GetTickCount() + 90000; + SCR_MapEntity.GetOnMapOpenComplete().Insert(Ready); + SCR_MapEntity.GetOnMapClose().Insert(Closed); + } + protected void Ready(MapConfiguration config) { m_Ready = config; } + protected void Closed(MapConfiguration config) + { + if (m_CloseRequested && config == m_Config) m_CloseObserved = true; + } + protected bool Check(bool condition, string detail) + { + if (condition) { Print("ERR SMOKE PASS: objective overlay " + detail); return true; } + Print("ERR SMOKE FAIL: objective overlay " + detail, LogLevel.ERROR); + Stop(); return false; + } + protected bool OwnsMap() + { + if (!m_Map || SCR_MapEntity.GetMapInstance() != m_Map || !m_Map.IsOpen()) return false; + return m_Config && m_Map.GetMapConfig() == m_Config && m_Config.RootWidgetRef == m_ExpectedRoot && m_Map.GetMapMenuRoot() == m_ExpectedRoot; + } + protected bool OpenStandard() + { + if (!Check(m_Map && !m_Map.IsOpen() && !m_Menu, "standard map starts from closed fixture-owned state")) return false; + m_Ready = null; + m_Menu = SCR_MapMenuUI.Cast(GetGame().GetMenuManager().OpenMenu(ChimeraMenuPreset.MapMenu)); + if (!Check(m_Menu && m_Map.IsOpen(), "ordinary native MapMenu opens without simulated input")) return false; + m_ExpectedRoot = m_Menu.GetRootWidget(); + m_Config = m_Map.GetMapConfig(); + return Check(OwnsMap() && m_Config.MapEntityMode == EMapEntityMode.FULLSCREEN, "ordinary full-screen configuration and exact menu root pinned"); + } + protected bool CloseStandard() + { + if (!Check(m_Menu && OwnsMap() && !m_CloseRequested, "only exact owned standard menu may request close once")) return false; + m_CloseRequested = true; + m_CloseObserved = false; + return Check(GetGame().GetMenuManager().CloseMenu(m_Menu), "native standard menu close request accepted"); + } + protected int StandardClosed() + { + if (!Check(m_CloseRequested && m_Map && SCR_MapEntity.GetMapInstance() == m_Map, "pending close retains original native map identity")) return -1; + if (m_Map.IsOpen()) + { + if (!Check(OwnsMap(), "pending close cannot adopt a foreign open map")) return -1; + return 0; + } + if (!m_CloseObserved) return 0; + if (!Check(!m_Presentation.ERR_ProbeRoot(), "native map close callback removes objective widget before reopening")) return -1; + m_Menu = null; + m_CloseRequested = false; + m_CloseObserved = false; + return 1; + } + protected Widget CreateSharedRoot() + { + Widget root = GetGame().GetWorkspace().CreateWidgets("{255E1265F702B4AE}UI/layouts/ERRMap/SharedMapRenderTarget.layout"); + if (!root) return null; + RTTextureWidget target = RTTextureWidget.Cast(root.FindAnyWidget("RTTexture0")); + if (!target) { root.RemoveFromHierarchy(); return null; } + // Expose the real root for geometry/screenshot checks. No model render target + // is bound, so these observations cannot qualify the physical held texture. + target.SetEnabled(false); + root.SetVisible(true); + return root; + } + protected bool OpenShared(Widget root) + { + if (!Check(root && !m_Map.IsOpen(), "separate real shared-map render layout exists before opening")) return false; + m_Ready = null; m_ExpectedRoot = root; + m_Config = m_Map.SetupMapConfig(EMapEntityMode.PLAIN, "{A786DD4868598F15}Configs/Map/MapPlain.conf", root); + if (!Check(m_Config != null, "native shared plain-map configuration prepared")) return false; + m_Map.OpenMap(m_Config); + return Check(OwnsMap(), "shared map uses exact authored root and configuration"); + } + protected bool ReadyToInspect() + { + return OwnsMap() && m_Ready == m_Config && !m_Map.IsZooming(); + } + protected void Center(float zoom) + { + int position[2]; m_Marker.GetWorldPos(position); + m_Map.ZoomPanSmooth(zoom, position[0] + 100.0 / zoom, position[1], 0.1); + } + protected bool Inspect(string label, bool rough) + { + if (!Check(ReadyToInspect(), "native configuration finished initialization and camera remains fixture-owned")) return false; + Widget root = m_Presentation.ERR_ProbeRoot(); + ERR_ObjectiveMapElement element = m_Presentation.ERR_ProbeElement(); + Widget container = m_ExpectedRoot.FindAnyWidget("ERR_ObjectiveOverlay"); + Widget nativeIcons = m_ExpectedRoot.FindAnyWidget("UIIconsContainer"); + if (!container && nativeIcons) container = nativeIcons.GetParent(); + if (!Check(root && element && container && container != m_Map.GetMapWidget() && root.GetParent() == container && m_Presentation.ERR_ProbeContainer() == container && m_Presentation.ERR_ProbeCanvas() == m_Map.GetMapWidget(), "actual authored layout handler belongs to the exact supported objective container and native canvas")) return false; + TextWidget text = TextWidget.Cast(root.FindAnyWidget("ObjectiveText")); + ImageWidget image = ImageWidget.Cast(root.FindAnyWidget("Image")); + if (!Check(text && image && text.GetText() == label && root.IsVisible(), "actual visible widget contains the current objective label and icon widget")) return false; + int width, height; image.GetImageSize(0, width, height); + if (!Check(width > 0 && height > 0, "actual icon image resource has nonzero native dimensions")) return false; + int point[2]; m_Marker.GetWorldPos(point); + int knowledge = 2; if (rough) knowledge = 1; + if (!Check(point[0] == ERR_DiscoveryMapRules.Coordinate(m_Point[0], knowledge) && point[1] == ERR_DiscoveryMapRules.Coordinate(m_Point[2], knowledge), "presentation retains only requested rough or identified map coordinates")) return false; + vector model = element.GetPos(); + if (!Check(model[0] == point[0] && model[2] == point[1], "native handler receives only frozen disclosed coordinates")) return false; + int x, y; m_Map.WorldToScreen(point[0], point[1], x, y, true); + float cx, cy, ox, oy; + m_Map.GetMapWidget().GetScreenPos(cx, cy); + container.GetScreenPos(ox, oy); + vector placed = FrameSlot.GetPos(root); + if (!Check(Math.AbsFloat(placed[0] - GetGame().GetWorkspace().DPIUnscale(cx + x - ox)) < 1 && Math.AbsFloat(placed[1] - GetGame().GetWorkspace().DPIUnscale(cy + y - oy)) < 1, "native frame position matches current pan zoom DPI and canvas-to-container offset")) return false; + float ix, iy, iw, ih; + image.GetScreenPos(ix, iy); image.GetScreenSize(iw, ih); + PrintFormat("ERR OBJECTIVE PIXELS: expected=%1,%2 actual=%3,%4 stage=%5", cx + x, cy + y, ix + iw * 0.5, iy + ih * 0.5, m_Stage); + return Check(Math.AbsFloat(ix + iw * 0.5 - cx - x) <= 3 && Math.AbsFloat(iy + ih * 0.5 - cy - y) <= 3, "actual rendered icon box is centered on disclosed native pixel position"); + } + int Tick(float now) + { + if (m_Done) return 1; + if (m_Stopped) return -1; + if (!Enabled() || !m_Mode || ERR_GameMode.Get() != m_Mode || !m_Mode.Active() || System.GetTickCount() >= m_Deadline) { Check(false, "fixed ninety-second deadline and original active mission"); return -1; } + if (now < m_Wait) return 0; + if (m_Stage >= 16 && m_Stage <= 23 && !Check(ReadyToInspect(), "capture hold retains exact active map before any following mutation")) return -1; + if (m_Stage == 0) + { + m_Map = SCR_MapEntity.GetMapInstance(); + m_Annotations = SCR_MapMarkerManagerComponent.GetInstance(); + PlayerController controller = GetGame().GetPlayerController(); + if (!Check(m_Map && !m_Map.IsOpen() && m_Annotations && controller && controller.GetControlledEntity(), "closed native map and actual local participant available")) return -1; + m_Point = controller.GetControlledEntity().GetOrigin() + "1037.25 0 1063.75"; + m_AnnotationCount = m_Annotations.ERR_ProbeAnnotationCount(); + m_Marker = new ERR_DiscoveryMarker(); + m_Marker.SetDiscoveryPosition(m_Point, 1); m_Marker.SetLocationIcon(ERR_LocationKind.RADIO, "", 1); + m_Marker.SetCustomText("Unknown location (500 m area)"); + m_Annotations.ERR_InsertDiscoveryMarker(m_Marker); + m_Presentation = m_Marker.ERR_ProbePresentation(); + if (!Check(m_Presentation != null, "real discovery insertion uses separate objective presentation")) return -1; + if (!OpenStandard()) return -1; + m_Stage = 1; m_Wait = now + 1000; return 0; + } + if (m_Stage == 1 || m_Stage == 7 || m_Stage == 9 || m_Stage == 11) + { + if (!ReadyToInspect()) return 0; + Center(0.3); m_Stage++; m_Wait = now + 1000; return 0; + } + if (m_Stage == 2) + { + if (!Inspect("Unknown location (500 m area)", true)) return -1; + System.MakeScreenshot("$logs:err-objective-map-standard-rough"); + Print("ERR OBJECTIVE CAPTURE READY: standard-rough"); + m_Stage = 16; m_Wait = now + 1500; return 0; + } + if (m_Stage == 16) + { + m_PreviousWidget = m_Presentation.ERR_ProbeRoot(); + m_Marker.SetDiscoveryPosition(m_Point, 2); m_Marker.SetLocationIcon(ERR_LocationKind.RADIO, "", 2); m_Marker.SetCustomText("Radio station"); + Center(0.6); m_Stage = 3; m_Wait = now + 1000; return 0; + } + if (m_Stage == 3) + { + if (!Inspect("Radio station", false)) return -1; + if (!Check(m_PreviousWidget == m_Presentation.ERR_ProbeRoot(), "rough to exact updates one existing widget")) return -1; + System.MakeScreenshot("$logs:err-objective-map-standard-exact"); + Print("ERR OBJECTIVE CAPTURE READY: standard-exact"); + m_Stage = 17; m_Wait = now + 1500; return 0; + } + if (m_Stage == 17) + { + float farX = 500; if (m_Point[0] < m_Map.GetMapSizeX() * 0.5) farX = m_Map.GetMapSizeX() - 500; + m_Map.ZoomPanSmooth(0.6, farX, 500, 0.1); + m_Stage = 4; m_Wait = now + 1000; return 0; + } + if (m_Stage == 4) + { + if (!Check(ReadyToInspect() && m_PreviousWidget == m_Presentation.ERR_ProbeRoot() && !m_PreviousWidget.IsVisible(), "native pan culls original objective without deleting or relocating its data")) return -1; + Center(0.3); m_Stage = 5; m_Wait = now + 1000; return 0; + } + if (m_Stage == 5) + { + if (!Inspect("Radio station", false)) return -1; + m_Marker.SetCustomText("Reported mine area (500 m); may still be active"); + m_Stage = 22; m_Wait = now + 750; return 0; + } + if (m_Stage == 22) + { + if (!Inspect("Reported mine area (500 m); may still be active", false)) return -1; + System.MakeScreenshot("$logs:err-objective-map-standard-long-warning"); + Print("ERR OBJECTIVE CAPTURE READY: standard-long-warning"); + m_Stage = 23; m_Wait = now + 1500; return 0; + } + if (m_Stage == 23) + { + m_Marker.SetCustomText("Radio station"); + if (!CloseStandard()) return -1; + m_Stage = 6; m_Wait = now + 500; return 0; + } + if (m_Stage == 6) + { + int closed = StandardClosed(); + if (closed <= 0) return closed; + if (!OpenStandard()) return -1; + m_Stage = 7; m_Wait = now + 1000; return 0; + } + if (m_Stage == 8) + { + if (!Inspect("Radio station", false)) return -1; + System.MakeScreenshot("$logs:err-objective-map-standard-reopened"); + Print("ERR OBJECTIVE CAPTURE READY: standard-reopened"); + m_Stage = 18; m_Wait = now + 1500; return 0; + } + if (m_Stage == 18) + { + if (!CloseStandard()) return -1; + m_Stage = 15; m_Wait = now + 500; return 0; + } + if (m_Stage == 15) + { + int closed = StandardClosed(); + if (closed <= 0) return closed; + m_SharedRoot = CreateSharedRoot(); + if (!OpenShared(m_SharedRoot)) return -1; + m_Stage = 9; m_Wait = now + 1000; return 0; + } + if (m_Stage == 10) + { + if (!Inspect("Radio station", false)) return -1; + System.MakeScreenshot("$logs:err-objective-map-shared-root"); + Print("ERR OBJECTIVE CAPTURE READY: shared-root"); + m_Stage = 19; m_Wait = now + 1500; return 0; + } + if (m_Stage == 19) + { + m_Marker.SetCustomText("Reported mine area (500 m); may still be active"); + m_Stage = 14; m_Wait = now + 750; return 0; + } + if (m_Stage == 14) + { + if (!Inspect("Reported mine area (500 m); may still be active", false)) return -1; + System.MakeScreenshot("$logs:err-objective-map-shared-long-warning"); + Print("ERR OBJECTIVE CAPTURE READY: shared-long-warning"); + m_Stage = 20; m_Wait = now + 1500; return 0; + } + if (m_Stage == 20) + { + MapConfiguration previousConfig = m_Config; + m_Map.CloseMap(); + if (!Check(!m_Presentation.ERR_ProbeRoot(), "closing shared root clears only owned objective widget")) return -1; + m_ReusedRoot = CreateSharedRoot(); + if (!OpenShared(m_ReusedRoot)) return -1; + if (!Check(m_Config == previousConfig && m_ReusedRoot != m_SharedRoot, "native plain config is reused with a distinct root")) return -1; + m_Stage = 11; m_Wait = now + 1000; return 0; + } + if (m_Stage == 12) + { + if (!Inspect("Reported mine area (500 m); may still be active", false)) return -1; + System.MakeScreenshot("$logs:err-objective-map-shared-reused-root"); + Print("ERR OBJECTIVE CAPTURE READY: shared-reused-root"); + m_Stage = 21; m_Wait = now + 1500; return 0; + } + if (!Check(m_Stage == 21, "only final captured stage can complete")) return -1; + m_Marker.OnDelete(); + if (!Check(m_Presentation.ERR_ProbeStopped() && OwnsMap(), "objective teardown releases its widget while leaving native map open")) return -1; + m_Stage = 13; + Stop(); m_Done = true; + if (!Check(m_Annotations && m_Annotations.ERR_ProbeAnnotationCount() == m_AnnotationCount, "existing annotation registry count preserved; no privacy settings changed")) return -1; + Print("ERR SMOKE COMPLETE: objective overlay native standard/shared-root geometry, data, pan zoom culling reopen and teardown; not held-texture, manual input, restricted-platform or multiplayer proof"); + return 1; + } + void Stop() + { + if (m_Stopped) return; + m_Stopped = true; + if (m_Marker) m_Marker.OnDelete(); + if (m_Presentation) m_Presentation.Stop(); + SCR_MapEntity.GetOnMapOpenComplete().Remove(Ready); + SCR_MapEntity.GetOnMapClose().Remove(Closed); + if (OwnsMap()) + { + if (m_Menu) + { + if (!m_CloseRequested) GetGame().GetMenuManager().CloseMenu(m_Menu); + } + else m_Map.CloseMap(); + } + if (m_SharedRoot) m_SharedRoot.RemoveFromHierarchy(); + if (m_ReusedRoot) m_ReusedRoot.RemoveFromHierarchy(); + m_Menu = null; m_SharedRoot = null; m_ReusedRoot = null; m_ExpectedRoot = null; m_Config = null; m_Ready = null; + } +} +modded class ERR_GameMode +{ + protected ref ERR_NativeObjectiveMapProbe m_ERRObjectiveMapProbe; + protected int m_ERRObjectiveMapDeadline; + protected override void EOnInit(IEntity owner) + { + super.EOnInit(owner); + if (SCR_Global.IsEditMode() || !Replication.IsServer() || !ERR_NativeObjectiveMapProbe.Enabled()) return; + m_ERRObjectiveMapDeadline = System.GetTickCount() + 180000; + OnStop().Insert(ERR_StopObjectiveMapProbe); + GetGame().GetCallqueue().CallLater(ERR_TickObjectiveMapProbe, 250, true); + } + protected void ERR_TickObjectiveMapProbe() + { + if (System.GetTickCount() >= m_ERRObjectiveMapDeadline || !PreparationError().IsEmpty()) + { + Print("ERR SMOKE FAIL: objective overlay preparation/overall deadline", LogLevel.ERROR); + ERR_StopObjectiveMapProbe(); return; + } + if (!Active() || !GetGame().GetPlayerController() || !GetGame().GetPlayerController().GetControlledEntity()) return; + if (!m_ERRObjectiveMapProbe) m_ERRObjectiveMapProbe = new ERR_NativeObjectiveMapProbe(this); + if (m_ERRObjectiveMapProbe.Tick(GetWorld().GetWorldTime()) != 0) ERR_StopObjectiveMapProbe(); + } + protected void ERR_StopObjectiveMapProbe() + { + if (GetGame()) GetGame().GetCallqueue().Remove(ERR_TickObjectiveMapProbe); + if (m_ERRObjectiveMapProbe) m_ERRObjectiveMapProbe.Stop(); + OnStop().Remove(ERR_StopObjectiveMapProbe); + } + void ~ERR_GameMode() { ERR_StopObjectiveMapProbe(); } +} +#endif --- /dev/null +++ b/tests/test_native_objective_map_probe.py @@ -0,0 +1,178 @@ +"""Actual fixture ownership/terminal cleanup plus explicit selector contracts.""" +import os,re +from pathlib import Path +from policy_harness import ROOT,body,run +root=Path(os.environ.get('ERR_NATIVE_OBJECTIVE_MAP_ROOT',ROOT));p=root/'addons/core/Scripts/Game/ERR/00_Core/ERR_NativeObjectiveMapProbe.c' +def adapt(s): + s=s.replace('protected ','').replace('null','nullptr').replace('SCR_MapEntity.','SCR_MapEntity::') + for name in ['m_Map','m_Config','m_Marker','m_Presentation','m_SharedRoot','m_ReusedRoot']: + s=s.replace(name+'.',name+'->') + for name in ['GetGame','GetMenuManager','GetOnMapOpenComplete','GetOnMapClose']: + s=s.replace(name+'().',name+'()->') + return s.replace('Remove(Ready)','Remove(this)').replace('Remove(Closed)','Remove(this)').replace('MapConfiguration config','Config* config') +methods='\n'.join(adapt(body(p,s)) for s in ['protected bool OwnsMap(', 'protected bool CloseStandard(', 'protected int StandardClosed(', 'protected void Closed(', 'void Stop(']) +run('''#include +#include +#include +using string=std::string; +struct Widget{int deletes=0;void RemoveFromHierarchy(){deletes++;}}; +struct Config{Widget*RootWidgetRef=nullptr;}; +struct Event{std::setowners;void Remove(void*p){owners.erase(p);}}; +struct SCR_MapEntity{static inline SCR_MapEntity*current=nullptr;static inline Event opened,closed;Config*config=nullptr;Widget*root=nullptr;bool open=true;int closes=0;static SCR_MapEntity*GetMapInstance(){return current;}static Event*GetOnMapOpenComplete(){return &opened;}static Event*GetOnMapClose(){return &closed;}bool IsOpen(){return open;}Config*GetMapConfig(){return config;}Widget*GetMapMenuRoot(){return root;}void CloseMap(){closes++;open=false;}}; +struct Marker{int deletes=0;void OnDelete(){deletes++;}};struct Presentation{int stops=0;Widget*root=nullptr;Widget*ERR_ProbeRoot(){return root;}void Stop(){stops++;root=nullptr;}};struct Menu{}; +struct Manager{int closes=0;bool asynchronous=false;bool CloseMenu(Menu*){closes++;if(!asynchronous&&SCR_MapEntity::current)SCR_MapEntity::current->CloseMap();return true;}}; +struct Game{Manager manager;Manager*GetMenuManager(){return &manager;}}game;Game*GetGame(){return &game;} +struct Probe{bool m_Stopped=false,m_CloseRequested=false,m_CloseObserved=false;bool Check(bool ok,string){if(!ok)Stop();return ok;}SCR_MapEntity*m_Map=nullptr;Config*m_Config=nullptr,*m_Ready=nullptr;Widget*m_ExpectedRoot=nullptr,*m_SharedRoot=nullptr,*m_ReusedRoot=nullptr;Marker*m_Marker=nullptr;Presentation*m_Presentation=nullptr;Menu*m_Menu=nullptr; +'''+methods+''' +}; +int main(){SCR_MapEntity map;SCR_MapEntity::current=↦Config cfg;Widget root,otherRoot,shared,reused;cfg.RootWidgetRef=&root;map.root=&root;map.config=&cfg; + Probe p;p.m_Map=↦p.m_Config=&cfg;p.m_ExpectedRoot=&root;Marker marker;Presentation presentation;Menu menu;p.m_Marker=▮p.m_Presentation=&presentation;p.m_Menu=&menu;p.m_SharedRoot=&shared;p.m_ReusedRoot=&reused; + assert(p.OwnsMap());cfg.RootWidgetRef=&otherRoot;assert(!p.OwnsMap());cfg.RootWidgetRef=&root;map.root=&otherRoot;assert(!p.OwnsMap());map.root=&root; + SCR_MapEntity replacement;SCR_MapEntity::current=&replacement;assert(!p.OwnsMap());SCR_MapEntity::current=↦ + SCR_MapEntity::opened.owners.insert(&p);p.Stop();assert(p.m_Stopped&&marker.deletes==1&&presentation.stops==1&&game.manager.closes==1&&map.closes==1&&shared.deletes==1&&reused.deletes==1&&!SCR_MapEntity::opened.owners.count(&p)); + p.Stop();assert(marker.deletes==1&&map.closes==1&&shared.deletes==1); + Probe interrupted;interrupted.m_Map=↦interrupted.m_Config=&cfg;interrupted.m_ExpectedRoot=&root;interrupted.m_Menu=&menu;map.open=true;map.root=&otherRoot;interrupted.Stop();assert(map.open&&map.closes==1&&game.manager.closes==1); + Probe partial;partial.Stop();assert(partial.m_Stopped&&game.manager.closes==1); + // Native CloseMenu can return while MapMenu.OnMenuClose has not happened. + map.root=&root;map.open=true;game.manager.asynchronous=true; + Probe pending;pending.m_Map=↦pending.m_Config=&cfg;pending.m_ExpectedRoot=&root;pending.m_Menu=&menu;pending.m_Presentation=&presentation;presentation.root=&shared; + assert(pending.CloseStandard()&&pending.m_Menu==&menu&&pending.m_CloseRequested&&map.open);assert(game.manager.closes==2); + assert(pending.StandardClosed()==0&&!pending.m_Stopped);assert(pending.StandardClosed()==0&&game.manager.closes==2); + Config foreign;pending.Closed(&foreign);assert(!pending.m_CloseObserved); + pending.Closed(&cfg);assert(pending.m_CloseObserved&&pending.StandardClosed()==0); // callback occurs before IsOpen clears + map.open=false;presentation.root=nullptr;assert(pending.StandardClosed()==1&&!pending.m_Menu&&!pending.m_CloseRequested);assert(game.manager.closes==2); + // Closed state alone cannot replace the observed exact native callback. + Probe noEvent;noEvent.m_Map=↦noEvent.m_Config=&cfg;noEvent.m_ExpectedRoot=&root;noEvent.m_Menu=&menu;noEvent.m_Presentation=&presentation;map.open=true;assert(noEvent.CloseStandard());map.open=false;assert(noEvent.StandardClosed()==0&&noEvent.m_Menu==&menu);noEvent.Stop(); + // A close request is not repeated by Stop or after foreign map adoption. + Probe cancel;cancel.m_Map=↦cancel.m_Config=&cfg;cancel.m_ExpectedRoot=&root;cancel.m_Menu=&menu;map.open=true;assert(cancel.CloseStandard());int requests=game.manager.closes;cancel.Stop();assert(game.manager.closes==requests&&map.open); + Probe displaced;displaced.m_Map=↦displaced.m_Config=&cfg;displaced.m_ExpectedRoot=&root;displaced.m_Menu=&menu;displaced.m_Presentation=&presentation;assert(displaced.CloseStandard());requests=game.manager.closes;map.root=&otherRoot;assert(displaced.StandardClosed()==-1&&displaced.m_Stopped&&map.open&&game.manager.closes==requests); + // Exact close callback with a leaked production widget still fails. + map.root=&root;Probe leak;leak.m_Map=↦leak.m_Config=&cfg;leak.m_ExpectedRoot=&root;leak.m_Menu=&menu;leak.m_Presentation=&presentation;assert(leak.CloseStandard());leak.Closed(&cfg);map.open=false;presentation.root=&shared;assert(leak.StandardClosed()==-1&&leak.m_Stopped); + +} +''') +s=p.read_text();assert s.startswith('#ifdef WORKBENCH') +assert 'System.GetTickCount() + 90000' in s and 'System.GetTickCount() + 180000' in s +assert s.count('System.MakeScreenshot(')==7 +assert s.count('ERR OBJECTIVE CAPTURE READY: ')==7 +assert s.count('m_Wait = now + 1500; return 0;')==7 +assert 'm_Stage >= 16 && m_Stage <= 23 && !Check(ReadyToInspect()' in s +assert 'm_Stage == 6' in s and 'm_Stage == 15' in s and s.count('int closed = StandardClosed();')==2 +assert 'SetBlocked(' not in s and 'SetPrivileged' not in s +base=(root/'addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c').read_text() +assert '"objective-map"' in base.split('ERR_NativeObjectiveProbe.BeginRound();')[0] +runner=(root/'tools/headless_workbench.py').read_text() +assert runner.index("objective-map requires --project core --rounds 1") < runner.index('steam, compat, profile =') +# Actual invalid CLI must refuse before nonexistent prefix/profile processing. +import subprocess,sys,tempfile +with tempfile.TemporaryDirectory(prefix='err-objective-selector-') as temporary: + sandbox=Path(temporary); profile=sandbox/'must-not-exist'; prefix=sandbox/'absent-prefix' + environment=os.environ.copy(); environment['PYTHONPATH']=str(ROOT/'tools') + for project,rounds in [('chat','1'),('core','2')]: + result=subprocess.run([sys.executable,str(root/'tools/headless_workbench.py'),'--smoke','objective-map','--project',project,'--rounds',rounds,'--compat',str(prefix),'--profile',str(profile)],capture_output=True,text=True,env=environment) + assert result.returncode==2 and 'objective-map requires --project core --rounds 1' in result.stderr,result.stderr + assert not profile.exists() and not prefix.exists() +print('PASS: actual native fixture root/config ownership and idempotent cleanup preserve foreign map, selector excludes ordinary route, fixed budgets/screenshots/UGC boundaries retained; native operations not executed') + +# Execute the complete actual Inspect method: parent identity and frame projection +# are checked independently from native image-box coordinates. These are API +# doubles, not proof of engine widget parenting, pixels or pointer transparency. +inspect=body(p,'protected bool Inspect(').replace('protected ','') +for t in ['Widget','ERR_ObjectiveMapElement','TextWidget','ImageWidget']: + inspect=re.sub(r'\b'+t+r' (?=\w)',t+'* ',inspect) +for name in ['m_Presentation','m_Map','m_ExpectedRoot','m_Marker','root','element','container','nativeIcons','text','image']: + inspect=inspect.replace(name+'.',name+'->') +for name in ['GetGame','GetWorkspace','GetMapWidget']: + inspect=inspect.replace(name+'().',name+'()->') +for name in ['TextWidget','ImageWidget','ERR_DiscoveryMapRules','FrameSlot','Math']: + inspect=inspect.replace(name+'.',name+'::') +run(r''' +#include +#include +#include +#include +using string=std::string;using vector=std::array; +struct Widget{Widget*parent=nullptr,*container=nullptr,*overlay=nullptr,*text=nullptr,*image=nullptr;vector placed={0,0,0};float sx=0,sy=0,width=36,height=36;bool visible=true;virtual ~Widget()=default;Widget*GetParent(){return parent;}Widget*FindAnyWidget(string n){if(n=="UIIconsContainer")return container;if(n=="ERR_ObjectiveOverlay")return overlay;if(n=="ObjectiveText")return text;if(n=="Image")return image;return nullptr;}bool IsVisible(){return visible;}void GetScreenPos(float&x,float&y){x=sx;y=sy;}void GetScreenSize(float&w,float&h){w=width;h=height;}}; +struct TextWidget:Widget{string label;static TextWidget*Cast(Widget*w){return dynamic_cast(w);}string GetText(){return label;}}; +struct ImageWidget:Widget{static ImageWidget*Cast(Widget*w){return dynamic_cast(w);}void GetImageSize(int,int&w,int&h){w=64;h=64;}}; +struct ERR_ObjectiveMapElement{vector pos;vector GetPos(){return pos;}}; +struct Presentation{Widget*root=nullptr,*canvas=nullptr,*container=nullptr;ERR_ObjectiveMapElement*element=nullptr;Widget*ERR_ProbeRoot(){return root;}Widget*ERR_ProbeCanvas(){return canvas;}Widget*ERR_ProbeContainer(){return container;}ERR_ObjectiveMapElement*ERR_ProbeElement(){return element;}}; +struct Map{Widget*canvas=nullptr;int x=240,y=180;Widget*GetMapWidget(){return canvas;}void WorldToScreen(int,int,int&ox,int&oy,bool pan){assert(pan);ox=x;oy=y;}}; +struct Marker{int x=6123,y=7027;void GetWorldPos(int p[2]){p[0]=x;p[1]=y;}}; +struct Workspace{float dpi=2;float DPIUnscale(float x){return x/dpi;}}; +struct Game{Workspace workspace;Workspace*GetWorkspace(){return &workspace;}}game;Game*GetGame(){return &game;} +struct FrameSlot{static vector GetPos(Widget*w){return w->placed;}}; +struct Math{static float AbsFloat(float x){return std::fabs(x);}}; +struct ERR_DiscoveryMapRules{static int Coordinate(float x,int knowledge){return knowledge==1?int(std::floor(x/500))*500+250:int(x);}}; +templatevoid PrintFormat(T...){} +struct Probe{Presentation*m_Presentation;Map*m_Map;Widget*m_ExpectedRoot;Marker*m_Marker;vector m_Point={6123,0,7027};int m_Stage=3;bool ready=true;bool ReadyToInspect(){return ready;}bool Check(bool b,string){return b;} +'''+inspect+r''' +}; +int main(){Widget menu,container,nativeIcons,canvas,root,foreign;TextWidget text;ImageWidget image;ERR_ObjectiveMapElement element;Presentation presentation;Map map;Marker marker; + menu.container=&nativeIcons;nativeIcons.parent=&container;root.parent=&container;root.text=&text;root.image=ℑtext.label="Radio station";element.pos={6123,0,7027}; + canvas.sx=100;canvas.sy=60;container.sx=24;container.sy=10; + presentation.root=&root;presentation.canvas=&canvas;presentation.container=&container;presentation.element=&element;map.canvas=&canvas; + // Canvas-relative projection240,180 -> screen340,240; supported parent has + // origin24,10, so slot(158,115) at DPI2, not slot(120,90). + root.placed={158,115,0};image.sx=322;image.sy=222; + Probe probe{&presentation,&map,&menu,&marker};assert(probe.Inspect(text.label,false)); + root.placed={120,90,0};assert(!probe.Inspect(text.label,false));root.placed={158,115,0}; + root.parent=&canvas;assert(!probe.Inspect(text.label,false));root.parent=&container; + nativeIcons.parent=&foreign;assert(!probe.Inspect(text.label,false));nativeIcons.parent=&container; + presentation.container=&foreign;assert(!probe.Inspect(text.label,false));presentation.container=&container; + presentation.canvas=&foreign;assert(!probe.Inspect(text.label,false));presentation.canvas=&canvas; + image.sx+=4;assert(!probe.Inspect(text.label,false));image.sx-=4; + element.pos[0]+=1;assert(!probe.Inspect(text.label,false));element.pos[0]-=1; + root.visible=false;assert(!probe.Inspect(text.label,false));root.visible=true; + probe.ready=false;assert(!probe.Inspect(text.label,false));probe.ready=true; + // Another valid parent origin and non-unit DPI cannot be compensated by + // dropping screen-origin offsets from the projection assertion. + container.sx=120;container.sy=110;game.workspace.dpi=1.25;root.placed={176,104,0};assert(probe.Inspect(text.label,false)); + // Shared render roots choose their dedicated overlay instead of native icons. + menu.overlay=&container;nativeIcons.parent=&foreign;assert(probe.Inspect(text.label,false)); + menu.overlay=&foreign;assert(!probe.Inspect(text.label,false));menu.overlay=&container; + marker.x=6250;marker.y=7250;element.pos={6250,0,7250};assert(probe.Inspect(text.label,true)); +} +''') +print('PASS: actual full Inspect rejects canvas parenting, wrong active container/canvas, omitted origin offset, wrong rendered box and stale coordinates; differing origins and DPI accepted') + +# Execute the added actual three phase blocks. Long-label capture must happen +# before reset/close, and the radio label must be restored for later reopening. +phases='\n'.join(body(p,'if (m_Stage == '+str(stage)+')') for stage in [5,22,23]) +phases=phases.replace('m_Marker.','m_Marker->').replace('System.','System::') +run(r''' +#include +#include +using string=std::string; +struct Marker{string label="Radio station";void SetCustomText(string text){label=text;}}; +struct System{static inline int captures=0;static void MakeScreenshot(string path){assert(path=="$logs:err-objective-map-standard-long-warning");captures++;}}; +int checkpoints=0;void Print(string text){assert(text=="ERR OBJECTIVE CAPTURE READY: standard-long-warning");checkpoints++;} +struct Probe{Marker*m_Marker;int m_Stage=5,closes=0,inspections=0;float m_Wait=0;bool Inspect(string text,bool rough){inspections++;return !rough&&m_Marker->label==text;}bool CloseStandard(){assert(m_Marker->label=="Radio station");closes++;return true;} + int Advance(float now){if(now').replace('m_Map.','m_Map->') +run(r''' +#include +#include +#include +struct Marker{int x=6250,y=7250;void GetWorldPos(int p[2]){p[0]=x;p[1]=y;}}; +struct Map{float zoom=0,x=0,y=0,duration=0;int calls=0;void ZoomPanSmooth(float z,float px,float py,float t){zoom=z;x=px;y=py;duration=t;calls++;}}; +struct Probe{Marker*m_Marker;Map*m_Map; +'''+center+r''' +}; +int main(){Marker marker;Map map;Probe p{&marker,&map};for(float zoom:{0.3f,0.6f}){p.Center(zoom);assert(map.zoom==zoom&&map.y==7250&&std::abs(map.duration-0.1f)<0.001f);assert(std::abs((marker.x-map.x)*zoom+100.0f)<0.01f);assert(marker.x==6250&&marker.y==7250);}assert(map.calls==2);} +''') +print('PASS: actual Center places marker100screenpx left of pan center at both fixture zooms without altering disclosed coordinates') --- a/tools/headless_workbench.py +++ b/tools/headless_workbench.py @@ -14,7 +14,7 @@ p.add_argument('--external-addons', type=Path, help='Installed RHS/WCS/BMP source root; expose only audited closure in a separate Chat profile') p.add_argument('--compat', type=Path, required=True, help='Disposable cloned Proton compatdata directory outside Steam') p.add_argument('--profile', type=Path, required=True, help='Separate disposable successor Workbench profile') -p.add_argument('--smoke', choices=['normal-escape-route', 'escape-route', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'hq-local-handoff', 'infantry-transfer', 'hq-body', 'convoy', 'supply-convoys', 'mounted-ammo', 'alarm-hearing', 'hq-memory', 'hq-awareness', 'aircraft-field', 'aircraft-route', 'native-end-active', 'native-end-preparing', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate', 'map-ownership', 'mortar-refresh', 'mortar-salvo', 'mortar-courier', 'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory') +p.add_argument('--smoke', choices=['normal-escape-route', 'escape-route', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'hq-local-handoff', 'infantry-transfer', 'hq-body', 'convoy', 'supply-convoys', 'mounted-ammo', 'alarm-hearing', 'hq-memory', 'hq-awareness', 'aircraft-field', 'aircraft-route', 'native-end-active', 'native-end-preparing', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate', 'map-ownership', 'mortar-refresh', 'mortar-salvo', 'mortar-courier', 'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory') p.add_argument('--maintenance-refuel', action='store_true', help='Opt-in normal-route check: original held Southern fuel can transfers finite fuel through its native UAZ action; one arranged tank depletion, no stock creation') p.add_argument('--mounted-swap-eligibility', action='store_true', help='Read-only mounted-ammo variant: one exact finite cargo/mounted magazine CanSwap call; no transfer or radio proof') p.add_argument('--mine-stock', action='store_true', help='Read-only mounted-ammo check of exact callback-proved finite PMN4 cargo before and after natural radio delivery') @@ -33,6 +33,7 @@ p.add_argument('--timeout', type=int, default=360) p.add_argument('--project', choices=['core', 'chat'], default='chat', help='Diagnostic fixture family (both load the combined mod)') a = p.parse_args() +if a.smoke == 'objective-map' and (a.project != 'core' or a.rounds != 1): p.error('objective-map requires --project core --rounds 1') if a.smoke == 'unconscious-camera' and a.project != 'core': p.error('unconscious-camera requires --project core to keep the general smoke fixture inactive') if a.mine_stock: if a.smoke != 'mounted-ammo' or a.project != 'chat' or a.rounds != 1 or a.timeout < 360: @@ -139,7 +140,7 @@ if a.recon_takeover_only: command += ['-errReconTakeoverOnly', '1'] if a.body_recovery_only: command += ['-errBodyRecoveryOnly', '1'] if a.project == 'core': - if a.smoke not in {'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'native-end-active', 'native-end-preparing', 'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture') + if a.smoke not in {'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'native-end-active', 'native-end-preparing', 'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture') command[command.index('-plugin=ERR_ValidateWorldPlugin')] = '-plugin=ERR_ValidateBaseWorldPlugin' command[command.index('-errSmoke')] = '-errBaseSmoke' if a.smoke in {'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'} and a.project != 'core': p.error('This fixture requires --project core')