Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
321 lines
26 KiB
Plaintext
321 lines
26 KiB
Plaintext
--- /dev/null
|
|
+++ b/addons/core/Scripts/Game/ERR/20_Intel/ERR_MineIntel.c
|
|
@@ -0,0 +1,167 @@
|
|
+// Historical evidence bound to an exact physical document. Undisclosed records
|
|
+// stay server-local; no live mine or soldier position is queried on recovery.
|
|
+class ERR_MineIntelRecord
|
|
+{
|
|
+ IEntity Mine;
|
|
+ vector Area;
|
|
+ ref array<IEntity> Folders = {};
|
|
+ bool Revealed;
|
|
+}
|
|
+class ERR_MineIntel
|
|
+{
|
|
+ protected ERR_GameMode m_Mode;
|
|
+ protected ref array<ref ERR_MineIntelRecord> m_Records = {};
|
|
+ protected int m_Bindings;
|
|
+ protected bool m_Stopped;
|
|
+ void ERR_MineIntel(ERR_GameMode mode) { m_Mode = mode; }
|
|
+ protected bool Current()
|
|
+ {
|
|
+ return Replication.IsServer() && !m_Stopped && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active();
|
|
+ }
|
|
+ protected bool Generic(IEntity folder)
|
|
+ {
|
|
+ return folder && folder.GetPrefabData() && folder.GetPrefabData().GetPrefabName() == ERR_Resources.IntelFolder()
|
|
+ && !folder.FindComponent(ERR_DocumentComponent) && m_Mode.Discovery() && !m_Mode.Discovery().IsSpent(folder);
|
|
+ }
|
|
+ // Called once at accepted engineer-job admission, before approaching the site.
|
|
+ void Candidates(IEntity actor, out array<IEntity> folders)
|
|
+ {
|
|
+ folders.Clear();
|
|
+ if (!Current() || !ERR_Actors.CanFight(actor) || ERR_Actors.Player(actor)) return;
|
|
+ if (!m_Mode.EnemyFaction() || SCR_FactionManager.SGetFaction(actor) != m_Mode.EnemyFaction()) return;
|
|
+ InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
|
|
+ if (!inventory) return;
|
|
+ array<IEntity> found = {}; ERR_DiscoveryService.Folders(inventory, found);
|
|
+ if (found.Count() > 128) return;
|
|
+ foreach (IEntity folder : found)
|
|
+ {
|
|
+ if (folders.Count() >= 16) break;
|
|
+ if (Generic(folder) && ERR_InfantryAmmoTransfer.StoredUnder(folder, actor) && !folders.Contains(folder)) folders.Insert(folder);
|
|
+ }
|
|
+ }
|
|
+ bool Seal(IEntity actor, IEntity mine, vector position, array<IEntity> candidates)
|
|
+ {
|
|
+ if (!Current() || !ERR_Actors.CanFight(actor) || ERR_Actors.Player(actor)) return false;
|
|
+ if (!m_Mode.EnemyFaction() || SCR_FactionManager.SGetFaction(actor) != m_Mode.EnemyFaction()) return false;
|
|
+ if (!candidates || candidates.IsEmpty() || candidates.Count() > 16 || m_Records.Count() >= 32 || m_Bindings >= 128) return false;
|
|
+ if (!ERR_ForceRules.FiniteVector(position) || !ERR_Actors.Alive(mine) || !mine.GetPrefabData()) return false;
|
|
+ if (mine.GetPrefabData().GetPrefabName() != ERR_EnemyLoadoutCatalog.MINE || mine.GetParent()) return false;
|
|
+ if (vector.Distance(mine.GetOrigin(), position) > 1) return false;
|
|
+ InventoryItemComponent item = InventoryItemComponent.Cast(mine.FindComponent(InventoryItemComponent));
|
|
+ SCR_PressureTriggerComponent trigger = SCR_PressureTriggerComponent.Cast(mine.FindComponent(SCR_PressureTriggerComponent));
|
|
+ if (!item || item.GetParentSlot() || !trigger || !trigger.IsActivated()) return false;
|
|
+ foreach (ERR_MineIntelRecord known : m_Records) if (known.Mine == mine) return false;
|
|
+ array<IEntity> held = {}; Candidates(actor, held);
|
|
+ ref ERR_MineIntelRecord record = new ERR_MineIntelRecord();
|
|
+ foreach (IEntity folder : candidates)
|
|
+ {
|
|
+ if (m_Bindings + record.Folders.Count() >= 128) break;
|
|
+ if (held.Contains(folder) && !record.Folders.Contains(folder)) record.Folders.Insert(folder);
|
|
+ }
|
|
+ if (record.Folders.IsEmpty()) return false;
|
|
+ record.Mine = mine;
|
|
+ record.Area = Vector(ERR_DiscoveryMapRules.Coordinate(position[0], 1), 0, ERR_DiscoveryMapRules.Coordinate(position[2], 1));
|
|
+ m_Bindings += record.Folders.Count();
|
|
+ m_Records.Insert(record);
|
|
+ return true;
|
|
+ }
|
|
+ bool Recover(IEntity player, IEntity folder)
|
|
+ {
|
|
+ if (!Current() || !m_Mode.Players() || !m_Mode.Players().CurrentActor(player) || !ERR_Actors.Alive(player)) return false;
|
|
+ if (!m_Mode.PlayerFaction() || SCR_FactionManager.SGetFaction(player) != m_Mode.PlayerFaction() || !Generic(folder)) return false;
|
|
+ if (!ERR_InfantryAmmoTransfer.StoredUnder(folder, player)) return false;
|
|
+ InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(player.FindComponent(InventoryStorageManagerComponent));
|
|
+ if (!inventory) return false;
|
|
+ array<IEntity> held = {}; ERR_DiscoveryService.Folders(inventory, held);
|
|
+ if (held.Count() > 128 || !held.Contains(folder)) return false;
|
|
+ bool revealed = false;
|
|
+ foreach (ERR_MineIntelRecord record : m_Records)
|
|
+ {
|
|
+ if (record.Revealed || !record.Folders.Contains(folder)) continue;
|
|
+ if (m_Mode.ERR_RevealMineArea(record.Area)) { record.Revealed = true; revealed = true; }
|
|
+ }
|
|
+ return revealed;
|
|
+ }
|
|
+ void Stop() { m_Stopped = true; m_Records.Clear(); m_Bindings = 0; }
|
|
+}
|
|
+
|
|
+modded class ERR_GameMode
|
|
+{
|
|
+ protected ref ERR_MineIntel m_ERRMineIntel;
|
|
+ // Only recovered, quantized historical warnings are replicated, including JIP.
|
|
+ [RplProp()]
|
|
+ protected ref array<vector> m_ERRMineAreas = {};
|
|
+ protected ref array<ref ERR_DiscoveryMarker> m_ERRMineMarkers = {};
|
|
+ protected bool m_ERRMineIntelStopped;
|
|
+ protected SCR_MapMarkerManagerComponent m_ERRMineMarkerOwner;
|
|
+ ERR_MineIntel MineIntel() { return m_ERRMineIntel; }
|
|
+ protected override void EOnInit(IEntity owner)
|
|
+ {
|
|
+ super.EOnInit(owner);
|
|
+ if (SCR_Global.IsEditMode()) return;
|
|
+ if (Replication.IsServer()) m_ERRMineIntel = new ERR_MineIntel(this);
|
|
+ OnStop().Insert(ERR_StopMineIntel);
|
|
+ if (RplSession.Mode() != RplMode.Dedicated) GetGame().GetCallqueue().CallLater(ERR_UpdateMineMarkers, 1500, true);
|
|
+ }
|
|
+ bool ERR_RevealMineArea(vector area)
|
|
+ {
|
|
+ if (!Replication.IsServer() || m_ERRMineIntelStopped || this != ERR_GameMode.Get() || !Active()) return false;
|
|
+ if (!ERR_ForceRules.FiniteVector(area)) return false;
|
|
+ // Canonicalize at the publication boundary too: never publish precise points.
|
|
+ vector cell = Vector(ERR_DiscoveryMapRules.Coordinate(area[0], 1), 0, ERR_DiscoveryMapRules.Coordinate(area[2], 1));
|
|
+ if (m_ERRMineAreas.Contains(cell)) return true;
|
|
+ if (m_ERRMineAreas.Count() >= 32) return false;
|
|
+ m_ERRMineAreas.Insert(cell);
|
|
+ Replication.BumpMe();
|
|
+ return true;
|
|
+ }
|
|
+ protected void ERR_ClearMineMarkers()
|
|
+ {
|
|
+ SCR_MapMarkerManagerComponent manager = m_ERRMineMarkerOwner;
|
|
+ foreach (ERR_DiscoveryMarker marker : m_ERRMineMarkers)
|
|
+ {
|
|
+ if (manager) manager.ERR_RemoveDiscoveryMarker(marker);
|
|
+ else if (marker) marker.OnDelete();
|
|
+ }
|
|
+ m_ERRMineMarkers.Clear();
|
|
+ m_ERRMineMarkerOwner = null;
|
|
+ }
|
|
+ protected void ERR_UpdateMineMarkers()
|
|
+ {
|
|
+ if (!GetGame() || m_ERRMineIntelStopped || this != ERR_GameMode.Get() || !Active()) { ERR_ClearMineMarkers(); return; }
|
|
+ if (!GetGame().GetPlayerController()) return;
|
|
+ SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
|
|
+ if (manager != m_ERRMineMarkerOwner) ERR_ClearMineMarkers();
|
|
+ if (!manager || !manager.GetMarkerConfig()) return;
|
|
+ if (m_ERRMineAreas.Count() > 32) { ERR_ClearMineMarkers(); return; }
|
|
+ if (m_ERRMineMarkers.Count() > m_ERRMineAreas.Count()) ERR_ClearMineMarkers();
|
|
+ m_ERRMineMarkerOwner = manager;
|
|
+ for (int i = m_ERRMineMarkers.Count(); i < m_ERRMineAreas.Count(); i++)
|
|
+ {
|
|
+ ERR_DiscoveryMarker marker = new ERR_DiscoveryMarker();
|
|
+ marker.SetType(SCR_EMapMarkerType.PLACED_CUSTOM);
|
|
+ marker.SetMarkerConfigID(0);
|
|
+ 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);
|
|
+ m_ERRMineMarkers.Insert(marker);
|
|
+ }
|
|
+ }
|
|
+ protected void ERR_StopMineIntel()
|
|
+ {
|
|
+ if (m_ERRMineIntelStopped) return;
|
|
+ m_ERRMineIntelStopped = true;
|
|
+ if (m_ERRMineIntel) m_ERRMineIntel.Stop();
|
|
+ if (GetGame() && GetGame().GetCallqueue()) GetGame().GetCallqueue().Remove(ERR_UpdateMineMarkers);
|
|
+ ERR_ClearMineMarkers();
|
|
+ if (Replication.IsServer()) { m_ERRMineAreas.Clear(); Replication.BumpMe(); }
|
|
+ OnStop().Remove(ERR_StopMineIntel);
|
|
+ }
|
|
+ void ~ERR_GameMode()
|
|
+ {
|
|
+ if (GetGame() && GetGame().GetCallqueue()) GetGame().GetCallqueue().Remove(ERR_UpdateMineMarkers);
|
|
+ if (m_ERRMineIntel) m_ERRMineIntel.Stop();
|
|
+ ERR_ClearMineMarkers();
|
|
+ }
|
|
+}
|
|
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMine.c
|
|
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMine.c
|
|
@@ -55,6 +55,7 @@
|
|
protected ref SCR_AIBehaviorBase m_Action;
|
|
protected ref ERR_EngineerMineRemoval m_Removal;
|
|
protected ref array<IEntity> m_Attempted;
|
|
+ protected ref array<IEntity> m_IntelFolders = {};
|
|
protected vector m_Position;
|
|
protected vector m_Rally;
|
|
protected float m_Started = -1;
|
|
@@ -204,6 +205,7 @@
|
|
// The shared exact-item tombstone survives pickup, transfer and late callbacks.
|
|
m_Attempted.Insert(m_Mine);
|
|
if (!Carried()) return Refuse();
|
|
+ if (m_Mode.MineIntel()) m_Mode.MineIntel().Candidates(m_Actor, m_IntelFolders);
|
|
m_Started = now; m_LastTick = now; m_State = 0;
|
|
Move(m_Position);
|
|
ERR_HQ.Event("ENGINEER MINE", "original carried PMN4 assigned; one approach and finite 180s deadline");
|
|
@@ -261,7 +263,11 @@
|
|
if (!ERR_EngineerMineRules.MayArm(Placed(), Owned() && Quiet(), vector.DistanceXZ(m_Actor.GetOrigin(), m_Position), m_FriendlyNear)) return;
|
|
m_Trigger.SetUser(m_Actor);
|
|
m_Trigger.ActivateTrigger();
|
|
- if (m_Trigger.IsActivated()) ERR_HQ.Event("ENGINEER MINE", "original PMN4 armed after engineer withdrawal and physical friendly clearance");
|
|
+ if (m_Trigger.IsActivated())
|
|
+ {
|
|
+ if (m_Mode.MineIntel()) m_Mode.MineIntel().Seal(m_Actor, m_Mine, m_Position, m_IntelFolders);
|
|
+ ERR_HQ.Event("ENGINEER MINE", "original PMN4 armed after engineer withdrawal and physical friendly clearance");
|
|
+ }
|
|
Stop();
|
|
}
|
|
}
|
|
--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryService.c
|
|
+++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryService.c
|
|
@@ -53,6 +53,8 @@
|
|
foreach (IEntity folder : folders)
|
|
{
|
|
if (!folder || m_Spent.Contains(folder) || m_Spent.Count() >= 4096) continue;
|
|
+ bool mineRecovered = false;
|
|
+ if (m_Mode.MineIntel()) mineRecovered = m_Mode.MineIntel().Recover(player, folder);
|
|
array<ERR_LocationComponent> locations = {};
|
|
m_Mode.Locations(locations);
|
|
array<ERR_LocationComponent> eligible = {};
|
|
@@ -70,7 +72,7 @@
|
|
}
|
|
if (eligible.IsEmpty())
|
|
{
|
|
- if (!document && MarkSpent(folder)) m_Mode.ERR_IntelExhausted(!sealedTarget.IsEmpty());
|
|
+ if (!document && MarkSpent(folder) && !mineRecovered) m_Mode.ERR_IntelExhausted(!sealedTarget.IsEmpty());
|
|
continue;
|
|
}
|
|
ERR_LocationComponent chosen = eligible.GetRandomElement();
|
|
--- /dev/null
|
|
+++ b/tests/test_mine_intel.py
|
|
@@ -0,0 +1,96 @@
|
|
+"""Actual mine evidence/publication/marker bodies with native doubles, not engine/MP proof."""
|
|
+import os,re
|
|
+from pathlib import Path
|
|
+from policy_harness import ROOT,body,run
|
|
+P=Path(os.environ.get('ERR_MINE_INTEL_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/20_Intel/ERR_MineIntel.c'))
|
|
+def cpp(s):
|
|
+ s=s.replace('protected ','').replace('ref ','').replace('out ','').replace('null','nullptr').replace('array<','Array<').replace(' = {};',';').replace('foreach (','for (')
|
|
+ s=s.replace('Array<IEntity> folders)', 'Array<IEntity>& folders)').replace('Array<IEntity> candidates)', 'Array<IEntity>& candidates)').replace('!candidates || ','')
|
|
+ for t in ['InventoryStorageManagerComponent','InventoryItemComponent','SCR_PressureTriggerComponent','ERR_MineIntelRecord','ERR_DiscoveryMarker','SCR_MapMarkerManagerComponent']:
|
|
+ s=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',s)
|
|
+ for ns in ['Replication','ERR_GameMode','ERR_Actors','SCR_FactionManager','ERR_Resources','ERR_DiscoveryService','ERR_InfantryAmmoTransfer','ERR_ForceRules','ERR_EnemyLoadoutCatalog','ERR_DiscoveryMapRules','InventoryStorageManagerComponent','InventoryItemComponent','SCR_PressureTriggerComponent','SCR_MapMarkerManagerComponent','ERR_LocationKind','SCR_EMapMarkerType','vector']:
|
|
+ s=s.replace(ns+'.',ns+'::')
|
|
+ for obj in ['m_Mode','folder','actor','mine','player','inventory','item','trigger','record','known','manager','marker','m_ERRMineIntel']:
|
|
+ s=re.sub(r'\b'+obj+r'\.',obj+'->',s)
|
|
+ s=s.replace('bool revealed;', 'bool revealed=false;')
|
|
+ s=s.replace('GetPrefabData().','GetPrefabData()->').replace('Discovery().','Discovery()->').replace('Players().','Players()->').replace('GetGame().','GetGame()->').replace('GetCallqueue().','GetCallqueue()->').replace('OnStop().','OnStop()->')
|
|
+ s=re.sub(r'FindComponent\((\w+)\)',r'FindComponent("\1")',s)
|
|
+ s=s.replace('Remove(ERR_UpdateMineMarkers)','Remove(1)').replace('Remove(ERR_StopMineIntel)','Remove(2)')
|
|
+ return s
|
|
+service='\n'.join(body(P,x) for x in ['protected bool Current(','protected bool Generic(','void Candidates(','bool Seal(','bool Recover(','void Stop('])
|
|
+destructor=body(P,'void ~ERR_GameMode(')
|
|
+mode='\n'.join(body(P,x) for x in ['bool ERR_RevealMineArea(','protected void ERR_ClearMineMarkers(','protected void ERR_UpdateMineMarkers(','protected void ERR_StopMineIntel('])
|
|
+# Enforce references are not modelled by raw C++ pointers. Lifecycle bodies only.
|
|
+run(r'''
|
|
+#include <cassert>
|
|
+#include <cmath>
|
|
+#include <string>
|
|
+#include <vector>
|
|
+#include <algorithm>
|
|
+struct vector {float a[3]={0,0,0};float& operator[](int n){return a[n];}bool operator==(const vector&b)const{return a[0]==b.a[0]&&a[1]==b.a[1]&&a[2]==b.a[2];}static float Distance(vector a,vector b){return std::sqrt(std::pow(a[0]-b[0],2)+std::pow(a[1]-b[1],2)+std::pow(a[2]-b[2],2));}};
|
|
+vector Vector(float x,float y,float z){return {{x,y,z}};}
|
|
+template<class T>struct Array:std::vector<T>{int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Clear(){this->clear();}bool Contains(T a){return std::find(this->begin(),this->end(),a)!=this->end();}void Insert(T a){this->push_back(a);}};
|
|
+struct Entity;using IEntity=Entity*;struct Prefab {std::string name="intel";std::string GetPrefabName(){return name;}};
|
|
+struct InventoryStorageManagerComponent {Array<IEntity> folders;static InventoryStorageManagerComponent*Cast(void*p){return (InventoryStorageManagerComponent*)p;}};
|
|
+struct InventoryItemComponent {void*slot=nullptr;void*GetParentSlot(){return slot;}static InventoryItemComponent*Cast(void*p){return (InventoryItemComponent*)p;}};
|
|
+struct SCR_PressureTriggerComponent {bool active=true;bool IsActivated(){return active;}static SCR_PressureTriggerComponent*Cast(void*p){return (SCR_PressureTriggerComponent*)p;}};
|
|
+struct Entity {bool alive=true,player=false,objective=false;int faction=1,reads=0;IEntity owner=nullptr,parent=nullptr;vector position;Prefab prefab;InventoryStorageManagerComponent*inv=nullptr;InventoryItemComponent item;SCR_PressureTriggerComponent trigger;
|
|
+ Prefab*GetPrefabData(){reads++;return &prefab;}IEntity GetParent(){reads++;return parent;}vector GetOrigin(){reads++;return position;}
|
|
+ void*FindComponent(std::string t){reads++;if(t=="ERR_DocumentComponent")return objective?this:nullptr;if(t=="InventoryStorageManagerComponent")return inv;if(t=="InventoryItemComponent")return &item;if(t=="SCR_PressureTriggerComponent")return &trigger;return nullptr;}};
|
|
+namespace Replication {bool server=true;int bumps=0;bool IsServer(){return server;}void BumpMe(){bumps++;}}
|
|
+namespace ERR_Actors {bool CanFight(IEntity e){return e&&e->alive;}bool Alive(IEntity e){return e&&e->alive;}bool Player(IEntity e){return e&&e->player;}}
|
|
+namespace SCR_FactionManager {int SGetFaction(IEntity e){return e?e->faction:0;}}
|
|
+namespace ERR_Resources {std::string IntelFolder(){return "intel";}}
|
|
+namespace ERR_EnemyLoadoutCatalog {std::string MINE="mine";}
|
|
+namespace ERR_DiscoveryService {void Folders(InventoryStorageManagerComponent*i,Array<IEntity>&f){f=i->folders;}}
|
|
+namespace ERR_InfantryAmmoTransfer {bool StoredUnder(IEntity f,IEntity a){return f&&a&&f->owner==a;}}
|
|
+namespace ERR_ForceRules {bool FiniteVector(vector a){return std::isfinite(a[0])&&std::isfinite(a[1])&&std::isfinite(a[2]);}}
|
|
+namespace ERR_DiscoveryMapRules {int Coordinate(float x,int){return std::floor(x/500.f)*500+250;}}
|
|
+struct DiscoveryService {Array<IEntity> spent;bool IsSpent(IEntity f){return spent.Contains(f);}};
|
|
+struct PlayersService {IEntity current=nullptr;bool CurrentActor(IEntity e){return e&&e==current;}};
|
|
+struct Queue {int removed=0;void Remove(int){removed++;}};
|
|
+struct Game {bool controller=true;Queue queue;bool GetPlayerController(){return controller;}Queue*GetCallqueue(){return &queue;}}game;Game*currentGame=&game;Game*GetGame(){return currentGame;}
|
|
+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 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;
|
|
+ static ERR_GameMode*Get(){return current;}bool Active(){return active;}int EnemyFaction(){return enemy;}int PlayerFaction(){return player;}DiscoveryService*Discovery(){return &discovery;}PlayersService*Players(){return &players;}Invoker*OnStop(){return &stop;}
|
|
+ void Destroy();bool ERR_RevealMineArea(vector);void ERR_ClearMineMarkers();void ERR_UpdateMineMarkers();void ERR_StopMineIntel();};
|
|
+struct ERR_MineIntelRecord {IEntity Mine=nullptr;vector Area;Array<IEntity> Folders;bool Revealed=false;};
|
|
+struct ERR_MineIntel {ERR_GameMode*m_Mode;Array<ERR_MineIntelRecord*>m_Records;int m_Bindings=0;bool m_Stopped=false;ERR_MineIntel(ERR_GameMode*m):m_Mode(m){}
|
|
+'''+cpp(service)+r'''};
|
|
+'''+re.sub(r'\b(bool|void) (ERR_\w+)\(',r'\1 ERR_GameMode::\2(',cpp(mode))+cpp(destructor).replace('void ~ERR_GameMode()', 'void ERR_GameMode::Destroy()')+r'''
|
|
+struct Fixture {ERR_GameMode mode;ERR_MineIntel intel{&mode};Entity engineer,player,folder,mine;InventoryStorageManagerComponent ei,pi;Array<IEntity> candidates;
|
|
+ Fixture(){ERR_GameMode::current=&mode;Replication::server=true;currentGame=&game;game.controller=true;SCR_MapMarkerManagerComponent::current=nullptr;mode.m_ERRMineIntel=&intel;mode.players.current=&player;player.player=true;player.faction=2;engineer.inv=&ei;player.inv=πfolder.owner=&engineer;ei.folders.Insert(&folder);mine.prefab.name="mine";mine.position=Vector(1030,0,2340);}
|
|
+ void Capture(){intel.Candidates(&engineer,candidates);assert(candidates.Count()==1);}
|
|
+ void Seal(){Capture();assert(intel.Seal(&engineer,&mine,mine.position,candidates));assert(mode.m_ERRMineAreas.empty());}
|
|
+ void Transfer(){ei.folders.Clear();pi.folders.Insert(&folder);folder.owner=&player;}
|
|
+};
|
|
+int main(){
|
|
+ Fixture base;base.Seal();assert(!base.intel.Seal(&base.engineer,&base.mine,base.mine.position,base.candidates));base.Transfer();int reads=base.mine.reads;int actorReads=base.engineer.reads;base.mine.alive=false;base.mine.position=Vector(9000,0,9000);base.intel.Recover(&base.player,&base.folder);assert(base.mode.m_ERRMineAreas.Count()==1&&base.mode.m_ERRMineAreas[0]==Vector(1250,0,2250));assert(base.mine.reads==reads&&base.engineer.reads==actorReads);base.intel.Recover(&base.player,&base.folder);assert(base.mode.m_ERRMineAreas.Count()==1);
|
|
+ // Original site content is never modified or looked up by the evidence service.
|
|
+ // An objective, foreign/spent folder or a newly issued folder cannot be sealed.
|
|
+ for(int fault=0;fault<6;fault++){Fixture f;f.Capture();Entity newFolder;newFolder.owner=&f.engineer;
|
|
+ switch(fault){case 0:f.folder.objective=true;break;case 1:f.folder.owner=&f.player;break;case 2:f.mode.discovery.spent.Insert(&f.folder);break;case 3:f.ei.folders.Clear();f.ei.folders.Insert(&newFolder);break;case 4:f.engineer.faction=2;break;case 5:f.engineer.player=true;break;}
|
|
+ assert(!f.intel.Seal(&f.engineer,&f.mine,f.mine.position,f.candidates)&&f.intel.m_Records.empty()&&f.mode.m_ERRMineAreas.empty());}
|
|
+ // Unarmed, moved, destroyed, attached and unrelated mines cannot create records.
|
|
+ for(int fault=0;fault<5;fault++){Fixture f;f.Capture();vector position=f.mine.position;switch(fault){case 0:f.mine.trigger.active=false;break;case 1:f.mine.position=Vector(9999,0,0);break;case 2:f.mine.alive=false;break;case 3:f.mine.item.slot=&f.ei;break;case 4:f.mine.prefab.name="other";break;}assert(!f.intel.Seal(&f.engineer,&f.mine,position,f.candidates));}
|
|
+ for(int fault=0;fault<8;fault++){Fixture f;f.Seal();f.Transfer();ERR_GameMode newer;Entity other;switch(fault){case 0:f.mode.players.current=&other;break;case 1:f.player.alive=false;break;case 2:f.player.faction=1;break;case 3:f.folder.owner=&other;break;case 4:f.pi.folders.Clear();break;case 5:f.mode.discovery.spent.Insert(&f.folder);break;case 6:ERR_GameMode::current=&newer;break;case 7:f.intel.Stop();break;}f.intel.Recover(&f.player,&f.folder);assert(f.mode.m_ERRMineAreas.empty());}
|
|
+ Fixture caps;caps.Capture();caps.intel.m_Bindings=128;assert(!caps.intel.Seal(&caps.engineer,&caps.mine,caps.mine.position,caps.candidates));caps.intel.m_Bindings=0;caps.intel.m_Records.resize(32,nullptr);assert(!caps.intel.Seal(&caps.engineer,&caps.mine,caps.mine.position,caps.candidates));
|
|
+ Fixture publish;for(int i=0;i<32;i++)assert(publish.mode.ERR_RevealMineArea(Vector(i*500,0,0)));assert(publish.mode.ERR_RevealMineArea(Vector(10,0,20)));assert(!publish.mode.ERR_RevealMineArea(Vector(32000,0,0)));assert(!publish.mode.ERR_RevealMineArea(Vector(NAN,0,0)));
|
|
+ // Failed publication must remain recoverable instead of consuming warning.
|
|
+ Fixture retry;retry.Seal();retry.Transfer();for(int i=0;i<32;i++)retry.mode.m_ERRMineAreas.Insert(Vector(i*500+250,0,250));retry.intel.Recover(&retry.player,&retry.folder);assert(!retry.intel.m_Records[0]->Revealed);retry.mode.m_ERRMineAreas.Clear();retry.intel.Recover(&retry.player,&retry.folder);assert(retry.intel.m_Records[0]->Revealed);
|
|
+ // Marker polling waits through preparation/JIP and missing native dependencies.
|
|
+ Fixture markers;markers.mode.m_ERRMineAreas.Insert(Vector(1250,0,2250));markers.mode.active=false;markers.mode.ERR_UpdateMineMarkers();assert(!markers.mode.m_ERRMineIntelStopped);markers.mode.active=true;markers.mode.ERR_UpdateMineMarkers();assert(markers.mode.m_ERRMineMarkers.empty());SCR_MapMarkerManagerComponent first,second;SCR_MapMarkerManagerComponent::current=&first;first.config=false;markers.mode.ERR_UpdateMineMarkers();assert(first.inserted.empty());first.config=true;game.controller=false;markers.mode.ERR_UpdateMineMarkers();assert(first.inserted.empty());game.controller=true;markers.mode.ERR_UpdateMineMarkers();markers.mode.ERR_UpdateMineMarkers();assert(first.inserted.Count()==1&&first.inserted[0]->position==Vector(1250,0,2250));assert(first.inserted[0]->label=="Reported mine area (500 m); may still be active");
|
|
+ SCR_MapMarkerManagerComponent::current=&second;markers.mode.ERR_UpdateMineMarkers();assert(first.removed.Count()==1&&second.inserted.Count()==1);markers.mode.active=false;markers.mode.ERR_UpdateMineMarkers();assert(second.removed.Count()==1);markers.mode.active=true;markers.mode.ERR_UpdateMineMarkers();assert(second.inserted.Count()==2);markers.mode.ERR_StopMineIntel();markers.mode.ERR_StopMineIntel();assert(markers.intel.m_Stopped&&markers.mode.m_ERRMineMarkers.empty()&&markers.mode.m_ERRMineAreas.empty()&&second.removed.Count()==2);markers.mode.ERR_UpdateMineMarkers();assert(second.inserted.Count()==2);
|
|
+ // Shrink rebuild must retain manager ownership for replacement marker cleanup.
|
|
+ Fixture shrink;SCR_MapMarkerManagerComponent shrinking;SCR_MapMarkerManagerComponent::current=&shrinking;shrink.mode.m_ERRMineAreas.Insert(Vector(250,0,250));shrink.mode.m_ERRMineAreas.Insert(Vector(750,0,250));shrink.mode.ERR_UpdateMineMarkers();assert(shrinking.inserted.Count()==2);shrink.mode.m_ERRMineAreas.resize(1);shrink.mode.ERR_UpdateMineMarkers();assert(shrinking.removed.Count()==2&&shrinking.inserted.Count()==3&&shrink.mode.m_ERRMineMarkerOwner==&shrinking);shrink.mode.ERR_StopMineIntel();assert(shrinking.removed.Count()==3);
|
|
+ Fixture destroyed;SCR_MapMarkerManagerComponent deletion;SCR_MapMarkerManagerComponent::current=&deletion;destroyed.mode.m_ERRMineAreas.Insert(Vector(250,0,250));destroyed.mode.ERR_UpdateMineMarkers();int removedCallbacks=game.queue.removed;destroyed.mode.Destroy();assert(destroyed.intel.m_Stopped&&destroyed.mode.m_ERRMineMarkers.empty()&&deletion.removed.Count()==1&&game.queue.removed==removedCallbacks+1);
|
|
+ Fixture bindings;Array<Entity> moreFolders;moreFolders.resize(16);bindings.ei.folders.Clear();for(auto&f:moreFolders){f.owner=&bindings.engineer;bindings.ei.folders.Insert(&f);}bindings.intel.Candidates(&bindings.engineer,bindings.candidates);assert(bindings.candidates.Count()==16);Array<Entity> mines;mines.resize(9);for(int i=0;i<9;i++){mines[i].prefab.name="mine";mines[i].position=Vector(30,0,40);bool sealed=bindings.intel.Seal(&bindings.engineer,&mines[i],mines[i].position,bindings.candidates);assert(sealed==(i<8));}assert(bindings.intel.m_Bindings==128&&bindings.intel.m_Records.Count()==8);
|
|
+ Fixture replica;Replication::server=false;assert(!replica.mode.ERR_RevealMineArea(Vector(1000,0,2000)));replica.intel.Candidates(&replica.engineer,replica.candidates);assert(replica.candidates.empty());
|
|
+}
|
|
+''')
|
|
+print('PASS: actual mine evidence sealing/recovery, historical coarse publication, caps, canonical ownership, marker join retry/owner-change/stop; native arming and replicated vector-array transport untested')
|