Deliver local sighting handoffs and preserve unsubmitted resupply attempts
This commit is contained in:
@@ -13,7 +13,7 @@ def trans(s):
|
||||
s=s.replace('string.Format','Format').replace('string ', 'std::string ')
|
||||
s=s.replace('Math.Round(distance).ToString()', 'std::to_string(int(distance))')
|
||||
s=re.sub(r'\b(\w+)\.ToString\(\)',r'std::to_string(\1)',s)
|
||||
for name in ['Math','vector']:s=s.replace(name+'.',name+'::')
|
||||
for name in ['Math','vector','ERR_HQLocalHandoff']:s=s.replace(name+'.',name+'::')
|
||||
for typ in ['IEntity','SCR_AIGroup','ERR_HQRoute','ERR_HQRadioNode','ERR_HQJournal']:
|
||||
s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
|
||||
s=s.replace('array<int> queue = {}','Arr<int> queue').replace('array<int> cell','Arr<int>* cell').replace('array<int> neighbors','Arr<int>* neighbors')
|
||||
@@ -43,8 +43,9 @@ struct SCR_AIGroup{bool enemy=true;};struct IEntity{vector pos;bool alive=true;i
|
||||
struct ERR_HQRadioNode{IEntity* Carrier{};SCR_AIGroup* Group{};int UnitId{},Capability{};bool Valid(bool manpack=false){return Carrier&&Carrier->alive&&!Carrier->captured&&Carrier->radio>=(manpack?2:1);}std::string Label(){return "unit "+std::to_string(UnitId);}};
|
||||
struct ERR_HQJournal{Arr<std::string> events;void Write(std::string kind,std::string text,int){events.Insert(kind+" "+text);}};
|
||||
'''
|
||||
routefields='Arr<ERR_HQRadioNode*> Nodes;bool Downlink{},Sending{},Failed{},Originated{};int Cursor{},MessageId{};float Due{},StartedAt=-1,OriginDue=-1;vector SentFrom;std::string Payload;'
|
||||
networkfields='Arr<ERR_HQRadioNode*> m_Nodes;Arr<int> m_Parents,m_Depths;Map<IEntity*,int> m_Indices;Map<std::string,Arr<int>*> m_Cells;bool Overflow{};'
|
||||
headers+='struct ERR_HQStatusSampler{};struct ERR_HQLocalHandoff{static bool Valid(SCR_AIGroup*,ERR_HQStatusSampler*,IEntity*,ERR_HQRadioNode*){return true;}};\n'
|
||||
routefields='SCR_AIGroup* LocalSource=nullptr;IEntity* LocalWitness=nullptr;ERR_HQStatusSampler* LocalRoster=nullptr;Arr<ERR_HQRadioNode*> Nodes;bool Downlink{},Sending{},Failed{},Originated{};int Cursor{},MessageId{};float Due{},StartedAt=-1,OriginDue=-1;vector SentFrom;std::string Payload;'
|
||||
networkfields='int m_LocalChecks=0;Arr<ERR_HQRadioNode*> m_Nodes;Arr<int> m_Parents,m_Depths;Map<IEntity*,int> m_Indices;Map<std::string,Arr<int>*> m_Cells;bool Overflow{};'
|
||||
route='struct ERR_HQRoute{'+routefields+'\n'+'\n'.join(trans(extract(x)) for x in ['int Advance(', 'protected string SenderLabel(', 'protected int Fail('])+'};\n'
|
||||
network='struct Network{'+networkfields+'\n'+'\n'.join(trans(extract(x)) for x in ['void Clear(', 'protected string Cell(', 'void Add(', 'void Build(', 'ERR_HQRoute Route('])+'};\n'
|
||||
tests=r'''
|
||||
|
||||
@@ -13,7 +13,7 @@ def cpp(s):
|
||||
s=re.sub(r'\b'+t+r' (?=\w)',t+'* ',s)
|
||||
for v in ('stock','report','unit','route'):
|
||||
s=s.replace(v+'.',v+'->')
|
||||
return s.replace('AmmoState.','AmmoState->')
|
||||
return s.replace('AmmoState.','AmmoState->').replace('Task.','Task->')
|
||||
policy=cpp(P.read_text()).replace('class ERR_HQAmmoReporting','struct ERR_HQAmmoReporting').rstrip()+';'
|
||||
methods='\n'.join(cpp(body(H,x)) for x in ('protected void StatusReport(', 'protected void QueueReports('))
|
||||
# The production hook must preserve the only existing delivery gate and use the policy per identity.
|
||||
@@ -44,10 +44,12 @@ struct Sampler{int observations=0,samples=0;bool valid=true;ERR_HQAmmoSnapshot s
|
||||
struct ERR_HQRoute{int MessageId=0;string Payload;};
|
||||
struct Network{bool available=true;int routes=0;ERR_HQRoute* Route(void*){routes++;return available?new ERR_HQRoute:nullptr;}};
|
||||
struct Journal{int seq=0,writes=0;int Message(){return ++seq;}void Write(string,string,int){writes++;}};
|
||||
struct ERR_HQUnit{void* Radio=this;ERR_HQRoute* Uplink=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;
|
||||
struct Task{float ArrivedAt=0;};
|
||||
struct ERR_HQUnit{struct Task* Task=nullptr;void* Radio=this;ERR_HQRoute* Uplink=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;
|
||||
float NextStatus=0;ERR_HQAmmoReporting AmmoReporting;Sampler StatusSampler;bool visible=true,contact=false,sweep=false;
|
||||
ERR_HQUnit(){AmmoReporting.Initialize(6,0);}};
|
||||
struct HQ{Network m_Network;Journal m_Journal;int delivered=0;
|
||||
bool OriginOccupied(void*,ERR_HQUnit*){return false;}
|
||||
void Observe(ERR_HQUnit* u,float){if(!u->Uplink&&u->Radio&&u->visible&&m_Network.available){u->contact=true;u->Uplink=new ERR_HQRoute;}}
|
||||
void ObserveBody(ERR_HQUnit*,float){}
|
||||
void Sweep(ERR_HQUnit* u,float){if(!u->Uplink&&u->Radio&&!u->visible&&m_Network.available)u->sweep=true;}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Actual local handoff predicate/history/network route/origin delivery state machine."""
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
from policy_harness import ROOT,body,run
|
||||
network=Path(os.environ.get('ERR_NETWORK_SOURCE',ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQNetwork.c'))
|
||||
helper=Path(os.environ.get('ERR_HANDOFF_SOURCE',ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQLocalHandoff.c'))
|
||||
# Reuse the existing native graph syntax translation, not its policy doubles.
|
||||
base=Path(os.environ.get('ERR_NETWORK_HARNESS',ROOT/'tests/hq_network_regression.py')).read_text()
|
||||
ns={'__file__':str(ROOT/'tests/hq_network_regression.py')}
|
||||
exec(base[:base.index("headers=r'''")],ns)
|
||||
base_trans=ns['trans']
|
||||
def trans(s):
|
||||
s=base_trans(s).replace('ref ','')
|
||||
s=s.replace('ERR_GameMode.Get()', 'ERR_GameMode::Get()').replace('ERR_EnemyEquipmentRules.','ERR_EnemyEquipmentRules::').replace('ERR_ForceRules.','ERR_ForceRules::').replace('ERR_HQLocalHandoff.','ERR_HQLocalHandoff::')
|
||||
s=re.sub(r'\bERR_GameMode (\w+)',r'ERR_GameMode* \1',s)
|
||||
s=re.sub(r'\bERR_HQStatusSampler (\w+)',r'ERR_HQStatusSampler* \1',s)
|
||||
s=s.replace('array<IEntity> busy','Arr<IEntity*>* busy').replace('array<IEntity>','Arr<IEntity*>').replace('array<float>','Arr<float>')
|
||||
s=s.replace('foreach (ERR_HQRadioNode* node : m_Nodes)','for (ERR_HQRadioNode* node : m_Nodes)')
|
||||
for name in ('roster','receiver','witness','busy'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('receiver->Carrier.GetOrigin()', 'receiver->Carrier->GetOrigin()')
|
||||
return s
|
||||
headers=r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <limits>
|
||||
struct vector{float v[3]{};float operator[](int i)const{return v[i];}static float DistanceXZ(vector a,vector b){return std::hypot(a[0]-b[0],a[2]-b[2]);}static float Distance(vector a,vector b){return std::sqrt(std::pow(a[0]-b[0],2)+std::pow(a[1]-b[1],2)+std::pow(a[2]-b[2],2));}};
|
||||
vector V(float x,float z=0,float y=0){return {{x,y,z}};}
|
||||
namespace Math{int Floor(float x){return std::floor(x);}}
|
||||
std::string Format(const char*,int x,int z){return std::to_string(x)+":"+std::to_string(z);}
|
||||
template<class T>struct Arr:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Clear(){this->clear();}void Insert(T x){this->push_back(x);}void InsertAt(T x,int i){this->insert(this->begin()+i,x);}void Remove(int i){this->erase(this->begin()+i);}int Find(T x){auto it=std::find(this->begin(),this->end(),x);return it==this->end()?-1:int(it-this->begin());}bool Contains(T x){return Find(x)>=0;}};
|
||||
template<class K,class V>struct Map:std::map<K,V>{void Clear(){this->clear();}bool Contains(K k){return this->count(k);}bool Find(K k,V& v){if(!Contains(k))return false;v=this->at(k);return true;}V Get(K k){if(!Contains(k))return {};return this->at(k);}void Insert(K k,V v){(*this)[k]=v;}};
|
||||
struct SCR_AIGroup{bool enemy=true,military=true;int players=0;};
|
||||
struct IEntity{vector pos;bool alive=true,captured=false,player=false;int radio=0;SCR_AIGroup* group=nullptr;vector GetOrigin(){return pos;}};
|
||||
struct ERR_GameMode{static ERR_GameMode*Get(){static ERR_GameMode m;return &m;}};
|
||||
struct ERR_EnemyEquipmentRules{static bool Eligible(ERR_GameMode* m,SCR_AIGroup* g,IEntity* a){return m&&g&&g->enemy&&g->military&&g->players==0&&a&&a->alive&&!a->captured&&!a->player&&a->group==g;}};
|
||||
struct ERR_ForceRules{static bool Finite(float x){return std::isfinite(x);}};
|
||||
struct ERR_HQRadioNode{IEntity* Carrier{};SCR_AIGroup* Group{};int UnitId{},Capability{};bool Valid(bool manpack=false){return ERR_EnemyEquipmentRules::Eligible(ERR_GameMode::Get(),Group,Carrier)&&Carrier->radio>=(manpack?2:1);}std::string Label(){return "unit "+std::to_string(UnitId);}};
|
||||
struct ERR_HQStatusSampler{SCR_AIGroup*group=nullptr;Arr<IEntity*>members;bool attributed=true;bool OwnsRoster(SCR_AIGroup*g){return group&&group==g;}int InitialStrength(){return members.Count();}IEntity*InitialMember(int i){return members[i];}bool CanAttribute(int i,IEntity*a){return attributed&&members[i]==a;}};
|
||||
struct ERR_HQJournal{Arr<std::string> events;void Write(std::string kind,std::string text,int){events.Insert(kind+" "+text);}};
|
||||
'''
|
||||
helper_code=trans(helper.read_text()).replace('class ERR_HQLocalHandoff','struct ERR_HQLocalHandoff').replace('class ERR_HQObservationHistory','struct ERR_HQObservationHistory').replace('\n}\n','\n};\n')
|
||||
route_fields='SCR_AIGroup* LocalSource=nullptr;IEntity* LocalWitness=nullptr;ERR_HQStatusSampler* LocalRoster=nullptr;Arr<ERR_HQRadioNode*> Nodes;bool Downlink{},Sending{},Failed{},Originated{};int Cursor{},MessageId{};float Due{},StartedAt=-1,OriginDue=-1;vector SentFrom;std::string Payload;'
|
||||
route='struct ERR_HQRoute{'+route_fields+'\n'+'\n'.join(trans(body(network,x)) for x in ['int Advance(', 'protected string SenderLabel(', 'protected int Fail('])+'};\n'
|
||||
network_fields='int m_LocalChecks=0;Arr<ERR_HQRadioNode*> m_Nodes;Arr<int> m_Parents,m_Depths;Map<IEntity*,int> m_Indices;Map<std::string,Arr<int>*> m_Cells;bool Overflow{};'
|
||||
graph='struct Network{'+network_fields+'\n'+'\n'.join(trans(body(network,x)) for x in ['void Clear(', 'protected string Cell(', 'void Add(', 'void Build(', 'ERR_HQRoute Route(', 'ERR_HQRoute LocalRoute('])+'};\n'
|
||||
run(headers+helper_code+route+graph+r'''
|
||||
struct Fixture{SCR_AIGroup source,receiver;IEntity witness,caller,other;ERR_HQStatusSampler roster;Network network;Arr<IEntity*>busy;
|
||||
Fixture(){witness.group=&source;caller.group=&receiver;caller.radio=2;caller.pos=V(100);roster.group=&source;roster.members={&witness};network.Add(&caller,&receiver,2,2);network.Build();}
|
||||
ERR_HQRoute* Local(){return network.LocalRoute(&source,&roster,&busy);}
|
||||
};
|
||||
int main(){
|
||||
{Fixture f;auto route=f.Local();assert(route&&route->Nodes[0]->Carrier==&f.caller&&route->LocalSource==&f.source&&route->LocalWitness==&f.witness&&route->LocalRoster==&f.roster);
|
||||
ERR_HQJournal journal;route->MessageId=17;route->Payload="unchanged sighting";assert(route->Advance(0,&journal)==0&&!route->Originated&&!route->Sending);
|
||||
assert(route->Advance(2999,&journal)==0&&!route->Originated);assert(route->Advance(3000,&journal)==0&&route->Originated&&route->Sending);
|
||||
// Local information is already handed over; later witness loss cannot erase it.
|
||||
f.witness.alive=false;assert(route->Advance(5000,&journal)==1&&route->MessageId==17&&route->Payload=="unchanged sighting");}
|
||||
for(int reason=0;reason<13;reason++){
|
||||
Fixture f;auto r=f.Local();ERR_HQJournal j;assert(r&&r->Advance(0,&j)==0);
|
||||
if(reason==0){f.witness.alive=false;}if(reason==1){f.witness.group=&f.receiver;}
|
||||
if(reason==2){f.witness.player=true;}if(reason==3){f.roster.attributed=false;}
|
||||
if(reason==4){f.caller.radio=0;}if(reason==5){f.caller.alive=false;}
|
||||
if(reason==6){f.caller.captured=true;}if(reason==7){f.caller.group=&f.source;}
|
||||
if(reason==8){f.caller.pos=V(100.01f);}if(reason==9){f.caller.pos=V(0,0,100.01f);}
|
||||
if(reason==10){r->LocalSource=nullptr;}if(reason==11){r->LocalWitness=nullptr;}
|
||||
if(reason==12){f.source.players=1;}
|
||||
assert(r->Advance(1000,&j)==-1&&r->Failed&&!r->Originated&&!r->Sending);assert(r->Advance(5000,&j)==-1);
|
||||
}
|
||||
// Never trade an invalid first member for an unowned replacement; a different original may hand over.
|
||||
{Fixture f;f.roster.members[0]=&f.other;assert(!f.Local());f.other.group=&f.source;f.roster.members.Insert(&f.witness);assert(f.Local());}
|
||||
{Fixture f;f.busy.Insert(&f.caller);assert(!f.Local());f.busy.Clear();assert(f.Local());}
|
||||
{Fixture f;f.caller.radio=1;f.network.Clear();f.network.Add(&f.caller,&f.receiver,2,1);f.network.Build();assert(!f.Local());}
|
||||
{Fixture f;f.roster.members.resize(33,&f.witness);assert(!f.Local());}
|
||||
{Fixture f;f.network.m_LocalChecks=4096;assert(!f.Local());f.network.Clear();f.network.Add(&f.caller,&f.receiver,2,2);f.network.Build();assert(f.Local());}
|
||||
{Fixture f;f.network.Overflow=true;assert(!f.Local());}
|
||||
// Direct routes remain unaffected and final radio loss is still terminal after local completion.
|
||||
{Fixture f;auto direct=f.network.Route(&f.caller);f.witness.alive=false;ERR_HQJournal j;assert(direct->Advance(0,&j)==0);assert(direct->Advance(3000,&j)==0);assert(direct->Advance(5000,&j)==1);}
|
||||
{Fixture f;auto r=f.Local();ERR_HQJournal j;r->Advance(0,&j);r->Advance(3000,&j);f.caller.radio=0;assert(r->Advance(5000,&j)==-1);}
|
||||
// Per-source watermark rejects duplicate/replayed/future/stale sightings including failed-route attempts.
|
||||
{ERR_HQObservationHistory history;IEntity target;assert(history.CanQueue(&target,100,100));history.Queued(&target,100,100);
|
||||
assert(!history.CanQueue(&target,100,101)&&!history.CanQueue(&target,99,101));assert(history.CanQueue(&target,101,101));
|
||||
assert(!history.CanQueue(&target,102,101)&&!history.CanQueue(&target,101,602));
|
||||
assert(!history.CanQueue(&target,std::numeric_limits<float>::quiet_NaN(),602));}
|
||||
{ERR_HQObservationHistory history;IEntity targets[65];for(int i=0;i<64;i++){history.Queued(&targets[i],100,100);}assert(!history.CanQueue(&targets[64],100,100));
|
||||
assert(history.CanQueue(&targets[64],120101,120101));assert(!history.CanQueue(&targets[0],100,120101));}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual original-roster local handoff,100m3D bound,busy carrier/budget exclusion,origin interruption including deletedsource,normal radio delivery and per-source replay history')
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Execute actual HQ route choice/visual queue/caller exclusion with radio boundary doubles."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,body,run
|
||||
p=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c'))
|
||||
s='\n'.join(body(p,x) for x in ('protected bool OriginOccupied(', 'protected ERR_HQRoute ObservationRoute(', 'protected void Observe(', 'protected void QueueReports('))
|
||||
s=s.replace('protected ','').replace('array<IEntity>','Arr<IEntity>').replace('array<ref ERR_HQVisualContact>','Arr<ERR_HQVisualContact*>').replace('foreach (','for (')
|
||||
for name in ('ERR_HQUnit','ERR_HQRoute','ERR_HQReport','ERR_HQVisualContact'):
|
||||
s=re.sub(r'\b'+name+r' (\w+)',name+r'* \1',s)
|
||||
for name in ('unit','other','route','own','seen','report'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
for name in ('Uplink','Task'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('Nodes[0].','Nodes[0]->').replace('m_Mode.Settings().Get','m_Mode->Settings()->Get').replace('ERR_HQObservation.','ERR_HQObservation::').replace('null','nullptr')
|
||||
s=s.replace('ERR_HQRoute* own;','ERR_HQRoute* own=nullptr;').replace('ERR_HQRoute* route;','ERR_HQRoute* route=nullptr;')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
using IEntity=void*;
|
||||
using string=std::string;
|
||||
template<class T>struct Arr:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;bool IsEmpty(){return this->empty();}int Count(){return this->size();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}void Insert(T v){this->push_back(v);}};
|
||||
struct Group{};
|
||||
struct Node{IEntity Carrier=nullptr;bool valid=true;bool Valid(bool){return valid;}};
|
||||
struct ERR_HQRoute{bool Originated=false,Failed=false;Group*LocalSource=nullptr;Arr<Node*> Nodes;int MessageId=0;string Payload;};
|
||||
struct ERR_HQVisualContact{IEntity Entity=nullptr;int Position=91;float ObservedAt=900;};
|
||||
struct Utility{bool visible=true;ERR_HQVisualContact sample;};
|
||||
struct ERR_HQObservation{static void Gather(Utility*u,float,Arr<ERR_HQVisualContact*>&out){if(u->visible)out.Insert(&u->sample);}template<class T,class A>static void Describe(T*,A&,float){} };
|
||||
struct Selector{int queues=0;ERR_HQVisualContact*Select(Arr<ERR_HQVisualContact*>&s){return s[0];}void Queued(IEntity){queues++;}};
|
||||
struct History{IEntity target=nullptr;float time=-1;bool CanQueue(IEntity t,float at,float){return t&&!(t==target&&at<=time);}void Queued(IEntity t,float at,float){target=t;time=at;}};
|
||||
struct ERR_HQReport{int MessageId=0,Position=0;IEntity Target=nullptr;Group*Source=nullptr;float ObservedAt=0;string Description(float){return "unaltered source sighting";}};
|
||||
struct Settings{int Get(string){return 5;}};
|
||||
struct Mode{struct Settings values;struct Settings*Settings(){return &values;}};
|
||||
struct Task{float ArrivedAt=0;};
|
||||
struct ERR_HQUnit{struct Group*Group=nullptr;struct Utility*Utility=nullptr;IEntity Radio=nullptr;float NextReport=0;ERR_HQRoute*Uplink=nullptr;ERR_HQReport*PendingReport=nullptr;Selector ReportSelection;History SentObservations;int StatusSampler=1;struct Task*Task=nullptr;};
|
||||
struct Network{ERR_HQRoute*own=nullptr,*local=nullptr;IEntity ownCarrier=nullptr;int attempts=0;Arr<IEntity>lastBusy;
|
||||
ERR_HQRoute*Route(IEntity caller){return caller==ownCarrier?own:nullptr;}
|
||||
ERR_HQRoute*LocalRoute(Group*source,int,Arr<IEntity>&busy){attempts++;lastBusy=busy;if(!local||busy.Contains(local->Nodes[0]->Carrier))return nullptr;local->LocalSource=source;return local;}};
|
||||
struct Journal{int next=0,reports=0;int Message(){return ++next;}void Write(string kind,string,int){if(kind=="REPORT")reports++;}};
|
||||
struct HQ{Mode mode;Mode*m_Mode=&mode;Network m_Network;Journal m_Journal;Arr<ERR_HQUnit*>m_Units;int statuses=0,bodies=0,sweeps=0;
|
||||
void StatusReport(ERR_HQUnit*,float){statuses++;}void ObserveBody(ERR_HQUnit*,float){bodies++;}void Sweep(ERR_HQUnit*,float){sweeps++;}
|
||||
''' + s.replace('static void Describe(auto*,auto&,float)','template<class T,class A>static void Describe(T*,A&,float)') + r'''
|
||||
};
|
||||
int main(){
|
||||
HQ h;Group sourceGroup,receiverGroup;int witness,carrier,target;Utility utility;utility.sample.Entity=⌖ERR_HQUnit source,receiver;source.Group=&sourceGroup;source.Utility=&utility;receiver.Group=&receiverGroup;receiver.Radio=&carrier;Node node;node.Carrier=&carrier;ERR_HQRoute local;local.Nodes={&node};h.m_Network.local=&local;h.m_Units={&source,&receiver};
|
||||
Utility quietUtility;quietUtility.visible=false;ERR_HQUnit quiet;quiet.Group=&sourceGroup;quiet.Utility=&quietUtility;
|
||||
h.Observe(&quiet,0);assert(h.m_Network.attempts==0&&quiet.NextReport==5000);
|
||||
h.Observe(&source,1000);assert(source.Uplink==&local&&source.PendingReport&&h.m_Journal.reports==1&&source.ReportSelection.queues==1);
|
||||
auto report=source.PendingReport;assert(report->Source==&sourceGroup&&report->Target==&target&&report->ObservedAt==900&&report->Position==91&&report->MessageId==local.MessageId);
|
||||
assert(receiver.PendingReport==nullptr&&receiver.Uplink==nullptr); // No recipient-owned rewrite or direct HQ intake.
|
||||
h.Observe(&source,1001);assert(source.PendingReport==report&&h.m_Journal.reports==1);
|
||||
// Receiver cannot originate its own reports during the source's physical origin hold.
|
||||
Task task;task.ArrivedAt=500;receiver.Task=&task;assert(h.OriginOccupied(&carrier,&receiver));h.QueueReports(&receiver,1001);assert(task.ArrivedAt==-1&&h.statuses==0&&h.sweeps==0);
|
||||
ERR_HQUnit contender;contender.Group=&sourceGroup;contender.Utility=&utility;assert(!h.ObservationRoute(&contender));
|
||||
local.Originated=true;assert(!h.OriginOccupied(&carrier,&receiver));
|
||||
// Explicit own pending uplink also excludes a candidate even after its origin phase.
|
||||
source.Uplink=nullptr;receiver.Uplink=&local;assert(!h.ObservationRoute(&contender));receiver.Uplink=nullptr;
|
||||
// Identical observed lineage never receives a second message id even after failure/slot release.
|
||||
source.NextReport=0;h.Observe(&source,1100);assert(!source.Uplink&&h.m_Journal.reports==1&&source.ReportSelection.queues==1);
|
||||
utility.sample.ObservedAt=1101;source.NextReport=0;h.Observe(&source,1101);assert(source.Uplink==&local&&h.m_Journal.reports==2&&source.PendingReport->ObservedAt==1101);
|
||||
// Prefer an actual own route; a currently invalid own caller falls back to the nearby usable route.
|
||||
source.Uplink=nullptr;Node ownNode;ownNode.Carrier=&witness;ERR_HQRoute own;own.Nodes={&ownNode};source.Radio=&witness;h.m_Network.ownCarrier=&witness;h.m_Network.own=&own;
|
||||
assert(h.ObservationRoute(&source)==&own);ownNode.valid=false;assert(h.ObservationRoute(&source)==&local);
|
||||
// A blocked fallback throttles the next no-radio attempt and creates no report.
|
||||
source.Radio=nullptr;h.m_Network.local=nullptr;source.NextReport=0;utility.sample.ObservedAt=2000;int messages=h.m_Journal.next;h.Observe(&source,2000);assert(source.NextReport==7000&&!source.Uplink&&h.m_Journal.next==messages);
|
||||
}
|
||||
'''.replace('static void Describe(auto*,auto&,float)','template<class T,class A>static void Describe(T*,A&,float)'))
|
||||
print('PASS: actual visual intake retains source/time/position/id, rejects replay, prefers usable own route, excludes simultaneous callers and conservatively resets negative-sweep dwell')
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Execute the actual bounded HQ report scheduler; radio integration stays native."""
|
||||
from policy_harness import ROOT, body, run
|
||||
from pathlib import Path
|
||||
import os
|
||||
source = ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQObservation.c'
|
||||
hq = ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c'
|
||||
hq = Path(os.environ.get('ERR_HQ_SOURCE',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;')
|
||||
|
||||
@@ -13,10 +13,11 @@ def cpp(s):
|
||||
s=s.replace('protected ','').replace('array<ref ERR_HQVisualContact>','Array<ERR_HQVisualContact*>')
|
||||
for t in ('ERR_HQAmmoSnapshot','ERR_HQUnit','ERR_HQVisualContact','ERR_HQRoute','ERR_HQReport','ERR_HQStatusReport','ERR_HQTask','ERR_HQSearchReport'):
|
||||
s=re.sub(r'\b'+t+r' (?=\w)',t+'* ',s)
|
||||
s=s.replace('ERR_HQRoute* route;', 'ERR_HQRoute* route=nullptr;')
|
||||
s=s.replace('ERR_HQReport* report;','ERR_HQReport* report = nullptr;')
|
||||
for v in ('unit','route','report','seen','task','stock'):
|
||||
s=s.replace(v+'.',v+'->')
|
||||
for v in ('Utility','ReportSelection','StatusSampler','Group'):
|
||||
for v in ('Utility','ReportSelection','StatusSampler','Group','Task'):
|
||||
s=s.replace('->'+v+'.','->'+v+'->')
|
||||
s=s.replace('m_Mode.Settings().Get(', 'm_Mode->Settings()->Get(')
|
||||
s=s.replace('ERR_HQPursuitRules.', 'ERR_HQPursuitRules::')
|
||||
@@ -56,11 +57,12 @@ struct ERR_HQSearchReport{int SourceId=0,MessageId=0,WatchMessageId=0,WatchReque
|
||||
string WatchSourceId;vector Position;float ObservedAt=0;bool SweepCompleted=false,ContactFound=false;};
|
||||
struct ERR_HQTask{bool SweepQueued=false;float ArrivedAt=-1;vector Destination;
|
||||
string WatchSourceId;int WatchMessageId=0,WatchRequestId=0,BeliefId=1,BeliefRevision=2;};
|
||||
struct ERR_HQRoute{int MessageId=0;string Payload;};
|
||||
struct ERR_HQRoute{Group* LocalSource=nullptr;int MessageId=0;string Payload;};
|
||||
struct Network{bool available=true;int routes=0;ERR_HQRoute* Route(void*){routes++;return available?new ERR_HQRoute:nullptr;}};
|
||||
struct Bodies{bool visible=false;bool Observe(Group*,float now,ERR_HQReport*& r){if(!visible)return false;r=new ERR_HQReport;r->ObservedAt=now;return true;}};
|
||||
struct Journal{int seq=0;int Message(){return ++seq;}void Write(string,string,int){}};
|
||||
struct ERR_HQUnit{int Id=1;struct Group ownGroup;struct Utility ownUtility;Selection selection;Sampler sampler;
|
||||
struct History{bool CanQueue(void*,float,float){return true;}void Queued(void*,float,float){}};
|
||||
struct ERR_HQUnit{History SentObservations;int Id=1;struct Group ownGroup;struct Utility ownUtility;Selection selection;Sampler sampler;
|
||||
struct Group* Group=&ownGroup;struct Utility* Utility=&ownUtility;void* Radio=this;
|
||||
Selection* ReportSelection=&selection;Sampler* StatusSampler=&sampler;
|
||||
ERR_HQRoute* Uplink=nullptr;ERR_HQReport* PendingReport=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;ERR_HQSearchReport* PendingSweep=nullptr;
|
||||
@@ -70,6 +72,8 @@ struct ERR_HQUnit{int Id=1;struct Group ownGroup;struct Utility ownUtility;Selec
|
||||
bool Eligible(Mode*){return eligible;}
|
||||
};
|
||||
struct HQ{Mode mode;Mode* m_Mode=&mode;Network m_Network;Bodies m_Bodies;Journal m_Journal;
|
||||
bool OriginOccupied(void*,ERR_HQUnit*){return false;}
|
||||
ERR_HQRoute* ObservationRoute(ERR_HQUnit* u){return u->Radio?m_Network.Route(u->Radio):nullptr;}
|
||||
bool Fighting(ERR_HQUnit* u){return u->fighting;}
|
||||
'''+methods+r'''
|
||||
void Legacy(ERR_HQUnit* unit,float now){
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Execute the actual finite native magazine transaction; engine behavior remains separate."""
|
||||
from pathlib import Path
|
||||
import os,re
|
||||
from policy_harness import ROOT,run
|
||||
from policy_harness import ROOT,run,body
|
||||
p=Path(os.environ.get('ERR_INFANTRY_TRANSFER_SOURCE',ROOT/'addons/ai/Scripts/Game/ERR/ERR_InfantryAmmoTransfer.c'))
|
||||
s=p.read_text()
|
||||
for forbidden in ('TrySpawn','DeleteEntity','SetOrigin','SetTransform','TryInsert','TryRemove','SetAmmoCount'):
|
||||
@@ -34,6 +34,15 @@ callbacks=[]
|
||||
for name in ('OnComplete','OnFailed','OnAllComplete','OnAllFailed'):
|
||||
m=re.search(r'protected override void '+name+r'\(\) (\{[^\n]+\})',callback)
|
||||
callbacks.append('void ERR_InfantryAmmoCallback::'+name+'() '+m[1].replace('Transfer.','Transfer->'))
|
||||
# Integrate the actual coordinator transfer selector with this actual leaf.
|
||||
coordinator_path=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',ROOT/'addons/ai/Scripts/Game/ERR/ERR_InfantryResupply.c'))
|
||||
transfer=body(coordinator_path,'protected void Transfer(float now)').replace('protected ','').replace('null','nullptr').replace('foreach (','for (')
|
||||
transfer=transfer.replace('ERR_HQAmmoSnapshot local','ERR_HQAmmoSnapshot* local')
|
||||
for name in ('ERR_Actors','ERR_HQAmmoSampler','ERR_InfantryAmmoTransfer','vector'):
|
||||
transfer=transfer.replace(name+'.',name+'::')
|
||||
for name in ('m_Move','actor','m_Truck','m_Recipient','local'):
|
||||
transfer=transfer.replace(name+'.',name+'->')
|
||||
transfer=transfer.replace('Entity.GetOrigin()', 'Entity->GetOrigin()')
|
||||
pre=r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
@@ -41,9 +50,10 @@ pre=r'''
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <functional>
|
||||
using string=std::string;
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Insert(T t){this->push_back(t);}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}};
|
||||
struct vector{float x=0,y=0,z=0;float Length(){return std::sqrt(x*x+y*y+z*z);}static float Distance(vector a,vector b){return vector{a.x-b.x,a.y-b.y,a.z-b.z}.Length();}};
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Insert(T t){this->push_back(t);}void RemoveItem(T t){this->erase(std::remove(this->begin(),this->end(),t),this->end());}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}};
|
||||
struct vector{float x=0,y=0,z=0;float Length(){return std::sqrt(x*x+y*y+z*z);}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}static float Distance(vector a,vector b){return vector{a.x-b.x,a.y-b.y,a.z-b.z}.Length();}};
|
||||
struct Entity;struct Group;struct Inventory;struct Storage;struct Item;struct Slot;struct Magazine;struct Weapon;struct Muzzle;struct Manager;struct Utility;struct Usage;struct Physics_;struct Record;
|
||||
using IEntity=Entity*;using Vehicle=Entity*;using SCR_AIGroup=Group*;using ERR_ForceRecord=Record*;
|
||||
using InventoryStorageManagerComponent=Inventory*;using BaseInventoryStorageComponent=Storage*;using InventoryItemComponent=Item*;using BaseMagazineComponent=Magazine*;
|
||||
@@ -76,18 +86,18 @@ struct Ledger{Array<Record*>records;Record*FindGroup(SCR_AIGroup g){for(auto*r:r
|
||||
struct ERR_GameMode{inline static ERR_GameMode*current=nullptr;bool active=true;Ledger*ledger=nullptr;static ERR_GameMode*Get(){return current;}bool Active(){return active;}Ledger*Forces(){return ledger;}int EnemyFaction(){return 1;}};
|
||||
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
|
||||
struct SCR_Global{inline static bool editor=false;static bool IsEditMode(){return editor;}};
|
||||
struct ERR_Actors{static bool Alive(IEntity a){return a&&a->alive;}};
|
||||
struct ERR_Actors{static bool CanFight(IEntity a){return a&&a->alive&&a->awake;}static bool Alive(IEntity a){return a&&a->alive;}};
|
||||
struct SCR_FactionManager{static int SGetFaction(IEntity a){return a?a->faction:-1;}};
|
||||
struct CompartmentAccessComponent{static IEntity GetVehicleIn(IEntity a){return a?a->vehicle:nullptr;}};
|
||||
struct ERR_ConvoyRules{static bool Captured(Vehicle v,SCR_AIGroup g){return v->captured||g->players>0;}static bool PartOf(IEntity a,IEntity root){return a==root;}};
|
||||
struct ERR_EnemyEquipmentRules{static bool Eligible(ERR_GameMode*m,SCR_AIGroup g,IEntity a){return m&&g&&a&&a->alive&&a->awake&&!a->player&&a->group==g&&g->players==0&&g->faction==m->EnemyFaction()&&g->utility&&g->utility->military;}};
|
||||
struct ERR_ForceRules{static bool Finite(float n){return std::isfinite(n);}};
|
||||
struct ERR_InfantryAmmoAccess{static inline bool clear=true;static inline int checks=0;static bool Clear(Vehicle donor,IEntity recipient){assert(donor&&recipient);checks++;return clear;}};
|
||||
struct Inventory{IEntity owner=nullptr;Array<IEntity>items;Array<Storage*>storages;Storage*selected=nullptr;bool allow=true,accept=true;int calls=0,callback=0;ScriptedInventoryOperationCallback*last=nullptr;
|
||||
struct Inventory{IEntity owner=nullptr;Array<IEntity>items;Array<Storage*>storages;Storage*selected=nullptr;bool allow=true,accept=true;int calls=0,callback=0;ScriptedInventoryOperationCallback*last=nullptr;std::function<void()> effect;
|
||||
IEntity GetOwner(){return owner;}void GetItems(Array<IEntity>&a,int purpose){assert(purpose==1);a=items;}void GetStorages(Array<Storage*>&a,int purpose){assert(purpose==1);a=storages;}
|
||||
Storage*FindStorageForItem(IEntity,int purpose){assert(purpose==1);return selected;}
|
||||
bool CanMoveItemToStorage(IEntity,Storage*,int slot){assert(slot==-1);return allow;}
|
||||
bool TryMoveItemToStorage(IEntity,Storage*,int slot,ScriptedInventoryOperationCallback*cb){assert(slot==-1);calls++;last=cb;if(callback==1){cb->OnComplete();}if(callback==2){cb->OnFailed();}return accept;}};
|
||||
bool TryMoveItemToStorage(IEntity,Storage*,int slot,ScriptedInventoryOperationCallback*cb){assert(slot==-1);calls++;last=cb;if(effect)effect();if(callback==1){cb->OnComplete();}if(callback==2){cb->OnFailed();}return accept;}};
|
||||
'''
|
||||
main=r'''
|
||||
struct Fixture{ERR_GameMode mode;Ledger ledger;Group donorGroup,recipientGroup,otherGroup;Record donorRecord,recipientRecord;Entity truck,recipient,driver,spareDriver,magEntity,rifle,foreign;
|
||||
@@ -102,7 +112,57 @@ struct Fixture{ERR_GameMode mode;Ledger ledger;Group donorGroup,recipientGroup,o
|
||||
ERR_InfantryAmmoTransfer Job(){return ERR_InfantryAmmoTransfer(&mode,&truck,&donorGroup,&recipient,&recipientGroup,&magEntity);}
|
||||
void Move(){source.items.clear();destination.items={&magEntity};item.slot=&toSlot;}
|
||||
};
|
||||
struct ERR_HQAmmoSnapshot{bool Complete=true,LauncherComplete=true;int SmallArmsMuzzles=1,SmallArmsRounds=0,SmallArmsChambers=0;};
|
||||
struct ERR_HQAmmoSampler{static void Inspect(IEntity actor,SCR_AIGroup group,ERR_HQAmmoSnapshot* stock){assert(actor&&actor->group==group&&stock);}};
|
||||
struct SupplyTruck{IEntity Entity;SCR_AIGroup Group;};
|
||||
struct SupplyRecipient{SCR_AIGroup Group;};
|
||||
struct Coordinator{ERR_GameMode* m_Mode;SupplyTruck* m_Truck;SupplyRecipient* m_Recipient;
|
||||
ERR_InfantryAmmoTransfer* m_Move=nullptr;Array<IEntity>m_Members,m_Stock,m_Spent,m_Served;IEntity m_Receiving=nullptr;
|
||||
int m_Delivered=0,m_TripDelivered=0,finishes=0,audits=0;
|
||||
void Finish(string){finishes++;if(m_Move)m_Move->Stop();}
|
||||
void Audit(string){audits++;}
|
||||
bool Contains(IEntity owner,IEntity item){return owner&&owner->inventory&&owner->inventory->items.Count()<=128&&owner->inventory->items.Contains(item);}
|
||||
'''+transfer+r'''
|
||||
};
|
||||
struct SupplyFixture:Fixture{SupplyTruck supplyTruck;SupplyRecipient supplyRecipient;Coordinator c;
|
||||
SupplyFixture():supplyTruck{&truck,&donorGroup},supplyRecipient{&recipientGroup}{
|
||||
c.m_Mode=&mode;c.m_Truck=&supplyTruck;c.m_Recipient=&supplyRecipient;c.m_Members={&recipient};c.m_Stock={&magEntity};}
|
||||
};
|
||||
int main(){
|
||||
// XZ approach can precede valid vertical reach, stopped state, capacity or clear access.
|
||||
// None of those no-submission outcomes consumes cargo or ends the trip; the same exact item can later submit once.
|
||||
for(int obstruction=0;obstruction<5;obstruction++){SupplyFixture f;
|
||||
if(obstruction==0){f.recipient.position.y=3;}if(obstruction==1){f.physics.velocity.x=1;}
|
||||
if(obstruction==2){f.destination.selected=nullptr;}if(obstruction==3){f.source.allow=false;}
|
||||
if(obstruction==4){ERR_InfantryAmmoAccess::clear=false;}
|
||||
for(int tick=0;tick<3;tick++){f.c.Transfer(tick*1000);assert(f.source.calls==0&&f.c.m_Spent.IsEmpty()&&!f.c.m_Move&&!f.c.m_Receiving&&f.c.finishes==0);}
|
||||
f.recipient.position.y=0;f.physics.velocity.x=0;f.destination.selected=&f.targetStorage;f.source.allow=true;ERR_InfantryAmmoAccess::clear=true;
|
||||
f.c.Transfer(3000);assert(f.source.calls==1&&f.c.m_Spent.Count()==1&&f.c.m_Move&&f.c.m_Move->Submitted());
|
||||
f.Move();f.source.last->OnComplete();f.c.Transfer(4000);assert(f.c.m_Delivered==1&&f.c.m_Served.Contains(&f.recipient)&&f.source.calls==1&&f.c.m_Spent.Count()==1);
|
||||
}
|
||||
// A capacity-blocked first member cannot prevent another original member from receiving this magazine.
|
||||
{SupplyFixture f;Fixture second;ERR_GameMode::current=&f.mode;
|
||||
second.recipient.group=&f.recipientGroup;second.recipientRecord.Group=&f.recipientGroup;
|
||||
f.c.m_Members.Insert(&second.recipient);f.destination.selected=nullptr;
|
||||
f.c.Transfer(0);assert(f.source.calls==1&&f.c.m_Receiving==&second.recipient&&f.c.m_Spent.Count()==1&&f.c.finishes==0);
|
||||
f.source.items.clear();second.destination.items={&f.magEntity};f.item.slot=&second.toSlot;f.source.last->OnComplete();
|
||||
f.c.Transfer(1000);assert(f.c.m_Delivered==1&&f.c.m_Served.Contains(&second.recipient)&&!f.c.m_Served.Contains(&f.recipient));}
|
||||
// A native API may complete both physical movement and callback inline. Account once on the next slow tick.
|
||||
{SupplyFixture f;f.source.callback=1;f.source.effect=[&](){f.Move();};f.c.Transfer(0);
|
||||
assert(f.source.calls==1&&f.c.m_Move&&f.c.m_Move->Submitted()&&f.c.m_Delivered==0&&f.c.m_Spent.Count()==1);
|
||||
f.c.Transfer(1000);f.c.Transfer(2000);assert(f.c.m_Delivered==1&&f.c.m_Served.Count()==1&&f.source.calls==1&&f.c.finishes==0);}
|
||||
// Crossing the native submission boundary burns the exact identity even if native refusal is synchronous.
|
||||
for(int refusal=0;refusal<2;refusal++){SupplyFixture f;if(refusal==0){f.source.accept=false;}else{f.source.callback=2;}
|
||||
f.c.Transfer(0);assert(f.source.calls==1&&f.c.m_Spent.Count()==1&&f.c.m_Move->Submitted());
|
||||
f.c.Transfer(1000);assert(f.c.finishes==1&&f.c.m_Spent.Count()==1&&f.source.calls==1);
|
||||
}
|
||||
// Accepted but unresolved loss stays retained/spent; changing geometry cannot cause a second native move.
|
||||
{SupplyFixture f;f.c.Transfer(0);f.recipient.position.x=8;f.c.Transfer(1000);
|
||||
assert(f.c.finishes==1&&f.c.m_Move&&f.c.m_Move->Unresolved()&&f.c.m_Spent.Count()==1&&f.source.calls==1);}
|
||||
// Several equivalent magazines do not multiply a blocked actor's expensive preflight work on one slow tick.
|
||||
{SupplyFixture f;Entity another=f.magEntity;f.source.items.Insert(&another);f.c.m_Stock.Insert(&another);
|
||||
ERR_InfantryAmmoAccess::clear=false;ERR_InfantryAmmoAccess::checks=0;f.c.Transfer(0);
|
||||
assert(ERR_InfantryAmmoAccess::checks==1&&f.source.calls==0&&f.c.m_Spent.IsEmpty());ERR_InfantryAmmoAccess::clear=true;}
|
||||
// A blocked native access check happens before submission and is terminal.
|
||||
{Fixture f;auto t=f.Job();ERR_InfantryAmmoAccess::clear=false;ERR_InfantryAmmoAccess::checks=0;
|
||||
assert(t.Tick(0)==FAILED&&!t.Submitted()&&!t.Unresolved()&&f.source.calls==0&&ERR_InfantryAmmoAccess::checks==1);
|
||||
|
||||
@@ -78,6 +78,10 @@ int main(){
|
||||
{Fixture f;f.s.m_State=3;f.physics.linear.x=std::numeric_limits<float>::quiet_NaN();f.s.Tick(0);assert(f.s.m_HeldAt==-1&&f.donor.NextStatus==-1);}
|
||||
{Fixture f;f.s.m_State=3;f.physics.angular.x=std::numeric_limits<float>::quiet_NaN();f.s.Tick(0);assert(f.s.m_HeldAt==-1&&f.donor.NextStatus==-1);}
|
||||
{Fixture f;f.s.m_Deadline=0;f.s.Tick(0);assert(f.s.m_State==1&&f.order.advances==0&&f.hq.token==0);}
|
||||
// Waiting for physical transfer never extends the original bounded rendezvous deadline.
|
||||
{Fixture f;f.s.m_State=5;f.s.m_Deadline=1000;f.s.Tick(0);assert(f.s.transfers==1&&f.s.m_State==5);
|
||||
f.s.Tick(1000);assert(f.s.transfers==1&&f.s.m_State==1&&f.hq.token==0&&!f.truck.SupplyHeld);}
|
||||
|
||||
{Fixture f;f.s.quiet=false;f.order.result=1;f.s.Tick(0);assert(f.s.m_State==1&&f.order.advances==0&&!f.truck.SupplyHeld);}
|
||||
{Fixture f;f.s.m_State=4;f.order.result=1;f.s.waypoints=false;f.s.Tick(0);assert(f.s.m_State==1&&f.s.waypointCalls==1&&f.s.transfers==0&&f.hq.token==0);}
|
||||
{Fixture f;f.s.m_State=3;f.s.m_HeldAt=100;ERR_HQWatchEntry report;report.ObservedAt=100;f.hq.watch.entry=&report;f.hq.next=nullptr;f.s.Tick(1000);assert(f.s.m_State==1&&f.hq.downlinks==1&&f.s.waypointCalls==0&&f.hq.token==0);}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Actual rendezvous waypoint geometry and native order insertion; no navigation proof."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
from policy_harness import ROOT,body,run
|
||||
P=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',ROOT/'addons/ai/Scripts/Game/ERR/ERR_InfantryResupply.c'))
|
||||
s=body(P,'protected bool Waypoints()').replace('protected ','')
|
||||
for name in ('AIWaypoint','ERR_Actors','ERR_Resources'):s=s.replace(name+'.',name+'::')
|
||||
for name in ('m_MoveWaypoint','m_HoldWaypoint','m_Recipient'):s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('Group.', 'Group->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
struct Vec{float x,y,z;};
|
||||
struct AIWaypoint{float radius=0;int kind;Vec position;void SetCompletionRadius(float r){radius=r;}static AIWaypoint* Cast(AIWaypoint* p){return p;}};
|
||||
struct ERR_Resources{static int MoveWaypoint(){return 1;}static int DefendWaypoint(){return 2;}};
|
||||
struct ERR_Actors{static inline int reject=0;static inline std::vector<AIWaypoint*>spawned;
|
||||
static Vec Ground(Vec p){p.y=2;return p;}
|
||||
static AIWaypoint* Spawn(int kind,Vec p){if(reject==kind)return nullptr;auto w=new AIWaypoint{0,kind,p};spawned.push_back(w);return w;}};
|
||||
struct Group{std::vector<AIWaypoint*>order;int activated=0;void AddWaypointAt(AIWaypoint* w,int index){assert(index==0);order.insert(order.begin(),w);}void ActivateAI(){activated++;}};
|
||||
struct Unit{struct Group* Group;};
|
||||
struct Service{AIWaypoint* m_MoveWaypoint=nullptr;AIWaypoint* m_HoldWaypoint=nullptr;Unit* m_Recipient;Vec m_Rendezvous;
|
||||
'''+s+r'''
|
||||
};
|
||||
int main(){
|
||||
Group group;Unit unit{&group};Service service;service.m_Recipient=&unit;service.m_Rendezvous={13,8,5};
|
||||
assert(service.Waypoints()&&group.activated==1&&group.order.size()==2);
|
||||
assert(group.order[0]==service.m_MoveWaypoint&&group.order[1]==service.m_HoldWaypoint);
|
||||
assert(group.order[0]->kind==1&&group.order[1]->kind==2);
|
||||
for(auto* w:group.order){assert(w->position.x==13&&w->position.y==2&&w->position.z==5&&w->radius==0.5f);}
|
||||
// A radius of 2 admitted a flat approach point 5m from the report center; 0.5 removes that allowance.
|
||||
const float reportX=10;const float oldArrivalX=service.m_Rendezvous.x+2;
|
||||
assert(oldArrivalX-reportX>4&&service.m_Rendezvous.x+service.m_MoveWaypoint->radius-reportX<4);
|
||||
// Failed partial construction never inserts either waypoint or activates the group.
|
||||
for(int failed:{1,2}){ERR_Actors::reject=failed;Group refused;Unit u{&refused};Service bad;bad.m_Recipient=&u;bad.m_Rendezvous={13,8,5};
|
||||
assert(!bad.Waypoints()&&refused.order.empty()&&refused.activated==0);}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual report-derived waypoint insertion/order and tighter positive radii; no formation navigation claim')
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Actual staged fixture transitions, immutable comparison and cleanup; no native proof."""
|
||||
import os,re,sys
|
||||
from pathlib import Path
|
||||
REPO=Path(os.environ.get('ERR_REPO','/home/jakub/devel/escapistsrereintroduced'));sys.path.insert(0,str(REPO/'tests'))
|
||||
from policy_harness import body,run
|
||||
P=Path(os.environ.get('ERR_LOCAL_PROBE_SOURCE',REPO/'addons/chat/Scripts/Game/ERR/ERR_NativeLocalHandoffProbe.c'))
|
||||
s='\n'.join(body(P,signature) for signature in ('protected bool SameSnapshot(','int Tick(ERR_HQ hq, float now)','void Stop()'))
|
||||
s=s.replace('protected ','').replace('null','nullptr').replace('bool restore;', 'bool restore = false;')
|
||||
s=s.replace('ERR_GameMode.Get()', 'Mode::Get()')
|
||||
for a,b in [('ERR_HQReport report','Report* report'),('ERR_HQ hq','HQ* hq'),('ERR_HQBelief before','Belief* before'),('ERR_HQBelief belief','Belief* belief'),('IEntity caller','Actor* caller')]:s=s.replace(a,b)
|
||||
for v in ('report','m_Payload','m_Source','m_Recipient','m_Mode','m_HQ','m_Route','hq','before','belief','m_Witness'):s=s.replace(v+'.',v+'->')
|
||||
s=s.replace('Memory().','Memory()->').replace('Nodes[0].','Nodes[0]->').replace('PendingReport.','PendingReport->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
struct Group{};
|
||||
struct Actor{int moved=0,last=0;void SetOrigin(int value){moved++;last=value;}};
|
||||
struct Mode{static inline Mode* current=nullptr;bool active=true;bool Active(){return active;}static Mode* Get(){return current;}};
|
||||
struct Report{int MessageId=1,Position=20,ObservedAt=0;Group* Source=nullptr;Mode* Target=nullptr;};
|
||||
struct Node{struct Group* Group;Actor* Carrier;};
|
||||
struct Nodes:std::vector<Node*>{int Count(){return size();}};
|
||||
struct Route{Group* LocalSource=nullptr;Actor* LocalWitness=nullptr;struct Nodes Nodes;bool Originated=false,Sending=false,Failed=false;int Cursor=0,result=0;int Advance(float,int){return result;}};
|
||||
struct Unit{struct Group* Group;Route* Uplink=nullptr;Report* PendingReport=nullptr;};
|
||||
struct Belief{int MessageId=0;struct Report* Report=nullptr;};
|
||||
struct Memory{Belief* belief=nullptr;Belief* Get(Mode*){return belief;}};
|
||||
struct HQ{struct Memory memory;int sourceToken=220001,recipientToken=220001;Group* source;int releases=0;int Journal(){return 0;}struct Memory* Memory(){return &memory;}int LogisticsToken(Group* g){return g==source?sourceToken:recipientToken;}void ReleaseLogistics(Group*g,int token){releases++;int& held=g==source?sourceToken:recipientToken;if(held==token)held=0;}bool ERR_ProbeSquadsReady(){return true;}};
|
||||
struct Probe{static constexpr int TOKEN=220001;Mode* m_Mode;HQ* m_HQ;Unit* m_Source;Unit* m_Recipient;Actor* m_Witness;Actor* m_Radio;Route* m_Route;Report* m_Payload;int m_OriginalPosition=7,m_DeliveredId=0,m_State=1;float m_Wait=0,m_Deadline=0;bool m_Stopped=false,m_Failed=false,m_Positioned=true,owned=true,canQueue=true;int removals=0;bool Enabled(){return true;}bool Original(Unit*,Actor*){return owned&&m_Mode&&m_Mode==Mode::Get()&&m_Mode->active;}bool Select(){return true;}bool Arrange(){return true;}bool Queue(float){if(canQueue){m_Source->Uplink=m_Route;m_Source->PendingReport=m_Payload;}return canQueue;}bool Check(bool value,std::string){if(!value){m_Failed=true;Stop();}return value;}int RemoveRadios(Unit*,Actor*){removals++;return 1;}
|
||||
METHODS
|
||||
};
|
||||
struct F{Mode mode;Group source,recipient;Actor witness,radio;Node node{&recipient,&radio};Route route;Report report;Unit src{&source},dst{&recipient};HQ hq;Probe p;Belief belief;F():p{&mode,&hq,&src,&dst,&witness,&radio,&route,&report}{Mode::current=&mode;hq.source=&source;route.LocalSource=&source;route.LocalWitness=&witness;route.Nodes.push_back(&node);report.Source=&source;report.Target=&mode;belief.MessageId=1;belief.Report=&report;}};
|
||||
int main(){
|
||||
{F f;assert(f.p.Tick(&f.hq,0)==0&&f.p.m_State==2&&!f.p.m_Failed);assert(f.p.Tick(&f.hq,1000)==0&&f.p.m_State==2);f.route.Originated=true;f.route.Cursor=1;f.hq.memory.belief=&f.belief;assert(f.p.Tick(&f.hq,6000)==0&&f.p.m_State==3&&f.p.m_DeliveredId==1);Report second=f.report;second.MessageId=2;second.ObservedAt=7000;Route retry=f.route;retry.Originated=false;retry.Cursor=0;f.p.m_Payload=&second;f.p.m_Route=&retry;assert(f.p.Tick(&f.hq,7000)==0&&f.p.m_State==4&&f.p.removals==1);assert(f.p.Tick(&f.hq,8000)==0);retry.Failed=true;assert(f.p.Tick(&f.hq,9000)==1&&f.p.m_Stopped&&f.hq.sourceToken==0&&f.hq.recipientToken==0&&f.witness.moved==1);}
|
||||
// An HQ belief appearing before the actual radio chain finishes cannot pass.
|
||||
{F f;f.p.Tick(&f.hq,0);f.hq.memory.belief=&f.belief;assert(f.p.Tick(&f.hq,1000)==-1&&f.p.m_Failed);}
|
||||
{F f;f.p.Tick(&f.hq,0);Report altered=f.report;altered.Position++;f.src.PendingReport=&altered;assert(f.p.Tick(&f.hq,1000)==-1);}
|
||||
{F f;Report other=f.report;other.Source=&f.recipient;assert(!f.p.SameSnapshot(&other));other=f.report;other.ObservedAt++;assert(!f.p.SameSnapshot(&other));}
|
||||
// A newer second belief invalidates interruption proof even if route failed.
|
||||
{F f;f.p.m_State=4;f.p.m_DeliveredId=1;f.belief.MessageId=2;f.hq.memory.belief=&f.belief;f.route.Failed=true;assert(f.p.Tick(&f.hq,0)==-1);}
|
||||
{F f;f.src.Uplink=&f.route;f.src.PendingReport=&f.report;f.p.Stop();assert(!f.src.Uplink&&!f.src.PendingReport&&f.witness.moved==1);int calls=f.hq.releases;f.p.Stop();assert(f.hq.releases==calls&&f.witness.moved==1);}
|
||||
{F f;Route other;f.src.Uplink=&other;f.src.PendingReport=&f.report;f.hq.sourceToken=99;f.hq.recipientToken=98;f.p.Stop();assert(f.src.Uplink==&other&&f.src.PendingReport==&f.report&&f.hq.sourceToken==99&&f.hq.recipientToken==98&&f.witness.moved==0);}
|
||||
{F f;Mode::current=nullptr;f.p.Stop();assert(f.witness.moved==0);}
|
||||
{F f;f.p.owned=false;f.p.Stop();assert(f.witness.moved==0);}
|
||||
}
|
||||
'''.replace('METHODS',s))
|
||||
assert 'ObservationRoute(unit)' in body(P,'ERR_HQRoute ERR_LocalFixtureQueue(')
|
||||
assert 'arranged.Snapshot()' in body(P,'ERR_HQRoute ERR_LocalFixtureQueue(')
|
||||
assert 'm_Wait = now + 1500' in body(P,'int Tick(ERR_HQ hq, float now)')
|
||||
print('PASS: actual fixture local/radio gating, immutable source/time/position, interrupted-report exclusion and exact-owner cleanup')
|
||||
|
||||
# Invalid fixture launch requests stop during argument validation, before any runtime work.
|
||||
import subprocess
|
||||
runner=Path(os.environ.get('ERR_LOCAL_RUNNER_SOURCE',REPO/'tools/headless_workbench.py'))
|
||||
code="import sys,runpy;sys.path.insert(0,"+repr(str(REPO/'tools'))+");p=sys.argv.pop(1);runpy.run_path(p,run_name='__main__')"
|
||||
for project,rounds,timeout in [('core','1','360'),('chat','2','360'),('chat','1','359')]:
|
||||
result=subprocess.run([sys.executable,'-c',code,str(runner),'--compat','/tmp/unused-local-handoff','--profile','/tmp/unused-local-handoff-profile','--smoke','hq-local-handoff','--project',project,'--rounds',rounds,'--timeout',timeout],capture_output=True,text=True)
|
||||
assert result.returncode==2 and 'hq-local-handoff requires Chat, one round and timeout at least360 seconds' in result.stderr,result.stderr
|
||||
print('PASS: local-handoff runner refuses wrong project/round count/insufficient deadline before runtime work')
|
||||
|
||||
# Fixture radio deletion must wait for allocator ownership to end, not merely
|
||||
# for a radio to appear in inventory or a late callback to claim completion.
|
||||
settled=body(P,'bool ERR_LocalFixtureSettled()')
|
||||
lookup=body(P,'bool ERR_LocalFixtureRadiosSettled(SCR_AIGroup group)')
|
||||
lookup=lookup.replace('SCR_AIGroup group','Group* group').replace('ERR_NativeLocalHandoffProbe.Enabled()','Enabled()')
|
||||
lookup=lookup.replace('foreach (ERR_EnemyEquipmentGroup state : m_Groups)','for (State* state : m_Groups)').replace('state.','state->').replace('Radio.','Radio->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
struct Radio{bool m_Stopped=false,m_Submitted=false;
|
||||
SETTLED
|
||||
};
|
||||
struct Group{};
|
||||
struct State{struct Group* Group;struct Radio* Radio;};
|
||||
struct Equipment{bool enabled=true;std::vector<State*> m_Groups;bool Enabled(){return enabled;}
|
||||
LOOKUP
|
||||
};
|
||||
int main(){Radio radio;assert(!radio.ERR_LocalFixtureSettled());radio.m_Submitted=true;assert(!radio.ERR_LocalFixtureSettled());radio.m_Stopped=true;assert(!radio.ERR_LocalFixtureSettled());radio.m_Submitted=false;assert(radio.ERR_LocalFixtureSettled());Group group,unknown;State state{&group,&radio};Equipment e;e.m_Groups.push_back(&state);assert(e.ERR_LocalFixtureRadiosSettled(&group));assert(!e.ERR_LocalFixtureRadiosSettled(&unknown));assert(!e.ERR_LocalFixtureRadiosSettled(nullptr));radio.m_Submitted=true;assert(!e.ERR_LocalFixtureRadiosSettled(&group));state.Radio=nullptr;assert(e.ERR_LocalFixtureRadiosSettled(&group));e.enabled=false;assert(!e.ERR_LocalFixtureRadiosSettled(&group));}
|
||||
'''.replace('SETTLED',settled).replace('LOOKUP',lookup))
|
||||
selection=body(P,'protected bool Select()')
|
||||
assert selection.index('ERR_LocalFixtureRadiosSettled(recipient)')<selection.index('ReserveLogistics(recipient.Group')
|
||||
assert selection.index('ERR_LocalFixtureRadiosSettled(source)')<selection.index('ReserveLogistics(source.Group')
|
||||
print('PASS: actual fixture allocator terminal/unsubmitted guard, original-group lookup and both candidate gates')
|
||||
Reference in New Issue
Block a user