Gate radio intel behind configurable site discovery; record Workshop 0.0.119
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -55,7 +55,7 @@ assert 'Apply_planeReconEnabled' not in names # Saved-state toggle pair, not a
|
||||
assert len(names) == len(set(names)), 'Menu widget names must be unique'
|
||||
for key in keys:
|
||||
assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key, 'Help_' + key))
|
||||
if key not in ('droneReconCount','droneCooldownSeconds','sarkaAngerPerCar','emptyRestartHours','aiDoctrine','aiShadow','motorbikeCount','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','fieldCampCount','fieldHospitalCount','startMode','civilianTrafficCount','roadblockCount','hunterSpawnDistanceMetres','objectGuardCount','objectGuardEvery','objectGuardHQMode','startWeaponCount','startMagazineCount'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names
|
||||
if key not in ('radioIntelSiteCount','droneReconCount','droneCooldownSeconds','sarkaAngerPerCar','emptyRestartHours','aiDoctrine','aiShadow','motorbikeCount','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','fieldCampCount','fieldHospitalCount','startMode','civilianTrafficCount','roadblockCount','hunterSpawnDistanceMetres','objectGuardCount','objectGuardEvery','objectGuardHQMode','startWeaponCount','startMagazineCount'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names
|
||||
else: assert 'Random_startMode' not in names
|
||||
for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'):
|
||||
assert name in names
|
||||
@@ -103,7 +103,7 @@ int main() {
|
||||
assert(Fields::FollowJournal(true,0,0,400,false));
|
||||
std::vector<std::pair<string,string>> cases={
|
||||
{"droneReconCount","mission drones"},{"droneCooldownSeconds","mission dronecooldown"},{"emptyRestartHours","mission emptyhours"},{"aiDoctrine","mission doctrine"},{"aiShadow","mission aishadow"},{"automaticEquipmentChancePercent","hq equipment"},{"startMode","mission start"},{"radioDefenceSeconds","mission radio"},{"aiIntelChancePercent","intel carrying"},
|
||||
{"intelRevealChancePercent","intel reveal"},{"startTimeMinutes","mission time"},
|
||||
{"intelRevealChancePercent","intel reveal"},{"radioIntelSiteCount","intel radiosites"},{"startTimeMinutes","mission time"},
|
||||
{"timeMultiplier","mission speed"},{"startDateCode","mission date"},
|
||||
{"weatherProfile","mission weather"},{"viewDistanceMetres","mission viewdistance"},{"reportIntervalSeconds","hq interval"},
|
||||
{"planeAltitudeMetres","mission planealtitude"},{"planeReplacementSeconds","mission planereplacement"},{"planeSectorSeconds","mission planesector"},{"fieldCampCount","mission camps"},{"fieldHospitalCount","mission hospitals"},{"objectGuardCount","mission objectguards"},{"objectGuardEvery","mission objectevery"},{"objectGuardHQMode","mission objecthq"},{"hunterSpawnDistanceMetres","mission hunterdistance"},{"roadblockCount","mission roadblocks"},{"civilianTrafficCount","mission traffic"},{"heliTimerMinutes","hq heli timer"},{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"},{"convoyCount","hq convoy count"},{"antiAirChancePercent","hq aa"},{"antiPersonnelMineChancePercent","hq mines"}};
|
||||
|
||||
@@ -67,7 +67,7 @@ int main(){
|
||||
assert(ha.Setting("intel","carrying").empty()&&ha.Setting("hq","stinger")=="antiAirChancePercent");
|
||||
assert(ia.Setting("intel","chance")=="aiIntelChancePercent"&&ia.Setting("intel","carrying")=="aiIntelChancePercent");
|
||||
assert(ia.Setting("hq","chance").empty()&&ia.Setting("intel","spawn").empty());
|
||||
HQHelp hh;IntelHelp ih;lines.clear();assert(hh.Help("hq",lines)&&lines.size()<=5);lines.clear();assert(ih.Help("intel",lines)&&lines.size()<=4);
|
||||
HQHelp hh;IntelHelp ih;lines.clear();assert(hh.Help("hq",lines)&&lines.size()<=5);lines.clear();assert(ih.Help("intel",lines)&&lines.size()<=5&&lines[0].find("radiosites")!=string::npos);
|
||||
lines.clear();assert(!hh.Help("unknown",lines)&&!ih.Help("unknown",lines)&&lines.empty());
|
||||
}
|
||||
''')
|
||||
|
||||
@@ -11,6 +11,9 @@ rules=(rules.replace('string ', 'std::string ').replace('out int ', 'int& ')
|
||||
.replace(' < 0) return false;', ' == std::string::npos) return false;').replace('text.ToInt()', 'std::stoi(text)'))
|
||||
run('#include <cassert>\n#include <string>\n'+rules+r'''
|
||||
int main(){
|
||||
assert(CoreSetting("intel","radiosites")=="radioIntelSiteCount");
|
||||
assert(Valid("radioIntelSiteCount",0)&&Valid("radioIntelSiteCount",5)&&Valid("radioIntelSiteCount",20));
|
||||
assert(!Valid("radioIntelSiteCount",-1)&&!Valid("radioIntelSiteCount",21));
|
||||
assert(CoreSetting("mission","doctrine")=="aiDoctrine");
|
||||
assert(CoreSetting("mission","aishadow")=="aiShadow");
|
||||
assert(CoreSetting("mission","restartempty")=="restartOnEmpty");
|
||||
|
||||
@@ -11,7 +11,7 @@ extension = (extension.replace('override ', '').replace('bool Execute(', 'bool E
|
||||
.replace('ERR_HQ hq', 'HQ* hq').replace('ERR_HQ.Cast(', 'HQ::Cast(')
|
||||
.replace('super.Extension(', 'Base::Extension(').replace('Math.Max(', 'std::max(')
|
||||
.replace('mode.', 'mode->').replace('intel.', 'intel->').replace('hq.', 'hq->')
|
||||
.replace('Settings().', 'Settings()->').replace('Journal().', 'Journal()->'))
|
||||
.replace('Settings().', 'Settings()->').replace('Discovery().', 'Discovery()->').replace('Journal().', 'Journal()->'))
|
||||
ledger = '\n'.join(body(discovery, signature) for signature in ('bool IsSpent(', 'bool CanMarkSpent(', 'bool MarkSpent('))
|
||||
ledger = ledger.replace('Replication.', 'Replication::')
|
||||
run(r'''
|
||||
@@ -26,11 +26,12 @@ template<class T> struct Array:std::vector<T>{void Insert(T v){this->push_back(v
|
||||
template<class T> struct Set:std::set<T>{bool Contains(T v){return this->count(v);}void Insert(T v){this->insert(v);}int Count(){return this->size();}};
|
||||
namespace Replication { bool server=true; bool IsServer(){return server;} }
|
||||
struct Number {int n;string ToString(){return std::to_string(n);}};
|
||||
struct Settings {Number Get(string key){return {key=="aiIntelChancePercent"?60:(key=="showAllSites"?0:15)};}};
|
||||
struct Settings {Number Get(string key){return {key=="aiIntelChancePercent"?60:(key=="showAllSites"?0:(key=="radioIntelSiteCount"?5:15))};}};
|
||||
struct Intel {int starts=0;bool canStart=true;bool BeginReroll(){starts++;return canStart;}string RerollStatus(){return "batch status";}};
|
||||
struct Journal {int Sequence(){return 0;}void Read(int,Array<string>&,int&) {}};
|
||||
struct HQ {struct Journal journal;static HQ* Cast(HQ* h){return h;}string Status(){return "hq";}struct Journal* Journal(){return &journal;}};
|
||||
struct Mode {struct Settings settings;struct Intel* intel=nullptr;struct HQ* hq=nullptr;struct Settings* Settings(){return &settings;}struct Intel* Intel(){return intel;}struct HQ* Command(){return hq;}};
|
||||
struct Gate {string RadioIntelStatus(){return "radio progress";}};
|
||||
struct Mode {Gate gate;Gate*Discovery(){return &gate;}struct Settings settings;struct Intel* intel=nullptr;struct HQ* hq=nullptr;struct Settings* Settings(){return &settings;}struct Intel* Intel(){return intel;}struct HQ* Command(){return hq;}};
|
||||
struct Base {bool Extension(Mode*,string,Array<string>&){return false;}};
|
||||
struct Admin:Base {
|
||||
'''+extension+r'''
|
||||
@@ -40,7 +41,7 @@ struct Ledger {Set<IEntity> m_Spent;
|
||||
};
|
||||
int main(){
|
||||
Admin admin;Mode mode;Array<string> replies;Intel intel;mode.intel=&intel;
|
||||
assert(admin.Extension(&mode,"intel show",replies));assert(replies.size()==3&&replies[0]=="aiIntelChancePercent=60"&&replies[1]=="showAllSites=0"&&replies[2]=="intelRevealChancePercent=15"&&intel.starts==0);
|
||||
assert(admin.Extension(&mode,"intel show",replies));assert(replies.size()==5&&replies[0]=="aiIntelChancePercent=60"&&replies[1]=="radioIntelSiteCount=5 (next round)"&&replies[2]=="radio progress"&&replies[3]=="showAllSites=0"&&replies[4]=="intelRevealChancePercent=15"&&intel.starts==0);
|
||||
replies.clear();assert(admin.Extension(&mode,"intel reroll show",replies));assert(intel.starts==0&&replies.size()==1&&replies[0]=="batch status");
|
||||
replies.clear();assert(admin.Extension(&mode,"intel reroll",replies));assert(intel.starts==1&&replies.size()==1);
|
||||
intel.canStart=false;replies.clear();assert(admin.Extension(&mode,"intel reroll",replies));assert(intel.starts==2&&replies.size()==2&&replies[1]=="batch status");
|
||||
|
||||
@@ -104,7 +104,7 @@ for n in ('location','record','issued','item','document'):
|
||||
selection=selection.replace(n+'.',n+'->')
|
||||
for n in ('ERR_GameMode','ERR_Actors','ERR_DiscoveryService','ERR_LocationKind'):
|
||||
selection=selection.replace(n+'.',n+'::')
|
||||
selection=selection.replace('ERR_GameMode::Get().','ERR_GameMode::Get()->').replace('GetRandomElement().Id()', 'GetRandomElement()->Id()')
|
||||
selection=selection.replace('ERR_GameMode::Get().','ERR_GameMode::Get()->').replace('GetRandomElement().Id()', 'GetRandomElement()->Id()').replace('Discovery().', 'Discovery()->')
|
||||
selection=selection.replace('bool known;', 'bool known=false;')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
@@ -117,7 +117,8 @@ template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void C
|
||||
template<class K,class V>struct Map:std::map<K,V>{bool Find(K k,V& v){auto i=this->find(k);if(i==this->end())return false;v=i->second;return true;}void Set(K k,V v){(*this)[k]=v;}};
|
||||
namespace ERR_LocationKind{enum{PRISON,EXTRACTION,RADIO,OUTPOST};}
|
||||
struct ERR_LocationComponent{string id;int knowledge=0,kind=ERR_LocationKind::OUTPOST;string Id(){return id;}int Knowledge(){return knowledge;}int Kind(){return kind;}};
|
||||
struct ERR_GameMode{Array<ERR_LocationComponent*> locations;static ERR_GameMode*Get(){static ERR_GameMode m;return &m;}void Locations(Array<ERR_LocationComponent*>&out){out=locations;}};
|
||||
struct Gate{bool RadioIntelAllowed(){return true;}};
|
||||
struct ERR_GameMode{Gate gate;Gate*Discovery(){return &gate;}Array<ERR_LocationComponent*> locations;static ERR_GameMode*Get(){static ERR_GameMode m;return &m;}void Locations(Array<ERR_LocationComponent*>&out){out=locations;}};
|
||||
struct ERR_DocumentComponent{string target;string TargetId(){return target;}};
|
||||
struct Entity;using IEntity=Entity*;
|
||||
struct InventoryStorageManagerComponent{Array<IEntity>folders;};
|
||||
|
||||
@@ -17,7 +17,7 @@ def cpp(s):
|
||||
s=s.replace('OnTick().Remove(ERR_FlushDiscoveryFeedback);','OnTick().Remove(0);')
|
||||
s=s.replace('string.Format(', 'Format(')
|
||||
return s
|
||||
methods='\n'.join(cpp(body(P,s)) for s in ('protected void ERR_QueueDiscoveryFeedback(', 'protected void ERR_FlushDiscoveryFeedback(', 'protected void ERR_ReceiveDiscoveryFeedback(', 'protected void ERR_StopDiscoveryFeedback(', 'void ERR_IntelExhausted('))
|
||||
methods='\n'.join(cpp(body(P,s)) for s in ('protected void ERR_QueueDiscoveryFeedback(', 'protected void ERR_FlushDiscoveryFeedback(', 'protected void ERR_ReceiveDiscoveryFeedback(', 'protected void ERR_StopDiscoveryFeedback(', 'void ERR_IntelExhausted(', 'void ERR_RadioIntelWaiting('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
@@ -44,7 +44,7 @@ struct ERR_GameMode {
|
||||
static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}
|
||||
bool active=true;int phase=ERR_Phase::RADIO_SEARCH;bool Active(){return active;}int Phase(){return phase;}World* GetWorld(){return &world;}
|
||||
Map<ERR_LocationComponent*,int> m_ERRIntelSeen,m_ERRIntelPending;
|
||||
float m_ERRIntelMessageAt=0,m_ERRIntelExhaustedAt=0;bool m_ERRIntelFeedbackStopped=false,m_ERRIntelDuplicateReported=false,m_ERRIntelExhaustionReported=false;int m_ERRIntelMessageSequence=0,m_ERRIntelReceivedSequence=0;
|
||||
float m_ERRIntelMessageAt=0,m_ERRIntelExhaustedAt=0;bool m_ERRRadioIntelWaitingReported=false,m_ERRIntelFeedbackStopped=false,m_ERRIntelDuplicateReported=false,m_ERRIntelExhaustionReported=false;int m_ERRIntelMessageSequence=0,m_ERRIntelReceivedSequence=0;
|
||||
Events discovered,tick;Events& OnLocationDiscovered(){return discovered;}Events& OnTick(){return tick;}
|
||||
int rpcCount=0;void Rpc(int sequence,string message){rpcCount++;ERR_ReceiveDiscoveryFeedback(sequence,message);}
|
||||
'''+methods+r'''
|
||||
@@ -99,6 +99,12 @@ int main(){
|
||||
surplus.ERR_StopDiscoveryFeedback();surplus.ERR_IntelExhausted();assert(surplus.rpcCount==3);
|
||||
ERR_GameMode freshRound;ERR_GameMode::current=&freshRound;world.time=0;freshRound.ERR_IntelExhausted();assert(freshRound.rpcCount==1);
|
||||
|
||||
// Fixed-document waiting feedback is once per round, regardless of scan count.
|
||||
ERR_GameMode held;ERR_GameMode::current=&held;
|
||||
for(int i=0;i<100;i++)held.ERR_RadioIntelWaiting();
|
||||
assert(held.rpcCount==1&&held.m_ERRIntelMessageSequence==1);
|
||||
ERR_GameMode stopped;ERR_GameMode::current=&stopped;stopped.ERR_StopDiscoveryFeedback();stopped.ERR_RadioIntelWaiting();assert(stopped.rpcCount==0);
|
||||
ERR_GameMode client;ERR_GameMode::current=&client;Replication::server=false;client.ERR_RadioIntelWaiting();assert(client.rpcCount==0);
|
||||
}
|
||||
''')
|
||||
for required in ('OnLocationDiscovered().Insert(ERR_QueueDiscoveryFeedback)', 'OnTick().Insert(ERR_FlushDiscoveryFeedback)', 'OnStop().Insert(ERR_StopDiscoveryFeedback)', '[RplRpc(RplChannel.Reliable, RplRcver.Broadcast)]'):
|
||||
|
||||
@@ -12,10 +12,11 @@ for typ in ('InventoryStorageManagerComponent','SCR_InventoryStorageManagerCompo
|
||||
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'):
|
||||
for obj in ('m_Mode','player','folder','target','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=s.replace('bool redirectRadio;', 'bool redirectRadio=false;')
|
||||
s=re.sub(r'FindComponent\((\w+)\)',r'FindComponent("\1")',s)
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
@@ -38,14 +39,14 @@ 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 Evidence{void Retarget(IEntity,string){}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;
|
||||
struct Mode{void ERR_RadioIntelWaiting(){}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{bool m_Stopped=false;float m_CleanupNow=0;Mode*m_Mode=nullptr;Array<IEntity>m_Spent,cleanup;void QueueCleanup(IEntity item,float){cleanup.Insert(item);}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;}
|
||||
struct Discovery{bool RadioIntelAllowed(){return true;}bool m_Stopped=false;float m_CleanupNow=0;Mode*m_Mode=nullptr;Array<IEntity>m_Spent,cleanup;void QueueCleanup(IEntity item,float){cleanup.Insert(item);}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;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Run the production radio gate, folder recovery and evidence targeting bodies.
|
||||
Native inventory/discovery calls are explicit doubles; the retained Workbench
|
||||
fixture separately exercises real inventory, world observation and tuning.
|
||||
"""
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
core = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryService.c'
|
||||
evidence = ROOT/'addons/core/Scripts/Game/ERR/20_Intel/ERR_IntelEvidence.c'
|
||||
|
||||
def cpp(s):
|
||||
s=s.replace('protected ', '').replace('foreach (', 'for (').replace('null', 'nullptr')
|
||||
s=s.replace('ERR_DocumentComponent.Cast(folder.FindComponent(ERR_DocumentComponent))','folder->document')
|
||||
s=s.replace('InventoryStorageManagerComponent.Cast(player.FindComponent(InventoryStorageManagerComponent))','player->inventory')
|
||||
s=s.replace('set<string> available = new set<string>();','Set<string> available;').replace('set<string> identified = new set<string>();','Set<string> identified;')
|
||||
s=s.replace('array<ERR_LocationComponent>', 'Array<ERR_LocationComponent*>').replace('array<IEntity>','Array<IEntity>')
|
||||
for t in ('ERR_GameMode','ERR_LocationComponent','ERR_SiteService','ERR_SiteRuntime','ERR_DocumentComponent','InventoryStorageManagerComponent','ERR_IssuedIntel','ERR_BodyEvidence'):
|
||||
s=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',s)
|
||||
s=s.replace('ERR_SiteRuntime* site;', 'ERR_SiteRuntime* site=nullptr;').replace('bool redirectRadio;', 'bool redirectRadio=false;').replace('bool changed;', 'bool changed=false;').replace('int assigned;', 'int assigned=0;')
|
||||
for n in ('Replication','ERR_LocationKind','ERR_Actors','SCR_FactionManager','ERR_GameMode','Math'):
|
||||
s=s.replace(n+'.', n+'::')
|
||||
for n in ('m_Mode','mode','location','target','chosen','sites','site','document','player','record','issued'):
|
||||
s=re.sub(r'\b'+n+r'\.', n+'->',s)
|
||||
for n in ('Settings','Sites','Players','MineIntel','Evidence','Discovery','Get','GetPlayerManager'):
|
||||
s=s.replace(n+'().',n+'()->')
|
||||
s=s.replace('site->Definition.', 'site->Definition->').replace('GetGame().','GetGame()->').replace('GetRandomElement().','GetRandomElement()->')
|
||||
s=s.replace('void ERR_DiscoveryService(', 'Service(')
|
||||
return s.replace('target->IsEmpty()', 'target.IsEmpty()')
|
||||
|
||||
gate='\n'.join(body(core,sig) for sig in ('void ERR_DiscoveryService(', 'protected bool CountsForRadioIntel(', 'protected void RefreshRadioIntel(', 'bool RadioIntelAllowed()', 'bool MarkSpent(', 'protected void Recover('))
|
||||
ev='\n'.join(body(evidence,sig) for sig in ('protected string NextTarget(', 'string Target(', 'void Retarget('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <algorithm>
|
||||
struct string:std::string{using std::string::string;using std::string::operator=;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T x){this->push_back(x);}void Clear(){this->clear();}int Count(){return this->size();}bool IsEmpty(){return this->empty();}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}T GetRandomElement(){return this->front();}};
|
||||
template<class T>struct Set:std::set<T>{void Insert(T x){this->insert(x);}int Count(){return this->size();}bool Contains(T x){return this->count(x);}};
|
||||
template<class K,class V>struct Map:std::map<K,V>{bool Find(K k,V& v){auto i=this->find(k);if(i==this->end())return false;v=i->second;return true;}void Set(K k,V v){(*this)[k]=v;}};
|
||||
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
namespace Math{int Min(int a,int b){return std::min(a,b);}int RandomInt(int,int){return 99;}}
|
||||
namespace ERR_LocationKind{enum{PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
|
||||
struct ERR_LocationComponent{string id;int kind=ERR_LocationKind::OUTPOST,knowledge=0;bool prepared=true;string Id(){return id;}int Kind(){return kind;}int Knowledge(){return knowledge;}bool Prepared(){return prepared;}bool Discover(int roll,int chance){if(!prepared||knowledge>=2)return false;knowledge=knowledge==1||roll<chance?2:1;return true;}};
|
||||
struct Definition{bool RevealOnEscape=false;};
|
||||
struct ERR_SiteRuntime{struct Definition*Definition=nullptr;};
|
||||
struct ERR_SiteService{std::map<string,ERR_SiteRuntime*>sites;ERR_SiteRuntime*Find(string id){return sites.count(id)?sites[id]:nullptr;}};
|
||||
struct ERR_DocumentComponent{string target;string TargetId(){return target;}};
|
||||
struct Entity;using IEntity=Entity*;
|
||||
struct InventoryStorageManagerComponent{Array<IEntity>folders;};
|
||||
struct Entity{bool alive=true,current=true;int faction=1;InventoryStorageManagerComponent*inventory=nullptr;ERR_DocumentComponent*document=nullptr;};
|
||||
namespace ERR_Actors{bool Alive(IEntity p){return p&&p->alive;}}
|
||||
namespace SCR_FactionManager{int SGetFaction(IEntity p){return p->faction;}}
|
||||
struct Roster{bool CurrentActor(IEntity p){return p&&p->current;}};
|
||||
struct Settings{int required=5,chance=100;int Get(string key){assert(key=="radioIntelSiteCount");return required;}int Runtime(string){return chance;}};
|
||||
struct Mines{bool Recover(IEntity,IEntity){return false;}};
|
||||
struct PM{int GetPlayerIdFromControlledEntity(IEntity){return 1;}};
|
||||
struct Game{PM pm;PM*GetPlayerManager(){return ±}}game;Game*GetGame(){return &game;}
|
||||
void PrintFormat(string,int,string,int){}
|
||||
struct ERR_IssuedIntel{IEntity Folder=nullptr;string SiteId;};
|
||||
struct ERR_BodyEvidence{Array<ERR_IssuedIntel*>Issued;bool Searched=false;};
|
||||
struct Service;struct Evidence;
|
||||
struct ERR_GameMode{inline static ERR_GameMode*current=nullptr;struct Settings settings;struct Evidence*evidence=nullptr;Service*discovery=nullptr;ERR_SiteService sites;Roster roster;Mines mines;Array<ERR_LocationComponent*>locations;bool active=true,preparing=false;int waiting=0,exhausted=0;
|
||||
static ERR_GameMode*Get(){return current;}struct Settings*Settings(){return &settings;}struct Evidence*Evidence(){return evidence;}Service*Discovery(){return discovery;}ERR_SiteService*Sites(){return &sites;}Roster*Players(){return &roster;}Mines*MineIntel(){return &mines;}int PlayerFaction(){return 1;}bool Active(){return active;}bool WorldPreparing(){return preparing;}void Locations(Array<ERR_LocationComponent*>&out){out=locations;}void ERR_RadioIntelWaiting(){waiting++;}void ERR_IntelExhausted(bool){exhausted++;}
|
||||
};
|
||||
struct Evidence{Array<ERR_BodyEvidence*>m_Roster;Map<string,int>m_TargetAssignments;bool m_Stopped=false;string NextTarget();string Target(IEntity);void Retarget(IEntity,string);};
|
||||
struct Service{ERR_GameMode*m_Mode;int m_RadioIntelRequired=0,m_RadioIntelIdentified=0;bool m_RadioIntelFinalized=false,m_RadioIntelUnlocked=false,m_Stopped=false;Set<IEntity>m_Spent;float m_CleanupNow=0;int cleanups=0;void QueueCleanup(IEntity,float){cleanups++;}void Folders(InventoryStorageManagerComponent*i,Array<IEntity>&out){out=i->folders;}
|
||||
'''+cpp(gate)+r'''
|
||||
};
|
||||
'''+cpp(ev).replace('string NextTarget(', 'string Evidence::NextTarget(').replace('string Target(', 'string Evidence::Target(').replace('void Retarget(', 'void Evidence::Retarget(')+r'''
|
||||
struct Fixture{ERR_GameMode mode;Evidence evidence;Service service;ERR_LocationComponent sites[9];Definition automatic;ERR_SiteRuntime autoSite;Entity player,folder;InventoryStorageManagerComponent inv;ERR_DocumentComponent doc;ERR_IssuedIntel issued;ERR_BodyEvidence record;
|
||||
Fixture():service(&mode){ERR_GameMode::current=&mode;Replication::server=true;mode.discovery=&service;mode.evidence=&evidence;player.inventory=&inv;inv.folders={&folder};
|
||||
for(int i=0;i<9;i++){sites[i].id="site-"+std::to_string(i);mode.locations.Insert(&sites[i]);}
|
||||
sites[0].kind=ERR_LocationKind::RADIO;sites[1].kind=ERR_LocationKind::PRISON;sites[2].kind=ERR_LocationKind::EXTRACTION;
|
||||
automatic.RevealOnEscape=true;autoSite.Definition=&automatic;mode.sites.sites[sites[3].id]=&autoSite;
|
||||
}
|
||||
void SealRadio(){issued.Folder=&folder;issued.SiteId=sites[0].id;record.Issued={&issued};evidence.m_Roster={&record};}
|
||||
void FixedRadio(){doc.target=sites[0].id;folder.document=&doc;}
|
||||
void Identify(int count){for(int i=4;i<4+count;i++)sites[i].knowledge=2;}
|
||||
};
|
||||
int main(){
|
||||
// Default boundary, exclusions and unique IDs (including duplicate observations).
|
||||
{Fixture f;for(int i=0;i<4;i++)f.sites[i].knowledge=2;f.Identify(4);f.sites[8].knowledge=1;
|
||||
f.mode.locations.Insert(&f.sites[4]);f.mode.locations.Insert(nullptr);
|
||||
assert(!f.service.RadioIntelAllowed()&&f.service.m_RadioIntelIdentified==4&&f.service.m_RadioIntelRequired==5);
|
||||
f.sites[8].knowledge=2;assert(f.service.RadioIntelAllowed());f.mode.locations.Clear();assert(f.service.RadioIntelAllowed());}
|
||||
// Setting edits are next-round only; fresh service snapshots the saved value.
|
||||
{Fixture f;f.mode.settings.required=0;assert(!f.service.RadioIntelAllowed());Service next(&f.mode);assert(next.RadioIntelAllowed());}
|
||||
// Preparation never lowers the requirement; completed sparse world caps it once.
|
||||
{Fixture f;f.mode.locations={&f.sites[4],&f.sites[5]};f.mode.preparing=true;f.Identify(2);
|
||||
assert(!f.service.RadioIntelAllowed()&&f.service.m_RadioIntelRequired==5);f.mode.preparing=false;assert(f.service.RadioIntelAllowed()&&f.service.m_RadioIntelRequired==2);}
|
||||
{Fixture f;f.mode.locations.Clear();f.mode.active=false;assert(!f.service.RadioIntelAllowed());f.mode.active=true;assert(f.service.RadioIntelAllowed()&&f.service.m_RadioIntelRequired==0);}
|
||||
// Unprepared sites cannot contribute knowledge or increase the final cap.
|
||||
{Fixture f;f.sites[8].prepared=false;f.Identify(4);assert(f.service.RadioIntelAllowed()&&f.service.m_RadioIntelRequired==4);}
|
||||
// Ordinary initial assignments and unsealed recovery both avoid radios.
|
||||
{Fixture f;assert(f.evidence.NextTarget()!=f.sites[0].id);f.service.Recover(&f.player);assert(f.sites[0].knowledge==0&&f.service.m_Spent.Contains(&f.folder)&&f.service.cleanups==1);}
|
||||
// Sealed radio folders redirect, including searched bodies; existing report
|
||||
// snapshots stay detached. Updated evidence remains after ordinary cleanup.
|
||||
{Fixture f;f.SealRadio();f.record.Searched=true;string queued=f.evidence.Target(&f.folder);f.service.Recover(&f.player);
|
||||
assert(queued==f.sites[0].id&&f.evidence.Target(&f.folder)!=queued&&f.sites[0].knowledge==0);
|
||||
assert(f.evidence.Target(&f.folder)==f.sites[3].id&&f.evidence.m_TargetAssignments[f.sites[3].id]==1);}
|
||||
// A fixed document stays unspent through four sites and processes automatically
|
||||
// on the next ordinary inventory scan after the fifth becomes fully known.
|
||||
{Fixture f;f.FixedRadio();f.Identify(4);f.service.Recover(&f.player);f.service.Recover(&f.player);
|
||||
assert(f.service.m_Spent.empty()&&f.service.cleanups==0&&f.sites[0].knowledge==0);
|
||||
f.sites[8].knowledge=2;f.service.Recover(&f.player);assert(f.service.m_Spent.Contains(&f.folder)&&f.sites[0].knowledge==2&&f.service.cleanups==0);}
|
||||
// If there are no alternatives yet, keep ordinary folders and issued targets.
|
||||
{Fixture f;f.SealRadio();f.mode.preparing=true;f.mode.locations={&f.sites[0]};f.service.Recover(&f.player);
|
||||
assert(f.service.m_Spent.empty()&&f.service.cleanups==0&&f.mode.exhausted==0&&f.evidence.Target(&f.folder)==f.sites[0].id);
|
||||
assert(f.evidence.NextTarget().IsEmpty());}
|
||||
// A redirected rough clue counts only on full identification, never on reuse.
|
||||
{Fixture f;f.SealRadio();f.mode.settings.chance=0;f.sites[3].knowledge=2;f.service.Recover(&f.player);
|
||||
assert(f.sites[4].knowledge==1&&f.evidence.Target(&f.folder)==f.sites[4].id);f.service.Recover(&f.player);assert(f.sites[4].knowledge==1);}
|
||||
// Stopped/client/wrong-owner inventory recovery preserves the authorization gate.
|
||||
for(int fault=0;fault<4;fault++){Fixture f;if(fault==0)Replication::server=false;if(fault==1)f.service.m_Stopped=true;if(fault==2)f.player.current=false;if(fault==3)f.player.faction=2;f.service.Recover(&f.player);assert(f.service.m_Spent.empty());}
|
||||
}
|
||||
''')
|
||||
print('PASS: radio intel boundary, exclusions, preparation cap, next-round configuration, sticky unlock, redirection/evidence, fixed retention, automatic recovery and lifecycle guards')
|
||||
@@ -5,7 +5,7 @@ from policy_harness import ROOT,body,run
|
||||
spec=importlib.util.spec_from_file_location('import_settings',BASE/'tools/import_settings.py')
|
||||
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
|
||||
result,deferred=m.merge({'configVersion':1,'reportIntervalSeconds':20.0,'supportSquadLimit':2,'responseRadiusMetres':1500,'convoyCount':3},{'aiIntelChancePercent':0,'intelRevealChancePercent':100})
|
||||
assert result==dict(droneReconCount=0,droneCooldownSeconds=300,aiDoctrine=0,aiShadow=0,planeAltitudeMetres=250,planeReplacementSeconds=60,planeSectorSeconds=120,planeReconEnabled=1,roadblockCount=4,civilianTrafficCount=3,startMode=0,keepWeaponOnUnconscious=1,selfEpinephrine=0,restartOnEmpty=1,emptyRestartHours=5,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,showAllSites=0,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
|
||||
assert result==dict(droneReconCount=0,droneCooldownSeconds=300,aiDoctrine=0,aiShadow=0,planeAltitudeMetres=250,planeReplacementSeconds=60,planeSectorSeconds=120,planeReconEnabled=1,roadblockCount=4,civilianTrafficCount=3,startMode=0,keepWeaponOnUnconscious=1,selfEpinephrine=0,restartOnEmpty=1,emptyRestartHours=5,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioIntelSiteCount=5,showAllSites=0,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
|
||||
assert deferred==[]
|
||||
for count in (0,1,3,20):
|
||||
values,notes=m.merge(ai={'convoyCount':count})
|
||||
|
||||
Reference in New Issue
Block a user