Integrate radio-delivered convoy cover and conservative squad ammunition reports

This commit is contained in:
2026-09-20 04:23:11 +02:00
parent 9b6596eb3e
commit 9205e2129a
88 changed files with 13934 additions and 28 deletions
+57
View File
@@ -0,0 +1,57 @@
"""Actual Stinger feedback and parsing, against a failure-injectable settings boundary."""
from pathlib import Path
import os
from policy_harness import ROOT,body,run
P=Path(os.environ.get('ERR_HQ_ADMIN_SOURCE',str(ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQAdminProvider.c')))
S=ROOT/'addons/core/Scripts/Game/ERR/ERR_Settings.c'
valid=body(S,'static bool Valid(').replace('string key','std::string key')
integer=body(ROOT/'addons/core/Scripts/Game/ERR/ERR_AdminService.c','static bool Integer(')
integer=(integer.replace('string text','std::string text').replace('out int value','int& value').replace('text.IsEmpty()','text.empty()').replace('text.Length()','int(text.size())').replace('text.Substring(','text.substr(').replace('"0123456789".IndexOf(', 'std::string("0123456789").find(').replace('< 0) return false;','== std::string::npos) return false;').replace('text.ToInt()','std::stoi(text)'))
method=body(P,'protected void StingerSetting(')
method=(method.replace('protected ','').replace('ERR_GameMode mode','ERR_GameMode* mode').replace('array<string>','Array<string>&').replace('mode.Settings().','mode->Settings()->').replace('ERR_CommandArguments.Integer','Integer').replace('mode->Settings()->Get(key).ToString()','std::to_string(mode->Settings()->Get(key))').replace('value.ToString()','std::to_string(value)').replace('int value;','int value=0;'))
run(r'''
#include <cassert>
#include <string>
#include <vector>
using string=std::string;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T x){this->push_back(x);}};
'''+valid+integer+r'''
struct Settings{int chance=15,writes=0;bool fail=false;
int Get(string key){assert(key=="stingerChanceOneIn");return chance;}
bool Set(string key,int value){writes++;if(fail||!Valid(key,value))return false;chance=value;return true;}};
struct ERR_GameMode{struct Settings settings;struct Settings* Settings(){return &settings;}};
struct Provider{
'''+method+r'''
};
void Effect(const Array<string>& r){assert(r.back().find("have not rolled yet")!=string::npos&&r.back().find("no reroll, removal or replenishment")!=string::npos);}
int main(){
ERR_GameMode mode;Provider p;Array<string> replies;
Array<string> show{"hq","stinger","show"};p.StingerSetting(&mode,show,replies);
assert(mode.settings.writes==0&&replies.back().find("stingerChanceOneIn=15")!=string::npos);Effect(replies);
Array<string> help{"hq","stinger","help"};p.StingerSetting(&mode,help,replies);
assert(mode.settings.writes==0&&replies.back().find("0 off, 1 every eligible soldier, 2..10000 one in N")!=string::npos);Effect(replies);
Array<string> setting{"hq","stinger","set","0"};
for(int n:{0,1,2,15,10000}){
setting[3]=std::to_string(n);p.StingerSetting(&mode,setting,replies);
assert(mode.settings.chance==n&&replies.back().find(" saved.")!=string::npos);Effect(replies);
}
for(string invalid:{"-1","1.0","1junk","10000000","10001","1e1","+1"}){
setting[3]=invalid;p.StingerSetting(&mode,setting,replies);
assert(mode.settings.chance==10000&&replies.back().find("previous setting retained")!=string::npos&&replies.back().find(" saved.")==string::npos);Effect(replies);
}
setting[3]="0";mode.settings.fail=true;p.StingerSetting(&mode,setting,replies);
assert(mode.settings.chance==10000&&replies.back().find("previous setting retained")!=string::npos);Effect(replies);
int writes=mode.settings.writes;
for(Array<string> malformed: {Array<string>{"hq","stinger"},Array<string>{"hq","stinger","set"},Array<string>{"hq","stinger","show","extra"}}){
p.StingerSetting(&mode,malformed,replies);assert(mode.settings.writes==writes&&replies.back().find("previous setting retained")!=string::npos);
}
}
''')
execute=body(P,'override bool Execute(')
assert execute.index('arguments.IndexOf("hq stinger ") == 0')<execute.index('if (arguments != "status"')
assert 'StingerSetting(mode, parts, replies); return true;' in execute
admin=body(ROOT/'addons/core/Scripts/Game/ERR/ERR_AdminService.c','void Execute(')
assert admin.index('if (!authorized)')<admin.index('Extension(mode, arguments, replies)')
for forbidden in ('Check(', 'RandomInt(', 'Spawn', 'ConvoyAllowance', 'SetAISkill', 'entry.'):
assert forbidden not in body(P,'protected void StingerSetting(')
print('PASS: actual Stinger show/set/help state precise actor-roll effects; read-only help, bounded values, malformed syntax and failed-save preservation; no rerolls/gameplay edits and no native persistence claim')
+2 -2
View File
@@ -24,7 +24,7 @@ struct IEntity{bool alive=true;};
struct AIAgent{IEntity* actor;int activations=0;IEntity* GetControlledEntity(){return actor;}void SetPermanentLOD(int){}void ActivateAI(){activations++;}};
struct SCR_AIVehicleUsageComponent{};
struct Manager{int calls=0;bool TryAddVehicle(SCR_AIVehicleUsageComponent*){calls++;return true;}};
struct SCR_AIGroupUtilityComponent{Manager* m_VehicleMgr;};
struct SCR_AIGroupUtilityComponent{Manager* m_VehicleMgr;int additions=0;void AddUsableVehicle(SCR_AIVehicleUsageComponent* usage){additions++;m_VehicleMgr->TryAddVehicle(usage);}};
struct Group{Array<AIAgent*>agents;Manager manager;SCR_AIGroupUtilityComponent utility{&manager};void GetAgents(Array<AIAgent*>&out){out=agents;}SCR_AIGroupUtilityComponent* GetGroupUtilityComponent(){return &utility;}void SetPermanentLOD(int){}void ActivateAI(){}};
struct Vehicle{SCR_AIVehicleUsageComponent usage;int GetOrigin(){return 1;}};
struct ERR_ConvoyVehicle{int Index=0;bool Registered=false,valid=true;struct Group* Group;struct Vehicle* Entity;Array<IEntity*>Members;bool CrewValid(void*){return valid;}};
@@ -46,7 +46,7 @@ int main(){
for(int i=0;i<6;i++){units[i].Index=i;units[i].Group=&groups[i];units[i].Entity=&vehicles[i];units[i].Members.Insert(&bodies[i]);agents[i].actor=&bodies[i];groups[i].agents.Insert(&agents[i]);task.Vehicles.Insert(&units[i]);}
units[2].valid=false;assert(!service.Admit(&task,100));assert(mode.published==2&&pool.binds==2&&mode.ledger.calls==2&&service.inspections==6);
units[2].valid=true;assert(service.Admit(&task,200));assert(mode.published==6&&pool.binds==6&&mode.ledger.calls==6&&mode.hq.reservations==6);
assert(service.Admit(&task,300));assert(mode.published==6&&pool.binds==6&&mode.ledger.calls==6);for(auto&g:groups){assert(g.manager.calls==1);}
assert(service.Admit(&task,300));assert(mode.published==6&&pool.binds==6&&mode.ledger.calls==6);for(auto&g:groups){assert(g.manager.calls==2&&g.utility.additions==1);}
Mode duplicate;Pool other;Service check{&duplicate,&other};ERR_ConvoyTask invalid;invalid.Receipt=&receipt;ERR_ConvoyVehicle wrong=units[0];wrong.Registered=false;wrong.Group=&groups[0];IEntity stranger;AIAgent extra{&stranger};groups[0].agents[0]=&extra;invalid.Vehicles.Insert(&wrong);assert(!check.Admit(&invalid,400)&&duplicate.ledger.calls==0);
groups[0].agents[0]=&agents[0];other.fail=true;assert(!check.Admit(&invalid,400)&&invalid.stopped&&!wrong.Registered&&duplicate.published==0);
}
+98
View File
@@ -0,0 +1,98 @@
"""Execute delivered convoy cover rules and the canonical physical executor."""
import os,re
from pathlib import Path
from policy_harness import ROOT,body,run
rules=Path(os.environ.get('ERR_CONVOY_COMBAT',str(ROOT/'addons/ai/Scripts/Game/ERR/ERR_ConvoyCombat.c')))
service=Path(os.environ.get('ERR_CONVOY_SERVICE',str(ROOT/'addons/ai/Scripts/Game/ERR/ERR_ConvoyService.c')))
accept=body(rules,'static bool Accept(')
for cls in ['ERR_ConvoyCombatState','ERR_ConvoyCombatOrder']:accept=accept.replace(cls+' ',cls+'* ')
accept=accept.replace('state.','state->').replace('order.','order->').replace('ERR_ForceRules.','ERR_ForceRules::')
preamble=r'''
#include <cassert>
#include <vector>
#include <string>
#include <algorithm>
#include <cmath>
using string=std::string;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
struct Group;
struct ERR_ConvoyCombatState{bool Held=false;int LastCommandId=0,ReportId=0;float ObservedAt=-1;};
struct ERR_ConvoyCombatOrder{int CommandId=0,ReportId=0,VehicleIndex=0;string SourceId;Group* Group=nullptr;bool Hold=false;float ObservedAt=-1,IssuedAt=-1,ExpiresAt=-1;};
namespace ERR_ForceRules{bool Finite(float n){return std::isfinite(n);}}
'''.replace('Group* Group','struct Group* Group')
run(preamble+'struct Rules{'+accept+r'''};
int main(){
ERR_ConvoyCombatState state;ERR_ConvoyCombatOrder hold;hold.CommandId=1;hold.ReportId=10;hold.Hold=true;hold.ObservedAt=100;hold.IssuedAt=1000;hold.ExpiresAt=31000;
assert(Rules::Accept(&state,&hold,1000));assert(!Rules::Accept(&state,&hold,31000));assert(!Rules::Accept(&state,&hold,999));
auto invalid=hold;invalid.ExpiresAt=31001;assert(!Rules::Accept(&state,&invalid,1000));invalid=hold;invalid.ObservedAt=NAN;assert(!Rules::Accept(&state,&invalid,1000));assert(!Rules::Accept(&state,&hold,NAN));
invalid=hold;invalid.IssuedAt=60101;invalid.ExpiresAt=70101;assert(!Rules::Accept(&state,&invalid,60101));
state.Held=true;state.LastCommandId=1;state.ReportId=10;state.ObservedAt=100;assert(!Rules::Accept(&state,&hold,1000));
auto refresh=hold;refresh.CommandId=2;refresh.ReportId=11;refresh.ObservedAt=200;assert(Rules::Accept(&state,&refresh,1000));refresh.ObservedAt=99;assert(!Rules::Accept(&state,&refresh,1000));
refresh=hold;refresh.CommandId=2;refresh.ObservedAt=101;assert(!Rules::Accept(&state,&refresh,1000));refresh=hold;refresh.CommandId=2;refresh.ReportId=9;assert(!Rules::Accept(&state,&refresh,1000));
auto resume=hold;resume.CommandId=3;resume.Hold=false;resume.IssuedAt=60100;resume.ExpiresAt=90100;assert(Rules::Accept(&state,&resume,60100));
resume.IssuedAt=60099;resume.ExpiresAt=90099;assert(!Rules::Accept(&state,&resume,60100));resume.IssuedAt=60100;resume.ExpiresAt=90100;
resume.ReportId=11;assert(!Rules::Accept(&state,&resume,60100));resume.ReportId=10;resume.ObservedAt=101;assert(!Rules::Accept(&state,&resume,60100));resume.ObservedAt=100;state.Held=false;assert(!Rules::Accept(&state,&resume,60100));
assert(!Rules::Accept(nullptr,&hold,1000)&&!Rules::Accept(&state,nullptr,1000));
}
''')
code=body(service,'bool ApplyCombatOrder(')
for cls in ['ERR_ConvoyTask','ERR_ConvoyCombatOrder','ERR_ConvoyVehicle','ERR_ForceRecord','AIAgent']:
code=re.sub(r'\b'+cls+r' ([A-Za-z_]\w*)',cls+r'* \1',code)
code=code.replace('array<AIAgent>','Array<AIAgent*>').replace('foreach (','for (')
for name in ['task','order','unit','record','agent','m_Mode']:code=code.replace(name+'.',name+'->')
code=code.replace('Receipt.','Receipt->').replace('Combat.','Combat->').replace('Route.','Route->').replace('Group.','Group->').replace('Forces().','Forces()->')
for cls in ['ERR_ConvoyCombatRules','ERR_ConvoyRules','ERR_ForceKind','ERR_HQ']:code=code.replace(cls+'.',cls+'::')
code=code.replace('string.Format','Format')
run(preamble+r'''
struct IEntity{};struct AIAgent{IEntity* entity;IEntity* GetControlledEntity(){return entity;}};
struct Group{Array<AIAgent*>agents;void GetAgents(Array<AIAgent*>&out){out=agents;}};
struct ERR_ForceRoute{int Count(){return 10;}};
enum class ERR_ForceKind{CONVOY,GARRISON};
struct ERR_ForceRecord{bool Physical=true,Eliminated=false;ERR_ForceKind Kind=ERR_ForceKind::CONVOY;int Id=3;};
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_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'''};
string Format(string,int,int,int,bool,int){return "event";}
namespace ERR_HQ{int events=0;void Event(string,string){events++;}}
struct Service{Mode* m_Mode;bool current=true;Array<ERR_ConvoyTask*>m_Tasks;bool Current(){return current;}
'''+code+r'''};
struct Case{Mode mode;Service service{&mode};ERR_ConvoyReceipt receipt;ERR_ForceRoute route;ERR_ConvoyTask task;ERR_ConvoyVehicle unit;Group group;IEntity member;AIAgent agent{&member};ERR_ConvoyCombatOrder command;
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 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);
ok.command.Hold=false;ok.command.CommandId=3;ok.command.IssuedAt=60200;ok.command.ExpiresAt=90200;
assert(ok.service.ApplyCombatOrder(&ok.task,&ok.command,60200)&&!ok.unit.Combat->Held&&ok.unit.waypoint&&ok.unit.orders==1&&ok.unit.RouteIndex==2);
assert(!ok.service.ApplyCombatOrder(&ok.task,&ok.command,60201)&&ok.unit.orders==1);
for(int failure=0;failure<15;failure++){
Case c;Group stranger;IEntity foreign;AIAgent foreignAgent{&foreign};
if(failure==0)c.service.current=false;
if(failure==1)c.service.m_Tasks.clear();
if(failure==2)c.task.Stopped=true;
if(failure==3)c.receipt.Consumed=false;
if(failure==4)c.command.SourceId="other";
if(failure==5)c.command.Group=&stranger;
if(failure==6)c.unit.valid=false;
if(failure==7)c.unit.Registered=false;
if(failure==8)c.unit.Released=true;
if(failure==9)c.mode.ledger.record.Physical=false;
if(failure==10)c.mode.ledger.record.Id=4;
if(failure==11)c.group.agents={&foreignAgent};
if(failure==12)c.command.VehicleIndex=1;
if(failure==13)ERR_ConvoyRules::captured=true;
if(failure==14)c.receipt.Records.clear();
assert(!c.service.ApplyCombatOrder(&c.task,&c.command,1000)&&c.unit.clears==0&&c.unit.orders==0&&!c.unit.Combat->Held&&c.unit.Combat->LastCommandId==0);
ERR_ConvoyRules::captured=false;
}
Case failed;assert(failed.service.ApplyCombatOrder(&failed.task,&failed.command,1000));failed.unit.failOrder=true;failed.command.Hold=false;failed.command.CommandId=2;failed.command.IssuedAt=60100;failed.command.ExpiresAt=90100;
assert(!failed.service.ApplyCombatOrder(&failed.task,&failed.command,60100)&&failed.unit.Released&&failed.unit.releases==1&&failed.unit.Combat->LastCommandId==2);
assert(!failed.service.ApplyCombatOrder(&failed.task,&failed.command,60101)&&failed.unit.orders==1&&failed.unit.releases==1);
}
''')
print('PASS: actual delivered cover freshness/lineage/replay and canonical executor; owned waypoint-only hold, one resume at preserved route index, invalid ownership/crew/ledger refusal and fail-closed native recreation')
+6 -3
View File
@@ -6,9 +6,9 @@ path=Path(os.environ.get('ERR_CONVOY_SERVICE',str(ROOT/'addons/ai/Scripts/Game/E
source=path.read_text();code=source[source.index('class ERR_ConvoyVehicle'):source.index('class ERR_ConvoyCargoCallback')]
code=re.sub(r'//[^\n]*','',code).replace('class ERR_ConvoyVehicle','struct ERR_ConvoyVehicle').replace('ref ','')
code=code.replace('array<IEntity>','Array<IEntity*>').replace('array<BaseCompartmentSlot>','Array<BaseCompartmentSlot*>').replace('array<AIAgent>','Array<AIAgent*>')
for typ in ['Vehicle','SCR_AIGroup','IEntity','BaseCompartmentSlot','AIWaypoint','ERR_GameMode','ERR_ForceRoute','ERR_ForceRecord','AIAgent']:
for typ in ['Vehicle','SCR_AIGroup','IEntity','BaseCompartmentSlot','AIWaypoint','ERR_GameMode','ERR_ForceRoute','ERR_ForceRecord','AIAgent','ERR_ConvoyCombatState']:
code=re.sub(r'\b'+typ+r' ([A-Za-z_]\w*)',typ+r'* \1',code)
for obj in ['Waypoint','Group','Entity','mode','record','agent','member','route']:
for obj in ['Waypoint','Group','Entity','mode','record','agent','member','route','Combat']:
code=re.sub(r'\b'+obj+r'\.',obj+'->',code)
for cls in ['SCR_EntityHelper','ERR_Actors','ERR_ConvoyRules','ERR_Resources','CompartmentAccessComponent','vector']:
code=code.replace(cls+'.',cls+'::')
@@ -21,7 +21,7 @@ run(r'''
#include <cmath>
struct vector{float x=0,y=0,z=0;static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}void Clear(){this->clear();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
struct SCR_AIGroup;struct Vehicle;
struct SCR_AIGroup;struct Vehicle;struct ERR_ConvoyCombatState{bool Held=false;};
struct IEntity{bool alive=true,player=false,awake=true;Vehicle* vehicle=nullptr;SCR_AIGroup* group=nullptr;IEntity* parent=nullptr;IEntity* GetParent(){return parent;}};
struct AIAgent{int lod=0;void SetPermanentLOD(int value){lod=value;}};
struct AIWaypoint:IEntity{float radius=0;void SetCompletionRadius(float value){radius=value;}bool IsWithinCompletionRadius(SCR_AIGroup* group);};
@@ -58,6 +58,9 @@ 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 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);
Case timeout;timeout.unit.Tick(&timeout.mode,&timeout.route,1000);timeout.unit.Tick(&timeout.mode,&timeout.route,601000);assert(timeout.unit.Released&&!timeout.unit.Waypoint&&timeout.mode.command.unregistrations==1&&!timeout.mode.ledger.record.Pinned);
timeout.unit.Release(&timeout.mode);timeout.unit.Tick(&timeout.mode,&timeout.route,999999);assert(timeout.mode.command.unregistrations==1&&timeout.group.adds==1&&timeout.group.lod==-1&&timeout.agent.lod==-1);
for(int reason=0;reason<6;reason++){
+38
View File
@@ -0,0 +1,38 @@
"""Delivered ammo gates new tasks without becoming active-order cancellation."""
from policy_harness import ROOT, body, run
H=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c'
for method in ('protected void Investigate(', 'protected void Retask(', 'protected void Assign('):
s=body(H,method)
assert 'if (m_Watch.ArmedSearchBlocked(unit.ForceSourceId, now)) continue;' in s
# Execute the production delivery admission before unrelated native waypoint creation.
s=body(H,'protected void DeliverOrder(')
s=s[:s.index(' if (task.Investigation())')]+' admitted++;\n }'
s=s.replace('protected ','').replace('null','nullptr').replace('ERR_HQUnit unit','ERR_HQUnit* unit')
s=s.replace('ERR_HQReport order','ERR_HQReport* order').replace('ERR_HQTask task','ERR_HQTask* task')
for name in ('unit','task','order'):s=s.replace(name+'.',name+'->')
run(r'''
#include <cassert>
#include <string>
struct ERR_HQReport{};
struct ERR_HQTask{float ExpiresAt=100000;bool ReleaseOnly=false;ERR_HQTask* ReplacesTask=nullptr;};
struct ERR_HQUnit{ERR_HQReport* PendingOrder=nullptr;ERR_HQTask* PendingTask=nullptr;ERR_HQTask* Task=nullptr;
std::string ForceSourceId="original-1";bool eligible=true;int cancellations=0;
bool Eligible(void*){return eligible;}void CancelPending(){cancellations++;PendingOrder=nullptr;PendingTask=nullptr;}};
struct Watch {bool empty=false;int calls=0;bool ArmedSearchBlocked(std::string s,float){assert(s=="original-1");calls++;return empty;}};
struct Journal {void Write(std::string,std::string){}};
struct HQ{Watch m_Watch;Journal m_Journal;void* m_Mode=nullptr;int admitted=0,lost=0,released=0;
void LostOrder(ERR_HQUnit* u,float){lost++;u->CancelPending();}void ReleaseUnit(ERR_HQUnit* u,float){released++;u->Task=nullptr;u->CancelPending();}
'''+s+r'''
};
int main(){HQ h;ERR_HQUnit u;ERR_HQReport report;ERR_HQTask active,next;u.Task=&active;
auto queue=[&](){u.PendingOrder=&report;u.PendingTask=&next;};
queue();h.m_Watch.empty=true;h.DeliverOrder(&u,1000);assert(h.lost==1&&h.admitted==0&&u.Task==&active&&!u.PendingTask);
queue();h.m_Watch.empty=false;h.DeliverOrder(&u,1000);assert(h.admitted==1&&u.Task==&active);
// Delivered release commands still work for empty squads and do not consult ammo.
next.ReleaseOnly=true;next.ReplacesTask=&active;queue();h.m_Watch.empty=true;int calls=h.m_Watch.calls;
h.DeliverOrder(&u,1000);assert(h.released==1&&!u.Task&&h.m_Watch.calls==calls);
// Stale orders fail before inventory knowledge is consulted.
next.ReleaseOnly=false;next.ExpiresAt=500;queue();h.DeliverOrder(&u,1000);assert(h.lost==2&&h.m_Watch.calls==calls);
}
''')
print('PASS: new/retasked search and radio delivery ammo gates; active tasks and delivered release preserved')
+162
View File
@@ -0,0 +1,162 @@
"""Execute actual bounded native-inventory inspection through deterministic native API adapters."""
from pathlib import Path
import os,re
from policy_harness import ROOT,run
P=Path(os.environ.get('ERR_AMMO_SOURCE',ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQAmmoSampler.c'))
s=P.read_text()
for forbidden in ('Spawn','SetAmmoCount','GetOrigin','GetWorld','GetAgents','TryMove','DeleteEntity'):
assert forbidden not in s,forbidden
code=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('class ','struct ')
code=code.replace('InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent))','actor->inventory')
code=code.replace('BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent))','actor->manager')
code=code.replace('BaseWeaponComponent.Cast(weaponEntity.FindComponent(BaseWeaponComponent))','weaponEntity->weapon')
code=code.replace('BaseMagazineComponent.Cast(item.FindComponent(BaseMagazineComponent))','item->magazine')
code=re.sub(r'\bERR_HQAmmoSnapshot (?=\w)', 'ERR_HQAmmoSnapshot* ',code)
code=code.replace('array<','Array<').replace('foreach (','for (').replace('null','nullptr')
for name in ('EWeaponType','EMuzzleType','Replication','ERR_Actors'):code=code.replace(name+'.',name+'::')
code=code.replace('EWeaponType kind','int kind').replace('EMuzzleType muzzleKind','int muzzleKind')
for name in ('weapon','muzzle','magazine','actor','group','result','inventory','manager','loaded','copy'):
code=re.sub(r'\b'+name+r'\.',name+'->',code)
code=code.replace('GetMagazineWell().','GetMagazineWell()->')
code=re.sub(r'\b(bool|int) (\w+);',r'\1 \2=0;',code)
code=code.replace('\n}\n','\n};\n')
run(r'''
#include <cassert>
#include <algorithm>
#include <vector>
using std::vector;
template<class T>struct Array:vector<T>{using vector<T>::vector;
int Count(){return this->size();}bool IsEmpty(){return this->empty();}
void Insert(T n){this->push_back(n);}bool Contains(T n){return std::find(this->begin(),this->end(),n)!=this->end();}};
struct Entity;struct Muzzle;struct Magazine;struct Weapon;struct Group;struct Inventory;struct Manager;
using IEntity=Entity*;using SCR_AIGroup=Group*;using BaseWeaponComponent=Weapon*;
using BaseMuzzleComponent=Muzzle*;using BaseMagazineComponent=Magazine*;
using InventoryStorageManagerComponent=Inventory*;using BaseWeaponManagerComponent=Manager*;
struct EWeaponType{static constexpr int WT_RIFLE=1,WT_SNIPERRIFLE=2,WT_MACHINEGUN=3,WT_HANDGUN=4,
WT_ROCKETLAUNCHER=5,WT_GRENADELAUNCHER=6,WT_FRAGGRENADE=7,WT_SMOKEGRENADE=8;};
struct EMuzzleType{static constexpr int MT_BaseMuzzle=0,MT_RPGMuzzle=1,MT_UGLMuzzle=2;};
struct Well{int kind=1,baseKind=0;int Type(){return kind;}bool IsInherited(int type){return kind==type||baseKind==type;}};
struct Magazine{IEntity owner=nullptr;Well* well=nullptr;int rounds=0,capacity=30;
IEntity GetOwner(){return owner;}Well* GetMagazineWell(){return well;}
int GetAmmoCount(){return rounds;}int GetMaxAmmoCount(){return capacity;}};
struct Muzzle{Well* well=nullptr;Magazine* magazine=nullptr;int kind=0,barrels=1;bool chamber=false;
int GetMuzzleType(){return kind;}Well* GetMagazineWell(){return well;}
int GetBarrelsCount(){return barrels;}bool IsBarrelChambered(int){return chamber;}
Magazine* GetMagazine(){return magazine;}};
struct Weapon{int kind=1;Array<Muzzle*>muzzles;int GetWeaponType(){return kind;}
void GetMuzzlesList(Array<Muzzle*>& out){out=muzzles;}};
struct Inventory{Array<IEntity>items;int reads=0;void GetItems(Array<IEntity>& out){reads++;out=items;}};
struct Manager{Array<IEntity>weapons;void GetWeaponsList(Array<IEntity>& out){out=weapons;}};
struct Group{bool slave=false;int players=0;bool IsSlave(){return slave;}int GetPlayerCount(bool){return players;}};
struct Entity{Inventory* inventory=nullptr;Manager* manager=nullptr;Weapon* weapon=nullptr;Magazine* magazine=nullptr;
Group* group=nullptr;bool alive=true,player=false;};
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
struct ERR_Actors{static bool Alive(IEntity a){return a&&a->alive;}static bool Player(IEntity a){return a&&a->player;}
static struct Group* Group(IEntity a){return a?a->group:nullptr;}};
'''+code+r'''
ERR_HQAmmoSnapshot Fresh(){ERR_HQAmmoSnapshot s;s.Complete=true;s.LauncherComplete=true;return s;}
int main(){
Group group,other;Well rifleWell,rocketWell;rocketWell.kind=2;
Entity actor,weaponEntity,loadedEntity,spareEntity,unrelatedEntity;
Inventory inv;Manager manager;Weapon weapon;Muzzle muzzle;Magazine loaded,spare,unrelated;
actor.group=&group;actor.inventory=&inv;actor.manager=&manager;
weaponEntity.weapon=&weapon;manager.weapons={&weaponEntity,&weaponEntity};weapon.muzzles={&muzzle,&muzzle};
muzzle.well=&rifleWell;muzzle.magazine=&loaded;muzzle.chamber=true;
loaded.owner=&loadedEntity;loaded.well=&rifleWell;loaded.rounds=30;loadedEntity.magazine=&loaded;
spare.owner=&spareEntity;spare.well=&rifleWell;spare.rounds=17;spareEntity.magazine=&spare;
unrelated.owner=&unrelatedEntity;unrelated.well=&rocketWell;unrelated.rounds=1;unrelated.capacity=1;unrelatedEntity.magazine=&unrelated;
inv.items={&loadedEntity,&loadedEntity,&spareEntity,&unrelatedEntity};
auto full=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&full);
assert(full.Valid(1)&&full.Complete&&full.Members==1&&full.SmallArmsMuzzles==1);
assert(full.SmallArmsRounds==47&&full.SmallArmsMagazines==2&&full.SmallArmsChambers==1&&!full.KnownSmallArmsEmpty());
auto wire=full.Snapshot();full.SmallArmsRounds=0;assert(wire->SmallArmsRounds==47); // detached delivery value
// Loaded magazine omitted from native storage enumeration is still counted exactly once.
inv.items={&spareEntity};auto omitted=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&omitted);
assert(omitted.SmallArmsRounds==47&&omitted.SmallArmsMagazines==2);
// Multiple weapons sharing one well never multiply a single physical magazine.
Entity secondEntity;Weapon second;Muzzle secondMuzzle;secondEntity.weapon=&second;
second.muzzles={&secondMuzzle};secondMuzzle.well=&rifleWell;secondMuzzle.magazine=&loaded;
manager.weapons.Insert(&secondEntity);auto shared=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&shared);
assert(shared.SmallArmsMuzzles==2&&shared.SmallArmsRounds==47&&shared.SmallArmsMagazines==2);
manager.weapons={&weaponEntity};
loaded.rounds=0;spare.rounds=0;muzzle.chamber=false;auto empty=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&empty);
assert(empty.KnownSmallArmsEmpty()&&empty.SmallArmsMagazines==2);
muzzle.chamber=true;auto chamber=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&chamber);
assert(chamber.SmallArmsRounds==0&&chamber.SmallArmsChambers==1&&!chamber.KnownSmallArmsEmpty());muzzle.chamber=false;
// Under-barrel launcher is classified by muzzle, independently of its parent rifle type.
Muzzle launcher;launcher.kind=EMuzzleType::MT_UGLMuzzle;launcher.well=&rocketWell;launcher.magazine=&unrelated;
weapon.muzzles.Insert(&launcher);auto mixed=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&mixed);
assert(mixed.Complete&&mixed.LauncherComplete&&mixed.SmallArmsRounds==0&&mixed.LauncherRounds==1);
assert(mixed.LauncherMagazines==1&&mixed.KnownSmallArmsEmpty()); // launcher stock is not rifle stock
launcher.well=nullptr;launcher.magazine=nullptr;auto loose=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&loose);
assert(loose.Complete&&!loose.LauncherComplete);weapon.muzzles={&muzzle};
// Empty/unavailable native launcher muzzles affect only launcher completeness.
Entity launcherEntity;Weapon launcherWeapon;launcherEntity.weapon=&launcherWeapon;
launcherWeapon.kind=EWeaponType::WT_ROCKETLAUNCHER;manager.weapons.Insert(&launcherEntity);
auto unavailableLauncher=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&unavailableLauncher);
assert(unavailableLauncher.Complete&&!unavailableLauncher.LauncherComplete&&unavailableLauncher.KnownSmallArmsEmpty());
launcherWeapon.muzzles={nullptr};auto nullLauncher=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&nullLauncher);
assert(nullLauncher.Complete&&!nullLauncher.LauncherComplete);
launcherWeapon.muzzles={nullptr,nullptr,nullptr,nullptr,nullptr};
auto cappedLauncher=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&cappedLauncher);
assert(cappedLauncher.Complete&&!cappedLauncher.LauncherComplete);
// A native loaded projectile with no magazine well is known to belong to the launcher.
launcherWeapon.muzzles={&launcher};launcher.well=nullptr;launcher.magazine=&unrelated;unrelated.well=nullptr;
auto knownProjectile=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&knownProjectile);
assert(knownProjectile.Complete&&!knownProjectile.LauncherComplete&&knownProjectile.KnownSmallArmsEmpty());
// Do not waive uncertainty if that same physical magazine also occurs on a small-arms muzzle.
muzzle.magazine=&unrelated;auto sharedUnknown=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&sharedUnknown);
assert(!sharedUnknown.Complete&&!sharedUnknown.KnownSmallArmsEmpty());muzzle.magazine=&loaded;
manager.weapons={&weaponEntity};inv.items.Insert(&unrelatedEntity);
auto looseUnknown=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&looseUnknown);
assert(looseUnknown.Complete&&looseUnknown.KnownSmallArmsEmpty()&&!looseUnknown.LauncherComplete);
assert(!ERR_HQAmmoSampler::Compatible(&unrelated,weapon.muzzles)); // stock requires a magazine well for reload
inv.items={&spareEntity};unrelated.well=&rocketWell;
// Stock search accepts a derived magazine well; exact reload compatibility is
// narrower. Preserve exact totals but make derived-only empty knowledge unknown.
Well derivedRifle;derivedRifle.kind=3;derivedRifle.baseKind=rifleWell.kind;
spare.well=&derivedRifle;spare.rounds=17;
assert(!ERR_HQAmmoSampler::Compatible(&spare,weapon.muzzles));
assert(ERR_HQAmmoSampler::InheritedCandidate(&spare,weapon.muzzles));
auto derived=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&derived);
assert(!derived.Complete&&!derived.KnownSmallArmsEmpty()&&derived.SmallArmsRounds==0);
assert(derived.LauncherComplete); // no launcher candidate from this well
spare.well=&rocketWell;auto unrelatedWell=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&unrelatedWell);
assert(unrelatedWell.Complete&&unrelatedWell.KnownSmallArmsEmpty());
assert(!ERR_HQAmmoSampler::InheritedCandidate(&spare,weapon.muzzles));
// Inheritance direction matches stock: magazine derives from the muzzle well.
spare.well=&rifleWell;muzzle.well=&derivedRifle;muzzle.magazine=nullptr;
auto reverse=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&reverse);
assert(reverse.Complete&&reverse.KnownSmallArmsEmpty());
muzzle.well=&rifleWell;muzzle.magazine=&loaded;spare.rounds=0;
assert(ERR_HQAmmoSampler::Compatible(&spare,weapon.muzzles));
assert(!ERR_HQAmmoSampler::InheritedCandidate(&spare,weapon.muzzles));
auto exactEmpty=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&exactEmpty);
assert(exactEmpty.Complete&&exactEmpty.KnownSmallArmsEmpty());
// The corresponding launcher category independently becomes unknown.
Well derivedRocket;derivedRocket.kind=4;derivedRocket.baseKind=rocketWell.kind;
unrelated.well=&derivedRocket;launcher.well=&rocketWell;launcher.magazine=nullptr;
weapon.muzzles.Insert(&launcher);inv.items.Insert(&unrelatedEntity);
auto derivedLauncher=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&derivedLauncher);
assert(derivedLauncher.Complete&&!derivedLauncher.LauncherComplete&&derivedLauncher.LauncherRounds==0);
weapon.muzzles={&muzzle};inv.items={&spareEntity};unrelated.well=&rocketWell;
// Missing/unsupported/malformed native data never establishes known empty ammunition.
manager.weapons.Insert(nullptr);auto vanishedWeapon=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&vanishedWeapon);
assert(!vanishedWeapon.Complete&&!vanishedWeapon.KnownSmallArmsEmpty());manager.weapons={&weaponEntity};
inv.items.Insert(nullptr);auto vanishedItem=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&vanishedItem);
assert(!vanishedItem.Complete&&!vanishedItem.KnownSmallArmsEmpty());inv.items={&spareEntity};
actor.manager=nullptr;auto missing=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&missing);assert(!missing.KnownSmallArmsEmpty());actor.manager=&manager;
loaded.well=nullptr;auto noWell=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&noWell);assert(!noWell.Complete);loaded.well=&rifleWell;
loaded.rounds=31;auto malformed=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&malformed);assert(!malformed.Complete);loaded.rounds=0;
weapon.kind=99;auto unsupported=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&unsupported);assert(!unsupported.Complete);weapon.kind=1;
muzzle.barrels=5;auto barrels=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&barrels);assert(!barrels.Complete);muzzle.barrels=1;
inv.items.resize(129,&spareEntity);auto capped=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&capped);assert(!capped.Complete);inv.items={&spareEntity};
int before=inv.reads;actor.player=true;auto player=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&player);assert(!player.Complete&&inv.reads==before);actor.player=false;
actor.group=&other;auto transferred=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&transferred);assert(!transferred.Complete&&inv.reads==before);actor.group=&group;
actor.alive=false;auto dead=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&dead);assert(!dead.Complete&&inv.reads==before);actor.alive=true;
Replication::server=false;auto client=Fresh();ERR_HQAmmoSampler::Inspect(&actor,&group,&client);assert(!client.Complete&&inv.reads==before);Replication::server=true;
auto count=Fresh();count.Members=32;ERR_HQAmmoSampler::Inspect(&actor,&group,&count);assert(!count.Complete&&count.Members==32);
assert(!empty.Valid(0));empty.SmallArmsRounds=-1;assert(!empty.Valid(1));
}
''')
print('PASS: actual own-inventory ammo inspection deduplicates loaded/carried/shared wells, separates chambers/launchers, freezes scalar snapshots and treats unsupported/capped/foreign inventory as unknown')
+1
View File
@@ -38,6 +38,7 @@ struct ERR_HQUnit {
SCR_AIGroup* Group=nullptr;AIWaypoint* Waypoint=nullptr;
ERR_HQTask *Task=nullptr,*PendingTask=nullptr;
ERR_HQReport *Assignment=nullptr,*PendingOrder=nullptr;
void* PendingConvoyOrder=nullptr;
ERR_HQRoute* Downlink=nullptr;bool PendingCheckin=false;
'''+adapt(unit)+r'''};
struct Watch {int calls=0;std::string source;int message=0,request=0;
+103
View File
@@ -0,0 +1,103 @@
"""Execute production HQ convoy planning/delivery with detached reports and radio-boundary doubles."""
from policy_harness import ROOT, body, run
import re
H = ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c'
def cpp(s):
s=s.replace('protected ', '').replace('null','nullptr').replace('foreach (','for (')
for x in ('ERR_HQPursuitRules','vector','string'): s=s.replace(x+'.',x+'::')
for typ in ('ERR_HQUnit','ERR_HQReport','ERR_HQWatchEntry','ERR_HQRoute','ERR_ConvoyCombatOrder','ERR_ConvoyVehicle'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for x in ('entry','unit','best','report','vehicle','contact','route','order','m_ConvoyTask'):
s=s.replace(x+'.',x+'->')
s=s.replace('->Combat.', '->Combat->').replace('->Receipt.', '->Receipt->')
s=s.replace('unit->Downlink.', 'unit->Downlink->')
s=re.sub(r'(ERR_\w+\* \w+);',r'\1 = nullptr;',s)
s=s.replace('bool accepted;', 'bool accepted = false;')
return s
methods='\n'.join(cpp(body(H,s)) for s in ('protected bool ConvoyPositionKnown(',
'protected ERR_HQReport ConvoyContact(', 'protected void PlanConvoy(', 'protected void DeliverConvoyOrder('))
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(')
assert 'm_Contacts' in body(H,'protected ERR_HQReport ConvoyContact(')
assert 'GetOrigin' not in methods and 'Target' not in methods
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <cmath>
#include <map>
struct string:std::string {using std::string::string; template<class...T>static string Format(T...){return "order";}};
struct vector {float x=0,z=0;static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
struct ERR_HQPursuitRules {static bool Finite(float n){return std::isfinite(n);}};
struct SCR_AIGroup{};
struct ERR_HQReport {int MessageId=0;float ObservedAt=-1;vector Position;};
struct ERR_HQWatchEntry {bool Closed=false;int Count=2;float ObservedAt=-1;vector Position;};
struct Watch {std::map<string,ERR_HQWatchEntry*> entries;ERR_HQWatchEntry* Find(string key){return entries.count(key)?entries[key]:nullptr;}};
struct Journal {int seq=100;int Message(){return ++seq;}void Write(string,string,int){}};
struct ERR_HQRoute {int MessageId=0;string Payload;int result=0;int Advance(float,Journal&){return result;}};
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 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;
bool ApplyCombatOrder(ConvoyTask* task,ERR_ConvoyCombatOrder* o,float){calls++;last=o;if(!task||!valid)return false;
auto s=task->Vehicles.at(o->VehicleIndex)->Combat;s->Held=o->Hold;s->LastCommandId=o->CommandId;s->ReportId=o->ReportId;s->ObservedAt=o->ObservedAt;return true;}};
struct ERR_HQUnit {SCR_AIGroup* Group=nullptr;bool ReservedForSupport=true;void* Radio=this;string ForceSourceId="vehicle-0";
ERR_HQRoute* Downlink=nullptr;ERR_ConvoyCombatOrder* PendingConvoyOrder=nullptr;void* PendingOrder=nullptr;void* PendingTask=nullptr;
bool PendingCheckin=false;float NextStatus=0;
'''+cancel+r'''
};
struct HQ {Watch m_Watch;Network m_Network;Journal m_Journal;Service m_Convoys;ConvoyTask* m_ConvoyTask=nullptr;
std::vector<ERR_HQUnit*> m_Units;std::vector<ERR_HQReport*> m_Contacts;int ordinary=0,lost=0;
void DeliverOrder(ERR_HQUnit*,float){ordinary++;}void LostOrder(ERR_HQUnit* u,float){lost++;u->CancelPending();}
'''+methods+r'''
void TickDownlink(ERR_HQUnit* unit,float now){
'''+downlink+r'''
}
};
int main(){
HQ h;SCR_AIGroup g;ERR_HQUnit u;u.Group=&g;h.m_Units={&u};ConvoyTask t;h.m_ConvoyTask=&t;
ERR_ConvoyVehicle v;v.Group=&g;t.Vehicles={&v};ERR_HQWatchEntry e;e.ObservedAt=1000;e.Position={0,0};
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};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);
h.TickDownlink(&u,4000);assert(!v.Combat->Held&&h.m_Convoys.calls==0); // transmitting, no application
u.Downlink->result=-1;h.TickDownlink(&u,5000);assert(!u.Downlink&&!u.PendingConvoyOrder&&!v.Combat->Held);
h.PlanConvoy(6000);u.Downlink->result=1;h.TickDownlink(&u,8000);assert(v.Combat->Held&&h.m_Convoys.calls==1&&h.ordinary==0);
h.PlanConvoy(9000);assert(!u.Downlink); // same delivered contact never duplicates a HOLD
ERR_HQReport newer=r;newer.MessageId=2;newer.ObservedAt=12000;h.m_Contacts={&newer};h.PlanConvoy(13000);
assert(u.PendingConvoyOrder->ReportId==2);u.Downlink->result=1;h.TickDownlink(&u,15000);assert(v.Combat->ReportId==2);
h.PlanConvoy(71000);assert(!u.Downlink); // less than60s quiet
h.PlanConvoy(72000);assert(u.Downlink&&!u.PendingConvoyOrder->Hold);
newer.MessageId=3;newer.ObservedAt=73000;u.Downlink->result=1;h.TickDownlink(&u,74000);
assert(v.Combat->Held&&h.m_Convoys.calls==2&&!u.Downlink); // new report rejects in-flight RESUME
h.m_Network.available=false;h.PlanConvoy(75000);assert(!u.Downlink&&v.Combat->Held);
h.m_Network.available=true;h.PlanConvoy(76000);assert(u.PendingConvoyOrder->Hold);u.Downlink->result=1;
newer.MessageId=4;newer.ObservedAt=77000;h.TickDownlink(&u,78000);assert(h.m_Convoys.calls==3&&v.Combat->ReportId==3); // a newer nearby report cannot starve HOLD delivery
h.PlanConvoy(79000);u.Downlink->result=1;h.TickDownlink(&u,80000);assert(v.Combat->ReportId==4);
h.PlanConvoy(137000);assert(u.Downlink&&!u.PendingConvoyOrder->Hold);u.Downlink->result=1;h.TickDownlink(&u,139000);
assert(!v.Combat->Held&&h.m_Convoys.calls==5);h.PlanConvoy(140000);assert(!u.Downlink);
// Report proximity uses detached Watch coordinates only; no live entity reads exist.
newer.ObservedAt=150000;newer.MessageId=5;newer.Position={401,0};h.PlanConvoy(151000);assert(!u.Downlink);
newer.Position={400,0};h.PlanConvoy(151000);assert(u.Downlink);u.CancelPending();
e.Closed=true;h.PlanConvoy(151000);assert(!u.Downlink);e.Closed=false;e.Count=0;h.PlanConvoy(151000);assert(!u.Downlink);e.Count=2;
e.ObservedAt=160000;h.PlanConvoy(151000);assert(!u.Downlink);e.ObservedAt=0;h.PlanConvoy(181000);assert(!u.Downlink);
e.ObservedAt=150000;v.Released=true;h.PlanConvoy(151000);assert(!u.Downlink);v.Released=false;v.Arrived=true;
h.PlanConvoy(151000);assert(!u.Downlink);v.Arrived=false;t.Ready=false;h.PlanConvoy(151000);assert(!u.Downlink);t.Ready=true;
u.ReservedForSupport=false;h.PlanConvoy(151000);assert(!u.Downlink);u.ReservedForSupport=true;
h.PlanConvoy(151000);assert(u.Downlink);h.m_Convoys.valid=false;u.Downlink->result=1;h.TickDownlink(&u,152000);
assert(!v.Combat->Held&&!u.Downlink&&h.ordinary==0); // service capture/death/canonical guards are authoritative
// A routine check-in shares the channel and cannot be displaced by convoy planning.
u.PendingCheckin=true;u.Downlink=new ERR_HQRoute;calls=h.m_Network.calls;h.PlanConvoy(153000);assert(h.m_Network.calls==calls);
u.Downlink->result=1;h.TickDownlink(&u,154000);assert(u.NextStatus==154000&&!u.Downlink&&h.ordinary==0);
}
''')
print('HQ convoy delivered-report planning and downlink regressions passed')
+52 -5
View File
@@ -1,8 +1,9 @@
"""Actual HQ convoy wrappers and authenticated command dispatch; native deployment is a double."""
from policy_harness import ROOT,body,run
import re
import re,os
from pathlib import Path
H=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c'
P=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQAdminProvider.c'
P=Path(os.environ.get('ERR_HQ_ADMIN_SOURCE',str(ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQAdminProvider.c')))
A=ROOT/'addons/core/Scripts/Game/ERR/ERR_AdminService.c'
def cpp(s):
for old,new in [('protected ',''),('override ',''),('null','nullptr'),('Replication.','Replication::'),
@@ -19,10 +20,16 @@ def cpp(s):
s=s.replace('foreach (string key : ERR_Settings::Keys())','for (string key : ERR_Settings::Keys())')
s=s.replace('mode->Settings()->Get(key).ToString()','string(std::to_string(mode->Settings()->Get(key)))')
s=s.replace('bool handled;','bool handled=false;')
s=s.replace('Array<string>& parts = {};','Array<string> parts = {};')
s=s.replace('out int ', 'int& ').replace('ERR_CommandArguments.Integer', 'ERR_CommandArguments::Integer')
s=s.replace('value.ToString()', 'string(std::to_string(value))')
s=s.replace('"0123456789".IndexOf(', 'string("0123456789").IndexOf(')
return s
hq='\n'.join(cpp(body(H,s)) for s in ('static bool MissionActive(', 'bool PrepareConvoy(', 'bool StartConvoy(',
'bool StartScenarioConvoy(', 'string ConvoyStatus('))
provider=cpp(body(P,'override bool Execute('))
provider=cpp(body(P,'protected void StingerSetting('))+cpp(body(P,'override bool Execute('))
integer=cpp(body(A,'static bool Integer('))
validator=cpp(body(ROOT/'addons/core/Scripts/Game/ERR/ERR_Settings.c','static bool Valid('))
extension=cpp(body(A,'protected bool Extension('))
# Execute the exact complete auth/normalization/extension-dispatch region.
# Unrelated settings/help parsers after this boundary are covered by their own suites.
@@ -35,16 +42,30 @@ run(r'''
#include <algorithm>
#include <cctype>
#include <map>
template<class T>struct Array;
struct string:std::string{
using std::string::string;string(const std::string& v):std::string(v){}
bool IsEmpty()const{return empty();}int Length()const{return size();}
int IndexOf(const char* s)const{auto p=find(s);return p==npos?-1:int(p);}
string Substring(int begin,int count)const{return substr(begin,count);}int ToInt()const{return std::stoi(*this);}
void Split(const char* separator,Array<string>& parts,bool skipEmpty)const;
int IndexOf(const std::string& s)const{auto p=find(s);return p==npos?-1:int(p);}
string Trim()const{auto a=find_first_not_of(" \t\r\n");if(a==npos)return "";
return substr(a,find_last_not_of(" \t\r\n")-a+1);}
void ToLower(){std::transform(begin(),end(),begin(),[](unsigned char c){return std::tolower(c);});}
};
template<class T>struct Array:std::vector<T>{void Clear(){this->clear();}void Insert(T n){this->push_back(n);}
int Count(){return this->size();}bool IsEmpty(){return this->empty();}};
void string::Split(const char* separator,Array<string>& parts,bool skipEmpty)const{
parts.Clear();size_t begin=0;const size_t width=std::char_traits<char>::length(separator);assert(width>0);
for(;;){auto end=find(separator,begin);string part=substr(begin,end==npos?npos:end-begin);
if(!skipEmpty||!part.empty())parts.Insert(part);
if(end==npos)break;
begin=end+width;
}
}
struct ERR_CommandArguments{
'''+integer+r'''
};
struct vector{float x=0,y=0,z=0;};
struct Math{static int Max(int a,int b){return std::max(a,b);}};
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
@@ -55,7 +76,13 @@ struct Definition{vector Position{12,3,45};};
struct ERR_SiteRuntime{struct Definition* Definition=nullptr;};
struct Sites{ERR_SiteRuntime* site=nullptr;int finds=0;string last;
ERR_SiteRuntime* Find(string id){finds++;last=id;return site;}};
struct ERR_Settings{static Array<string> Keys(){return {}; }int Get(string){return 1;}};
struct ERR_Settings{
int stinger=15,writes=0;bool saveFails=false;
static Array<string> Keys(){return {}; }int Get(string key){return key=="stingerChanceOneIn"?stinger:1;}
'''+validator+r'''
bool Set(string key,int value){writes++;if(saveFails||!Valid(key,value))return false;
assert(key=="stingerChanceOneIn");stinger=value;return true;}
};
struct ERR_GameMode{
inline static ERR_GameMode* current=nullptr;bool active=true,settingsReady=true;Ledger ledger;
struct Sites sites;ERR_Settings settings;bool sitesReady=true;
@@ -130,6 +157,26 @@ int main(){
assert(!provider.Execute(&mode,"hq convoy start extra",replies)&&pool.calls==0);
Provider unavailable(nullptr);replies.Clear();assert(unavailable.Execute(&mode,"hq convoy start",replies));
assert(replies[0]=="HQ unavailable.");
// Stinger commands pass the same actual Core authority and provider dispatch boundary.
admin.Execute(&mode,false,"hq stinger set 0",replies);assert(mode.settings.writes==0&&mode.settings.stinger==15);
Replication::server=false;admin.Execute(&mode,true,"hq stinger set 0",replies);
assert(mode.settings.writes==0&&replies.IsEmpty());Replication::server=true;
mode.settingsReady=false;admin.Execute(&mode,true,"hq stinger set 0",replies);
assert(mode.settings.writes==0&&replies[0]=="Mission settings are not ready.");mode.settingsReady=true;
admin.Execute(&mode,true," HQ STINGER SET 0 ",replies);
assert(mode.settings.writes==1&&mode.settings.stinger==0&&pool.calls==0);
assert(replies[0].find("have not rolled yet")!=string::npos&&replies[0].find("no reroll, removal or replenishment")!=string::npos);
admin.Execute(&mode,true,"hq stinger show",replies);
assert(mode.settings.writes==1&&replies[0].find("stingerChanceOneIn=0")!=string::npos);
admin.Execute(&mode,true,"hq stinger help",replies);
assert(mode.settings.writes==1&&replies[0].find("2..10000 one in N")!=string::npos);
mode.settings.saveFails=true;admin.Execute(&mode,true,"hq stinger set 15",replies);
assert(mode.settings.stinger==0&&replies[0].find("previous setting retained")!=string::npos);mode.settings.saveFails=false;
int writes=mode.settings.writes;
admin.Execute(&mode,true,"hq stinger set 1 extra",replies);assert(mode.settings.writes==writes&&mode.settings.stinger==0);
admin.Execute(&mode,true,"hq stinger set 1junk",replies);assert(mode.settings.writes==writes&&mode.settings.stinger==0);
admin.Execute(&mode,true,"hq stinger set 10001",replies);assert(mode.settings.stinger==0&&replies[0].find("previous setting retained")!=string::npos);
replies.Clear();assert(unavailable.Execute(&mode,"hq stinger set 1",replies)&&replies[0]=="HQ unavailable.");
// Canonical authored site and preparation failures refuse without consuming the finite receipt.
mode.sitesReady=false;assert(!hq.StartScenarioConvoy());mode.sitesReady=true;
mode.sites.site=nullptr;assert(!hq.StartScenarioConvoy());mode.sites.site=&site;
+13 -2
View File
@@ -10,11 +10,12 @@ def cpp(s):
for old,new in [('protected ',''),('null','nullptr'),('Replication.','Replication::'),
('ERR_Actors.','ERR_Actors::'),('ERR_HQPursuitRules.','ERR_HQPursuitRules::'),
('SCR_FactionManager.','SCR_FactionManager::'),('ERR_HQRadio.','ERR_HQRadio::'),('Math.','Math::'),
('sourceId.IsEmpty()','sourceId.empty()'),('sourceId.Length()','sourceId.size()')]:s=s.replace(old,new)
('ERR_HQAmmoSampler.','ERR_HQAmmoSampler::'),('sourceId.IsEmpty()','sourceId.empty()'),('sourceId.Length()','sourceId.size()')]:s=s.replace(old,new)
for typ in ('IEntity','AIAgent','SCR_AIGroup','Faction','ERR_HQStatusReport','DamageManagerComponent'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for var in ('actor','agent','group','report','m_Group','damage'):s=s.replace(var+'.',var+'->')
s=s.replace('foreach (AIAgent* agent : m_Agents)','for (AIAgent* agent : m_Agents)')
s=s.replace('report->AmmoState.', 'report->AmmoState->')
return s
methods='\n'.join(cpp(body(P,s)) for s in ('bool Bound()', 'protected bool Eligible(', 'bool Bind(',
'string InitialSourceId(', 'int InitialStrength(', 'bool OwnsRoster(', 'IEntity InitialMember(',
@@ -56,7 +57,10 @@ struct ERR_Actors{
};
struct SCR_FactionManager{static Faction* SGetFaction(IEntity* a){return a?a->faction:nullptr;}};
struct ERR_HQRadio{static int Capability(IEntity* a){return ERR_Actors::CanFight(a)&&a->radio?1:0;}};
struct ERR_HQStatusReport{
struct ERR_HQAmmoSnapshot{bool Complete=false,LauncherComplete=false;int Members=0;};
struct ERR_HQAmmoSampler{inline static Array<IEntity*> sampled;
static void Inspect(IEntity* actor,SCR_AIGroup* group,ERR_HQAmmoSnapshot* state){assert(actor&&actor->group==group);sampled.Insert(actor);state->Members++;}};
struct ERR_HQStatusReport{ERR_HQAmmoSnapshot* AmmoState=nullptr;
string SourceId;int MessageId=0;vector Position;int Strength=0,Count=0,Fit=0,ConfirmedLosses=0;
float ObservedAt=-1;Array<int> DeathSlots;
};
@@ -90,6 +94,10 @@ int main(){
assert(!sample.CanAttribute(1,&members[2]));std::reverse(group.agents.begin(),group.agents.end());
auto first=sample.Sample(1000,1,&members[0]);assert(first&&first->Count==4&&first->Fit==4&&first->Strength==4);
assert(first->Position.x==100&&first->SourceId=="site");
assert(first->AmmoState&&first->AmmoState->Complete&&first->AmmoState->Members==4);
assert(ERR_HQAmmoSampler::sampled.Count()==4);
for(auto* actor:ERR_HQAmmoSampler::sampled)assert(actor>=&members[0]&&actor<=&members[3]);
ERR_HQAmmoSampler::sampled.Clear();
group.position.x=200;assert(first->Position.x==100); // detached snapshot
members[1].unconscious=true;auto injured=sample.Sample(2000,2,&members[0]);
assert(injured&&injured->Count==4&&injured->Fit==3&&injured->ConfirmedLosses==0);
@@ -109,6 +117,7 @@ int main(){
sample.m_Members[2]=nullptr;members[3].hasDamage=false;
auto absent=sample.Sample(4000,4,&members[0]);
assert(absent&&absent->Count==1&&absent->ConfirmedLosses==1);
assert(!absent->AmmoState->Complete&&absent->AmmoState->Members==1); // missing original identity/component => unknown
members[3].hasDamage=true;members[3].player=true;assert(sample.ObserveDeaths());
assert(!sample.CanAttribute(3,&members[3]));
members[3].player=false;members[3].damage.dead=true;assert(sample.ObserveDeaths()&&sample.m_Deaths==1);
@@ -116,7 +125,9 @@ int main(){
// Never adopt an externally attached replacement as one of the authored original members.
IEntity replacement;replacement.group=&group;replacement.faction=&enemy;AIAgent replacementAgent;replacementAgent.actor=&replacement;
group.agents.Insert(&replacementAgent);assert(!sample.Sample(5000,5,&replacement));
ERR_HQAmmoSampler::sampled.Clear();
auto original=sample.Sample(5000,5,&members[0]);assert(original&&original->Strength==4&&original->Count==1);
assert(ERR_HQAmmoSampler::sampled.Count()==1&&ERR_HQAmmoSampler::sampled[0]==&members[0]); // no replacement/dead/released inventory sampling
members[0].radio=false;assert(!sample.Sample(6000,6,&members[0]));members[0].radio=true;
members[0].unconscious=true;assert(!sample.Sample(6000,6,&members[0]));members[0].unconscious=false;
group.players=1;assert(!sample.ObserveDeaths()&&!sample.Sample(6000,6,&members[0]));group.players=0;
+3 -2
View File
@@ -20,7 +20,7 @@ def cpp(s):
s=s.replace('->'+v+'.','->'+v+'->')
s=s.replace('m_Mode.Settings().Get(', 'm_Mode->Settings()->Get(')
s=s.replace('ERR_HQObservation.','ERR_HQObservation::').replace('vector.DistanceXZ','vector::DistanceXZ').replace('string.Format','Format')
return s
return s.replace('AmmoState.', 'AmmoState->')
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,7 +43,8 @@ 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 ERR_HQStatusReport{int MessageId=0,Count=4,Fit=4,ConfirmedLosses=0;string SourceId="own-1";float ObservedAt=0;};
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 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;
+31 -3
View File
@@ -9,13 +9,14 @@ def cpp(s):
('ERR_HQWatchRules.','ERR_HQWatchRules::'),('null','nullptr'),
('report.SourceId.IsEmpty()','report.SourceId.empty()'),('report.SourceId.Length()','report.SourceId.size()')]:
s=s.replace(old,new)
for typ in ('ERR_HQStatusReport','ERR_HQWatchEntry','ERR_HQReport','ERR_HQDeliveredCasualties'):
for typ in ('ERR_HQStatusReport','ERR_HQWatchEntry','ERR_HQReport','ERR_HQDeliveredCasualties','ERR_HQAmmoSnapshot'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for var in ('entry','report','validation'):s=s.replace(var+'.',var+'->')
s=s.replace('entry->Casualties.', 'entry->Casualties->')
s=s.replace('report->CasualtySourceId.IsEmpty()', 'report->CasualtySourceId.empty()')
s=s.replace('report->CasualtySourceId.Length()', 'report->CasualtySourceId.size()')
s=s.replace('report->DeathSlots,', '&report->DeathSlots,')
s=s.replace('entry->AmmoState.', 'entry->AmmoState->').replace('report->AmmoState.', 'report->AmmoState->')
return s
helper=(ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQDeliveredCasualties.c').read_text()
helper=helper.replace('class ','struct ').replace('protected ','').replace('ref ','').replace('Math.','Math::')
@@ -23,10 +24,15 @@ helper=helper.replace('array<int> slots','Array<int>* slots').replace('slots.',
helper=helper.replace('array<int>','Array<int>').replace('foreach (','for (')
helper=helper.replace(' : slots)', ' : *slots)').replace('int m_Strength;', 'int m_Strength=0;')
helper=helper.replace('\n}\n','\n};\n')
ammo_source=Path(os.environ.get('ERR_AMMO_SOURCE',ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQAmmoSampler.c')).read_text()
ammo=ammo_source[:ammo_source.index('class ERR_HQAmmoSampler')]
ammo=ammo.replace('class ','struct ').replace('ERR_HQAmmoSnapshot Snapshot(', 'ERR_HQAmmoSnapshot* Snapshot(')
ammo=ammo.replace('ERR_HQAmmoSnapshot copy', 'ERR_HQAmmoSnapshot* copy').replace('copy.', 'copy->')
ammo=re.sub(r'\b(bool|int) (\w+);',r'\1 \2=0;',ammo).replace('\n}\n','\n};\n')
rules='\n'.join(cpp(body(P,s)) for s in ('static bool Valid(', 'static bool RequestDue(',
'static bool InvestigationDue(', 'static int SquadLimit('))
methods='\n'.join(cpp(body(P,s)) for s in ('int Count()', 'ERR_HQWatchEntry Find(', 'ERR_HQWatchEntry At(',
'int ConfirmedLosses()', 'int SquadLimit(int baseline)', 'protected ERR_HQWatchEntry Create(',
'int ConfirmedLosses()', 'int SquadLimit(int baseline)', 'bool ArmedSearchBlocked(', 'protected ERR_HQWatchEntry Create(',
'protected void ApplyLosses(', 'bool Ingest(', 'bool IngestBody(', 'bool RequestDue(string',
'bool MarkRequested(', 'bool InvestigationDue(string', 'bool MarkInvestigating(', 'bool FinishInvestigation(', 'void Clear('))
common='\n'.join(cpp(body(U,s)) for s in ('static bool Finite(', 'static bool PositionValid('))
@@ -64,7 +70,7 @@ template<class K,class V>struct Map:std::map<K,V>{
struct ERR_HQPursuitRules{
'''+common+r'''
};
'''+helper+r'''
'''+helper+ammo+r'''
struct ERR_HQReport{
string TravelMode="body",CasualtySourceId="patrol-1";bool Distress=false;int MessageId=99,CasualtySlot=0,CasualtyStrength=4;
vector Position{900,30,900},Velocity;float ObservedAt=1000,DetailsObservedAt=-1;
@@ -74,10 +80,12 @@ struct ERR_HQReport{
};
struct ERR_HQStatusReport{
string SourceId="patrol-1";int MessageId=1;vector Position{100,20,300};
ERR_HQAmmoSnapshot* AmmoState=nullptr;
int Strength=4,Count=4,Fit=4,ConfirmedLosses=0;float ObservedAt=0;Array<int> DeathSlots;
};
struct ERR_HQWatchEntry{
string SourceId;int MessageId=0,RequestId=0;vector Position;
ERR_HQAmmoSnapshot* AmmoState=nullptr;
int Strength=0,Count=-1,Fit=-1,ConfirmedLosses=0;
ERR_HQDeliveredCasualties* Casualties=new ERR_HQDeliveredCasualties();float LastBodyDeliveredAt=-1;
float ObservedAt=-1,LastDeliveredAt=-1,AskedAt=-1,NextAttempt=0;
@@ -110,6 +118,26 @@ int main(){
assert(R::SquadLimit(2,0)==2&&R::SquadLimit(2,1)==2&&R::SquadLimit(2,2)==3);
assert(R::SquadLimit(2,-1)==2&&R::SquadLimit(2,15)==8);
assert(R::SquadLimit(100,0)==8&&R::SquadLimit(2,2147483647)==8);
Watch ammunition;ERR_HQStatusReport ammoReport;ERR_HQAmmoSnapshot ammoState;
ammoState.Complete=true;ammoState.LauncherComplete=true;ammoState.Members=4;ammoState.SmallArmsMuzzles=4;
ammoReport.AmmoState=&ammoState;
assert(!ammunition.ArmedSearchBlocked("patrol-1",1000)); // local scan is not HQ knowledge
assert(ammunition.Ingest(&ammoReport,1000)&&ammunition.ArmedSearchBlocked("patrol-1",1000));
ammoState.SmallArmsRounds=120;assert(ammunition.ArmedSearchBlocked("patrol-1",1001)); // immutable delivered copy
assert(ammunition.ArmedSearchBlocked("patrol-1",180000)&&!ammunition.ArmedSearchBlocked("patrol-1",180001));
assert(!ammunition.ArmedSearchBlocked("patrol-1",-1)&&!ammunition.ArmedSearchBlocked("patrol-1",NAN));
ammoReport.MessageId=2;ammoReport.ObservedAt=2000;
assert(ammunition.Ingest(&ammoReport,2000)&&!ammunition.ArmedSearchBlocked("patrol-1",2000));
ammoReport.MessageId=3;ammoReport.ObservedAt=3000;ammoState.SmallArmsRounds=0;ammoState.Complete=false;
assert(ammunition.Ingest(&ammoReport,3000)&&!ammunition.ArmedSearchBlocked("patrol-1",3000)); // unknown != empty
ammoReport.MessageId=4;ammoReport.ObservedAt=4000;ammoState.Complete=true;ammoState.SmallArmsChambers=1;
assert(ammunition.Ingest(&ammoReport,4000)&&!ammunition.ArmedSearchBlocked("patrol-1",4000));
ammoReport.MessageId=5;ammoReport.ObservedAt=5000;ammoState.SmallArmsChambers=0;
assert(ammunition.Ingest(&ammoReport,5000)&&ammunition.ArmedSearchBlocked("patrol-1",5000));
ERR_HQReport casualty;casualty.MessageId=901;casualty.ObservedAt=5000;
assert(ammunition.IngestBody(&casualty,5001)&&!ammunition.ArmedSearchBlocked("patrol-1",5001)); // changed roster requires fresh inventory evidence
ammoReport.MessageId=6;ammoReport.ObservedAt=6000;ammoReport.AmmoState=nullptr;
assert(ammunition.Ingest(&ammoReport,6000)&&!ammunition.ArmedSearchBlocked("patrol-1",6000)); // missing payload clears stale ammo claim
Watch watch;ERR_HQStatusReport report;
// Until an actual delivery calls Ingest the local status produces no HQ roster or missing case.
assert(watch.Count()==0&&!watch.RequestDue(report.SourceId,999999));
+40
View File
@@ -66,3 +66,43 @@ int main(){
}
''')
print('PASS: actual native fixture lookup selects exact BODY kind/identity/message from bounded evaluations, never sighting-only Get')
# Native ammo proof extends the already-real status delivery; it never changes gear or sender payloads.
for forbidden in ('SetAmmoCount(', 'ClearChamber(', 'TryMoveItem', 'TryDelete', 'TrySpawn'):
assert forbidden not in p.read_text(),forbidden
native_ammo=body(p,'protected bool NativeAmmo(')
for evidence in ('InitialMember(slot)', 'ERR_Actors.Alive(actor)', 'ERR_Actors.Group(actor) != unit.Group',
'CanAttribute(slot, actor)', 'ERR_HQAmmoSampler.Inspect(actor, unit.Group, inspected)',
'manager.GetWeaponsList(weapons)', 'loaded.GetAmmoCount()',
'loadedIdentities.Contains(loaded.GetOwner())', 'queued.SmallArmsRounds >= nativeLoadedRounds',
'SameAmmo(queued, inspected)', 'nativeLoadedRounds > 0'):
assert evidence in native_ammo,evidence
assert 'PendingStatus =' not in native_ammo and 'AmmoState =' not in native_ammo
for frozen in ('m_AmmoAtQueue = m_Status.AmmoState.Snapshot()',
'status.AmmoState != m_Status.AmmoState', 'status.AmmoState != m_AmmoAtQueue',
'SameAmmo(status.AmmoState, m_AmmoAtQueue)', 'SameAmmo(stable.AmmoState, m_AmmoAtQueue)'):
assert frozen in p.read_text(),frozen
compare=body(p,'protected bool SameAmmo(').replace('protected ', '').replace('ERR_HQAmmoSnapshot ', 'ERR_HQAmmoSnapshot* ')
compare=compare.replace('a.', 'a->').replace('b.', 'b->')
run(r'''
#include <cassert>
struct ERR_HQAmmoSnapshot{
bool Complete=true,LauncherComplete=false;
int Members=5,SmallArmsMuzzles=5,SmallArmsRounds=150,SmallArmsMagazines=10,SmallArmsChambers=5;
int LauncherRounds=2,LauncherMagazines=2,LauncherChambers=0;
};
struct Probe{
'''+compare+r'''
};
int main(){
Probe probe;ERR_HQAmmoSnapshot a,b;
assert(probe.SameAmmo(&a,&b)&&!probe.SameAmmo(nullptr,&b)&&!probe.SameAmmo(&a,nullptr));
int* fields[]={&b.Members,&b.SmallArmsMuzzles,&b.SmallArmsRounds,&b.SmallArmsMagazines,&b.SmallArmsChambers,
&b.LauncherRounds,&b.LauncherMagazines,&b.LauncherChambers};
for(auto* field:fields){(*field)++;assert(!probe.SameAmmo(&a,&b));(*field)--;}
b.Complete=false;assert(!probe.SameAmmo(&a,&b));b.Complete=true;
b.LauncherComplete=true;assert(!probe.SameAmmo(&a,&b));b.LauncherComplete=false;
assert(probe.SameAmmo(&a,&b));
}
''')
print('PASS: actual ammo snapshot comparison checks every scalar/category flag; native probe requires real roster rescan, loaded-round lower bound and separate immutable pending/Watch objects')