Integrate finite infantry resupply with delivered low-ammunition reports

This commit is contained in:
2026-09-20 07:32:24 +02:00
parent 915476f709
commit 782c163c68
44 changed files with 5690 additions and 31 deletions
+4 -1
View File
@@ -52,7 +52,7 @@ struct ERR_ForceRecord{bool Physical=true,Eliminated=false;ERR_ForceKind Kind=ER
struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(Group*){return &record;}};
struct Mode{Ledger ledger;Ledger* Forces(){return &ledger;}};
struct ERR_ConvoyReceipt{bool Consumed=true;string SourceId="convoy-1";Array<int>Records{3};};
struct ERR_ConvoyVehicle{IEntity vehicle;IEntity* Entity=&vehicle;bool Registered=true,Released=false,Arrived=false,valid=true,waypoint=true,failOrder=false;int Index=0,RouteIndex=2,clears=0,orders=0,releases=0;struct Group* Group;Array<IEntity*>Members;ERR_ConvoyCombatState state;ERR_ConvoyCombatState* Combat=&state;bool CrewValid(Mode*){return valid;}void ClearOrder(){clears++;waypoint=false;}bool Order(ERR_ForceRoute*,float){if(waypoint)return true;orders++;if(failOrder)return false;waypoint=true;return true;}void Release(Mode*){releases++;Released=true;Combat->Held=false;}};
struct ERR_ConvoyVehicle{IEntity vehicle;IEntity* Entity=&vehicle;bool SupplyHeld=false,Registered=true,Released=false,Arrived=false,valid=true,waypoint=true,failOrder=false;int Index=0,RouteIndex=2,clears=0,orders=0,releases=0;struct Group* Group;Array<IEntity*>Members;ERR_ConvoyCombatState state;ERR_ConvoyCombatState* Combat=&state;bool CrewValid(Mode*){return valid;}void ClearOrder(){clears++;waypoint=false;}bool Order(ERR_ForceRoute*,float){if(waypoint)return true;orders++;if(failOrder)return false;waypoint=true;return true;}void Release(Mode*){releases++;Released=true;Combat->Held=false;}};
struct ERR_ConvoyTask{bool Stopped=false,Ready=true;ERR_ConvoyReceipt* Receipt;ERR_ForceRoute* Route;Array<ERR_ConvoyVehicle*>Vehicles;};
namespace ERR_ConvoyRules{bool captured=false;bool Captured(IEntity*,Group*){return captured;}};
struct ERR_ConvoyCombatRules{'''+accept+r'''};
@@ -64,6 +64,9 @@ struct Case{Mode mode;Service service{&mode};ERR_ConvoyReceipt receipt;ERR_Force
Case(){unit.Group=&group;unit.Members={&member};group.agents={&agent};task.Receipt=&receipt;task.Route=&route;task.Vehicles={&unit};service.m_Tasks={&task};command.CommandId=1;command.ReportId=10;command.SourceId=receipt.SourceId;command.Group=&group;command.Hold=true;command.ObservedAt=100;command.IssuedAt=1000;command.ExpiresAt=31000;}
};
int main(){
Case supply;assert(supply.service.ApplyCombatOrder(&supply.task,&supply.command,1000));
supply.unit.SupplyHeld=true;supply.command.CommandId=2;supply.command.Hold=false;supply.command.IssuedAt=60100;supply.command.ExpiresAt=90100;
assert(supply.service.ApplyCombatOrder(&supply.task,&supply.command,60100));assert(!supply.unit.Combat->Held&&!supply.unit.waypoint&&supply.unit.orders==0&&supply.unit.releases==0);
Case ok;assert(ok.service.ApplyCombatOrder(&ok.task,&ok.command,1000));assert(ok.unit.Combat->Held&&ok.unit.clears==1&&!ok.unit.waypoint&&ok.unit.RouteIndex==2);
assert(!ok.service.ApplyCombatOrder(&ok.task,&ok.command,1001)&&ok.unit.clears==1);ok.command.CommandId=2;ok.command.ReportId=11;ok.command.ObservedAt=200;
assert(ok.service.ApplyCombatOrder(&ok.task,&ok.command,1002)&&ok.unit.clears==1&&ok.unit.Combat->ReportId==11);
+4
View File
@@ -58,6 +58,10 @@ int main(){
offset.group.within=false;offset.unit.Tick(&offset.mode,&offset.route,3000);offset.unit.Tick(&offset.mode,&offset.route,4000);assert(offset.unit.RouteIndex==1&&offset.group.adds==2&&offset.group.removes==1);
// A close vehicle origin alone is not the native seated-group arrival predicate.
Case notNative;notNative.vehicle.position={99,0,0};notNative.unit.Tick(&notNative.mode,&notNative.route,1000);notNative.unit.Tick(&notNative.mode,&notNative.route,2000);assert(notNative.unit.RouteIndex==0&&notNative.group.adds==1);
Case supply;supply.unit.SupplyToken=100001;supply.unit.Tick(&supply.mode,&supply.route,1000);assert(supply.group.adds==1); // reservation alone does not deliver hold
supply.unit.SupplyHeld=true;supply.unit.ClearOrder();supply.group.within=true;supply.unit.Tick(&supply.mode,&supply.route,700000);assert(supply.unit.RouteIndex==0&&!supply.unit.Released&&supply.group.adds==1);
supply.unit.SupplyToken=0;supply.unit.SupplyHeld=false;supply.group.within=false;supply.unit.Tick(&supply.mode,&supply.route,701000);assert(supply.group.adds==2&&supply.unit.RouteIndex==0);
supply.unit.Release(&supply.mode);assert(supply.unit.SupplyToken==0&&!supply.unit.SupplyHeld);
Case held;held.unit.Tick(&held.mode,&held.route,1000);held.unit.Combat->Held=true;held.unit.ClearOrder();held.group.within=true;assert(!held.unit.Order(&held.route,2000)&&held.group.adds==1);
held.unit.Tick(&held.mode,&held.route,700000);assert(held.unit.RouteIndex==0&&held.group.adds==1&&!held.unit.Released&&!held.unit.Waypoint);
held.driver.alive=false;held.unit.Tick(&held.mode,&held.route,700001);assert(held.unit.Released&&!held.unit.Combat->Held);
+110
View File
@@ -0,0 +1,110 @@
"""Actual local ammo-report policy and HQ scheduler bodies; no native inventory/radio claim."""
from pathlib import Path
import os,re
from policy_harness import ROOT,body,run
BASE=Path(os.environ.get('ERR_AMMO_REPORTING_ROOT',ROOT))
P=BASE/'addons/ai/Scripts/Game/ERR/ERR_HQAmmoReporting.c'
H=BASE/'addons/ai/Scripts/Game/ERR/ERR_HQ.c'
A=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQAmmoSampler.c'
def cpp(s):
s=s.replace('protected ','').replace('ERR_HQPursuitRules.', 'ERR_HQPursuitRules::').replace('string.Format','Format')
for t in ('ERR_HQStatusReport','ERR_HQAmmoSnapshot','ERR_HQUnit','ERR_HQRoute'):
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->')
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.
source=H.read_text()
assert 'unit.AmmoReporting.Initialize(unit.Id, GetGame().GetWorld().GetWorldTime());' in source
assert 'if (delivered == 1)' in source and 'm_Watch.Ingest(unit.PendingStatus, now)' in source
assert 'm_Watch' not in methods and 'm_Network.Route(unit->Radio)' in methods
run(r'''
#include <cassert>
#include <cmath>
#include <string>
#include <vector>
#include <limits>
using string=std::string;
template<class... T>string Format(T...){return "sample";}
struct ERR_HQPursuitRules{static bool Finite(float v){return std::isfinite(v)&&v>-1e20&&v<1e20;}};
struct ERR_HQAmmoSnapshot{
bool Complete=true;int Members=4,SmallArmsMuzzles=4,SmallArmsRounds=121,SmallArmsMagazines=4,SmallArmsChambers=0;
int LauncherRounds=0,LauncherMagazines=0,LauncherChambers=0;
'''+body(A,' bool Valid(')+r'''
};
struct ERR_HQStatusReport{ERR_HQAmmoSnapshot* AmmoState=nullptr;int MessageId=0,Count=4,Fit=4,ConfirmedLosses=0;string SourceId="formation-1";float ObservedAt=0;};
'''+policy+r'''
struct Sampler{int observations=0,samples=0;bool valid=true;ERR_HQAmmoSnapshot stock;
void ObserveDeaths(){observations++;}
ERR_HQStatusReport* Sample(float now,int id,void*){samples++;if(!valid)return nullptr;
auto r=new ERR_HQStatusReport;r->ObservedAt=now;r->MessageId=id;r->AmmoState=new ERR_HQAmmoSnapshot(stock);return r;}};
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;
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;
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;}
'''+methods+r'''
void Finish(ERR_HQUnit& u,bool success=true){assert(u.Uplink);if(u.PendingStatus&&success)delivered++;
u.Uplink=nullptr;u.PendingStatus=nullptr;u.contact=false;}
};
int main(){
// Exact inclusive threshold counts chambers, ignores launcher stock, requires the complete current living roster.
ERR_HQAmmoReporting p; p.Initialize(6,0);ERR_HQStatusReport r;ERR_HQAmmoSnapshot a;r.AmmoState=&a;
assert(!p.Urgent(&r,0));a.SmallArmsRounds=120;assert(p.Urgent(&r,0));
a.SmallArmsRounds=119;a.SmallArmsChambers=2;assert(!p.Urgent(&r,0));a.SmallArmsChambers=1;assert(p.Urgent(&r,0));
a.SmallArmsRounds=0;a.SmallArmsChambers=0;a.LauncherRounds=100;assert(p.Urgent(&r,0));
a.Complete=false;assert(!p.Urgent(&r,0));a.Complete=true;
a.SmallArmsMuzzles=0;assert(!p.Urgent(&r,0));a.SmallArmsMuzzles=4;
a.Members=3;assert(!p.Urgent(&r,0));r.Count=3;assert(p.Urgent(&r,0));
a.Members=0;r.Count=0;assert(!p.Urgent(&r,0));a.Members=33;r.Count=33;assert(!p.Urgent(&r,0));
a.Members=4;r.Count=4;a.SmallArmsRounds=-1;assert(!p.Urgent(&r,0));a.SmallArmsRounds=0;
r.ObservedAt=1;assert(!p.Urgent(&r,0));r.ObservedAt=0;assert(!p.Urgent(&r,1));
assert(!p.Urgent(nullptr,0));r.AmmoState=nullptr;assert(!p.Urgent(&r,0));r.AmmoState=&a;
assert(!p.Urgent(&r,std::numeric_limits<float>::quiet_NaN()));
// Each registration phase is bounded to a 0..25 s delay. Late evaluations scan once, never catch up in bursts.
for(int id=1;id<=128;id++){ERR_HQAmmoReporting phase;phase.Initialize(id,1000);float first=1000+(id%6)*5000;
assert(!phase.ScanDue(first-1)&&phase.ScanDue(first));phase.Scanned(first);
assert(!phase.ScanDue(first+29999)&&phase.ScanDue(first+30000));phase.Scanned(first+999999);
assert(!phase.ScanDue(first+999999));}
// A fresh complete low report uses the status-first shared uplink, never immediate delivered knowledge.
HQ h;ERR_HQUnit u;u.StatusSampler.stock.SmallArmsRounds=0;u.NextStatus=180000;
h.QueueReports(&u,0);assert(u.PendingStatus&&!u.contact&&h.delivered==0&&u.PendingStatus->MessageId>0);
auto pending=u.PendingStatus;h.QueueReports(&u,1000);assert(u.PendingStatus==pending&&u.StatusSampler.samples==1);
h.Finish(u,false);assert(h.delivered==0);h.QueueReports(&u,30000);assert(u.contact&&!u.PendingStatus);
h.Finish(u);h.QueueReports(&u,60000);assert(u.PendingStatus);h.Finish(u);assert(h.delivered==1);
// Adequate/incomplete early samples consume no frame and leave room for positive contact or a quiet sweep.
HQ healthy;ERR_HQUnit good;good.NextStatus=180000;
healthy.QueueReports(&good,0);assert(good.contact&&!good.PendingStatus&&healthy.m_Journal.writes==0);
healthy.Finish(good);good.visible=false;good.StatusSampler.stock.Complete=false;
healthy.QueueReports(&good,30000);assert(good.sweep&&!good.Uplink);
// No route/radio means no snapshot or invented radio receipt. Early no-route attempts are bounded.
HQ off;ERR_HQUnit absent;absent.NextStatus=180000;off.m_Network.available=false;
off.QueueReports(&absent,0);off.QueueReports(&absent,1000);assert(off.m_Network.routes==1&&absent.StatusSampler.samples==0);
absent.Radio=nullptr;off.QueueReports(&absent,30000);assert(absent.StatusSampler.samples==0);
absent.Radio=&absent;off.m_Network.available=true;absent.StatusSampler.stock.SmallArmsRounds=0;
off.QueueReports(&absent,30000);assert(absent.PendingStatus&&off.delivered==0);off.Finish(absent);
// Requested status bypasses the urgent cooldown and scan deadline, but waits for the existing visual frame.
off.QueueReports(&absent,31000);assert(absent.contact);absent.NextStatus=32000;
off.QueueReports(&absent,32000);assert(absent.contact&&!absent.PendingStatus);
off.Finish(absent);off.QueueReports(&absent,33000);assert(absent.PendingStatus&&absent.PendingStatus->ObservedAt==33000);
// Routine unavailable route remains immediately retryable independently of bounded optional scans.
HQ routine;ERR_HQUnit late;routine.m_Network.available=false;routine.QueueReports(&late,0);
routine.m_Network.available=true;routine.QueueReports(&late,1000);assert(late.PendingStatus);
// A minute cooldown prevents persistent low stock from starving five-second contacts over six-second routes.
HQ busy;ERR_HQUnit firing;firing.StatusSampler.stock.SmallArmsRounds=0;int contacts=0,status=0;float finish=-1;
for(int now=0;now<=600000;now+=1000){
if(firing.Uplink&&now>=finish){if(firing.PendingStatus)status++;else contacts++;busy.Finish(firing);}
bool occupied=firing.Uplink;busy.QueueReports(&firing,now);if(!occupied&&firing.Uplink)finish=now+6000;
}
assert(status==10&&contacts==90&&firing.StatusSampler.samples<=21);
}
''')
print('PASS: actual early ammunition policy and status-first shared uplink; complete threshold, phases, cooldown, failures and requested check-ins')
+4 -2
View File
@@ -20,7 +20,8 @@ methods='\n'.join(cpp(body(H,s)) for s in ('protected bool ConvoyPositionKnown('
downlink=cpp(body(H,'if (unit.Downlink)\n'))
cancel=cpp(body(H,'void CancelPending('))
src=H.read_text()
assert src.index('PlanConvoy(now); Assign(now);') > src.index('override void Tick(')
tick=body(H,'override void Tick(')
assert tick.index('PlanConvoy(now);') < tick.index('PlanResupply(now);') < tick.index('Assign(now);')
assert 'm_Contacts' in body(H,'protected ERR_HQReport ConvoyContact(')
assert 'GetOrigin' not in methods and 'Target' not in methods
run(r'''
@@ -41,7 +42,7 @@ struct ERR_HQRoute {int MessageId=0;string Payload;int result=0;int Advance(floa
struct Network {bool available=true;int calls=0;ERR_HQRoute* Route(void*,bool down){assert(down);calls++;return available?new ERR_HQRoute:nullptr;}};
struct ERR_ConvoyCombatOrder {int CommandId=0,ReportId=0,VehicleIndex=0;string SourceId;SCR_AIGroup* Group=nullptr;bool Hold=false;float ObservedAt=-1,IssuedAt=0,ExpiresAt=0;};
struct CombatState {bool Held=false;int LastCommandId=0,ReportId=0;float ObservedAt=-1;};
struct ERR_ConvoyVehicle {int Index=0;SCR_AIGroup* Group=nullptr;bool Released=false,Arrived=false,Registered=true;CombatState state;CombatState* Combat=&state;};
struct ERR_ConvoyVehicle {int SupplyToken=0,Index=0;SCR_AIGroup* Group=nullptr;bool Released=false,Arrived=false,Registered=true;CombatState state;CombatState* Combat=&state;};
struct Receipt {string SourceId="convoy-1";};
struct ConvoyTask {bool Stopped=false,Ready=true;struct Receipt receipt;struct Receipt* Receipt=&receipt;std::vector<ERR_ConvoyVehicle*> Vehicles;};
struct Service {int calls=0;bool valid=true;ERR_ConvoyCombatOrder* last=nullptr;
@@ -66,6 +67,7 @@ int main(){
ERR_HQReport r;r.MessageId=1;r.ObservedAt=2000;r.Position={200,0};
h.PlanConvoy(3000);assert(!u.Downlink); // no delivered own status or target report
h.m_Watch.entries[u.ForceSourceId]=&e;h.PlanConvoy(3000);assert(!u.Downlink);
h.m_Contacts={&r};v.SupplyToken=100001;h.PlanConvoy(3000);assert(!u.Downlink);v.SupplyToken=0;
h.m_Contacts={&r};h.m_Network.available=false;h.PlanConvoy(3000);assert(!u.Downlink&&!v.Combat->Held);
h.m_Network.available=true;h.PlanConvoy(3000);assert(u.Downlink&&u.PendingConvoyOrder->Hold);
auto pending=u.PendingConvoyOrder;int calls=h.m_Network.calls;h.PlanConvoy(4000);assert(h.m_Network.calls==calls&&u.PendingConvoyOrder==pending);
+10 -4
View File
@@ -11,16 +11,19 @@ assert legacy in original or 'QueueReports(unit, now);' in original
def cpp(s):
s=s.replace('protected ','').replace('array<ref ERR_HQVisualContact>','Array<ERR_HQVisualContact*>')
for t in ('ERR_HQUnit','ERR_HQVisualContact','ERR_HQRoute','ERR_HQReport','ERR_HQStatusReport','ERR_HQTask','ERR_HQSearchReport'):
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_HQReport* report;','ERR_HQReport* report = nullptr;')
for v in ('unit','route','report','seen','task'):
for v in ('unit','route','report','seen','task','stock'):
s=s.replace(v+'.',v+'->')
for v in ('Utility','ReportSelection','StatusSampler','Group'):
s=s.replace('->'+v+'.','->'+v+'->')
s=s.replace('m_Mode.Settings().Get(', 'm_Mode->Settings()->Get(')
s=s.replace('ERR_HQPursuitRules.', 'ERR_HQPursuitRules::')
s=s.replace('ERR_HQObservation.','ERR_HQObservation::').replace('vector.DistanceXZ','vector::DistanceXZ').replace('string.Format','Format')
return s.replace('AmmoState.', 'AmmoState->')
policy_path=Path(os.environ.get('ERR_AMMO_REPORTING_ROOT', ROOT))/'addons/ai/Scripts/Game/ERR/ERR_HQAmmoReporting.c'
policy=cpp(policy_path.read_text()).replace('class ERR_HQAmmoReporting','struct ERR_HQAmmoReporting').rstrip()+';'
methods='\n'.join(cpp(body(P,s)) for s in ('protected void Observe(', 'protected void ObserveBody(', 'protected void StatusReport(', 'protected void Sweep(', 'protected void QueueReports('))
run(r'''
#include <cassert>
@@ -43,8 +46,10 @@ struct ERR_HQObservation{static void Gather(Utility* u,float now,Array<ERR_HQVis
struct Selection{int attempts=0;ERR_HQVisualContact* Select(Array<ERR_HQVisualContact*>& a){return a.front();}void Queued(void*){attempts++;}};
struct ERR_HQReport{int MessageId=0;void* Target=nullptr;Group* Source=nullptr;vector Position;float ObservedAt=0;
string Description(float){return "contact";}ERR_HQReport* Snapshot(){return new ERR_HQReport(*this);}};
struct AmmoSnapshot{bool Complete=false;int SmallArmsRounds=0,SmallArmsChambers=0;};
struct ERR_HQStatusReport{AmmoSnapshot* AmmoState=nullptr;int MessageId=0,Count=4,Fit=4,ConfirmedLosses=0;string SourceId="own-1";float ObservedAt=0;};
struct ERR_HQPursuitRules{static bool Finite(float value){return std::isfinite(value);}};
struct ERR_HQAmmoSnapshot{bool Complete=false;int SmallArmsRounds=0,SmallArmsChambers=0,SmallArmsMuzzles=0,Members=0;bool Valid(int){return false;}};
struct ERR_HQStatusReport{ERR_HQAmmoSnapshot* AmmoState=nullptr;int MessageId=0,Count=4,Fit=4,ConfirmedLosses=0;string SourceId="own-1";float ObservedAt=0;};
'''+policy+r'''
struct Sampler{int observations=0,samples=0;bool valid=true;void ObserveDeaths(){observations++;}
ERR_HQStatusReport* Sample(float now,int id,void*){samples++;if(!valid)return nullptr;auto r=new ERR_HQStatusReport;r->MessageId=id;r->ObservedAt=now;return r;}};
struct ERR_HQSearchReport{int SourceId=0,MessageId=0,WatchMessageId=0,WatchRequestId=0,BeliefId=0,BeliefRevision=0;
@@ -59,6 +64,7 @@ struct ERR_HQUnit{int Id=1;struct Group ownGroup;struct Utility ownUtility;Selec
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;
ERR_HQAmmoReporting AmmoReporting;
float NextReport=0,NextStatus=0;bool fighting=false,eligible=true;
ERR_HQTask* Task=nullptr;void* Assignment=nullptr;void* Waypoint=nullptr;
bool Eligible(Mode*){return eligible;}
+156
View File
@@ -0,0 +1,156 @@
"""Execute the actual finite native magazine transaction; engine behavior remains separate."""
from pathlib import Path
import os,re
from policy_harness import ROOT,run
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'):
assert forbidden not in s,forbidden
callback=s[s.index('class ERR_InfantryAmmoCallback'):s.index('// One exact physical')]
code=s[s.index('class ERR_InfantryAmmoTransfer\n'):]
code=re.sub(r'//[^\n]*','',code).replace('protected ','').replace('class ','struct ')
for src,dst in {
'BaseMagazineComponent.Cast(exactMagazine.FindComponent(BaseMagazineComponent))':'exactMagazine->magazine',
'BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent))':'actor->weapons',
'BaseWeaponComponent.Cast(rifle.FindComponent(BaseWeaponComponent))':'rifle->weapon',
'InventoryItemComponent.Cast(entity.FindComponent(InventoryItemComponent))':'entity->item',
'InventoryItemComponent.Cast(m_Magazine.FindComponent(InventoryItemComponent))':'m_Magazine->item',
'SCR_AIVehicleUsageComponent.Cast(m_Donor.FindComponent(SCR_AIVehicleUsageComponent))':'m_Donor->usage',
'InventoryStorageManagerComponent.Cast(m_Donor.FindComponent(InventoryStorageManagerComponent))':'m_Donor->inventory',
'InventoryStorageManagerComponent.Cast(m_Recipient.FindComponent(InventoryStorageManagerComponent))':'m_Recipient->inventory',
}.items():code=code.replace(src,dst)
code=code.replace('ref ERR_InfantryAmmoCallback','ERR_InfantryAmmoCallback*').replace('array<','Array<').replace('foreach (','for (').replace('null','nullptr')
code=re.sub(r'\bERR_GameMode (?=\w)','ERR_GameMode* ',code)
code=code.replace('void ERR_InfantryAmmoTransfer(','ERR_InfantryAmmoTransfer(').replace('EWeaponType kind','int kind').replace('EMuzzleType muzzleKind','int muzzleKind')
for name in ('ERR_InfantryAmmoTransferState','ERR_GameMode','ERR_EnemyEquipmentRules','ERR_ForceRules','Replication','SCR_Global','ERR_Actors','SCR_FactionManager','ERR_ConvoyRules','CompartmentAccessComponent','EWeaponType','EMuzzleType','EStoragePurpose','vector'):
code=code.replace(name+'.',name+'::')
for name in ('m_Mode','m_Donor','m_DonorGroup','m_Recipient','m_RecipientGroup','m_Source','m_Destination','m_Storage','m_Callback','magazine','manager','weapon','muzzle','entity','item','storage','inventory','donorRecord','recipientRecord','utility','usage','physics'):
code=re.sub(r'\b'+name+r'\.',name+'->',code)
code=code.replace('GetMagazineWell().','GetMagazineWell()->').replace('GetParentSlot().','GetParentSlot()->').replace('Forces().','Forces()->').replace('m_VehicleMgr.','m_VehicleMgr->').replace('GetPilotCompartmentSlot().','GetPilotCompartmentSlot()->')
code=re.sub(r'\b(bool|int|Vehicle|IEntity|SCR_AIGroup|ERR_ForceRecord|InventoryStorageManagerComponent|BaseInventoryStorageComponent|ERR_GameMode\*|ERR_InfantryAmmoCallback\*) (\w+);',r'\1 \2{};',code)
code=code.replace('\n}\n','\n};\n')
callback=callback.replace('class ERR_InfantryAmmoCallback : ScriptedInventoryOperationCallback','').strip()[1:-1]
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->'))
pre=r'''
#include <cassert>
#include <cmath>
#include <string>
#include <vector>
#include <algorithm>
#include <limits>
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();}};
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*;
using BaseWeaponManagerComponent=Manager*;using BaseWeaponComponent=Weapon*;using BaseMuzzleComponent=Muzzle*;
using SCR_AIGroupUtilityComponent=Utility*;using SCR_AIVehicleUsageComponent=Usage*;using Physics=Physics_*;
enum ERR_InfantryAmmoTransferState{FAILED=-1,PENDING,SUCCEEDED};
struct ERR_InfantryAmmoTransfer;
struct ScriptedInventoryOperationCallback{virtual void OnComplete(){}virtual void OnFailed(){}virtual void OnAllComplete(){}virtual void OnAllFailed(){}virtual ~ScriptedInventoryOperationCallback()=default;};
struct ERR_InfantryAmmoCallback:ScriptedInventoryOperationCallback{ERR_InfantryAmmoTransfer*Transfer=nullptr;void OnComplete()override;void OnFailed()override;void OnAllComplete()override;void OnAllFailed()override;};
struct EWeaponType{static const int WT_RIFLE=1,WT_SNIPERRIFLE=2,WT_MACHINEGUN=3,WT_HANDGUN=4,WT_GRENADELAUNCHER=5,WT_ROCKETLAUNCHER=6;};
struct EMuzzleType{static const int MT_BaseMuzzle=0,MT_UGLMuzzle=1,MT_RPGMuzzle=2;};struct EStoragePurpose{static const int PURPOSE_DEPOSIT=1;};
struct Well{int type=1,parent=0;int Type(){return type;}bool IsInherited(int t){return type==t||parent==t;}};
struct Magazine{Well*well=nullptr;int rounds=30,capacity=30;Well*GetMagazineWell(){return well;}int GetAmmoCount(){return rounds;}int GetMaxAmmoCount(){return capacity;}};
struct Muzzle{Well*well=nullptr;int kind=0;Well*GetMagazineWell(){return well;}int GetMuzzleType(){return kind;}};
struct Weapon{IEntity owner=nullptr;Array<Muzzle*>muzzles;int kind=1;IEntity GetOwner(){return owner;}int GetWeaponType(){return kind;}void GetMuzzlesList(Array<Muzzle*>&a){a=muzzles;}};
struct Manager{Array<IEntity>weapons;void GetWeaponsList(Array<IEntity>&a){a=weapons;}};
struct Storage{IEntity owner=nullptr;IEntity GetOwner(){return owner;}};
struct Slot{Storage*storage=nullptr;IEntity occupant=nullptr;Storage*GetStorage(){return storage;}IEntity GetOccupant(){return occupant;}};
struct Item{Slot*slot=nullptr;Slot*GetParentSlot(){return slot;}};
struct Physics_{vector velocity,angular;vector GetVelocity(){return velocity;}vector GetAngularVelocity(){return angular;}};
struct Usage{Slot*pilot=nullptr;Slot*GetPilotCompartmentSlot(){return pilot;}};
struct VehicleManager{IEntity vehicle=nullptr;IEntity FindVehicle(IEntity v){return vehicle==v?v:nullptr;}};
struct Utility{VehicleManager*m_VehicleMgr=nullptr;bool military=true;bool IsMilitary(){return military;}};
struct Group{int faction=1,players=0;bool slave=false;Utility*utility=nullptr;int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}bool IsSlave(){return slave;}Utility*GetGroupUtilityComponent(){return utility;}};
struct Entity{bool alive=true,awake=true,player=false,captured=false;int faction=1;Group*group=nullptr;IEntity vehicle=nullptr,parent=nullptr;
Inventory*inventory=nullptr;Magazine*magazine=nullptr;Manager*weapons=nullptr;Weapon*weapon=nullptr;Item*item=nullptr;Usage*usage=nullptr;Physics_*physics=nullptr;vector position;
IEntity GetParent(){return parent;}Physics_*GetPhysics(){return physics;}vector GetOrigin(){return position;}};
struct Record{SCR_AIGroup Group=nullptr;bool Physical=true,Eliminated=false;};
struct Ledger{Array<Record*>records;Record*FindGroup(SCR_AIGroup g){for(auto*r:records)if(r->Group==g)return r;return nullptr;}};
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 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 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;
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;}};
'''
main=r'''
struct Fixture{ERR_GameMode mode;Ledger ledger;Group donorGroup,recipientGroup,otherGroup;Record donorRecord,recipientRecord;Entity truck,recipient,driver,spareDriver,magEntity,rifle,foreign;
VehicleManager vehicleManager;Utility donorUtility,recipientUtility;Physics_ physics;Usage usage;Slot pilot,fromSlot,toSlot;Storage sourceStorage,targetStorage;Inventory source,destination;Item item;Magazine magazine;Manager weapons;Weapon weapon;Muzzle muzzle;Well well;
Fixture(){ERR_GameMode::current=&mode;mode.ledger=&ledger;donorRecord.Group=&donorGroup;recipientRecord.Group=&recipientGroup;ledger.records={&donorRecord,&recipientRecord};
donorGroup.utility=&donorUtility;recipientGroup.utility=&recipientUtility;donorUtility.m_VehicleMgr=&vehicleManager;vehicleManager.vehicle=&truck;
truck.physics=&physics;truck.usage=&usage;usage.pilot=&pilot;pilot.occupant=&driver;driver.group=&donorGroup;driver.vehicle=&truck;spareDriver.group=&donorGroup;spareDriver.vehicle=&truck;
recipient.group=&recipientGroup;recipient.position.x=4;truck.inventory=&source;source.owner=&truck;recipient.inventory=&destination;destination.owner=&recipient;
sourceStorage.owner=&truck;targetStorage.owner=&recipient;source.storages={&sourceStorage};destination.storages={&targetStorage};destination.selected=&targetStorage;
source.items={&magEntity};magEntity.item=&item;item.slot=&fromSlot;fromSlot.storage=&sourceStorage;toSlot.storage=&targetStorage;magEntity.magazine=&magazine;magazine.well=&well;
recipient.weapons=&weapons;weapons.weapons={&rifle};rifle.weapon=&weapon;weapon.owner=&rifle;weapon.muzzles={&muzzle};muzzle.well=&well;}
ERR_InfantryAmmoTransfer Job(){return ERR_InfantryAmmoTransfer(&mode,&truck,&donorGroup,&recipient,&recipientGroup,&magEntity);}
void Move(){source.items.clear();destination.items={&magEntity};item.slot=&toSlot;}
};
int main(){
// Read-only selection follows the native inherited-well direction, excluding launcher muzzles and weapons.
{Fixture f;assert(ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));Well derived;derived.type=2;derived.parent=1;f.magazine.well=&derived;assert(ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));
f.magazine.well=&f.well;f.muzzle.well=&derived;assert(!ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));
f.muzzle.well=&f.well;f.muzzle.kind=EMuzzleType::MT_UGLMuzzle;assert(!ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));
f.muzzle.kind=0;f.weapon.kind=EWeaponType::WT_ROCKETLAUNCHER;assert(!ERR_InfantryAmmoTransfer::Compatible(&f.recipient,&f.magEntity));}
// Exactly one source move; neither a callback alone nor physical movement alone is a successful transaction.
{Fixture f;auto t=f.Job();assert(t.Tick(100)==PENDING&&f.source.calls==1&&t.Unresolved());f.source.last->OnComplete();assert(t.Tick(101)==PENDING&&t.Unresolved());
f.Move();assert(t.Tick(102)==SUCCEEDED&&!t.Unresolved());t.Stop();assert(t.Tick(103)==SUCCEEDED&&f.source.calls==1);}
{Fixture f;auto t=f.Job();t.Tick(100);f.Move();assert(t.Tick(101)==PENDING&&t.Unresolved());f.source.last->OnAllComplete();assert(t.Tick(102)==SUCCEEDED);}
// Exact destination slot and known donor absence are mandatory even after success callback.
{Fixture f;auto t=f.Job();t.Tick(100);f.Move();f.item.slot=&f.fromSlot;f.source.last->OnComplete();assert(t.Tick(101)==PENDING&&t.Unresolved());
f.item.slot=&f.toSlot;for(int i=0;i<513;i++)f.source.items.Insert(&f.foreign);assert(t.Tick(102)==PENDING&&t.Unresolved());}
// Late callbacks resolve retained reservations after Stop without Tick or further operations; the stopped task stays failed.
{Fixture f;auto t=f.Job();t.Tick(0);t.Stop();assert(t.Unresolved());f.Move();f.source.last->OnAllComplete();assert(!t.Unresolved());assert(t.Tick(2)==FAILED&&f.source.calls==1);}
{Fixture f;auto t=f.Job();t.Tick(0);t.Stop();f.source.last->OnAllFailed();assert(!t.Unresolved());assert(t.Tick(1)==FAILED&&f.source.calls==1);}
{Fixture f;auto t=f.Job();t.Tick(0);f.source.last->OnComplete();f.source.last->OnFailed();assert(t.Unresolved());f.Move();assert(!t.Unresolved()&&t.Tick(1)==SUCCEEDED);}
// Failure/rejection and the 8s deadline never submit a second move.
for(int failure=0;failure<3;failure++){Fixture f;if(failure==0)f.source.accept=false;if(failure==1)f.source.callback=2;auto t=f.Job();t.Tick(100);
if(failure==2){assert(t.Tick(8100)==FAILED&&t.Unresolved());}else assert(t.Tick(101)==FAILED&&!t.Unresolved());
for(int i=0;i<10;i++)t.Tick(8200+i);assert(f.source.calls==1);}
// Invalid physical state before submission spends no native action; the terminal object cannot revive.
for(int invalid=0;invalid<25;invalid++){Fixture f;auto t=f.Job();
if(invalid==0)Replication::server=false;if(invalid==1)SCR_Global::editor=true;if(invalid==2)f.mode.active=false;
if(invalid==3)f.truck.alive=false;if(invalid==4)f.recipient.alive=false;if(invalid==5)f.recipient.awake=false;if(invalid==6)f.recipient.player=true;
if(invalid==7)f.recipient.vehicle=&f.truck;if(invalid==8)f.truck.captured=true;if(invalid==9)f.donorGroup.players=1;
if(invalid==10)f.recipient.group=&f.otherGroup;if(invalid==11)f.donorRecord.Physical=false;if(invalid==12)f.recipientRecord.Eliminated=true;
if(invalid==13)f.vehicleManager.vehicle=&f.foreign;if(invalid==14)f.driver.alive=false;if(invalid==15)f.driver.vehicle=nullptr;
if(invalid==16)f.physics.velocity.x=.251f;if(invalid==17)f.physics.angular.x=.101f;if(invalid==18)f.recipient.position.x=4.001f;
if(invalid==19)f.truck.faction=2;if(invalid==20)f.driver.faction=2;if(invalid==21)f.recipientGroup.slave=true;
if(invalid==22)f.magazine.rounds=0;if(invalid==23)f.magazine.rounds=31;if(invalid==24)f.physics.velocity.x=std::numeric_limits<float>::quiet_NaN();
assert(t.Tick(100)==FAILED&&f.source.calls==0&&!t.Unresolved());Replication::server=true;SCR_Global::editor=false;assert(t.Tick(200)==FAILED&&f.source.calls==0);}
// Native stock/storage identity and capacity guards reject shared, foreign, oversized and inaccessible inventory.
for(int invalid=0;invalid<9;invalid++){Fixture f;auto t=f.Job();if(invalid==0)f.source.items.clear();if(invalid==1)f.destination.items={&f.magEntity};
if(invalid==2)f.fromSlot.storage=&f.targetStorage;if(invalid==3)f.sourceStorage.owner=&f.foreign;if(invalid==4)f.destination.selected=nullptr;
if(invalid==5)f.targetStorage.owner=&f.foreign;if(invalid==6)f.source.allow=false;if(invalid==7)f.destination.storages.clear();if(invalid==8){for(int i=0;i<65;i++)f.destination.storages.Insert(&f.targetStorage);}
assert(t.Tick(0)==FAILED&&f.source.calls==0);}
// Conditions and original native driver/ledger identities remain strict while an accepted move is pending.
for(int changed=0;changed<4;changed++){Fixture f;auto t=f.Job();t.Tick(100);Record replacement=f.recipientRecord;
if(changed==0)f.pilot.occupant=&f.spareDriver;if(changed==1)f.ledger.records[1]=&replacement;if(changed==2)f.physics.velocity.x=1;if(changed==3)f.recipient.position.x=8;
assert(t.Tick(101)==FAILED&&t.Unresolved());f.source.last->OnFailed();assert(!t.Unresolved()&&f.source.calls==1);}
{Fixture f;auto t=f.Job();t.Tick(100);assert(t.Tick(99)==FAILED&&t.Unresolved());}
// Clothing storage ownership follows native inventory slots and bounds cycles.
{Fixture f;Entity vest;Item vestItem;Slot vestSlot;Storage body;body.owner=&f.recipient;vestSlot.storage=&body;vestItem.slot=&vestSlot;vest.item=&vestItem;f.targetStorage.owner=&vest;
auto t=f.Job();assert(t.Tick(0)==PENDING);f.Move();f.source.last->OnComplete();assert(t.Tick(1)==SUCCEEDED);
vestSlot.storage=&f.targetStorage;assert(!ERR_InfantryAmmoTransfer::StoredUnder(&vest,&f.recipient));}
}
'''
# Brace tiny fixture statements to retain -Werror without suppressing useful compiler diagnostics.
main=re.sub(r'if\((invalid|changed|failure)==(\d+)\)([^;{}]+);',r'if(\1==\2){\3;}',main)
main=main.replace('for(int i=0;i<10;i++)t.Tick(8200+i);','for(int i=0;i<10;i++){t.Tick(8200+i);}')
run(pre+code+'\n'.join(callbacks)+main)
print('PASS: actual single-item transfer, physical ownership/native callback proof, stop quarantine, compatibility and strict original actor/vehicle boundaries')
+130
View File
@@ -0,0 +1,130 @@
"""Actual coordinator decisions/transitions with native boundaries doubled; no engine 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
SOURCE=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',REPO/'addons/ai/Scripts/Game/ERR/ERR_InfantryResupply.c'))
def method(signature):
s=body(SOURCE,signature).replace('protected ','').replace('null','nullptr')
s=s.replace('vector.DistanceXZ','Vec::DistanceXZ').replace('ERR_ForceRules.','ERR_ForceRules::').replace('ERR_InfantryResupplyRules.','ERR_InfantryResupplyRules::').replace('ERR_Actors.','ERR_Actors::')
s=s.replace(' + "3 0 0"',' + Vec{3,0,0}')
s=re.sub(r'string\.Format\("walk to reported stopped ammo truck at %1", m_Rendezvous\)', '"rendezvous"', s)
for kind in ['ERR_HQWatchEntry','Physics']:
s=re.sub(r'\b'+kind+r' (\w+)',kind+r'* \1',s)
# Enforce native entity/reference member access becomes C++ pointer member access.
for name in ['entry','recipient','donor','stopped','m_HQ','m_Move','m_Recipient','m_Donor','m_Truck','m_Order','physics']:
s=s.replace(name+'.',name+'->')
for name in ['AmmoState','Group','Combat','Entity']:
s=s.replace(name+'.',name+'->')
s=s.replace('Watch().','Watch()->').replace('GetVelocity().','GetVelocity().').replace('GetAngularVelocity().','GetAngularVelocity().')
return s
rules='\n'.join(method(x) for x in ['static bool Fresh','static bool Need','static bool Pair'])
methods='\n'.join(method(x) for x in ['protected void ClearWaypoints','protected void Finish','void Tick(float now)','void Stop()'])
code=r'''
#include <cassert>
#include <cmath>
#include <limits>
#include <string>
#include <vector>
#include <algorithm>
using string=std::string;
struct Vec {float x=0,y=0,z=0; float Length(){return std::sqrt(x*x+y*y+z*z);} static float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);} Vec operator+(Vec b){return {x+b.x,y+b.y,z+b.z};} string ToString(){return "position";}};
struct ERR_ForceRules {static bool Finite(float n){return std::isfinite(n);} static bool FiniteVector(Vec v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}};
struct Ammo {bool Complete=true,valid=true;int Members=2,SmallArmsMuzzles=2,SmallArmsRounds=60,SmallArmsChambers=0;bool Valid(int){return valid;}};
struct ERR_HQWatchEntry {bool Closed=false;int Count=2,Fit=2,SourceId=1;float ObservedAt=0;Vec Position;Ammo* AmmoState=nullptr;};
struct ERR_InfantryResupplyRules {
RULES
};
struct Waypoint {int id;};
struct Group {std::vector<Waypoint*> points;void RemoveWaypoint(Waypoint* p){points.erase(std::remove(points.begin(),points.end(),p),points.end());}};
struct ERR_Actors {static inline int deleted=0;static void DeleteSpawned(Waypoint*){deleted++;}};
struct Physics {Vec linear,angular;Vec GetVelocity(){return linear;} Vec GetAngularVelocity(){return angular;}};
struct Entity {Physics* physics=nullptr;Physics* GetPhysics(){return physics;}};
struct Combat {bool Held=false;};
struct Truck {int SupplyToken=7,clears=0;bool SupplyHeld=false;struct Combat* Combat=nullptr;struct Entity* Entity=nullptr;void ClearOrder(){clears++;}};
struct Unit {struct Group* Group=nullptr;int ForceSourceId=1;float NextStatus=-1;};
struct Leaf {int stops=0;bool unresolved=true;void Stop(){stops++;}bool Unresolved(){return unresolved;}};
struct Route {int result=0,advances=0;int Advance(float,int){advances++;return result;}};
struct Watch {ERR_HQWatchEntry* entry=nullptr;ERR_HQWatchEntry* Find(int){return entry;}};
struct HQ {int token=7,releases=0,downlinks=0;struct Watch watch;Route* next=nullptr;int Journal(){return 0;}int LogisticsToken(Group*){return token;}void ReleaseLogistics(Group*,int owned){releases++;if(token==owned)token=0;}struct Watch* Watch(){return &watch;} Route* Downlink(Group*,string,string,int){downlinks++;return next;}};
struct Service {
HQ* m_HQ;Truck* m_Truck;Unit* m_Recipient;Unit* m_Donor;Leaf* m_Move=nullptr;Route* m_Order=nullptr;Entity* m_Receiving=nullptr;
Waypoint* m_MoveWaypoint=nullptr;Waypoint* m_HoldWaypoint=nullptr;Vec m_Rendezvous;int m_State=2,m_Token=7,m_ReportId=11;
float m_NextTick=0,m_Deadline=200000,m_HeldAt=-1;bool m_Stopped=false,valid=true,quiet=true,roster=true,waypoints=true;int audits=0,transfers=0,waypointCalls=0,stocks=0;
bool Current(){return !m_Stopped;}bool TruckValid(){return valid;}bool Quiet(Unit*){return quiet;}bool Roster(){return roster;}void Audit(string){audits++;}void Stock(float){stocks++;}bool Waypoints(){waypointCalls++;return waypoints;}void Transfer(float){transfers++;}
METHODS
};
struct Fixture {Physics physics;Entity entity{&physics};Combat combat;Truck truck;Group group,donorGroup;Unit recipient{&group},donor{&donorGroup};HQ hq;Route order,next;Service s;
Fixture(){truck.Combat=&combat;truck.Entity=&entity;s.m_HQ=&hq;s.m_Truck=&truck;s.m_Recipient=&recipient;s.m_Donor=&donor;s.m_Order=&order;hq.next=&next;}
};
int main(){
Ammo ammo;ERR_HQWatchEntry demand,donor;demand.AmmoState=&ammo;donor.SourceId=2;donor.Position={200,500,0};
assert(ERR_InfantryResupplyRules::Need(&demand,120000));assert(ERR_InfantryResupplyRules::Pair(&demand,&donor,120000));
assert(!ERR_InfantryResupplyRules::Fresh(&demand,120001));assert(!ERR_InfantryResupplyRules::Fresh(nullptr,0));
demand.ObservedAt=1;assert(!ERR_InfantryResupplyRules::Fresh(&demand,0));demand.ObservedAt=0;
ammo.SmallArmsChambers=1;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.SmallArmsChambers=0;
ammo.Complete=false;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.Complete=true;
ammo.Members=1;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.Members=2;
ammo.valid=false;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.valid=true;
donor.SourceId=1;assert(!ERR_InfantryResupplyRules::Pair(&demand,&donor,0));donor.SourceId=2;donor.Position.x=201;assert(!ERR_InfantryResupplyRules::Pair(&demand,&donor,0));
demand.Position.x=std::numeric_limits<float>::quiet_NaN();assert(!ERR_InfantryResupplyRules::Need(&demand,0));demand.Position.x=0;
{Fixture f;f.s.Tick(0);assert(f.s.m_State==2&&!f.truck.SupplyHeld&&f.truck.clears==0&&f.s.waypointCalls==0);f.order.result=1;f.s.Tick(1000);assert(f.s.m_State==3&&f.truck.SupplyHeld&&f.truck.clears==1&&f.s.m_Order==nullptr);f.physics.linear.x=1;f.s.Tick(2000);assert(f.s.m_HeldAt==-1&&f.donor.NextStatus==-1);f.physics.linear.x=0;f.s.Tick(3000);assert(f.s.m_HeldAt==3000&&f.donor.NextStatus==3000);ERR_HQWatchEntry report;report.Position={20,0,5};report.ObservedAt=2999;f.hq.watch.entry=&report;f.s.Tick(4000);assert(f.hq.downlinks==0);report.ObservedAt=3000;f.s.Tick(5000);assert(f.s.m_State==4&&f.hq.downlinks==1&&f.s.m_Rendezvous.x==23);f.s.Tick(6000);assert(f.s.m_State==4&&f.s.waypointCalls==0&&f.s.transfers==0);f.next.result=1;f.s.Tick(7000);assert(f.s.m_State==5&&f.s.waypointCalls==1&&f.s.transfers==1);}
{Fixture f;f.order.result=-1;f.s.Tick(0);assert(f.s.m_State==1&&f.hq.token==0&&f.truck.SupplyToken==0&&!f.truck.SupplyHeld&&f.truck.clears==0);}
{Fixture f;f.s.m_State=4;f.order.result=-1;f.s.Tick(0);assert(f.s.waypointCalls==0&&f.s.transfers==0&&f.hq.token==0);}
{Fixture f;f.hq.token=99;f.truck.SupplyToken=99;f.truck.SupplyHeld=true;f.s.Tick(0);assert(f.s.m_State==1&&f.hq.token==99&&f.truck.SupplyToken==99&&f.truck.SupplyHeld);}
{Fixture f;Waypoint own1{1},own2{2},other{3};f.group.points={&own1,&other,&own2};f.s.m_MoveWaypoint=&own1;f.s.m_HoldWaypoint=&own2;Leaf leaf;f.s.m_Move=&leaf;f.truck.SupplyHeld=true;f.s.Stop();assert(f.group.points.size()==1&&f.group.points[0]==&other&&leaf.stops==1&&f.s.m_Move==&leaf&&f.hq.token==0&&!f.truck.SupplyHeld&&f.s.m_Stopped);int audits=f.s.audits;f.s.Stop();f.s.Tick(5000);assert(f.s.audits==audits&&leaf.stops==1);}
{Fixture f;Leaf leaf;f.s.m_Move=&leaf;f.s.Finish("timeout");f.s.Tick(0);assert(f.s.m_Move==&leaf);leaf.unresolved=false;f.s.Tick(1000);assert(f.s.m_Move==nullptr);}
{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);}
{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);}
}
'''.replace('RULES',rules).replace('METHODS',methods)
run(code)
print('infantry resupply actual rules, radio transitions, owned cleanup and unresolved-operation tests passed')
# Recheck permanent initial-roster eligibility during an active trip.
src=SOURCE
s=body(src,'protected bool Roster()').replace('protected ','')
s=s.replace('array<AIAgent> agents = {};','Array<Agent*> agents;')
s=s.replace('foreach (AIAgent agent : agents)','for (Agent* agent : agents)').replace('foreach (IEntity actor : m_Members)','for (Actor* actor : m_Members)')
for x in ['m_Recipient','agent']:s=s.replace(x+'.',x+'->')
s=s.replace('StatusSampler.','StatusSampler->').replace('Group.','Group->')
for x in ['ERR_Actors','SCR_FactionManager','CompartmentAccessComponent']:s=s.replace(x+'.',x+'::')
s=s.replace('m_Mode.','m_Mode->').replace('bool living;', 'bool living = false;')
run(r'''
#include <cassert>
#include <vector>
#include <algorithm>
template<class T> struct Array:std::vector<T>{using std::vector<T>::vector;bool IsEmpty(){return this->empty();}int Count(){return this->size();}bool Contains(T a){return std::find(this->begin(),this->end(),a)!=this->end();}};
struct Group;
struct Actor{bool alive=true,player=false,fight=true;Group* group=nullptr;int faction=1;};
struct Agent{Actor* actor;Actor* GetControlledEntity(){return actor;}};
struct Group{Array<Agent*> agents;void GetAgents(Array<Agent*>& out){out=agents;}};
struct Sampler{Group* group;bool released=false;int strength=1;Array<Actor*> original;int InitialStrength(){return strength;}Actor* InitialMember(int slot){return slot>=0&&slot<(int)original.size()?original[slot]:nullptr;}bool OwnsRoster(Group* g){return g==group;}bool CanAttribute(int,Actor*){return !released;}};
struct Unit{struct Group* Group;Sampler* StatusSampler;};
struct Mode{int EnemyFaction(){return 1;}};
struct ERR_Actors{static bool Alive(Actor* a){return a->alive;}static bool Player(Actor* a){return a->player;}static struct Group* Group(Actor* a){return a->group;}static bool CanFight(Actor* a){return a->fight;}};
struct SCR_FactionManager{static int SGetFaction(Actor* a){return a->faction;}};
struct CompartmentAccessComponent{static bool GetVehicleIn(Actor*){return false;}};
struct Service{Unit* m_Recipient;Mode* m_Mode;Array<Actor*> m_Members;bool Quiet(Unit*){return true;}
METHOD
};
int main(){Group group;Actor soldier;soldier.group=&group;Agent agent{&soldier};group.agents={&agent};Sampler sampler{&group,false,1,{&soldier}};Unit unit{&group,&sampler};Mode mode;Service service{&unit,&mode,{&soldier}};assert(service.Roster());sampler.released=true;assert(!sampler.CanAttribute(0,&soldier));
// Permanent sampler release survives returning to the physical group.
assert(!service.Roster());sampler.released=false;assert(service.Roster());
sampler.strength=33;assert(!service.Roster());sampler.strength=0;assert(!service.Roster());sampler.strength=1;
Actor foreign;foreign.group=&group;sampler.original={&foreign};assert(!service.Roster());
// Initial roster order, not current GetAgents order, determines the slot.
sampler.strength=2;sampler.original={&foreign,&soldier};assert(service.Roster());
soldier.player=true;assert(!service.Roster());soldier.player=false;
soldier.group=nullptr;assert(!service.Roster());soldier.group=&group;
soldier.alive=false;assert(!service.Roster());soldier.alive=true;
Agent extra{&foreign};group.agents.push_back(&extra);assert(!service.Roster());
}
'''.replace('METHOD',s))
print('PASS: actual resupply Roster preserves permanent release, original slot mapping and bounded ownership')
+6 -3
View File
@@ -1,8 +1,9 @@
"""Actual Intel/HQ methods and readiness handoff; native expansion is an adapter."""
import re
import re,os
from pathlib import Path
from policy_harness import ROOT, body, run
intel=ROOT/'addons/intel/Scripts/Game/ERR/ERR_IntelService.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'))
ready=ROOT/'addons/ai/Scripts/Game/ERR/ERR_MortarReadiness.c'
system=ROOT/'addons/ai/Scripts/Game/ERR/ERR_MortarSystem.c'
def adapt(s):
@@ -82,7 +83,8 @@ struct GroupService{int registrations=0;Set<SCR_AIGroup> seen;Array<int> ids;Arr
void RegisterGroup(SCR_AIGroup g,int radioId=0,bool fitRadios=true){ids.Insert(radioId);fits.Insert(fitRadios);if(!seen.Contains(g)){seen.Insert(g);registrations++;}}};
struct Mortars{SCR_AIGroup owned=nullptr;bool FindGroup(SCR_AIGroup group){return group&&group==owned;}};
struct BodyService:GroupService{void RegisterGroup(SCR_AIGroup g,Sampler& s){assert(s.Bound()&&s.group==g);GroupService::RegisterGroup(g);}};
struct Unit{string HomeId,ForceSourceId;bool HomeGuard=false;float NextStatus=0;Sampler StatusSampler;int Id=0;SCR_AIGroup Group=nullptr;struct Utility* Utility=nullptr;float NextReport=0;};using ERR_HQUnit=Unit*;
struct Reporting{int initializations=0;void Initialize(int id,float now){assert(id>0&&now==10);initializations++;}};
struct Unit{Reporting AmmoReporting;string HomeId,ForceSourceId;bool HomeGuard=false;float NextStatus=0;Sampler StatusSampler;int Id=0;SCR_AIGroup Group=nullptr;struct Utility* Utility=nullptr;float NextReport=0;};using ERR_HQUnit=Unit*;
struct HQ{Mode* m_Mode;GroupService m_Equipment;BodyService m_Bodies;int m_NextId=0;Array<ERR_HQUnit> m_Units;Mortars* m_Mortars=nullptr;
'''+hqbind+hqregister+r'''
};
@@ -112,6 +114,7 @@ int main(){
for(int n=0;n<5;n++){assert(ready.Finish(&mode,&group)==1);intel.RegisterGroup(&group);hq.RegisterGroup(&group);}
assert(mode.publications==1&&Math::rolls==4&&hq.m_Units.Count()==1);
assert(hq.m_Units[0]->StatusSampler.bindings==1&&hq.m_Equipment.registrations==1&&hq.m_Bodies.registrations==1);
assert(hq.m_Units[0]->AmmoReporting.initializations==1);
assert(hq.m_Equipment.ids.Count()==1&&hq.m_Equipment.ids[0]==1&&!hq.m_Equipment.fits[0]);
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