Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
197 lines
27 KiB
Plaintext
197 lines
27 KiB
Plaintext
--- a/tests/test_engineer_mine.py
|
|
+++ b/tests/test_engineer_mine.py
|
|
@@ -18,7 +18,7 @@
|
|
for field in ['StatusSampler','InfantryCombat','m_UtilityComponent']:
|
|
s=s.replace(field+'.',field+'->')
|
|
s=s.replace('InfantryCombat->Held','InfantryCombat.Held')
|
|
- s=s.replace('GetPrefabData().','GetPrefabData()->').replace('EnemyFaction().','EnemyFaction()->').replace('Forces().','Forces()->')
|
|
+ s=s.replace('GetPrefabData().','GetPrefabData()->').replace('EnemyFaction().','EnemyFaction()->').replace('Forces().','Forces()->').replace('MineIntel().','MineIntel()->')
|
|
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->')
|
|
s=re.sub(r'FindComponent\((\w+)\)',r'FindComponent("\1")',s)
|
|
s=s.replace('CheckFriendly, queryFlags: EQueryEntitiesFlags.ALL','[this](IEntity entity){return CheckFriendly(entity);}, 1')
|
|
@@ -39,7 +39,7 @@
|
|
struct vector {float x=0,y=0,z=0;float LengthSq(){return x*x+y*y+z*z;}
|
|
static float Distance(vector a,vector b){return std::sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)+(a.z-b.z)*(a.z-b.z));}
|
|
static float DistanceXZ(vector a,vector b){a.y=b.y=0;return Distance(a,b);}};
|
|
-template<class T>struct Array:std::vector<T>{int Count(){return this->size();}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}void Insert(T t){this->push_back(t);}};
|
|
+template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;int Count(){return this->size();}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}void Insert(T t){this->push_back(t);}};
|
|
struct Entity;using IEntity=Entity*;struct SCR_AIGroup;struct SCR_ChimeraAIAgent;struct SCR_InventoryStorageManagerComponent;struct ERR_EngineerMineRemoval;
|
|
struct Faction {bool enemy=false;bool IsFactionEnemy(Faction* f){return f&&f->enemy;}};
|
|
struct Physics {vector velocity;vector GetVelocity(){return velocity;}};
|
|
@@ -68,7 +68,11 @@
|
|
struct ERR_HQReport{};struct ERR_ForceRecord {SCR_AIGroup* Group=nullptr;bool Physical=true,Eliminated=false;string SourceId="original";int InitialStrength=1;};
|
|
struct Ledger {ERR_ForceRecord* record=nullptr;ERR_ForceRecord* FindGroup(SCR_AIGroup*){return record;}};
|
|
struct ERR_HQUnit {SCR_AIGroup* Group=nullptr;ERR_HQTask* Task=nullptr;ERR_HQReport* Assignment=nullptr;ERR_HQTask* PendingTask=nullptr;ERR_HQReport* PendingOrder=nullptr;Sampler* StatusSampler=nullptr;struct{bool Held=false;}InfantryCombat;bool ReservedForSupport=false;int LogisticsReservation=0;string ForceSourceId="original";};
|
|
-struct ERR_GameMode {static inline ERR_GameMode* current=nullptr;bool active=true;Faction* faction=nullptr;Ledger* ledger=nullptr;static ERR_GameMode* Get(){return current;}bool Active(){return active;}Faction* EnemyFaction(){return faction;}Ledger* Forces(){return ledger;}};
|
|
+struct MineIntel {int candidates=0,seals=0;IEntity capturedActor=nullptr,sealedMine=nullptr;vector position;Array<IEntity> available,captured,sealed;
|
|
+ void Candidates(IEntity actor,Array<IEntity>&out){candidates++;capturedActor=actor;out=available;captured=out;}
|
|
+ void Seal(IEntity actor,IEntity mine,vector p,Array<IEntity>&folders){assert(actor==capturedActor&&mine&&mine->trigger&&mine->trigger->active);seals++;sealedMine=mine;position=p;sealed=folders;assert(folders==captured);}
|
|
+};
|
|
+struct ERR_GameMode {struct MineIntel mineIntel;bool haveMineIntel=true;struct MineIntel* MineIntel(){return haveMineIntel?&mineIntel:nullptr;}static inline ERR_GameMode* current=nullptr;bool active=true;Faction* faction=nullptr;Ledger* ledger=nullptr;static ERR_GameMode* Get(){return current;}bool Active(){return active;}Faction* EnemyFaction(){return faction;}Ledger* Forces(){return ledger;}};
|
|
namespace ERR_ForceRules {bool Finite(float f){return std::isfinite(f);}bool FiniteVector(vector v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}}
|
|
namespace Replication {bool server=true;bool IsServer(){return server;}}
|
|
namespace ERR_HunterCombat {bool engaged=false;bool Active(SCR_AIGroup*){return engaged;}}
|
|
@@ -88,19 +92,26 @@
|
|
'''+cpp(callback)+r'''};
|
|
bool SCR_InventoryStorageManagerComponent::TryRemoveItemFromInventory(IEntity,void*,ERR_EngineerMineRemoval* cb){calls++;callback=cb;if(inlineResult>0)cb->OnComplete();if(inlineResult<0)cb->OnFailed();return accepted;}
|
|
struct ERR_EngineerMine {
|
|
- ERR_GameMode* m_Mode=nullptr;ERR_HQUnit* m_Unit=nullptr;SCR_AIGroup* m_Group=nullptr;ERR_HQTask* m_Task=nullptr;ERR_HQReport* m_Assignment=nullptr;ERR_ForceRecord* m_Record=nullptr;string m_SourceId;int m_Strength=0;IEntity m_Actor=nullptr;int m_Slot=0;IEntity m_Mine=nullptr;SCR_PressureTriggerComponent* m_Trigger=nullptr;SCR_InventoryStorageManagerComponent* m_Inventory=nullptr;SCR_AIUtilityComponent* m_Utility=nullptr;SCR_AIBehaviorBase* m_Action=nullptr;ERR_EngineerMineRemoval* m_Removal=nullptr;Array<IEntity>* m_Attempted=nullptr;vector m_Position,m_Rally;float m_Started=-1,m_LastTick=-1,m_PreparedAt=-1,m_NextClearance=0;int m_State=0,m_Checked=0;bool m_FriendlyNear=false,m_Begun=false,m_Stopped=false;
|
|
+ ERR_GameMode* m_Mode=nullptr;ERR_HQUnit* m_Unit=nullptr;SCR_AIGroup* m_Group=nullptr;ERR_HQTask* m_Task=nullptr;ERR_HQReport* m_Assignment=nullptr;ERR_ForceRecord* m_Record=nullptr;string m_SourceId;int m_Strength=0;IEntity m_Actor=nullptr;int m_Slot=0;IEntity m_Mine=nullptr;SCR_PressureTriggerComponent* m_Trigger=nullptr;SCR_InventoryStorageManagerComponent* m_Inventory=nullptr;SCR_AIUtilityComponent* m_Utility=nullptr;SCR_AIBehaviorBase* m_Action=nullptr;ERR_EngineerMineRemoval* m_Removal=nullptr;Array<IEntity>* m_Attempted=nullptr;Array<IEntity> m_IntelFolders;vector m_Position,m_Rally;float m_Started=-1,m_LastTick=-1,m_PreparedAt=-1,m_NextClearance=0;int m_State=0,m_Checked=0;bool m_FriendlyNear=false,m_Begun=false,m_Stopped=false;
|
|
'''+cpp(methods)+r'''};
|
|
struct Fixture {Faction faction,hostile{true};SCR_AIGroup group;SCR_AIUtilityComponent utility;SCR_ChimeraAIAgent agent;SCR_InventoryStorageManagerComponent inventory;Entity actor,mine;Prefab prefab;Physics physics;InventoryItemComponent item;SCR_PressureTriggerComponent trigger;Sampler roster;ERR_HQTask task;ERR_HQReport report;ERR_ForceRecord record;Ledger ledger;ERR_GameMode mode;ERR_HQUnit unit;Array<IEntity> attempted;ERR_EngineerMine controller{&attempted};
|
|
Fixture(){ERR_GameMode::current=&mode;Replication::server=true;ERR_HunterCombat::engaged=false;currentGame=&game;game.world.query.clear();game.world.queries=0;
|
|
group.faction=&faction;actor.group=&group;actor.faction=&faction;actor.agent=&agent;actor.inventory=&inventory;agent.m_UtilityComponent=&utility;
|
|
mine.prefab=&prefab;mine.physics=&physics;mine.item=&item;mine.trigger=&trigger;mine.stored=true;mine.storageOwner=&actor;item.slot=&inventory;inventory.items.Insert(&mine);roster.members.Insert(&actor);
|
|
record.Group=&group;ledger.record=&record;mode.faction=&faction;mode.ledger=&ledger;unit.Group=&group;unit.Task=&task;unit.Assignment=&report;unit.StatusSampler=&roster;}
|
|
- void Begin(){assert(controller.Begin(&mode,&unit,&task,{30,0,0},1000));assert(attempted.Count()==1&&utility.additions==1);}
|
|
+ void Begin(){assert(controller.Begin(&mode,&unit,&task,{30,0,0},1000));assert(attempted.Count()==1&&utility.additions==1&&mode.mineIntel.candidates==1&&mode.mineIntel.capturedActor==&actor&&mode.mineIntel.seals==0);}
|
|
void Prepare(){Begin();actor.position={30,0,0};controller.Tick(2000);assert(controller.m_State==1&&utility.additions==2);controller.Tick(12000);assert(inventory.calls==1);assert(controller.m_State==2||controller.Finished());}
|
|
void Drop(){mine.position=actor.position;mine.stored=false;mine.storageOwner=nullptr;item.slot=nullptr;inventory.items.clear();}
|
|
void Withdraw(){Prepare();Drop();inventory.callback->OnComplete();controller.Tick(12001);assert(controller.m_State==3&&mine.moves==1&&utility.additions==3);}
|
|
};
|
|
int main(){
|
|
+ // Candidate list is captured once at accepted Begin; only successful native
|
|
+ // activation seals that same original list, even if carried folders change.
|
|
+ Fixture evidence;Entity originalFolder,lateFolder;evidence.mode.mineIntel.available.Insert(&originalFolder);evidence.Withdraw();
|
|
+ evidence.mode.mineIntel.available={&lateFolder};evidence.actor.position={0,0,0};evidence.controller.Tick(13000);
|
|
+ assert(evidence.mode.mineIntel.candidates==1&&evidence.mode.mineIntel.seals==1&&evidence.mode.mineIntel.sealed==Array<IEntity>{&originalFolder});
|
|
+ assert(evidence.mode.mineIntel.sealedMine==&evidence.mine&&evidence.mode.mineIntel.position.x==30);evidence.controller.Stop();evidence.controller.Tick(14000);assert(evidence.mode.mineIntel.seals==1);
|
|
+ Fixture noService;noService.mode.haveMineIntel=false;assert(noService.controller.Begin(&noService.mode,&noService.unit,&noService.task,{30,0,0},1000));assert(!noService.mode.mineIntel.candidates);
|
|
// Immutable total deadline, rollback/NaN clock refusal and continuous dwell.
|
|
assert(ERR_EngineerMineRules::Within(1000,1000,180999));assert(!ERR_EngineerMineRules::Within(1000,1000,181000));
|
|
assert(!ERR_EngineerMineRules::Within(1000,2000,1999));assert(!ERR_EngineerMineRules::Within(1000,2000,NAN));
|
|
@@ -111,7 +122,7 @@
|
|
dwell.actor.position={30,0,0};dwell.controller.Tick(12001);dwell.controller.Tick(22000);assert(!dwell.inventory.calls);dwell.controller.Tick(22001);assert(dwell.inventory.calls==1&&dwell.controller.m_Started==1000);
|
|
// Accepted unknown removal cannot retry, place or arm; original deadline wins.
|
|
Fixture unknown;unknown.Prepare();unknown.Drop();unknown.controller.Tick(12001);unknown.controller.Tick(180999);assert(unknown.inventory.calls==1&&!unknown.mine.moves&&!unknown.trigger.calls);
|
|
- auto held=unknown.inventory.callback;unknown.controller.Tick(181000);assert(unknown.controller.Finished());held->OnComplete();unknown.controller.Tick(181001);assert(!unknown.mine.moves&&!unknown.trigger.calls&&unknown.attempted.Count()==1);
|
|
+ auto held=unknown.inventory.callback;unknown.controller.Tick(181000);assert(unknown.controller.Finished());held->OnComplete();unknown.controller.Tick(181001);assert(!unknown.mine.moves&&!unknown.trigger.calls&&unknown.attempted.Count()==1&&unknown.mode.mineIntel.seals==0);
|
|
// Synchronous completion on a false native return is evidence, not a second call.
|
|
Fixture inlineOk;inlineOk.inventory.accepted=false;inlineOk.inventory.inlineResult=1;inlineOk.Prepare();inlineOk.Drop();inlineOk.controller.Tick(12001);assert(inlineOk.controller.m_State==3&&inlineOk.inventory.calls==1);
|
|
Fixture refused;refused.inventory.accepted=false;refused.Prepare();assert(refused.controller.Finished()&&refused.inventory.calls==1);refused.inventory.callback->OnComplete();refused.Drop();refused.controller.Tick(13000);assert(!refused.trigger.calls&&!refused.mine.moves);
|
|
@@ -126,7 +137,7 @@
|
|
// Native ownership still attached to the original actor may settle normally.
|
|
Fixture settling;settling.Prepare();settling.inventory.callback->OnComplete();settling.controller.Tick(12001);assert(!settling.controller.Finished()&&!settling.mine.moves);settling.Drop();settling.controller.Tick(12002);assert(settling.controller.m_State==3&&settling.mine.moves==1);
|
|
// Cancellation retains callback and item tombstone and only completes own action.
|
|
- Fixture stopped;stopped.Prepare();SCR_AIBehaviorBase unrelated;stopped.utility.actions.Insert(&unrelated);auto owned=stopped.controller.m_Action;stopped.controller.Stop();stopped.controller.Stop();stopped.Drop();stopped.inventory.callback->OnComplete();stopped.controller.Tick(13000);assert(owned->completed==1&&!unrelated.completed&&!stopped.mine.moves&&!stopped.trigger.calls&&stopped.attempted.Contains(&stopped.mine));
|
|
+ Fixture stopped;stopped.Prepare();SCR_AIBehaviorBase unrelated;stopped.utility.actions.Insert(&unrelated);auto owned=stopped.controller.m_Action;stopped.controller.Stop();stopped.controller.Stop();stopped.Drop();stopped.inventory.callback->OnComplete();stopped.controller.Tick(13000);assert(owned->completed==1&&!unrelated.completed&&!stopped.mine.moves&&!stopped.trigger.calls&&stopped.attempted.Contains(&stopped.mine)&&stopped.mode.mineIntel.seals==0);
|
|
// Original identity changes abort before irreversible work, including replacement
|
|
// task/report/ledger, recycled formation slot, capture and changed inventory.
|
|
for(int fault=0;fault<13;fault++){Fixture f;f.Begin();ERR_HQTask replacementTask;ERR_HQReport replacementReport;ERR_ForceRecord replacementRecord;SCR_InventoryStorageManagerComponent replacementInventory;SCR_AIUtilityComponent replacementUtility;
|
|
@@ -136,7 +147,7 @@
|
|
for(int fault=0;fault<3;fault++){Fixture f;f.Withdraw();if(fault==0)f.mine.parent=&f.actor;if(fault==1)f.mine.position={40,0,0};if(fault==2)f.trigger.active=true;f.actor.position={0,0,0};f.controller.Tick(13000);assert(f.controller.Finished()&&!f.trigger.calls);}
|
|
Fixture arm;arm.Withdraw();arm.actor.position={10.001f,0,0};arm.controller.Tick(13000);assert(!arm.trigger.calls);arm.actor.position={10,0,0};
|
|
Entity friendly;FactionAffiliationComponent affiliation;affiliation.faction=&arm.faction;friendly.affiliation=&affiliation;game.world.query.Insert(&friendly);arm.controller.Tick(14000);assert(!arm.trigger.calls);game.world.query.clear();arm.controller.Tick(14500);assert(!arm.trigger.calls);arm.controller.Tick(15000);assert(arm.trigger.calls==1&&arm.trigger.user==&arm.actor&&arm.controller.Finished());arm.controller.Tick(16000);assert(arm.trigger.calls==1);
|
|
- Fixture rejected;rejected.Withdraw();rejected.trigger.allow=false;rejected.actor.position={0,0,0};rejected.controller.Tick(13000);assert(rejected.trigger.calls==1&&!rejected.trigger.active&&rejected.controller.Finished());rejected.controller.Tick(14000);assert(rejected.trigger.calls==1);
|
|
+ Fixture rejected;rejected.Withdraw();rejected.trigger.allow=false;rejected.actor.position={0,0,0};rejected.controller.Tick(13000);assert(rejected.trigger.calls==1&&!rejected.trigger.active&&rejected.controller.Finished());rejected.controller.Tick(14000);assert(rejected.trigger.calls==1&&rejected.mode.mineIntel.seals==0);
|
|
Fixture moving;moving.Withdraw();moving.actor.position={0,0,0};moving.physics.velocity={1,0,0};moving.controller.Tick(13000);assert(!moving.trigger.calls);moving.physics.velocity={0,0,0};moving.controller.Tick(14000);assert(moving.trigger.calls==1);
|
|
Fixture bounded;bounded.Withdraw();bounded.actor.position={0,0,0};Entity prop;prop.character=false;for(int i=0;i<257;i++)game.world.query.Insert(&prop);bounded.controller.Tick(13000);assert(!bounded.trigger.calls&&bounded.controller.m_FriendlyNear);
|
|
// Exact original item stays quarantined across controllers; never adopted twice.
|
|
@@ -150,7 +161,7 @@
|
|
// Every irreversible stage repeats original task/actor custody checks.
|
|
for(int fault=0;fault<7;fault++){Fixture f;f.Withdraw();f.actor.position={0,0,0};ERR_HQTask newer;ERR_HQReport newerReport;ERR_GameMode newerMode;SCR_PressureTriggerComponent foreignTrigger;
|
|
switch(fault){case 0:f.unit.Task=&newer;break;case 1:f.unit.Assignment=&newerReport;break;case 2:f.actor.alive=false;break;case 3:f.actor.player=true;break;case 4:ERR_GameMode::current=&newerMode;break;case 5:f.mine.trigger=&foreignTrigger;break;case 6:f.task.ExpiresAt=13000;break;}
|
|
- f.controller.Tick(13000);assert(f.controller.Finished()&&!f.trigger.calls&&!foreignTrigger.calls);}
|
|
+ f.controller.Tick(13000);assert(f.controller.Finished()&&!f.trigger.calls&&!foreignTrigger.calls&&f.mode.mineIntel.seals==0);}
|
|
// Unknown or unconscious friendlies still block; hostile bodies and dead bodies
|
|
// do not fabricate a friendly. Static scenery isn't a soldier or vehicle.
|
|
Fixture friendPolicy;friendPolicy.Begin();Entity unknownPerson,unconscious,dead,enemy,scenery;DamageManagerComponent destroyed;destroyed.destroyed=true;dead.damage=&destroyed;FactionAffiliationComponent enemyAff;enemyAff.faction=&friendPolicy.hostile;enemy.affiliation=&enemyAff;scenery.character=false;
|
|
@@ -158,7 +169,7 @@
|
|
friendPolicy.controller.m_FriendlyNear=false;unconscious.alive=false;assert(!friendPolicy.controller.CheckFriendly(&unconscious)&&friendPolicy.controller.m_FriendlyNear);
|
|
friendPolicy.controller.m_FriendlyNear=false;assert(friendPolicy.controller.CheckFriendly(&dead)&&!friendPolicy.controller.m_FriendlyNear);assert(friendPolicy.controller.CheckFriendly(&enemy));assert(friendPolicy.controller.CheckFriendly(&scenery));
|
|
// Already-armed/default-live/unknown stock is not adopted or newly quarantined.
|
|
- for(int fault=0;fault<5;fault++){Fixture f;if(fault==0)f.trigger.active=true;if(fault==1)f.trigger.byDefault=true;if(fault==2)f.mine.alive=false;if(fault==3)f.prefab.name="unrelated";if(fault==4)f.mine.storageOwner=nullptr;assert(!f.controller.Begin(&f.mode,&f.unit,&f.task,{30,0,0},1000));assert(f.attempted.empty()&&!f.utility.additions&&!f.inventory.calls);}
|
|
+ for(int fault=0;fault<5;fault++){Fixture f;if(fault==0)f.trigger.active=true;if(fault==1)f.trigger.byDefault=true;if(fault==2)f.mine.alive=false;if(fault==3)f.prefab.name="unrelated";if(fault==4)f.mine.storageOwner=nullptr;assert(!f.controller.Begin(&f.mode,&f.unit,&f.task,{30,0,0},1000));assert(f.attempted.empty()&&!f.utility.additions&&!f.inventory.calls&&f.mode.mineIntel.candidates==0&&f.mode.mineIntel.seals==0);}
|
|
Fixture clock;clock.Begin();clock.controller.Tick(2000);clock.controller.Tick(1999);assert(clock.controller.Finished());
|
|
Fixture server;server.Begin();Replication::server=false;server.controller.Tick(2000);assert(server.controller.Finished()&&!server.inventory.calls);Replication::server=true;
|
|
|
|
--- /dev/null
|
|
+++ b/tests/test_mine_intel_discovery_integration.py
|
|
@@ -0,0 +1,86 @@
|
|
+"""Execute the complete production Recover body with controlled inventory and services.
|
|
+This verifies integration ordering and retained site-clue semantics, not native custody.
|
|
+"""
|
|
+import os,re
|
|
+from pathlib import Path
|
|
+from policy_harness import ROOT,body,run
|
|
+P=Path(os.environ.get('ERR_DISCOVERY_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryService.c'))
|
|
+s=body(P,'protected void Recover(')
|
|
+s=s.replace('protected ','').replace('null','nullptr').replace('foreach (','for (').replace('array<','Array<')
|
|
+for typ in ('InventoryStorageManagerComponent','ERR_DocumentComponent','ERR_LocationComponent'):
|
|
+ s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
|
|
+s=s.replace('Array<ERR_LocationComponent>','Array<ERR_LocationComponent*>').replace('bool mineRecovered;', 'bool mineRecovered=false;')
|
|
+for ns in ('Replication','ERR_Actors','SCR_FactionManager','InventoryStorageManagerComponent','ERR_DocumentComponent','ERR_LocationKind','Math'):
|
|
+ s=s.replace(ns+'.',ns+'::')
|
|
+for obj in ('m_Mode','player','folder','location','document','chosen'):
|
|
+ s=re.sub(r'\b'+obj+r'\.',obj+'->',s)
|
|
+for method in ('Players','Evidence','Settings','MineIntel','GetGame','GetPlayerManager'):
|
|
+ s=s.replace(method+'().',method+'()->')
|
|
+s=re.sub(r'FindComponent\((\w+)\)',r'FindComponent("\1")',s)
|
|
+run(r'''
|
|
+#include <cassert>
|
|
+#include <string>
|
|
+#include <vector>
|
|
+#include <algorithm>
|
|
+struct string:std::string{using std::string::string;using std::string::operator=;bool IsEmpty(){return empty();}};
|
|
+template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;int Count(){return this->size();}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}void Insert(T t){this->push_back(t);}bool IsEmpty(){return this->empty();}T GetRandomElement(){return this->front();}};
|
|
+struct Entity;using IEntity=Entity*;
|
|
+struct Faction{};
|
|
+struct ERR_DocumentComponent{string target;string TargetId(){return target;}static ERR_DocumentComponent* Cast(void*p){return (ERR_DocumentComponent*)p;}};
|
|
+struct InventoryStorageManagerComponent{Array<IEntity> folders;static InventoryStorageManagerComponent* Cast(void*p){return (InventoryStorageManagerComponent*)p;}};
|
|
+struct Entity{bool alive=true,canonical=true;Faction*faction=nullptr;InventoryStorageManagerComponent*inventory=nullptr;ERR_DocumentComponent*document=nullptr;void*FindComponent(std::string type){if(type=="InventoryStorageManagerComponent")return inventory;if(type=="ERR_DocumentComponent")return document;return nullptr;}};
|
|
+namespace Replication{bool server=true;bool IsServer(){return server;}}
|
|
+namespace ERR_Actors{bool Alive(IEntity e){return e&&e->alive;}}
|
|
+namespace SCR_FactionManager{Faction*SGetFaction(IEntity e){return e?e->faction:nullptr;}}
|
|
+namespace ERR_LocationKind{enum{PRISON,RADIO,EXTRACTION,DEPOT};}
|
|
+namespace Math{int RandomInt(int,int){return 99;}}
|
|
+struct ERR_LocationComponent{string id;int kind=ERR_LocationKind::RADIO,knowledge=0,calls=0;bool allow=true;int Kind(){return kind;}int Knowledge(){return knowledge;}string Id(){return id;}bool Discover(int,int){calls++;if(!allow)return false;knowledge++;return true;}};
|
|
+struct Roster{IEntity CurrentActor(IEntity e){return e&&e->canonical?e:nullptr;}};
|
|
+struct Evidence{IEntity sealed=nullptr;string target;string Target(IEntity e){return e==sealed?target:"";}};
|
|
+struct Settings{int Runtime(string){return 15;}};
|
|
+struct MineIntel{bool newlyRecovered=false;int calls=0;Array<IEntity> recovered;Array<IEntity>*spent=nullptr;IEntity lastPlayer=nullptr;bool Recover(IEntity player,IEntity folder){assert(spent&&!spent->Contains(folder));calls++;lastPlayer=player;recovered.Insert(folder);return newlyRecovered;}};
|
|
+struct Mode{Roster roster;struct Evidence evidence;struct Settings settings;struct MineIntel mine;Faction*faction=nullptr;bool haveMine=true;Array<ERR_LocationComponent*>locations;int exhausted=0;
|
|
+ Roster*Players(){return &roster;}Faction*PlayerFaction(){return faction;}struct Evidence*Evidence(){return &evidence;}struct Settings*Settings(){return &settings;}struct MineIntel*MineIntel(){return haveMine?&mine:nullptr;}void Locations(Array<ERR_LocationComponent*>&out){out=locations;}void ERR_IntelExhausted(bool){exhausted++;}};
|
|
+struct PlayerManager{int GetPlayerIdFromControlledEntity(IEntity){return 1;}};struct Game{PlayerManager manager;PlayerManager*GetPlayerManager(){return &manager;}}game;Game*GetGame(){return &game;}
|
|
+template<class...T>void PrintFormat(T...){}
|
|
+struct Discovery{Mode*m_Mode=nullptr;Array<IEntity>m_Spent;void Folders(InventoryStorageManagerComponent*i,Array<IEntity>&out){out=i->folders;}bool MarkSpent(IEntity e){if(!m_Spent.Contains(e))m_Spent.Insert(e);return true;}
|
|
+'''+s+r'''
|
|
+};
|
|
+struct Fixture{Faction friendly,foreign;Mode mode;Discovery discovery;Entity player,second,folder;InventoryStorageManagerComponent firstInv,secondInv;ERR_DocumentComponent objective;ERR_LocationComponent radio,other;
|
|
+ Fixture(){Replication::server=true;mode.faction=&friendly;discovery.m_Mode=&mode;mode.mine.spent=&discovery.m_Spent;player.faction=&friendly;second.faction=&friendly;player.inventory=&firstInv;second.inventory=&secondInv;firstInv.folders.Insert(&folder);radio.id="radio";other.id="other";mode.locations={&other,&radio};}}
|
|
+;
|
|
+int main(){
|
|
+ // Authored target wins over incidental locations, and each exact document
|
|
+ // advances ordinary discovery once across transfer between two valid players.
|
|
+ Fixture f;f.objective.target="radio";f.folder.document=&f.objective;
|
|
+ f.discovery.Recover(&f.player);assert(f.radio.knowledge==1&&f.other.knowledge==0&&f.discovery.m_Spent.Contains(&f.folder));
|
|
+ assert(f.mode.mine.calls==1&&f.mode.mine.lastPlayer==&f.player&&f.mode.mine.recovered[0]==&f.folder);
|
|
+ f.firstInv.folders.clear();f.secondInv.folders.Insert(&f.folder);f.discovery.Recover(&f.second);assert(f.radio.calls==1&&f.other.calls==0&&f.mode.mine.calls==1);
|
|
+ // Existing sealed generic-folder target survives the supplemental hook.
|
|
+ Fixture sealed;sealed.mode.evidence.sealed=&sealed.folder;sealed.mode.evidence.target="radio";sealed.discovery.Recover(&sealed.player);assert(sealed.radio.knowledge==1&&sealed.other.knowledge==0);
|
|
+ // Unknown authored target stays unspent; a supplement cannot consume it first.
|
|
+ Fixture missing;missing.objective.target="absent";missing.folder.document=&missing.objective;missing.discovery.Recover(&missing.player);assert(missing.discovery.m_Spent.IsEmpty()&&missing.radio.calls==0&&missing.other.calls==0);
|
|
+ // Failed site discovery also retains the original retry semantics.
|
|
+ Fixture denied;denied.objective.target="radio";denied.folder.document=&denied.objective;denied.radio.allow=false;denied.discovery.Recover(&denied.player);assert(denied.discovery.m_Spent.IsEmpty()&&denied.radio.calls==1);
|
|
+ // Generic exhausted clue retains the existing spent/exhaustion behavior.
|
|
+ Fixture exhausted;exhausted.radio.knowledge=2;exhausted.other.knowledge=2;exhausted.discovery.Recover(&exhausted.player);assert(exhausted.discovery.m_Spent.Contains(&exhausted.folder)&&exhausted.mode.exhausted==1);
|
|
+ // A newly published mine warning is useful even after site clues are exhausted.
|
|
+ // Spend the original generic folder once without the contradictory exhaustion notice.
|
|
+ Fixture mineOnly;mineOnly.mode.mine.newlyRecovered=true;mineOnly.radio.knowledge=2;mineOnly.other.knowledge=2;
|
|
+ mineOnly.discovery.Recover(&mineOnly.player);assert(mineOnly.mode.mine.calls==1&&mineOnly.discovery.m_Spent.Contains(&mineOnly.folder)&&mineOnly.mode.exhausted==0);
|
|
+ mineOnly.discovery.Recover(&mineOnly.player);assert(mineOnly.mode.mine.calls==1&&mineOnly.mode.exhausted==0);
|
|
+ // New warning supplements, never replaces, an available site clue.
|
|
+ Fixture both;both.mode.mine.newlyRecovered=true;both.discovery.Recover(&both.player);assert(both.other.knowledge==1&&both.discovery.m_Spent.Contains(&both.folder)&&both.mode.exhausted==0);
|
|
+ // No mine service still produces the ordinary exhausted-folder notice.
|
|
+ Fixture noWarning;noWarning.mode.haveMine=false;noWarning.radio.knowledge=2;noWarning.other.knowledge=2;noWarning.discovery.Recover(&noWarning.player);assert(noWarning.discovery.m_Spent.Contains(&noWarning.folder)&&noWarning.mode.exhausted==1);
|
|
+ // Missing optional service cannot stop ordinary discovery.
|
|
+ Fixture absent;absent.mode.haveMine=false;absent.discovery.Recover(&absent.player);assert(absent.other.knowledge==1&&absent.discovery.m_Spent.Contains(&absent.folder));
|
|
+ // A distinct folder transferred before recovery is recovered by its real holder.
|
|
+ Fixture transferred;transferred.firstInv.folders.clear();transferred.secondInv.folders.Insert(&transferred.folder);transferred.discovery.Recover(&transferred.player);assert(transferred.mode.mine.calls==0);transferred.discovery.Recover(&transferred.second);assert(transferred.mode.mine.calls==1&&transferred.mode.mine.lastPlayer==&transferred.second);
|
|
+ // Full ledger and pre-spent documents never call the extra service.
|
|
+ Fixture full;full.discovery.m_Spent.resize(4096,nullptr);full.discovery.Recover(&full.player);assert(full.mode.mine.calls==0&&full.other.calls==0);
|
|
+ // Rejected actor never reaches either inventory or supplemental recovery.
|
|
+ for(int fault=0;fault<4;fault++){Fixture rejected;if(fault==0)rejected.player.canonical=false;if(fault==1)rejected.player.alive=false;if(fault==2)rejected.player.faction=&rejected.foreign;if(fault==3)Replication::server=false;rejected.discovery.Recover(&rejected.player);assert(rejected.mode.mine.calls==0&&rejected.discovery.m_Spent.IsEmpty()&&rejected.radio.calls==0);}
|
|
+}
|
|
+''')
|
|
+print('PASS: actual full document recovery retains authored/sealed targets, one shared spent ledger, retry/exhaustion semantics and canonical multi-player ownership alongside supplemental mine intel')
|