Integrate owned Intel rerolls, discovery feedback and fair HQ reports

This commit is contained in:
2026-09-19 23:00:02 +02:00
parent 6cf989a060
commit 27989b85a3
46 changed files with 5756 additions and 15 deletions
+79
View File
@@ -0,0 +1,79 @@
"""Execute the actual bounded HQ report scheduler; radio integration stays native."""
from policy_harness import ROOT, body, run
source = ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQObservation.c'
hq = ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c'
select = body(source, 'ERR_HQVisualContact Select(')
select = select.replace('array<ref ERR_HQVisualContact> samples', 'Array<ERR_HQVisualContact> samples')
select = select.replace('ERR_HQVisualContact selected;', 'ERR_HQVisualContact selected = nullptr;')
select = select.replace('foreach (ERR_HQVisualContact sample : samples)', 'for (ERR_HQVisualContact sample : samples)')
select = select.replace('sample.Entity', 'sample->Entity')
queued = body(source, 'void Queued(')
run(r'''
#include <cassert>
#include <algorithm>
#include <vector>
struct Entity {int id;}; using IEntity=Entity*;
struct Sample {IEntity Entity;}; using ERR_HQVisualContact=Sample*;
template<class T> struct Array: std::vector<T> {
using std::vector<T>::vector;
int Count(){return this->size();}
int Find(T a){auto i=std::find(this->begin(),this->end(),a);return i==this->end()?-1:int(i-this->begin());}
void Remove(int i){this->erase(this->begin()+i);}
void Insert(T a){this->push_back(a);}
};
struct Selection {
Array<IEntity> m_Recent;
'''+select+queued+r'''
};
int main(){
Entity actors[80]{}; Sample samples[80]{};
for(int i=0;i<80;i++){actors[i].id=i;samples[i].Entity=&actors[i];}
Array<ERR_HQVisualContact> visible{&samples[0],&samples[1],&samples[2]};
Selection choice;
assert(!choice.Select({}));assert(!choice.Select({nullptr}));
Sample invalid{nullptr};assert(!choice.Select({&invalid}));
// Failed route allocation doesn't consume a turn: repeated Select is read only.
for(int i=0;i<5;i++)assert(choice.Select(visible)==&samples[0]);
assert(choice.m_Recent.empty());choice.Queued(nullptr);assert(choice.m_Recent.empty());
// Stable native order: every visible actor is reported before anyone repeats.
for(int i=0;i<30;i++){
auto selected=choice.Select(visible);assert(selected==&samples[i%3]);
choice.Queued(selected->Entity);assert(choice.m_Recent.Count()==3||i<2);
}
// Reordering native targets does not reset fairness. New contacts take priority.
assert(choice.Select({&samples[2],&samples[1],&samples[0]})==&samples[0]);
assert(choice.Select({&samples[0],&samples[3]})==&samples[3]);
choice.Queued(&actors[3]);
// Lost sight never reports a remembered actor; only current samples can win.
assert(choice.Select({&samples[2]})==&samples[2]);
// Interrupted delivery counts as an attempt, allowing another current contact.
choice.Queued(&actors[0]);assert(choice.Select(visible)==&samples[1]);
choice.Queued(&actors[1]);assert(choice.Select(visible)==&samples[2]);
// Repeated queue identity is deduplicated, and vanished references are purged.
choice.Queued(&actors[1]);assert(choice.m_Recent.Count()==4);
choice.m_Recent[0]=nullptr;choice.Queued(&actors[4]);
assert(choice.m_Recent.Find(nullptr)==-1);
// History and full 64-contact scheduling remain bounded under turnover.
Selection many;Array<ERR_HQVisualContact> sixtyFour;
for(int i=0;i<64;i++)sixtyFour.Insert(&samples[i]);
for(int i=0;i<192;i++){
auto selected=many.Select(sixtyFour);assert(selected==&samples[i%64]);
many.Queued(selected->Entity);assert(many.m_Recent.Count()<=64);
}
for(int i=64;i<80;i++)many.Queued(&actors[i]);
assert(many.m_Recent.Count()==64&&many.m_Recent.Find(&actors[0])==-1);
// Scheduling is per squad/round, never shared HQ intelligence.
Selection other;assert(other.Select(visible)==&samples[0]);
}
''')
observe=body(hq,'protected void Observe(')
assert 'samples[0]' not in observe
assert observe.index('ERR_HQObservation.Gather(')<observe.index('unit.ReportSelection.Select(samples)')
assert observe.index('unit.Uplink || now < unit.NextReport')<observe.index('unit.ReportSelection.Select(samples)')
assert observe.index('if (!route) return;')<observe.index('unit.PendingReport = report;')<observe.index('unit.Uplink = route;')<observe.index('unit.ReportSelection.Queued(seen.Entity)')
assert observe.count('unit.ReportSelection.Queued(')==1
assert 'Accept(' not in observe and 'AddWaypoint' not in observe
assert 'report.ObservedAt = seen.ObservedAt;' in observe
assert 'ERR_HQObservation.Describe(report, samples, now);' in observe
assert 'ref ERR_HQReportSelection ReportSelection = new ERR_HQReportSelection();' in hq.read_text()
print('PASS: actual per-squad bounded report selector rotates visible targets, survives ordering/turnover, records only queued attempts, and preserves radio intake ordering')
+57
View File
@@ -0,0 +1,57 @@
"""Run actual companion dispatch and discovery ledger bodies; native RPC is separate."""
from policy_harness import ROOT, body, run
admin = ROOT / 'addons/core/Scripts/Game/ERR/ERR_AdminService.c'
chat = ROOT / 'addons/chat/Scripts/Game/ERR/ERR_AdminCommands.c'
discovery = ROOT / 'addons/core/Scripts/Game/ERR/ERR_DiscoveryService.c'
execute = body(admin, 'void Execute(')
assert execute.index('if (!Replication.IsServer()') < execute.index('if (!authorized)') < execute.index('if (Extension(mode, arguments, replies))')
extension = body(chat, 'protected override bool Extension(')
extension = (extension.replace('protected override ', '').replace('array<string>', 'Array<string>&')
.replace('ERR_GameMode mode', 'Mode* mode').replace('ERR_IntelService intel', 'Intel* intel')
.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()->'))
ledger = '\n'.join(body(discovery, signature) for signature in ('bool IsSpent(', 'bool CanMarkSpent(', 'bool MarkSpent('))
ledger = ledger.replace('Replication.', 'Replication::')
run(r'''
#include <cassert>
#include <algorithm>
#include <string>
#include <vector>
#include <set>
using string=std::string;
using IEntity=int;
template<class T> struct Array:std::vector<T>{void Insert(T v){this->push_back(v);}bool IsEmpty(){return this->empty();}};
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: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 Base {bool Extension(Mode*,string,Array<string>&){return false;}};
struct Admin:Base {
'''+extension+r'''
};
struct Ledger {Set<IEntity> m_Spent;
'''+ledger+r'''
};
int main(){
Admin admin;Mode mode;Array<string> replies;Intel intel;mode.intel=&intel;
assert(admin.Extension(&mode,"intel show",replies));assert(replies.size()==2&&replies[0]=="aiIntelChancePercent=60"&&replies[1]=="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");
mode.intel=nullptr;replies.clear();assert(admin.Extension(&mode,"intel reroll",replies));assert(replies.size()==1&&intel.starts==2);
for(string invalid:{"intel reroll all","intel reroll show extra","intel reroll set 50","intel showing","intel"}) {replies.clear();assert(!admin.Extension(&mode,invalid,replies));assert(replies.empty()&&intel.starts==2);}
Ledger ledger;assert(!ledger.CanMarkSpent(0)&&!ledger.MarkSpent(0)&&!ledger.IsSpent(0));
Replication::server=false;assert(!ledger.MarkSpent(1)&&ledger.m_Spent.empty());Replication::server=true;
assert(ledger.MarkSpent(1)&&ledger.MarkSpent(1)&&ledger.IsSpent(1)&&ledger.m_Spent.size()==1);
for(int i=2;i<=4096;i++)assert(ledger.MarkSpent(i));
assert(!ledger.CanMarkSpent(4097)&&ledger.CanMarkSpent(1)&&!ledger.MarkSpent(4097)&&!ledger.IsSpent(4097)&&ledger.MarkSpent(1)&&ledger.m_Spent.size()==4096);
Replication::server=false;assert(!ledger.MarkSpent(1)&&!ledger.MarkSpent(4097)&&ledger.m_Spent.size()==4096);
}
''')
print('PASS: actual Intel commands distinguish status/start/failure, preserve authorization gate and reject malformed requests; spent ledger is idempotent, server-only and bounded')
+86
View File
@@ -0,0 +1,86 @@
"""Execute production feedback queue/delivery bodies with fake engine boundaries."""
from policy_harness import ROOT, body, run
P=ROOT/'addons/intel/Scripts/Game/ERR/ERR_IntelDiscoveryFeedback.c'
source=P.read_text()
def cpp(s):
s=s.replace('protected ', '').replace('static ', '')
for name in ('Replication','Math','ERR_Phase','ERR_LocationKind','ERR_GameMode','ERR_IntelDiscoveryText','SCR_Global','SCR_ChatComponent'):
s=s.replace(name+'.',name+'::')
s=s.replace('ERR_LocationComponent location','ERR_LocationComponent* location')
s=s.replace('location.', 'location->').replace('GetWorld().','GetWorld()->').replace('GetGame().','GetGame()->')
s=s.replace('int seen;','int seen=0;').replace('int identified;','int identified=0;').replace('int rough;','int rough=0;')
s=s.replace('foreach (ERR_LocationComponent* location, int knowledge : m_ERRIntelPending)','for (auto [location, knowledge] : m_ERRIntelPending.values)')
s=s.replace('Rpc(ERR_ReceiveDiscoveryFeedback, ', 'Rpc(')
s=s.replace('OnLocationDiscovered().Remove(ERR_QueueDiscoveryFeedback);','OnLocationDiscovered().Remove(0);')
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('))
run(r'''
#include <cassert>
#include <string>
#include <map>
#include <vector>
struct string:std::string {using std::string::string;using std::string::operator=;string(std::string s):std::string(s){}bool IsEmpty()const{return empty();}};
string Format(string text,int identified,int rough){text.replace(text.find("%1"),2,std::to_string(identified));text.replace(text.find("%2"),2,std::to_string(rough));return text;}
struct Replication {static bool server;static bool IsServer(){return server;}};bool Replication::server=true;
struct SCR_Global {static bool edit;static bool IsEditMode(){return edit;}};bool SCR_Global::edit=false;
struct SCR_ChatComponent {static std::vector<string> messages;static void RadioProtocolMessage(string text){messages.push_back(text);}};std::vector<string> SCR_ChatComponent::messages;
struct Math {static float Max(float a,float b){return a>b?a:b;}};
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,EXTRACTION};}
namespace ERR_LocationKind {enum {PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT};}
struct ERR_LocationComponent {int kind=ERR_LocationKind::OUTPOST,knowledge=1;string label="Hidden outpost";int Kind(){return kind;}int Knowledge(){return knowledge;}string Label(){return label;}};
template<class K,class V> struct Map {std::map<K,V> values;bool Find(K k,V& v){auto i=values.find(k);if(i==values.end())return false;v=i->second;return true;}bool Contains(K k){return values.count(k);}int Count(){return values.size();}bool IsEmpty(){return values.empty();}void Set(K k,V v){values[k]=v;}void Clear(){values.clear();}};
struct World {float time=0;float GetWorldTime(){return time;}} world;
struct Game {bool controller=true;bool GetPlayerController(){return controller;}} game;
Game* currentGame=&game;Game* GetGame(){return currentGame;}
struct Events {int removes=0;void Remove(int){removes++;}};
struct ERR_IntelDiscoveryText {static
'''+cpp(body(P,'static string Message('))+r'''
};
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;bool m_ERRIntelFeedbackStopped=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'''
};
ERR_GameMode* ERR_GameMode::current=nullptr;
int main(){
ERR_GameMode m;ERR_GameMode::current=&m;ERR_LocationComponent site;
// Do not reveal startup sites, prison or automatic helicopter disclosure.
m.phase=ERR_Phase::PREPARING;m.ERR_QueueDiscoveryFeedback(&site,0,1);assert(m.m_ERRIntelPending.IsEmpty());
m.phase=ERR_Phase::RADIO_SEARCH;for(int kind:{ERR_LocationKind::PRISON,ERR_LocationKind::EXTRACTION}){site.kind=kind;m.ERR_QueueDiscoveryFeedback(&site,0,1);}assert(m.m_ERRIntelPending.IsEmpty());
site.kind=ERR_LocationKind::OUTPOST;Replication::server=false;m.ERR_QueueDiscoveryFeedback(&site,0,1);assert(m.m_ERRIntelPending.IsEmpty());Replication::server=true;
m.ERR_QueueDiscoveryFeedback(nullptr,0,1);m.ERR_QueueDiscoveryFeedback(&site,1,1);m.ERR_QueueDiscoveryFeedback(&site,0,2);m.ERR_QueueDiscoveryFeedback(&site,-1,1);assert(m.m_ERRIntelPending.IsEmpty());
// Rough clues do not disclose labels, kinds or coordinates. Listen callbacks dedupe.
m.ERR_QueueDiscoveryFeedback(&site,0,1);m.ERR_FlushDiscoveryFeedback(1499);assert(SCR_ChatComponent::messages.empty());
m.ERR_FlushDiscoveryFeedback(1500);assert(m.rpcCount==1&&SCR_ChatComponent::messages.size()==1);assert(SCR_ChatComponent::messages.back().find("Hidden")==string::npos);assert(SCR_ChatComponent::messages.back().find("500 m")!=string::npos);
m.ERR_QueueDiscoveryFeedback(&site,0,1);assert(m.m_ERRIntelPending.IsEmpty());
site.knowledge=2;world.time=2000;m.ERR_QueueDiscoveryFeedback(&site,1,2);m.ERR_FlushDiscoveryFeedback(6499);assert(m.rpcCount==1);
m.ERR_FlushDiscoveryFeedback(6500);assert(m.rpcCount==2&&SCR_ChatComponent::messages.back().find("Hidden outpost")!=string::npos);
// A same-batch rough→exact update becomes one final identified location.
ERR_GameMode batch;ERR_GameMode::current=&batch;ERR_LocationComponent a,b;world.time=0;
batch.ERR_QueueDiscoveryFeedback(&a,0,1);a.knowledge=2;batch.ERR_QueueDiscoveryFeedback(&a,1,2);batch.ERR_QueueDiscoveryFeedback(&b,0,1);batch.ERR_FlushDiscoveryFeedback(1500);
assert(SCR_ChatComponent::messages.back().find("1 identified locations and 1 search areas")!=string::npos);
// End/restart cancels pending output and removes both hooks, new mode is clean.
world.time=2000;b.knowledge=2;batch.ERR_QueueDiscoveryFeedback(&b,1,2);batch.ERR_StopDiscoveryFeedback();assert(batch.m_ERRIntelSeen.IsEmpty()&&batch.m_ERRIntelPending.IsEmpty()&&batch.discovered.removes==1&&batch.tick.removes==1);
int calls=batch.rpcCount;batch.ERR_FlushDiscoveryFeedback(99999);batch.ERR_QueueDiscoveryFeedback(&b,0,2);assert(batch.rpcCount==calls&&batch.m_ERRIntelPending.IsEmpty());
ERR_GameMode next;ERR_GameMode::current=&next;auto shown=SCR_ChatComponent::messages.size();batch.ERR_ReceiveDiscoveryFeedback(999,"old round");assert(SCR_ChatComponent::messages.size()==shown);
// Remote delivery gates old sequence, absent controller, terminal and editor state.
Replication::server=false;next.ERR_ReceiveDiscoveryFeedback(1,"remote");next.ERR_ReceiveDiscoveryFeedback(1,"duplicate");assert(SCR_ChatComponent::messages.size()==shown+1);
game.controller=false;next.ERR_ReceiveDiscoveryFeedback(2,"absent");game.controller=true;next.active=false;next.ERR_ReceiveDiscoveryFeedback(2,"ended");next.active=true;SCR_Global::edit=true;next.ERR_ReceiveDiscoveryFeedback(2,"editor");SCR_Global::edit=false;assert(next.m_ERRIntelReceivedSequence==1);
currentGame=nullptr;next.ERR_ReceiveDiscoveryFeedback(2,"no game");currentGame=&game;
next.ERR_ReceiveDiscoveryFeedback(2,"new");assert(SCR_ChatComponent::messages.size()==shown+2);
// Queue is bounded even for malformed/unusually large authored scenarios.
Replication::server=true;ERR_GameMode bounded;ERR_GameMode::current=&bounded;ERR_LocationComponent many[65];for(auto& item:many)bounded.ERR_QueueDiscoveryFeedback(&item,0,1);assert(bounded.m_ERRIntelSeen.Count()==64&&bounded.m_ERRIntelPending.Count()==64);
assert(ERR_IntelDiscoveryText::Message(0,0,"secret").empty());assert(ERR_IntelDiscoveryText::Message(1,0,"")=="A location identified. Check your field map.");
}
''')
for required in ('OnLocationDiscovered().Insert(ERR_QueueDiscoveryFeedback)', 'OnTick().Insert(ERR_FlushDiscoveryFeedback)', 'OnStop().Insert(ERR_StopDiscoveryFeedback)', '[RplRpc(RplChannel.Reliable, RplRcver.Broadcast)]'):
assert required in source
for forbidden in ('HideHint(', 'CallLater(', 'Discover(', 'SetKnowledge('):
assert forbidden not in source
print('PASS: production Intel feedback coalescing, rough secrecy, rate limit, bounded dedupe, local/remote delivery and round cleanup; native UI/RPC not exercised')
+8 -6
View File
@@ -11,12 +11,12 @@ def adapt(s):
s=re.sub(r'foreach \((\w+) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
for n in ('Replication','ERR_Actors','Math','ERR_Resources','ERR_DiscoveryService','SCR_InventoryStorageManagerComponent','SCR_AIGroupUtilityComponent','ERR_MortarSupplyState'):
s=s.replace(n+'.',n+'::')
for n in ('group','member','actor','request','inventory','known','unit','m_Mode','m_Battery','mode'):
for n in ('group','member','actor','request','inventory','known','unit','m_Mode','m_Battery','mode','m_Reroll'):
s=s.replace(n+'.',n+'->')
s=s.replace('Actor.FindComponent(', 'Actor->FindComponent(').replace('SCR_InventoryStorageManagerComponent inventory', 'SCR_InventoryStorageManagerComponent* inventory').replace('SCR_AIGroupUtilityComponent utility', 'SCR_AIGroupUtilityComponent* utility')
s=s.replace('Settings().','Settings()->').replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->')
s=s.replace('FindComponent(SCR_InventoryStorageManagerComponent)','FindComponent()').replace('FindComponent(SCR_AIGroupUtilityComponent)','FindComponent()')
s=s.replace('purpose: EStoragePurpose.PURPOSE_ANY','EStoragePurpose::PURPOSE_ANY')
s=s.replace('purpose: EStoragePurpose.PURPOSE_ANY','EStoragePurpose::PURPOSE_ANY').replace('cb: ', '').replace('Operation.', 'Operation->').replace('new ERR_IntelInsertCallback()', 'new Callback()')
s=s.replace('ERR_IntelRequest request = new ERR_IntelRequest();','ERR_IntelRequest request = new Request();')
s=s.replace('ERR_HQUnit unit = new ERR_HQUnit();','ERR_HQUnit unit = new Unit();')
s=re.sub(r'^.*(?:Print|m_Journal.Write)\([^\n]*$', ';',s,flags=re.M)
@@ -50,7 +50,8 @@ template<class T> struct Set:std::set<T>{bool Contains(T a){return this->count(a
namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace Math{int rolls=0;int RandomInt(int,int){rolls++;return 0;}}
namespace EStoragePurpose{const int PURPOSE_ANY=0;}
struct Inventory {int requests=0;bool accept=true,folder=false;bool TrySpawnPrefabToStorage(int,int){requests++;return accept;}static Inventory* Cast(void* p){return static_cast<Inventory*>(p);}};
struct Callback{bool Finished=false,Succeeded=false;};
struct Inventory {int requests=0;bool accept=true,folder=false;bool TrySpawnPrefabToStorage(int,int,Callback*){requests++;return accept;}static Inventory* Cast(void* p){return static_cast<Inventory*>(p);}};
using SCR_InventoryStorageManagerComponent=Inventory;
struct Actor{bool alive=true,player=false;Inventory inventory;void* FindComponent(){return &inventory;}};
using IEntity=Actor*;
@@ -62,8 +63,9 @@ struct Utility{static Utility* Cast(void* p){return static_cast<Utility*>(p);}};
struct Group{Array<AIAgent> members;Utility utility;int faction=1;void GetAgents(Array<AIAgent>& out){out=members;}void* FindComponent(){return &utility;}int GetFaction(){return faction;}};using SCR_AIGroup=Group*;
struct Settings{int chance=100;int Get(const char*){return chance;}};
struct Mode{struct Settings settings;int publications=0;struct Settings* Settings(){return &settings;}int EnemyFaction(){return 1;}void RegisterGroup(SCR_AIGroup);};
struct Request{IEntity Actor=nullptr;int Attempts=0;bool Accepted=false;};using ERR_IntelRequest=Request*;
struct Intel{Mode* m_Mode;Array<ERR_IntelRequest> m_Requests;Set<IEntity> m_Rolled;
struct Request{Callback* Operation=nullptr;IEntity Actor=nullptr;int Attempts=0;bool Accepted=false;};using ERR_IntelRequest=Request*;
struct Reroll{void Tick(){}};
struct Intel{Mode* m_Mode;Array<ERR_IntelRequest> m_Requests;Set<IEntity> m_Rolled;bool m_Stopped=false;Reroll* m_Reroll=nullptr;void Unsettled(IEntity){}
'''+register+tick+observation+r'''
};
struct Clock{float GetWorldTime(){return 10;}Clock* GetWorld(){return this;}}clock_;Clock* GetGame(){return &clock_;}
@@ -92,7 +94,7 @@ int main(){
assert(mode.publications==1&&Math::rolls==4&&hq.m_Units.Count()==1);
intel.Tick(1);for(Actor& a:actors)assert(a.inventory.requests==1);
for(int n=0;n<5;n++){intel.Tick(2+n);}for(Actor& a:actors)assert(a.inventory.requests==1); // accepted asynchronous spawn never reissued
for(Actor& a:actors){a.inventory.folder=true;}intel.Tick(10);assert(intel.m_Requests.Count()==0);
for(Actor& a:actors){a.inventory.folder=true;}for(auto request:intel.m_Requests){request->Operation->Finished=true;request->Operation->Succeeded=true;}intel.Tick(10);assert(intel.m_Requests.Count()==0);
for(Actor& a:actors)a.inventory.folder=false; // real item subsequently picked up/dropped: assignment latch unchanged
intel.RegisterGroup(&group);intel.Tick(11);assert(Math::rolls==4&&intel.m_Requests.Count()==0);
actors[0].alive=false;intel.RegisterGroup(&group);intel.Tick(12);assert(Math::rolls==4);for(Actor& a:actors)assert(a.inventory.requests==1);
+97
View File
@@ -0,0 +1,97 @@
"""Execute actual Intel reroll methods against adversarial async inventory adapters."""
import re
from policy_harness import ROOT, body, run
service=ROOT/'addons/intel/Scripts/Game/ERR/ERR_IntelService.c'
batch=ROOT/'addons/intel/Scripts/Game/ERR/ERR_IntelReroll.c'
def adapt(text,cls):
text=text.replace('protected ', '').replace('null','nullptr')
text=re.sub(r'array<([^>]+)> (\w+) = \{\};',r'Array<\1> \2;',text)
text=text.replace('set<IEntity> cohort = new set<IEntity>();','Set<IEntity> cohort;')
text=re.sub(r'foreach \((\w+) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',text)
for n in ('Replication','ERR_Actors','Math','ERR_Resources','ERR_DiscoveryService','SCR_InventoryStorageManagerComponent','InventoryItemComponent'):
text=text.replace(n+'.',n+'::')
text=text.replace('SCR_InventoryStorageManagerComponent inventory','Inventory* inventory').replace('InventoryItemComponent item','Item* item')
for n in ('m_Mode','m_Service','m_Reroll','entry','request','inventory','actor','group','member','known','folder','item'):
text=text.replace(n+'.',n+'->')
text=text.replace('Actor.FindComponent(', 'Actor->FindComponent(').replace('Removing.FindComponent(', 'Removing->FindComponent(').replace('Removing.GetParent()', 'Removing->GetParent()')
text=text.replace('Settings().','Settings()->').replace('Discovery().','Discovery()->')
text=text.replace('FindComponent(SCR_InventoryStorageManagerComponent)','FindComponent(INVENTORY)').replace('FindComponent(InventoryItemComponent)','FindComponent(ITEM)').replace('FindComponent(ERR_DocumentComponent)','FindComponent(DOCUMENT)')
text=text.replace('purpose: EStoragePurpose.PURPOSE_ANY','0').replace('cb: ', '')
text=text.replace('Operation.', 'Operation->')
text=text.replace('new ERR_IntelRerollEntry()', 'new Entry()').replace('new ERR_IntelReroll(', 'new Batch(').replace('new ERR_IntelRequest()', 'new Request()').replace('new ERR_IntelInsertCallback()', 'new Callback()')
text=re.sub(r'^.*Print\([^\n]*$', ';',text,flags=re.M)
text=re.sub(r'^(\w+) (\w+)\(',lambda m:m[1]+' '+cls+'::'+m[2]+'(',text)
# Defaults belong only to declarations in C++.
text=text.replace('bool failed = false, bool skipped = false','bool failed, bool skipped')
return text
service_methods=[('void RegisterGroup(', 'void RegisterGroup(SCR_AIGroup group);'),('void Tick(', 'void Tick(float now);'),('bool BeginReroll(', 'bool BeginReroll();'),('bool HasPendingInsertion(', 'bool HasPendingInsertion(IEntity actor);'),('void Unsettled(', 'void Unsettled(IEntity actor);'),('bool Settled(', 'bool Settled(IEntity actor);'),('void Stop(', 'void Stop();')]
batch_methods=[('void Add(', 'void Add(IEntity actor);'),('bool Running(', 'bool Running();'),('void Tick(', 'void Tick();'),('protected void Finish(', 'void Finish(ERR_IntelRerollEntry entry,bool failed=false,bool skipped=false);'),('protected void Fail(', 'void Fail(ERR_IntelRerollEntry entry,string reason);'),('protected void Process(', 'void Process(ERR_IntelRerollEntry entry);'),('void Stop(', 'void Stop();')]
code=r'''
#include <cassert>
#include <algorithm>
#include <vector>
#include <set>
#include <string>
using string=std::string;
template<class T> struct Array:std::vector<T>{int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Insert(T a){this->push_back(a);}void Remove(int i){this->erase(this->begin()+i);}void Clear(){this->clear();}bool Contains(T a){return std::find(this->begin(),this->end(),a)!=this->end();}};
template<class T> struct Set:std::set<T>{bool Contains(T a){return this->count(a);}void Insert(T a){this->insert(a);}void RemoveItem(T a){this->erase(a);}void Clear(){this->clear();}};
namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace Math{int rolls=0;int RandomInt(int,int){rolls++;return 0;}int Min(int a,int b){return std::min(a,b);}}
const int INVENTORY=1,ITEM=2,DOCUMENT=3;
struct Actor;using IEntity=Actor*;
struct Item{bool attached=false;bool GetParentSlot(){return attached;}static Item* Cast(void* p){return (Item*)p;}};using InventoryItemComponent=Item;
struct Callback{bool Finished=false,Succeeded=false;};
struct Inventory{Array<IEntity> folders;int spawns=0,removes=0;bool acceptSpawn=true,acceptRemove=true;bool TrySpawnPrefabToStorage(int,int,Callback*){spawns++;return acceptSpawn;}bool TryRemoveItemFromInventory(IEntity){removes++;return acceptRemove;}static Inventory* Cast(void* p){return (Inventory*)p;}};using SCR_InventoryStorageManagerComponent=Inventory;
struct Actor{bool alive=true,player=false,document=false,parent=false,hasInventory=true;Item item;Inventory inv;void* FindComponent(int kind){if(kind==INVENTORY)return hasInventory?&inv:nullptr;if(kind==ITEM)return &item;return document?this:nullptr;}bool GetParent(){return parent;}};
struct Agent{IEntity actor;IEntity GetControlledEntity(){return actor;}};using AIAgent=Agent*;
struct Group{Array<AIAgent> agents;void GetAgents(Array<AIAgent>& out){out=agents;}};using SCR_AIGroup=Group*;
namespace ERR_Actors{bool Alive(IEntity a){return a&&a->alive;}bool Player(IEntity a){return a&&a->player;}}
namespace ERR_Resources{int IntelFolder(){return 1;}}
struct Discovery{Set<IEntity> spent;bool capacity=true;static void Folders(Inventory* i,Array<IEntity>& out){out=i->folders;}bool IsSpent(IEntity a){return spent.Contains(a);}bool CanMarkSpent(IEntity a){return capacity;}bool MarkSpent(IEntity a){if(!capacity)return false;spent.Insert(a);return true;}};using ERR_DiscoveryService=Discovery;
struct Settings{int chance=100;int Get(const char*){return chance;}};
struct Mode{bool active=true;struct Settings settings;struct Discovery discovery;Set<IEntity> owned;bool Active(){return active;}struct Settings* Settings(){return &settings;}struct Discovery* Discovery(){return &discovery;}void Own(IEntity a){owned.Insert(a);}};
struct Entry{string WaitingFor="inventory";Callback* Operation=nullptr;IEntity Actor=nullptr,Removing=nullptr;int Attempts=0;bool GiveIntel=false,Inserting=false,Accepted=false,Done=false;};using ERR_IntelRerollEntry=Entry*;
struct Request{Callback* Operation=nullptr;IEntity Actor=nullptr;int Attempts=0;bool Accepted=false;};using ERR_IntelRequest=Request*;
struct Service;struct Batch;
struct Batch{Mode* m_Mode;Service* m_Service;Array<ERR_IntelRerollEntry> m_Entries;int m_Percent=0,m_Next=0,m_Completed=0,m_Failed=0,m_Skipped=0;bool m_Cancelled=false;Batch(Mode* m,Service* s,int p):m_Mode(m),m_Service(s),m_Percent(p){}
'''+ '\n'.join(x[1] for x in batch_methods)+r'''
};
struct Service{Mode* m_Mode;Array<ERR_IntelRequest> m_Requests;Set<IEntity> m_Rolled,m_Unsettled;Batch* m_Reroll=nullptr;bool m_Stopped=false;Service(Mode* m):m_Mode(m){}
'''+ '\n'.join(x[1] for x in service_methods)+r'''
};
'''+ '\n'.join(adapt(body(service,x[0]),'Service') for x in service_methods)+ '\n'.join(adapt(body(batch,x[0]),'Batch') for x in batch_methods)+r'''
void Add(Service& s,Actor& a){s.m_Rolled.Insert(&a);}
void Drain(Service& s,int n=70){for(int i=0;i<n;i++)s.Tick(i);}
int main(){
// Authority, terminal/stopped rounds, snapshot chance/cohort and duplicate guard.
Mode m;Service s(&m);Actor a,late;Add(s,a);Replication::server=false;assert(!s.BeginReroll());Replication::server=true;m.active=false;assert(!s.BeginReroll());m.active=true;m.settings.chance=0;assert(s.BeginReroll());assert(!s.BeginReroll());m.settings.chance=100;Add(s,late);Drain(s);assert(s.m_Reroll->m_Entries.Count()==1&&s.m_Reroll->m_Percent==0&&a.inv.spawns==0&&late.inv.spawns==0&&s.m_Reroll->m_Completed==1);
// Old unaccepted work cancelled, accepted native spawn retained and observed.
Mode pendingMode;Service pending(&pendingMode);Actor p,folder;Add(pending,p);auto req=new Request();req->Actor=&p;pending.m_Requests.Insert(req);pendingMode.settings.chance=0;assert(pending.BeginReroll());assert(pending.m_Requests.IsEmpty());Drain(pending);assert(p.inv.spawns==0);
auto accepted=new Request();accepted->Actor=&p;accepted->Accepted=true;accepted->Operation=new Callback();pending.m_Requests.Insert(accepted);assert(pending.BeginReroll());pending.Tick(0);assert(pending.m_Requests.Count()==1&&p.inv.removes==0&&p.inv.spawns==0);p.inv.folders.Insert(&folder);folder.item.attached=true;pending.Tick(1);assert(pending.m_Requests.Count()==1&&p.inv.removes==0);accepted->Operation->Finished=true;accepted->Operation->Succeeded=true;pending.Tick(1);assert(pending.m_Requests.IsEmpty()&&p.inv.removes==1&&pendingMode.discovery.IsSpent(&folder)&&pendingMode.owned.Contains(&folder));pending.Tick(2);assert(p.inv.removes==1);p.inv.folders.Clear();folder.item.attached=false;pending.Tick(3);assert(!pending.m_Reroll->Running()&&p.inv.spawns==0);
// Accepted insertion never repeated. A vanished/late native insertion blocks rerolls.
Mode delayedMode;Service delayed(&delayedMode);Actor d;Add(delayed,d);assert(delayed.BeginReroll());Drain(delayed);assert(d.inv.spawns==1&&delayed.m_Reroll->m_Failed==1&&delayed.m_Unsettled.Contains(&d));assert(delayed.BeginReroll());Drain(delayed);assert(d.inv.spawns==1&&delayed.m_Reroll->m_Failed==1);Actor arrived;d.inv.folders.Insert(&arrived);assert(delayed.BeginReroll());delayed.Tick(0);assert(d.inv.removes==0&&delayed.m_Unsettled.Contains(&d));
// Arbitrary transferred folder never acknowledges a pending reroll insertion.
Mode extraMode;Service extra(&extraMode);Actor e,unrelated;Add(extra,e);assert(extra.BeginReroll());extra.Tick(0);assert(e.inv.spawns==1);e.inv.folders.Insert(&unrelated);extra.Tick(1);assert(extra.m_Reroll->Running()&&!extra.BeginReroll()&&e.inv.spawns==1);auto op=extra.m_Reroll->m_Entries[0]->Operation;op->Finished=true;op->Succeeded=true;extra.Tick(2);assert(!extra.m_Reroll->Running()&&extra.m_Reroll->m_Failed==0);
// A surviving collected/spent folder or targeted mission document is untouched.
Mode safeMode;Service safe(&safeMode);Actor collector,spent,mission,doc;Add(safe,collector);Add(safe,mission);collector.inv.folders.Insert(&spent);safeMode.discovery.spent.Insert(&spent);mission.inv.folders.Insert(&doc);doc.document=true;assert(safe.BeginReroll());Drain(safe);assert(safe.m_Reroll->m_Skipped==2&&collector.inv.removes==0&&mission.inv.removes==0&&safeMode.discovery.IsSpent(&spent));
// Newly player-controlled/dead cohort members skipped without touching inventory.
Mode skipMode;Service skip(&skipMode);Actor player,dead;Add(skip,player);Add(skip,dead);assert(skip.BeginReroll());player.player=true;dead.alive=false;Drain(skip);assert(skip.m_Reroll->m_Skipped==2&&player.inv.spawns==0&&dead.inv.spawns==0);
// Removal ownership race and spent capacity refusal never create replacement.
Mode raceMode;Service race(&raceMode);Actor ai,f;Add(race,ai);ai.inv.folders.Insert(&f);assert(race.BeginReroll());race.Tick(0);assert(ai.inv.removes==1);ai.inv.folders.Clear();f.item.attached=true;race.Tick(1);assert(race.m_Reroll->m_Skipped==1&&ai.inv.spawns==0);
Mode capMode;Service cap(&capMode);Actor ca,cf;Add(cap,ca);ca.inv.folders.Insert(&cf);capMode.discovery.capacity=false;assert(cap.BeginReroll());Drain(cap);assert(cap.m_Reroll->m_Failed==1&&ca.inv.removes==0&&ca.inv.spawns==0);
// Rejected native removal preserves usable unspent documents for a retry.
Mode rejectMode;Service reject(&rejectMode);Actor ra,rf;Add(reject,ra);ra.inv.folders.Insert(&rf);ra.inv.acceptRemove=false;rejectMode.settings.chance=0;assert(reject.BeginReroll());reject.Tick(0);assert(reject.m_Reroll->Running()&&reject.m_Reroll->m_Failed==0&&!rejectMode.discovery.IsSpent(&rf)&&!rejectMode.owned.Contains(&rf));ra.inv.acceptRemove=true;reject.Tick(1);assert(ra.inv.removes==2&&rejectMode.discovery.IsSpent(&rf)&&rejectMode.owned.Contains(&rf));ra.inv.folders.Clear();rf.item.attached=false;reject.Tick(2);assert(!reject.m_Reroll->Running()&&reject.m_Reroll->m_Failed==0);
// Persistent refusal exhausts exactly thirty native attempts without spending.
Mode refusalMode;Service refusal(&refusalMode);Actor pa,pf;Add(refusal,pa);pa.inv.folders.Insert(&pf);pa.inv.acceptRemove=false;assert(refusal.BeginReroll());Drain(refusal);assert(pa.inv.removes==30&&refusal.m_Reroll->m_Failed==1&&!refusalMode.discovery.IsSpent(&pf)&&!refusalMode.owned.Contains(&pf)&&refusal.m_Reroll->m_Entries[0]->WaitingFor=="removal-refused");
// Tick budget, bounded missing inventory, Stop prevents further work/restart.
Mode manyMode;Service many(&manyMode);Actor actors[20];for(auto& actor:actors){actor.hasInventory=false;Add(many,actor);}assert(many.BeginReroll());many.Tick(0);int attempts=0;for(auto entry:many.m_Reroll->m_Entries)attempts+=entry->Attempts;assert(attempts==8);Drain(many,100);assert(many.m_Reroll->m_Failed==20);many.Stop();assert(!many.BeginReroll()&&many.m_Rolled.empty()&&many.m_Requests.IsEmpty()&&many.m_Reroll->m_Cancelled);for(auto entry:many.m_Reroll->m_Entries)assert(!entry->Actor&&!entry->Removing);
}
'''
run(code)
source=batch.read_text()
assert 'DeleteEntity' not in source and 'CallLater' not in source
assert source.index('CanMarkSpent(folder)') < source.index('TryRemoveItemFromInventory(folder)') < source.index('Discovery().MarkSpent(folder)')
assert 'm_Mode.Own(folder)' in source
assert '%3/%4 AI processed, %5 failed, %6 skipped.' in source
assert 'ERR_IntelService Intel()' in service.read_text()
print('PASS: actual reroll snapshot, authority, cancellation, ownership, accepted operations, quarantine, bounded work and stop methods; no native runtime claim')