103 lines
12 KiB
Python
103 lines
12 KiB
Python
"""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','SCR_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','SCR_InventoryStorageManagerComponent','SCR_EntityHelper','ERR_DocumentComponent','ERR_LocationKind','Math'):
|
|
s=s.replace(ns+'.',ns+'::')
|
|
for obj in ('m_Mode','player','folder','location','document','chosen','scrInventory'):
|
|
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{virtual ~InventoryStorageManagerComponent()=default;Array<IEntity> folders;static InventoryStorageManagerComponent* Cast(void*p){return (InventoryStorageManagerComponent*)p;}};
|
|
struct SCR_InventoryStorageManagerComponent:InventoryStorageManagerComponent{bool acceptDelete=true;int deleteCalls=0;IEntity deleted=nullptr;static SCR_InventoryStorageManagerComponent*Cast(InventoryStorageManagerComponent*p){return dynamic_cast<SCR_InventoryStorageManagerComponent*>(p);}bool TryDeleteItem(IEntity item){deleteCalls++;if(acceptDelete)deleted=item;return acceptDelete;}};
|
|
struct Entity{bool alive=true,canonical=true,deleted=false;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 SCR_EntityHelper{void DeleteEntityAndChildren(IEntity e){e->deleted=true;}}
|
|
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,retired=nullptr;string target;string Target(IEntity e){return e==sealed?target:"";}void Retire(IEntity e){retired=e;}};
|
|
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{bool worldPreparing=false;bool WorldPreparing(){return worldPreparing;}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;SCR_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.firstInv.deleteCalls==0&&!f.folder.deleted&&!f.mode.evidence.retired); // objective stays physical
|
|
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);
|
|
assert(sealed.firstInv.deleted==&sealed.folder&&sealed.mode.evidence.retired==&sealed.folder&&!sealed.folder.deleted);
|
|
sealed.discovery.Recover(&sealed.player);assert(sealed.firstInv.deleteCalls==1); // never consume twice
|
|
Fixture refusal;refusal.firstInv.acceptDelete=false;refusal.discovery.Recover(&refusal.player);
|
|
assert(refusal.firstInv.deleteCalls==1&&refusal.folder.deleted&&refusal.mode.evidence.retired==&refusal.folder);
|
|
Fixture generic;InventoryStorageManagerComponent bare;bare.folders.Insert(&generic.folder);generic.player.inventory=&bare;generic.discovery.Recover(&generic.player);
|
|
assert(generic.folder.deleted&&generic.mode.evidence.retired==&generic.folder); // missing scripted manager fallback
|
|
Fixture failed;failed.other.allow=false;failed.discovery.Recover(&failed.player);
|
|
assert(failed.firstInv.deleteCalls==0&&!failed.folder.deleted&&!failed.mode.evidence.retired); // no clue, no deletion
|
|
// 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);
|
|
// A generic folder remains usable until deferred locations are registered.
|
|
Fixture pending;pending.mode.worldPreparing=true;pending.mode.locations.clear();pending.discovery.Recover(&pending.player);
|
|
assert(pending.discovery.m_Spent.IsEmpty()&&pending.mode.exhausted==0);
|
|
pending.mode.locations={&pending.radio};pending.discovery.Recover(&pending.player);
|
|
assert(pending.radio.knowledge==1&&pending.discovery.m_Spent.Contains(&pending.folder));
|
|
// 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')
|