Integrate delivered HQ memory, finite physical convoy and RHS WCS dependencies

This commit is contained in:
2026-09-20 02:55:30 +02:00
parent e7b4141f69
commit a2aab17171
230 changed files with 37976 additions and 139 deletions
+8 -3
View File
@@ -54,8 +54,13 @@ int main(){
assert(network.Route(&c[0])->Nodes.Count()==1);assert(network.Route(&c[3])->Nodes.Count()==4);assert(!network.Route(&c[4]));
ERR_HQJournal journal;auto up=network.Route(&c[2]);assert(up->Nodes.Count()==3);
assert(up->Advance(0,&journal)==0);assert(up->Advance(1999,&journal)==0);assert(up->Advance(2000,&journal)==0);
c[2].alive=false;assert(up->Advance(4000,&journal)==0);c[0].alive=false;assert(up->Advance(6000,&journal)==1);
assert(journal.events.back().find("HQ received")!=std::string::npos); // cannot recall an emitted uplink
c[2].alive=false;assert(up->Advance(4000,&journal)==0);c[0].alive=false;assert(up->Advance(6000,&journal)==-1);
assert(journal.events.back().find("LOST")!=std::string::npos); // final transmission needs its live sender
for(auto& carrier:c)carrier.alive=true;
auto good=network.Route(&c[2]);assert(good->Advance(0,&journal)==0);assert(good->Advance(2000,&journal)==0);
c[2].alive=false;assert(good->Advance(4000,&journal)==0);assert(good->Advance(6000,&journal)==1);
assert(journal.events.back().find("HQ received")!=std::string::npos); // an already completed earlier hop survives its sender
auto severed=network.Route(&c[0]);assert(severed->Advance(0,&journal)==0);c[0].radio=0;assert(severed->Advance(2000,&journal)==-1);c[0].radio=2;
for(auto& carrier:c)carrier.alive=true;
auto lost=network.Route(&c[2]);assert(lost->Advance(0,&journal)==0);c[1].radio=0;assert(lost->Advance(2000,&journal)==-1);assert(lost->Advance(4000,&journal)==-1);
c[1].radio=1;auto moving=network.Route(&c[2]);moving->Advance(0,&journal);c[1].pos=V(499);assert(moving->Advance(2000,&journal)==-1);c[1].pos=V(500);
@@ -68,7 +73,7 @@ int main(){
network.Clear();assert(!network.Route(&c[0]));
Network crowded;std::vector<IEntity> many(2050);for(auto& carrier:many){carrier.radio=2;crowded.Add(&carrier,&group,1,2);}assert(crowded.Overflow&&crowded.m_Nodes.Count()==2048);
crowded.Build();assert(crowded.Overflow); // bounded dense graph work
std::cout<<"PASS: real-carrier shortest-hop paths, 500m boundary, three-hop cap, no-manpack failure, hop timing, emitted-message survival, radio loss, capture, motion, timeout, reset and network bounds\n";
std::cout<<"PASS: real-carrier shortest-hop paths, 500m boundary, three-hop cap, no-manpack failure, hop timing, completed-hop survival, interrupted final uplink, radio loss, capture, motion, timeout, reset and network bounds\n";
}
'''
with tempfile.TemporaryDirectory(prefix='eai-hq-network-') as tmp:
+33
View File
@@ -0,0 +1,33 @@
"""Exercise actual actor-group lookup with distinct inherited vehicle and controlled character agents."""
from policy_harness import ROOT, body, run
source=body(ROOT/'addons/core/Scripts/Game/ERR/ERR_Actors.c','static SCR_AIGroup Group(')
source=source.replace('static SCR_AIGroup Group(IEntity actor)', 'static SCR_AIGroup* Group(IEntity* actor)')
source=source.replace('AIControlComponent control', 'AIControlComponent* control')
source=source.replace('AIControlComponent.Cast(', 'AIControlComponent::Cast(').replace('SCR_AIGroup.Cast(', 'SCR_AIGroup::Cast(')
source=source.replace('actor.FindComponent(AIControlComponent)', 'actor->FindComponent()')
source=source.replace('control.', 'control->').replace('GetControlAIAgent().', 'GetControlAIAgent()->').replace('null', 'nullptr')
run(r'''
#include <cassert>
struct SCR_AIGroup {static SCR_AIGroup* Cast(SCR_AIGroup* g){return g;}};
struct Agent {SCR_AIGroup* parent=nullptr;SCR_AIGroup* GetParentGroup(){return parent;}};
struct AIControlComponent {
Agent* inherited=nullptr;Agent* controlled=nullptr;int inheritedReads=0;
static AIControlComponent* Cast(AIControlComponent* c){return c;}
Agent* GetAIAgent(){inheritedReads++;return inherited;}Agent* GetControlAIAgent(){return controlled;}
};
struct IEntity {AIControlComponent* control=nullptr;AIControlComponent* FindComponent(){return control;}};
struct Actors {
'''+source+r'''
};
int main(){
assert(!Actors::Group(nullptr));IEntity actor;assert(!Actors::Group(&actor));
AIControlComponent control;actor.control=&control;assert(!Actors::Group(&actor));
SCR_AIGroup foot,convoy,vehicle;Agent personal{&foot},delegated{&vehicle};
control.controlled=&personal;control.inherited=&personal;assert(Actors::Group(&actor)==&foot);
personal.parent=&convoy;control.inherited=&delegated;
assert(Actors::Group(&actor)==&convoy&&control.inheritedReads==0);
control.controlled=nullptr;assert(!Actors::Group(&actor)&&control.inheritedReads==0);
control.controlled=&personal;personal.parent=nullptr;assert(!Actors::Group(&actor));
}
''')
print('PASS: actual actor group lookup retains controlled character membership when seated/inherited vehicle agents differ; missing control or parent refuses')
+2 -1
View File
@@ -8,7 +8,8 @@ source = ROOT / 'addons/core/Scripts/Game/ERR/ERR_AdminMenu.c'
layout = ROOT / 'addons/core/UI/layouts/ERR/AdminMenu.layout'
keys = re.findall(r'"([A-Za-z]+)"', body(source, 'static array<string> Keys('))
settings = re.findall(r'"([A-Za-z]+)"', body(ROOT / 'addons/core/Scripts/Game/ERR/ERR_Settings.c', 'static array<string> Keys('))
assert len(keys) == 10 and set(keys) == set(settings)
assert len(keys) == 10 and set(settings) - set(keys) == {"stingerChanceOneIn"}
assert set(keys) <= set(settings) # New text-command setting does not alter the fixed native menu.
# Native layouts place child widget declarations inside an anonymous Children
# block. A missing brace can preserve most widget names yet prevent menu load.
def validate_layout_structure(text):
+1 -1
View File
@@ -62,7 +62,7 @@ int main(){
admin.Register(&replacement);assert(admin.m_Providers.size()==2&&admin.JournalSequence()==20);
HQAliases ha;IntelAliases ia;
assert(ha.Setting("hq","interval")=="reportIntervalSeconds"&&ha.Setting("hq","squads")=="supportSquadLimit"&&ha.Setting("hq","radius")=="responseRadiusMetres");
assert(ha.Setting("intel","carrying").empty()&&ha.Setting("hq","stinger").empty());
assert(ha.Setting("intel","carrying").empty()&&ha.Setting("hq","stinger")=="stingerChanceOneIn");
assert(ia.Setting("intel","chance")=="aiIntelChancePercent"&&ia.Setting("intel","carrying")=="aiIntelChancePercent");
assert(ia.Setting("hq","chance").empty()&&ia.Setting("intel","spawn").empty());
HQHelp hh;IntelHelp ih;lines.clear();assert(hh.Help("hq",lines)&&lines.size()<=4);lines.clear();assert(ih.Help("intel",lines)&&lines.size()<=4);
+13 -1
View File
@@ -128,7 +128,13 @@ class PackedChatArchiveTests(unittest.TestCase):
runtime=self.session/'input/addons';candidate=self.fixture.root/'chat-candidate';candidate.mkdir()
for project in archiver.ADDON_ORDER:
folder=runtime/project;folder.mkdir(exist_ok=True)
(folder/'addon.gproj').write_bytes((runner.ROOT/'addons'/project/'addon.gproj').read_bytes())
# This schema-2 archive represents the frozen vanilla-only closure. Current
# source projects may add external dependencies that this payload does not carry.
name,guid,dependencies=runner.PACKED_PROJECTS[project]
deps=[runner.PACKED_BASE]+[runner.PACKED_PROJECTS[key][1] for key in dependencies]
declaration=f'GameProject {{ ID "{name}" GUID "{guid}" TITLE "Synthetic {project}" Dependencies {{ '
declaration+=' '.join('"'+guid+'"' for guid in deps)+' } }\n'
(folder/'addon.gproj').write_text(declaration)
(folder/'data.pak').write_bytes(('fixture-pak-'+project).encode())
(folder/'resourceDatabase.rdb').write_bytes(('fixture-db-'+project).encode())
shutil.copytree(folder,candidate/project)
@@ -180,6 +186,12 @@ class PackedChatArchiveTests(unittest.TestCase):
path=self.packed['input']/'ai/addon.gproj';path.write_bytes((self.packed['input']/'core/addon.gproj').read_bytes())
data=path.read_bytes();self.meta['artifacts']['ai/addon.gproj']={'bytes':len(data),'sha256':archiver.digest(data)};self.update_metadata()
with self.assertRaises(ValueError):archiver.inspect(self.session)
def test_external_dependency_cannot_expand_frozen_packed_closure(self):
path=self.packed['input']/'ai/addon.gproj'
path.write_text(path.read_text().replace('Dependencies {','Dependencies { "595F2BF2F44836FB"'))
data=path.read_bytes();self.meta['artifacts']['ai/addon.gproj']={'bytes':len(data),'sha256':archiver.digest(data)}
self.update_metadata()
with self.assertRaisesRegex(ValueError,'identity or exact declared dependencies'):archiver.inspect(self.session)
def test_each_companion_loaded_mount_and_database_evidence_required(self):
for fragment in ("under name ERR_Ai", "name=ERR_Intel path=", "guid: '9AAEE9041D79809E'", 'header verified project=chat', 'local participant verified project=chat'):
self.reseal_console(self.content().replace(fragment,'missing'))
+5 -2
View File
@@ -48,7 +48,10 @@ struct Retained : Command {
void Tick(float) override {tactics++;events.push_back("tactics");}
void Stop() override {stops++;pending=false;events.push_back("stop");}
};
struct ForceBoundary {int clears=0;void Clear(){clears++;}};
struct Mode {
ForceBoundary m_Forces,m_ForceAgents;
int forceTicks=0;void TickForces(float){forceTicks++;}
bool m_Stopped=false,m_NativeEndHandled=false,m_StartedPlayers=false;int m_Phase=1;
float m_NoSurvivorsSince=-1;std::string m_EndReason;
Work extraction{0,"extraction",{}},radio{0,"radio",{}},recovery{0,"recovery",{}};
@@ -67,7 +70,7 @@ int main(){
// Retained work receives exactly one maintenance call on an empty server,
// including later ticks, without tactical decisions, observer updates or OnTick.
{reset();Mode m;Retained c;m.m_Command=&c;m.m_NoSurvivorsSince=100;
m.ServerTick();world.now=2234;m.ServerTick();assert(c.maintenance==2&&c.time==2234&&c.tactics==0&&c.pending);
m.ServerTick();world.now=2234;m.ServerTick();assert(c.maintenance==2&&c.time==2234&&c.tactics==0&&c.pending&&m.forceTicks==2);
assert(players.controller.updates==0&&m.m_OnTick.calls==0&&m.m_NoSurvivorsSince==-1);}
// Populated ticks have exactly one maintenance call before ordinary decisions.
{reset();players.count=1;Mode m;Retained c;m.m_Command=&c;m.ServerTick();assert(c.maintenance==1&&c.tactics==1);
@@ -93,7 +96,7 @@ int main(){
// Stop removes the existing callback before the command receives terminal Stop;
// no maintenance drain loop or new scheduling has been introduced in Core.
{reset();Mode m;Retained c;m.m_Command=&c;Entity group{true},other{false};m.m_OwnedEntities={&group,&other};
m.StopServices();m.StopServices();m.ServerTick();assert(c.stops==1&&c.maintenance==0&&!c.pending&&deleted==1);
m.StopServices();m.StopServices();m.ServerTick();assert(c.stops==1&&c.maintenance==0&&!c.pending&&deleted==1&&m.m_Forces.clears==1&&m.m_ForceAgents.clears==1&&m.forceTicks==0);
assert((queue.removed==std::vector<std::string>{"StartScenario","ServerTick","BindLocalObserver"}));
assert((events==std::vector<std::string>{"stop","onstop"}));}
}
+143
View File
@@ -0,0 +1,143 @@
"""Execute actual convoy readiness admission and finite cargo request policy."""
import os,re
from pathlib import Path
from policy_harness import ROOT,body,run
path=Path(os.environ.get('ERR_CONVOY_SERVICE',str(ROOT/'addons/ai/Scripts/Game/ERR/ERR_ConvoyService.c')))
s=body(path,'protected bool Admit(').replace('protected ','')
s=s.replace('SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent))','&unit->Entity->usage').replace('ERR_HQ.Cast(m_Mode.Command())','m_Mode->Command()')
s=s.replace('array<AIAgent>','Array<AIAgent*>')
for cls in ['ERR_ConvoyTask','ERR_ConvoyVehicle','AIAgent','SCR_AIGroupUtilityComponent','SCR_AIVehicleUsageComponent','ERR_ForceRecord','ERR_HQ']:
s=re.sub(r'\b'+cls+r' ([A-Za-z_]\w*)',cls+r'* \1',s)
for obj in ['task','unit','agent','utility','m_Mode','m_Pool','record','hq','active']:
s=re.sub(r'\b'+obj+r'\.',obj+'->',s)
s=s.replace('unit->Index.ToString()','std::to_string(unit->Index)').replace('Group.','Group->').replace('Entity.','Entity->').replace('Receipt.','Receipt->').replace('m_VehicleMgr.','m_VehicleMgr->').replace('Forces().','Forces()->')
s=s.replace('null','nullptr')
s=s.replace('ERR_Actors.','ERR_Actors::').replace('ERR_ForceKind.','ERR_ForceKind::').replace('foreach (','for (')
run(r'''
#include <cassert>
#include <vector>
#include <string>
#include <algorithm>
using string=std::string;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}int Count(){return this->size();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
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 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;}};
struct ERR_ConvoyReceipt{string SourceId="allocation";};
struct ERR_ConvoyTask{ERR_ConvoyReceipt* Receipt;Array<ERR_ConvoyVehicle*>Vehicles;bool stopped=false;void Stop(void*,string){stopped=true;}};
enum class ERR_ForceKind{CONVOY};
struct ERR_ForceRecord{bool Pinned=false;};
struct Ledger{int calls=0;bool fail=false;Array<Group*>registered;ERR_ForceRecord record;ERR_ForceRecord* Register(string,ERR_ForceKind,int,int,void*,Group* group,float){calls++;if(fail)return nullptr;registered.Insert(group);return &record;}};
struct ERR_HQ{int reservations=0;void ReserveGroup(Group*){reservations++;}};
struct Mode{Ledger ledger;ERR_HQ hq;int published=0;Ledger* Forces(){return &ledger;}ERR_HQ* Command(){return &hq;}void RegisterGroup(Group*group){assert(ledger.registered.Contains(group));published++;}};
struct Pool{int binds=0;bool fail=false;bool BindRecord(ERR_ConvoyReceipt*,int,ERR_ForceRecord*){binds++;return !fail;}};
namespace ERR_Actors{bool CanFight(IEntity*a){return a&&a->alive;}}
struct Service{Mode* m_Mode;Pool* m_Pool;int inspections=0;void InspectReadiness(ERR_ConvoyVehicle*,float){inspections++;}
''' + s + r'''
};
int main(){
Mode mode;Pool pool;Service service{&mode,&pool};ERR_ConvoyReceipt receipt;ERR_ConvoyTask task;task.Receipt=&receipt;
ERR_ConvoyVehicle units[6];Group groups[6];Vehicle vehicles[6];IEntity bodies[6];AIAgent agents[6];
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);}
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);
}
''')
cargo=body(path,'protected void IssueCargo(').replace('protected ','')
cargo=cargo.replace('InventoryStorageManagerComponent.Cast(task.Vehicles[1].Entity.FindComponent(InventoryStorageManagerComponent))','task->Vehicles[1]->Entity->inventory')
for cls in ['ERR_ConvoyTask','InventoryStorageManagerComponent','ERR_ConvoyCargoCallback']:
cargo=re.sub(r'\b'+cls+r' ([A-Za-z_]\w*)',cls+r'* \1',cargo)
for obj in ['task','inventory','operation']:
cargo=re.sub(r'\b'+obj+r'\.',obj+'->',cargo)
for cls in ['ERR_HQ','ERR_ConvoyResources','EStoragePurpose']:cargo=cargo.replace(cls+'.',cls+'::')
cargo=cargo.replace('null','nullptr')
run(r'''
#include <cassert>
#include <vector>
#include <string>
using ResourceName=int;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}int Count(){return this->size();}};
struct ERR_ConvoyCargoCallback{ResourceName Prefab=0;bool Finished=false;};
struct InventoryStorageManagerComponent{int requests=0,rifles=0,magazines=0;bool accept=true;bool TrySpawnPrefabToStorage(int prefab,void*,int,int,ERR_ConvoyCargoCallback*){requests++;if(prefab==1)rifles++;else magazines++;return accept;}};
struct Entity{InventoryStorageManagerComponent* inventory;};
struct Vehicle{struct Entity* Entity;};
struct ERR_ConvoyTask{bool CargoIssued=false,Stopped=false;Array<Vehicle*>Vehicles;Array<ERR_ConvoyCargoCallback*>Cargo;};
namespace ERR_HQ{void Event(std::string,std::string){}}
namespace ERR_ConvoyResources{int Rifle(){return 1;}int Magazine(){return 2;}}
namespace EStoragePurpose{int PURPOSE_DEPOSIT=1;}
struct Service{
''' + cargo + r'''
};
int main(){
InventoryStorageManagerComponent inventory;Entity entity{&inventory};Vehicle truck{&entity};ERR_ConvoyTask task;for(int i=0;i<6;i++){task.Vehicles.Insert(&truck);}Service service;
service.IssueCargo(&task);assert(task.CargoIssued&&inventory.requests==56&&inventory.rifles==8&&inventory.magazines==48&&task.Cargo.Count()==56);service.IssueCargo(&task);assert(inventory.requests==56);
inventory.accept=false;ERR_ConvoyTask reject;reject.Vehicles=task.Vehicles;service.IssueCargo(&reject);assert(inventory.requests==112);service.IssueCargo(&reject);assert(inventory.requests==112);for(auto*op:reject.Cargo){assert(op->Finished);}
ERR_ConvoyTask absent;absent.Vehicles=task.Vehicles;entity.inventory=nullptr;service.IssueCargo(&absent);assert(absent.CargoIssued&&inventory.requests==112);
ERR_ConvoyTask stopped;stopped.Vehicles=task.Vehicles;stopped.Stopped=true;service.IssueCargo(&stopped);assert(!stopped.CargoIssued&&stopped.Cargo.Count()==0);
}
''')
print('PASS: actual native readiness exact membership, one-time per-vehicle ledger admission/reservation, partial readiness retry without duplicate records and one-pass56-item cargo allocation without replenishment')
# Diagnostics themselves execute against API doubles: health/membership failures,
# native capacity visibility and all-six reporting stay read-only and throttled.
methods='\n'.join(body(path,s) for s in ('protected string ReadinessReason(', 'protected void InspectReadiness('))
methods=methods.replace('protected ','').replace('SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent))','unit->Entity->usage')
methods=methods.replace('array<AIAgent>','Array<AIAgent*>').replace('foreach (','for (').replace('string.Format','Format')
for cls in ['ERR_ConvoyVehicle','IEntity','AIAgent','SCR_AIGroupUtilityComponent','SCR_AIVehicleUsageComponent']:
methods=re.sub(r'\b'+cls+r' ([A-Za-z_]\w*)',cls+r'* \1',methods)
for obj in ['unit','agent','utility','m_Mode']:
methods=re.sub(r'\b'+obj+r'\.',obj+'->',methods)
methods=methods.replace('Group.','Group->').replace('Entity.','Entity->').replace('unit->Seats[i].','unit->Seats[i]->')
for cls in ['ERR_Actors','ERR_ConvoyRules','CompartmentAccessComponent','ERR_HQ']:methods=methods.replace(cls+'.',cls+'::')
methods=methods.replace('int agents, activeAgents;', 'int agents=0, activeAgents=0;').replace('bool activeGroup;', 'bool activeGroup=false;')
run(r'''
#include <cassert>
#include <vector>
#include <string>
#include <algorithm>
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 Vehicle;
struct IEntity{bool alive=true,player=false,awake=true;struct Group* group=nullptr;struct Vehicle* vehicle=nullptr;};
struct AIAgent{IEntity* entity=nullptr;bool active=false;IEntity* GetControlledEntity(){return entity;}bool IsAIActivated(){return active;}};
struct SCR_AIVehicleUsageComponent{};struct Manager{};
struct SCR_AIGroupUtilityComponent{Manager* m_VehicleMgr=nullptr;};
struct Group{int faction=1;bool active=false;Array<AIAgent*>agents;SCR_AIGroupUtilityComponent* utility=nullptr;int GetFaction(){return faction;}bool IsAIActivated(){return active;}void GetAgents(Array<AIAgent*>&a){a=agents;}SCR_AIGroupUtilityComponent* GetGroupUtilityComponent(){return utility;}};
struct Vehicle:IEntity{SCR_AIVehicleUsageComponent* usage=nullptr;};
struct Seat{IEntity* occupant=nullptr;IEntity* GetOccupant(){return occupant;}};
struct ERR_ConvoyVehicle{struct Group* Group=nullptr;struct Vehicle* Entity=nullptr;IEntity* Driver=nullptr;Array<IEntity*>Members;Array<Seat*>Seats;bool Registered=false;int Index=0;float NextReadinessNote=0;};
struct Mode{int EnemyFaction(){return 1;}};
namespace ERR_Actors{bool Alive(IEntity*e){return e&&e->alive;}bool CanFight(IEntity*e){return Alive(e)&&e->awake;}bool Player(IEntity*e){return e&&e->player;}struct Group* Group(IEntity*e){return e?e->group:nullptr;}}
namespace CompartmentAccessComponent{IEntity* GetVehicleIn(IEntity*e){return e?e->vehicle:nullptr;}}
namespace ERR_ConvoyRules{bool PartOf(IEntity*e,IEntity*root){return e==root;}}
int loggedMembers=0,loggedAgents=0,loggedActive=0;bool loggedGroup=false;
string Format(string,int,string reason,int members,int agents,int active,bool group,int,bool){loggedMembers=members;loggedAgents=agents;loggedActive=active;loggedGroup=group;return reason;}
namespace ERR_HQ{int calls=0;string last;void Event(string,string reason){calls++;last=reason;}}
struct Service{Mode* m_Mode;'''+methods+r'''};
int main(){
Mode mode;Service service{&mode};ERR_ConvoyVehicle unit;Group group;Vehicle vehicle;IEntity actor;AIAgent agent;Seat seat;Manager manager;SCR_AIGroupUtilityComponent utility{&manager};SCR_AIVehicleUsageComponent usage;
unit.Group=&group;unit.Entity=&vehicle;unit.Driver=&actor;unit.Members={&actor};unit.Seats={&seat};actor.group=&group;actor.vehicle=&vehicle;agent.entity=&actor;group.agents={&agent};group.utility=&utility;vehicle.usage=&usage;seat.occupant=&actor;
assert(service.ReadinessReason(&unit)=="readiness guards satisfied; awaiting admission");
actor.group=nullptr;assert(service.ReadinessReason(&unit)=="member native agent/parent group mismatch");actor.group=&group;
actor.awake=false;assert(service.ReadinessReason(&unit)=="driver absent/dead/unconscious or controller unavailable");actor.awake=true;
actor.vehicle=nullptr;assert(service.ReadinessReason(&unit)=="driver outside intended vehicle");actor.vehicle=&vehicle;
group.agents.clear();assert(service.ReadinessReason(&unit)=="native agent count differs from original crew");group.agents={nullptr};assert(service.ReadinessReason(&unit)=="null native agent");group.agents={&agent};
group.utility=nullptr;assert(service.ReadinessReason(&unit)=="group utility unavailable");group.utility=&utility;
utility.m_VehicleMgr=nullptr;assert(service.ReadinessReason(&unit)=="group vehicle manager unavailable");utility.m_VehicleMgr=&manager;
vehicle.usage=nullptr;assert(service.ReadinessReason(&unit)=="vehicle usage component unavailable");vehicle.usage=&usage;
seat.occupant=nullptr;assert(service.ReadinessReason(&unit)=="intended seat occupant mismatch");seat.occupant=&actor;
service.InspectReadiness(&unit,100);assert(ERR_HQ::calls==1&&unit.NextReadinessNote==5100&&loggedMembers==1&&loggedAgents==1&&loggedActive==0&&!loggedGroup);
service.InspectReadiness(&unit,1000);assert(ERR_HQ::calls==1);
agent.active=true;group.active=true;service.InspectReadiness(&unit,5100);assert(ERR_HQ::calls==2&&loggedActive==1&&loggedGroup);
unit.Registered=true;service.InspectReadiness(&unit,10100);assert(ERR_HQ::calls==3&&ERR_HQ::last=="already admitted");
assert(unit.Members.size()==1&&unit.Group==&group&&unit.Entity==&vehicle&&group.agents.size()==1);
}
''')
print('PASS: actual per-vehicle readiness reasons, native agent/activity counts, intended-seat diagnostics, five-second throttle and read-only observation')
+50
View File
@@ -0,0 +1,50 @@
"""Run actual canonical convoy receipt and shared ledger-cap methods."""
import os,re
from pathlib import Path
from policy_harness import ROOT,run
p=Path(os.environ.get('ERR_CONVOY_BUDGET',str(ROOT/'addons/ai/Scripts/Game/ERR/ERR_ConvoyBudget.c')))
s=p.read_text();s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('ref ','').replace('class ','struct ')
s=s.replace('array<int>','Array<int>').replace('array<ERR_ConvoyReceipt>','Array<ERR_ConvoyReceipt*>')
for typ in ['ERR_GameMode','ERR_ForceLedger','ERR_ForceRecord','ERR_ConvoyReceipt']:
s=re.sub(r'\b'+typ+r' ([A-Za-z_]\w*)',typ+r'* \1',s)
for obj in ['m_Mode','ledger','record','receipt','known']:s=s.replace(obj+'.',obj+'->')
for cls in ['Math','Replication','ERR_GameMode','ERR_ForceKind']:s=s.replace(cls+'.',cls+'::')
s=s.replace('Forces().','Forces()->').replace('foreach (','for (').replace('null','nullptr').replace('void ERR_ConvoyPool(','ERR_ConvoyPool(').replace('vehicle.ToString()','std::to_string(vehicle)').replace('\n}\n','\n};\n')
s=re.sub(r'\b(bool|int|[A-Za-z_]\w*\*) ([A-Za-z_]\w*);',r'\1 \2 {};',s)
run(r'''
#include <cassert>
#include <vector>
#include <string>
#include <algorithm>
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}int Length(){return size();}};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}void Insert(T v){this->push_back(v);}int Count(){return this->size();}};
namespace Math{int Clamp(int a,int low,int high){return std::clamp(a,low,high);}}
namespace Replication{bool server=true;bool IsServer(){return server;}}
enum class ERR_ForceKind{GARRISON,CONVOY};
struct ERR_ForceRecord{int Id=0,InitialStrength=0,Strength=0;bool Physical=true,Eliminated=false;void* Group=this;ERR_ForceKind Kind=ERR_ForceKind::CONVOY;string SourceId;};
struct ERR_ForceLedger{Array<ERR_ForceRecord*>records;int Count(){return records.Count();}ERR_ForceRecord* At(int i){return records[i];}ERR_ForceRecord* Find(int id){for(auto* r:records){if(r->Id==id)return r;}return nullptr;}};
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;ERR_ForceLedger ledger;bool active=true;static ERR_GameMode* Get(){return current;}bool Active(){return active;}ERR_ForceLedger* Forces(){return &ledger;}};
''' + s + r'''
int main(){
ERR_GameMode mode;ERR_GameMode::current=&mode;ERR_ForceRecord guard;guard.Id=1;guard.Strength=41;mode.ledger.records.Insert(&guard);
ERR_ConvoyPool pool(&mode,3);auto receipt=pool.Reserve("road-a");assert(receipt&&pool.CommittedBodies()==60);assert(pool.Reserve("road-a")==receipt);assert(!pool.Reserve("road-b"));
ERR_ConvoyReceipt forged;assert(!pool.Consume(&forged));assert(pool.Consume(receipt)&&!pool.Consume(receipt));
for(int i=0;i<6;i++){
auto* r=new ERR_ForceRecord();r->Id=i+2;r->InitialStrength=ERR_ConvoyPool::CrewCount(i);r->Strength=r->InitialStrength;r->SourceId="road-a-vehicle-"+std::to_string(i);mode.ledger.records.Insert(r);
assert(pool.BindRecord(receipt,i,r));assert(pool.BindRecord(receipt,i,r));assert(pool.CommittedBodies()==60);
}
assert(receipt->OutstandingBodies==0&&receipt->OutstandingGroups==0);assert(!pool.Consume(receipt));
// Dead/captured/released forces never refund a consumed receipt's convoy slot.
guard.Strength=0;guard.Eliminated=true;auto second=pool.Reserve("road-b");assert(second);assert(pool.Consume(second));assert(!pool.Consume(receipt));
ERR_GameMode other;ERR_GameMode::current=&other;assert(!pool.Reserve("wrong-world")&&!pool.Consume(second));ERR_GameMode::current=&mode;
ERR_GameMode crowded;ERR_GameMode::current=&crowded;ERR_ForceRecord many;many.Strength=42;crowded.ledger.records.Insert(&many);ERR_ConvoyPool limited(&crowded,8);assert(!limited.Reserve("too-many"));
many.Strength=41;auto reserved=limited.Reserve("exact");assert(reserved);many.Strength=42;assert(!limited.Consume(reserved));many.Strength=41;assert(limited.Consume(reserved));
ERR_ForceRecord invalid;invalid.Id=2;invalid.InitialStrength=2;invalid.Strength=2;invalid.SourceId="exact-vehicle-0";
assert(!limited.BindRecord(reserved,0,&invalid));crowded.ledger.records.Insert(&invalid);invalid.Kind=ERR_ForceKind::GARRISON;assert(!limited.BindRecord(reserved,0,&invalid));invalid.Kind=ERR_ForceKind::CONVOY;
invalid.Strength=1;assert(!limited.BindRecord(reserved,0,&invalid));invalid.Strength=2;invalid.Physical=false;assert(!limited.BindRecord(reserved,0,&invalid));invalid.Physical=true;assert(limited.BindRecord(reserved,0,&invalid));
Replication::server=false;assert(!limited.Reserve("client"));Replication::server=true;
ERR_GameMode formations;ERR_GameMode::current=&formations;for(int i=0;i<123;i++){auto* r=new ERR_ForceRecord();r->Strength=0;r->Physical=false;formations.ledger.records.Insert(r);}ERR_ConvoyPool capped(&formations,8);assert(!capped.Reserve("formation-cap"));
ERR_GameMode finite;ERR_GameMode::current=&finite;ERR_ConvoyPool one(&finite,1);assert(one.Reserve("first"));assert(!one.Reserve("second"));
}
''')
print('PASS: actual convoy receipt identity/one-use, shared60-body and128-formation caps, outstanding/native-ledger accounting, exact19-body composition, source reuse, mode/client guards and no consumed receipt refunds')
+127
View File
@@ -0,0 +1,127 @@
"""Actual convoy dispatch transaction and exact native seat-allocation policy."""
import os,re
from pathlib import Path
from policy_harness import ROOT,body,run
path=Path(os.environ.get('ERR_CONVOY_SERVICE',str(ROOT/'addons/ai/Scripts/Game/ERR/ERR_ConvoyService.c')))
source=path.read_text()
# Dispatch's native calls are doubled; budget behavior is executed separately.
code=body(path,'ERR_ConvoyTask Dispatch(')
code=re.sub(r'//[^\n]*','',code)
code=code.replace('array<vector>','Array<vector>&')
code=code.replace('SCR_AIGroup.Cast(', 'static_cast<SCR_AIGroup*>(').replace('Vehicle.Cast(', 'static_cast<Vehicle*>(')
code=code.replace('FactionAffiliationComponent.Cast(unit.Entity.FindComponent(FactionAffiliationComponent))','&unit->Entity->affiliation').replace('AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent))','&unit->Entity->movement')
for cls in ['ERR_ConvoyTask','ERR_ConvoyReceipt','ERR_ForceRoute','ERR_ConvoyVehicle','FactionAffiliationComponent','AICarMovementComponent']:
code=re.sub(r'\b'+cls+r' ([A-Za-z_]\w*)',cls+r'* \1',code)
for obj in ['m_Mode','m_Pool','task','unit','affiliation','movement']:
code=re.sub(r'\b'+obj+r'\.',obj+'->',code)
for cls in ['ERR_ForceRules','ERR_ConvoyRules','ERR_ConvoyResources','ERR_Actors','ERR_HQ']:
code=code.replace(cls+'.',cls+'::')
code=code.replace('EnemyFaction().','EnemyFaction()->').replace('Group.Get','Group->Get').replace('null','nullptr')
run(r'''
#include <cassert>
#include <vector>
#include <string>
using string=std::string;struct vector{};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}int Count(){return this->size();}};
struct Faction{string key="USSR";string GetFactionKey(){return key;}};Faction enemy;
struct IEntity{virtual ~IEntity()=default;};
struct FactionAffiliationComponent{void SetAffiliatedFaction(Faction*){}};
struct AICarMovementComponent{int speed=0;void SetCruiseSpeed(int v){speed=v;}};
struct SCR_AIGroup:IEntity{Faction* GetFaction(){return &enemy;}int GetAgentsCount(){return 0;}};
struct Vehicle:IEntity{FactionAffiliationComponent affiliation;AICarMovementComponent movement;};
struct ERR_GameMode{Array<IEntity*>owned;Faction* EnemyFaction(){return &enemy;}void Own(IEntity* e){owned.Insert(e);}};
struct ERR_ForceRoute{};
struct ERR_ConvoyReceipt{bool used=false;};
struct ERR_ConvoyPool{int consumes=0;bool Consume(ERR_ConvoyReceipt* r){if(!r||r->used)return false;r->used=true;consumes++;return true;}};
struct ERR_ConvoyVehicle{int Index=0;Vehicle* Entity=nullptr;SCR_AIGroup* Group=nullptr;};
struct ERR_ConvoyTask{ERR_ConvoyReceipt* Receipt=nullptr;ERR_ForceRoute* Route=nullptr;float Deadline=0;Array<ERR_ConvoyVehicle*>Vehicles;bool stopped=false;void Stop(ERR_GameMode*,string){stopped=true;}};
namespace ERR_ForceRules{bool Finite(float f){return f==f;}}
namespace ERR_ConvoyRules{bool valid=true,clear=true,empty=true;bool EmptyCrewTemplate(SCR_AIGroup*){return empty;}bool GroundClear(Array<vector>&,Array<vector>&){return clear;}bool Plan(Array<vector>&,Array<vector>&,ERR_ForceRoute*){return valid;}}
namespace ERR_ConvoyResources{bool available=true;bool Available(){return available;}string GroupPrefab(){return "group";}string VehiclePrefab(int i){return "vehicle"+std::to_string(i);}}
namespace ERR_HQ{void Event(string,string){}}
namespace ERR_Actors{int calls=0,fail=0;IEntity* Spawn(string prefab,vector,vector=vector{}){calls++;if(calls==fail)return nullptr;if(prefab=="group")return new SCR_AIGroup();return new Vehicle();}}
struct Service{ERR_GameMode* m_Mode;ERR_ConvoyPool* m_Pool;Array<ERR_ConvoyTask*>m_Tasks;bool current=true;int crews=0,failCrew=-1;
bool Current(){return current;}bool FillCrew(ERR_ConvoyVehicle* unit){crews++;return unit->Index!=failCrew;}
''' + code + r'''
};
int main(){
ERR_GameMode mode;ERR_ConvoyPool pool;Service service{&mode,&pool};ERR_ConvoyReceipt receipt;ERR_ForceRoute route;Array<vector>positions(6),angles(6);
ERR_ConvoyRules::valid=false;assert(!service.Dispatch(&receipt,positions,angles,&route,100));assert(!receipt.used&&ERR_Actors::calls==0);ERR_ConvoyRules::valid=true;
ERR_ConvoyRules::clear=false;assert(!service.Dispatch(&receipt,positions,angles,&route,100));assert(!receipt.used&&ERR_Actors::calls==0);ERR_ConvoyRules::clear=true;
ERR_ConvoyResources::available=false;assert(!service.Dispatch(&receipt,positions,angles,&route,100));assert(!receipt.used&&ERR_Actors::calls==0);ERR_ConvoyResources::available=true;
enemy.key="US";assert(!service.Dispatch(&receipt,positions,angles,&route,100));assert(!receipt.used&&ERR_Actors::calls==0);enemy.key="USSR";
auto task=service.Dispatch(&receipt,positions,angles,&route,100);assert(task&&task->Vehicles.Count()==6&&!task->stopped&&receipt.used&&pool.consumes==1&&ERR_Actors::calls==12&&mode.owned.Count()==12&&service.crews==6);
assert(!service.Dispatch(&receipt,positions,angles,&route,100));assert(ERR_Actors::calls==12);
for(int failed=1;failed<=12;failed++){
ERR_GameMode partial;ERR_ConvoyPool budget;Service run{&partial,&budget};ERR_ConvoyReceipt ticket;ERR_Actors::calls=0;ERR_Actors::fail=failed;
auto result=run.Dispatch(&ticket,positions,angles,&route,0);assert(result&&result->stopped&&ticket.used&&budget.consumes==1&&run.m_Tasks.Count()==1);int attempted=ERR_Actors::calls;
assert(!run.Dispatch(&ticket,positions,angles,&route,1)&&ERR_Actors::calls==attempted);assert(partial.owned.Count()==attempted-1);
}
ERR_Actors::fail=0;ERR_ConvoyRules::empty=false;ERR_GameMode unexpected;ERR_ConvoyPool rejectBudget;Service wrongTemplate{&unexpected,&rejectBudget};ERR_ConvoyReceipt rejectTicket;
auto rejected=wrongTemplate.Dispatch(&rejectTicket,positions,angles,&route,100);assert(rejected&&rejected->stopped&&rejectTicket.used&&wrongTemplate.crews==0);ERR_ConvoyRules::empty=true;
ERR_Actors::fail=0;ERR_GameMode seats;ERR_ConvoyPool budget;Service noCrew{&seats,&budget};noCrew.failCrew=2;ERR_ConvoyReceipt ticket;
auto partial=noCrew.Dispatch(&ticket,positions,angles,&route,100);assert(partial&&partial->stopped&&partial->Vehicles.Count()==3&&ticket.used&&seats.owned.Count()==6);
}
''')
# Execute the seat-selection and creation body. Failed capacity cannot shrink
# the plan, and every native successful character is immediately owned.
fill=body(path,'protected bool FillCrew(').replace('protected ','')
fill=re.sub(r'//[^\n]*','',fill).replace('array<BaseCompartmentSlot>','Array<BaseCompartmentSlot*>')
for cls in ['ERR_ConvoyVehicle','BaseCompartmentSlot','AIGroup','IEntity']:
fill=re.sub(r'\b'+cls+r' ([A-Za-z_]\w*)',cls+r'* \1',fill)
for obj in ['unit','slot','seat','m_Mode']:
fill=re.sub(r'\b'+obj+r'\.',obj+'->',fill)
for cls in ['ERR_ConvoyRules','ECompartmentType','ERR_ConvoyPool','ERR_ConvoyResources']:
fill=fill.replace(cls+'.',cls+'::')
fill=fill.replace('foreach (','for (')
run(r'''
#include <cassert>
#include <vector>
using ResourceName=int;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}int Count(){return this->size();}};
enum class ECompartmentType{PILOT,TURRET,CARGO};struct IEntity{};struct AIGroup{};
struct BaseCompartmentSlot{ECompartmentType type;bool occupied=false,accessible=true,fail=false;int spawns=0;ECompartmentType GetType(){return type;}bool IsCompartmentAccessible(){return accessible;}bool IsOccupied(){return occupied;}IEntity* SpawnCharacterInCompartment(ResourceName,AIGroup*&,ResourceName){spawns++;return fail?nullptr:new IEntity();}};
struct Vehicle{Array<BaseCompartmentSlot*>seats;};
struct ERR_ConvoyVehicle{int Index=0;Vehicle* Entity;AIGroup* Group;Array<BaseCompartmentSlot*>Seats;Array<IEntity*>Members;IEntity* Driver=nullptr;};
namespace ERR_ConvoyRules{bool CollectSeats(Vehicle* v,Array<BaseCompartmentSlot*>& out){out=v->seats;return true;}}
namespace ERR_ConvoyPool{int CrewCount(int i){if(i==1)return 1;if(i==2)return 9;if(i==4)return 3;return 2;}}
namespace ERR_ConvoyResources{int Soldier(){return 1;}int GroupPrefab(){return 2;}}
struct Mode{int owned=0;void Own(IEntity*){owned++;}};
struct Service{Mode* m_Mode;
''' + fill + r'''
};
int main(){
for(int i=0;i<6;i++){
Vehicle vehicle;AIGroup group;Mode mode;Service service{&mode};ERR_ConvoyVehicle unit;unit.Index=i;unit.Entity=&vehicle;unit.Group=&group;
BaseCompartmentSlot driver{ECompartmentType::PILOT},gunner{ECompartmentType::TURRET},commander{ECompartmentType::TURRET};vehicle.seats={&driver,&gunner,&commander};
BaseCompartmentSlot seats[8];for(auto& seat:seats){seat.type=ECompartmentType::CARGO;vehicle.seats.Insert(&seat);}
assert(service.FillCrew(&unit));assert(unit.Members.Count()==ERR_ConvoyPool::CrewCount(i)&&mode.owned==unit.Members.Count()&&unit.Driver==unit.Members[0]);
}
Vehicle vehicle;AIGroup group;Mode mode;Service service{&mode};ERR_ConvoyVehicle unit;unit.Index=2;unit.Entity=&vehicle;unit.Group=&group;
BaseCompartmentSlot driver{ECompartmentType::PILOT};vehicle.seats={&driver};assert(!service.FillCrew(&unit)&&driver.spawns==0&&mode.owned==0);
BaseCompartmentSlot seats[8];for(auto& seat:seats){seat.type=ECompartmentType::CARGO;vehicle.seats.Insert(&seat);}seats[3].fail=true;
assert(!service.FillCrew(&unit)&&unit.Members.Count()==4&&mode.owned==4);assert(seats[4].spawns==0);
}
''')
print('PASS: actual convoy dispatch validates before receipt consumption, consumes once before native mutation, retains all12 partial-failure paths, refuses implicit faction replacement, and allocates exact19 bodies without capacity shrinking')
# The resource must suppress authored allocation before native EOnInit, not try
# to purge a queued default squad after custom seat occupants have been added.
prefab=ROOT/'addons/ai/Prefabs/ERR/ConvoyCrew.et'
text=prefab.read_text()
assert '"{000CD338713F2B5A}Prefabs/AI/Groups/Group_Base.et"' in text
assert 'm_bSpawnImmediately 0' in text and 'm_faction "USSR"' in text
assert re.search(r'm_aUnitPrefabSlots\s*{\s*}',text)
assert 'Group_USSR_Base.et' not in text
identity=re.search(r'Name "([^"\n]+)"',prefab.with_name(prefab.name+'.meta').read_text())[1]
assert identity in body(path,'static ResourceName GroupPrefab(')
empty=body(path,'static bool EmptyCrewTemplate(')
empty=empty.replace('SCR_AIGroup group','SCR_AIGroup* group').replace('group.','group->').replace('m_aUnitPrefabSlots.IsEmpty()', 'm_aUnitPrefabSlots->IsEmpty()')
run(r'''
#include <cassert>
struct Slots{bool empty=true;bool IsEmpty(){return empty;}};
struct SCR_AIGroup{bool immediate=false;int count=0;Slots* m_aUnitPrefabSlots=nullptr;bool GetSpawnImmediately(){return immediate;}int GetAgentsCount(){return count;}};
'''+empty+r'''
int main(){SCR_AIGroup group;Slots slots;assert(EmptyCrewTemplate(&group));group.m_aUnitPrefabSlots=&slots;assert(EmptyCrewTemplate(&group));slots.empty=false;assert(!EmptyCrewTemplate(&group));slots.empty=true;group.immediate=true;assert(!EmptyCrewTemplate(&group));group.immediate=false;group.count=1;assert(!EmptyCrewTemplate(&group));assert(!EmptyCrewTemplate(nullptr));}
''')
print('PASS: own memberless prefab disables automatic spawn before initialization; actual admission rejects populated, immediate or authored-slot templates before finite crew creation')
+77
View File
@@ -0,0 +1,77 @@
"""Actual native convoy ownership/waypoint policy with small engine API doubles."""
import os,re
from pathlib import Path
from policy_harness import ROOT,run
path=Path(os.environ.get('ERR_CONVOY_SERVICE',str(ROOT/'addons/ai/Scripts/Game/ERR/ERR_ConvoyService.c')))
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']:
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']:
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+'::')
code=code.replace('AIWaypoint.Cast(', 'static_cast<AIWaypoint*>(').replace('Forces().','Forces()->').replace('Command().','Command()->').replace('foreach (','for (').replace('null','nullptr').replace('\n}\n','\n};\n')
code=re.sub(r'\b(bool|int|float|[A-Za-z_]\w*\*) ([A-Za-z_]\w*);',r'\1 \2 {};',code)
run(r'''
#include <cassert>
#include <vector>
#include <algorithm>
#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 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);};
struct SCR_AIGroup:IEntity{bool within=false;AIWaypoint* current=nullptr;int lod=0,faction=1,removes=0,adds=0;Array<AIAgent*> agents;int GetFaction(){return faction;}void SetPermanentLOD(int value){lod=value;}void GetAgents(Array<AIAgent*>& out){out=agents;}void RemoveWaypoint(AIWaypoint*){removes++;}void AddWaypoint(AIWaypoint*){adds++;}};
bool AIWaypoint::IsWithinCompletionRadius(SCR_AIGroup* group){return group->within;}
struct Vehicle:IEntity{vector position;vector GetOrigin(){return position;}};
struct BaseCompartmentSlot{};
struct ERR_ForceRecord{bool Pinned=true;};
struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(SCR_AIGroup*){return &record;}};
struct Command{int unregistrations=0;void UnregisterGroup(SCR_AIGroup*){unregistrations++;}};
struct ERR_GameMode{struct Ledger ledger;struct Command command;int EnemyFaction(){return 1;}struct Ledger* Forces(){return &ledger;}struct Command* Command(){return &command;}};
struct ERR_ForceRoute{Array<vector> points;int Count(){return points.size();}vector At(int i){assert(i>=0&&i<Count());return points[i];}};
namespace ERR_Actors{int spawns=0;bool fail=false;bool Alive(IEntity* a){return a&&a->alive;}bool CanFight(IEntity* a){return Alive(a)&&a->awake;}bool Player(IEntity* a){return a&&a->player;}SCR_AIGroup* Group(IEntity* a){return a?a->group:nullptr;}IEntity* Spawn(int,vector){spawns++;return fail?nullptr:new AIWaypoint();}}
namespace ERR_Resources{int MoveWaypoint(){return 1;}}
namespace CompartmentAccessComponent{IEntity* GetVehicleIn(IEntity* a){return a?a->vehicle:nullptr;}}
namespace ERR_ConvoyRules{bool PartOf(IEntity* e,IEntity* root){int depth=0;while(e&&depth++<32){if(e==root)return true;e=e->GetParent();}return false;}}
namespace SCR_EntityHelper{int deletes=0;void DeleteEntityAndChildren(AIWaypoint* p){deletes++;delete p;}}
''' + code + r'''
struct Case{
ERR_GameMode mode;Vehicle vehicle;SCR_AIGroup group;IEntity driver;AIAgent agent;ERR_ConvoyVehicle unit;ERR_ForceRoute route;
Case(){unit.Entity=&vehicle;unit.Group=&group;unit.Driver=&driver;unit.Members.Insert(&driver);driver.vehicle=&vehicle;driver.group=&group;group.agents.Insert(&agent);route.points={{100,0,0},{300,0,0}};}
};
int main(){
Case travel;travel.unit.Tick(&travel.mode,&travel.route,1000);assert(travel.group.adds==1&&travel.unit.Waypoint&&travel.unit.LegDeadline==601000);
travel.unit.Tick(&travel.mode,&travel.route,2000);assert(travel.group.adds==1&&travel.unit.LegDeadline==601000);
travel.vehicle.position={100,0,0};travel.group.within=true;travel.unit.Tick(&travel.mode,&travel.route,3000);assert(travel.unit.RouteIndex==1&&travel.group.adds==2&&travel.group.removes==1);
travel.vehicle.position={300,0,0};travel.unit.Tick(&travel.mode,&travel.route,4000);assert(travel.unit.Arrived&&!travel.unit.Waypoint&&travel.group.removes==2);
travel.unit.Tick(&travel.mode,&travel.route,5000);assert(travel.group.adds==2);
// Native completion may use seated actors ahead of the vehicle origin; native
// current WP can already be removed while the controller still owns its handle.
Case offset;offset.unit.Tick(&offset.mode,&offset.route,1000);offset.vehicle.position={64.5,0,0};offset.group.within=true;offset.group.current=nullptr;
assert(vector::DistanceXZ(offset.vehicle.position,offset.route.At(0))>35);
offset.unit.Tick(&offset.mode,&offset.route,2000);assert(offset.unit.RouteIndex==1&&offset.group.adds==2&&offset.group.removes==1&&offset.unit.Waypoint);
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 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++){
Case lost;lost.unit.Tick(&lost.mode,&lost.route,1000);
if(reason==0)lost.driver.alive=false;
if(reason==1)lost.driver.player=true;
if(reason==2)lost.driver.vehicle=nullptr;
if(reason==3)lost.group.faction=2;
if(reason==4)lost.vehicle.alive=false;
if(reason==5)lost.driver.group=nullptr;
lost.unit.Tick(&lost.mode,&lost.route,2000);assert(lost.unit.Released&&!lost.unit.Waypoint&&lost.mode.command.unregistrations==1);
assert(lost.unit.Entity==&lost.vehicle&&lost.unit.Group==&lost.group&&lost.unit.Members[0]==&lost.driver);
}
Case failed;ERR_Actors::fail=true;failed.unit.Tick(&failed.mode,&failed.route,1000);assert(failed.unit.Released&&failed.group.adds==0);ERR_Actors::fail=false;
}
''')
print('PASS: actual native seated-group arrival outside35m, native waypoint removal, idempotent advance, and convoy waypoint idempotence/arrival/deadline, missing/dead/captured/reparented crew refusal, failed native waypoint release, original vehicle/member preservation and no replacement')
+60
View File
@@ -0,0 +1,60 @@
"""Execute actual footprint sampling and native query callback policy with API doubles."""
import re
from policy_harness import ROOT,body,run
p=ROOT/'addons/ai/Scripts/Game/ERR/ERR_ConvoyPlacement.c'
code='\n'.join(body(p,s) for s in ('protected bool Occupancy(', 'static bool Terrain(', 'bool Clear('))
code=code.replace('null','nullptr').replace('protected ','').replace('IEntity entity','IEntity* entity').replace('entity.GetVObject()', 'entity->GetVObject()')
code=code.replace('TraceBox volume', 'TraceBox* volume').replace('volume.', 'volume->').replace('\"0 0.01 0\"', 'Vector(0,.01,0)')
for cls in ('Math','ERR_ForceRules','ERR_Actors','ChimeraWorldUtils','TraceFlags'):code=code.replace(cls+'.',cls+'::')
code=code.replace('GetGame().GetWorld().','GetGame()->GetWorld()->').replace('GetGame().GetWorld()', 'GetGame()->GetWorld()')
code=code.replace('low, high, Occupancy','low, high, [this](IEntity* entity){return Occupancy(entity);}')
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <vector>
#include <string>
struct vector{float x=0,y=0,z=0;float& operator[](int i){return i==0?x:i==1?y:z;}vector operator+(vector b)const{return{x+b.x,y+b.y,z+b.z};}vector operator-(vector b)const{return{x-b.x,y-b.y,z-b.z};}vector operator*(float n)const{return{x*n,y*n,z*n};}};
vector Vector(float x,float y,float z){return{x,y,z};}
namespace Math{float DEG2RAD=3.141592653589793/180;float Sin(float n){return std::sin(n);}float Cos(float n){return std::cos(n);}float Min(float a,float b){return std::min(a,b);}float Max(float a,float b){return std::max(a,b);}}
namespace ERR_ForceRules{bool Finite(float n){return std::isfinite(n);}bool FiniteVector(vector v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}}
struct IEntity{bool visible=false;bool GetVObject(){return visible;}};
struct TraceBox {vector Start,End,Mins,Maxs;int Flags=0;IEntity* TraceEnt=nullptr;};
namespace TraceFlags{constexpr int ENTS=1,OCEAN=2,WORLD=4;}
struct World{int traces=0;float fraction=1;IEntity* hit=nullptr;TraceBox last;float TraceMove(TraceBox* volume,void*){traces++;last=*volume;volume->TraceEnt=hit;return fraction;}int queries=0,visits=0;vector low,high;std::vector<IEntity*> entities;template<class C>void QueryEntitiesByAABB(vector a,vector b,C callback){queries++;low=a;high=b;for(auto e:entities){visits++;if(!callback(e))break;}}};
struct Game{World world;World* GetWorld(){return &world;}}game;Game* GetGame(){return &game;}
namespace ERR_Actors{int grounds=0;float elevation=10,slope=0;bool invalid=false;vector Ground(vector v){grounds++;v.y=invalid?NAN:elevation+slope*v.x;return v;}}
namespace ChimeraWorldUtils{int waters=0;bool water=false;bool TryGetWaterSurfaceSimple(World*,vector){waters++;return water;}}
struct Placement{float m_LastTrace=-1;std::string m_LastReason;float m_LastLowest=0,m_LastHighest=0;IEntity* m_FirstObstruction=nullptr;bool m_Obstructed=false;int m_Visited=0;'''+code+r'''};
void reset(){game.world=World();ERR_Actors::grounds=0;ERR_Actors::elevation=10;ERR_Actors::slope=0;ERR_Actors::invalid=false;ChimeraWorldUtils::waters=0;ChimeraWorldUtils::water=false;}
bool near(float a,float b){return std::abs(a-b)<.001;}
int main(){
Placement p;reset();assert(p.Clear({0,10,0},0));assert(ERR_Actors::grounds==9&&ChimeraWorldUtils::waters==9&&game.world.queries==1);
assert(near(game.world.low.x,-2)&&near(game.world.high.x,2)&&near(game.world.low.z,-4.5)&&near(game.world.high.z,4.5));assert(near(game.world.low.y,10.05)&&near(game.world.high.y,13.5));
assert(game.world.traces==1&&game.world.last.Flags==(TraceFlags::ENTS|TraceFlags::OCEAN));
assert(near(game.world.last.Start.y+game.world.last.Mins.y,10.05)&&near(game.world.last.Start.y+game.world.last.Maxs.y,13.5));
assert(near(game.world.last.Mins.x,-2)&&near(game.world.last.Maxs.x,2)&&near(game.world.last.Mins.z,-4.5)&&near(game.world.last.Maxs.z,4.5));
assert(game.world.last.End.y>game.world.last.Start.y&&near(game.world.last.End.y-game.world.last.Start.y,.01));
reset();assert(p.Clear({0,10,0},90));assert(near(game.world.low.x,-4.5)&&near(game.world.high.x,4.5)&&near(game.world.low.z,-2)&&near(game.world.high.z,2));
reset();assert(!p.Clear({NAN,10,0},0)&&game.world.queries==0&&ERR_Actors::grounds==0);assert(!p.Clear({0,10,0},NAN));
reset();ERR_Actors::slope=.2;assert(!p.Clear({0,10,0},0)&&game.world.queries==0); // 0.8m cross-footprint rise.
reset();ERR_Actors::elevation=.29;assert(!p.Clear({0,.29,0},0)&&game.world.queries==0);
reset();assert(!p.Clear({0,10.31,0},0)&&game.world.queries==0);
reset();ChimeraWorldUtils::water=true;assert(!p.Clear({0,10,0},0)&&game.world.queries==0&&ERR_Actors::grounds==1);
reset();ERR_Actors::invalid=true;assert(!p.Clear({0,10,0},0)&&game.world.queries==0);
IEntity abstract,obstacle;obstacle.visible=true;
reset();game.world.entities={&abstract,&obstacle,&abstract};assert(p.Clear({0,10,0},0)&&game.world.visits==2&&!p.m_Obstructed&&!p.m_FirstObstruction&&p.m_LastReason=="clear");
// Broad tree/cable bounds overlap, but their actual low geometry does not: accept.
assert(game.world.traces==1&&p.m_LastTrace==1);
// Actual geometry refuses even when the broad diagnostic callback found only abstract entities.
reset();game.world.entities={&abstract};game.world.fraction=.4;game.world.hit=&obstacle;
assert(!p.Clear({0,10,0},0)&&p.m_Obstructed&&p.m_FirstObstruction==&obstacle&&p.m_LastReason=="native filled volume collision");
for(float fraction:{0.f,.999f,NAN,INFINITY}){reset();game.world.fraction=fraction;assert(!p.Clear({0,10,0},0));}
// New query resets previous obstruction. Abstract logic entities do not block.
reset();game.world.entities={nullptr,&abstract};assert(p.Clear({0,10,0},0)&&game.world.visits==2&&!p.m_Obstructed&&!p.m_FirstObstruction&&p.m_LastReason=="clear");
reset();game.world.entities.assign(128,&abstract);assert(p.Clear({0,10,0},0)&&game.world.visits==128);
reset();game.world.entities.assign(130,&abstract);assert(p.Clear({0,10,0},0)&&game.world.visits==129&&game.world.traces==1&&p.m_LastReason=="clear");
assert(Placement::Terrain(10,9.7,10.4));assert(!Placement::Terrain(10,9.7,10.451));assert(!Placement::Terrain(10,10.1,10));assert(!Placement::Terrain(10,NAN,10));
}
''')
print('PASS: actual filled rotated-footprint TraceBox and nine terrain/water samples; broad tree/cable bounds alone accepted, actual collision/nonfinite trace refused, nonzero sweep and ENTS/OCEAN flags, bounded diagnostic query and reset')
+82
View File
@@ -0,0 +1,82 @@
"""Execute installed road geometry methods; native road/terrain queries remain engine work."""
import os,re
from policy_harness import ROOT, body, run
from pathlib import Path
p=Path(os.environ.get('ERR_CONVOY_ROAD_PLAN',str(ROOT/'addons/ai/Scripts/Game/ERR/ERR_ConvoyRoadPlan.c')))
code='\n'.join(body(p,s) for s in ('static bool Sample(', 'static bool CandidateStart(', 'static bool FromPolyline(', 'static bool Prefer('))
code=re.sub(r'//[^\n]*','',code)
code=code.replace('out array<vector>','Array<vector>&').replace('array<vector>','Array<vector>')
code=code.replace('out vector','vector&').replace('out float','float&').replace('out ERR_ForceRoute','ERR_ForceRoute*&')
code=re.sub(r'ERR_ForceRoute (\w+)',r'ERR_ForceRoute* \1',code)
code=code.replace('foreach (','for (').replace('null','nullptr')
for cls in ('vector','ERR_ForceRules','Math'):code=code.replace(cls+'.',cls+'::')
for obj in ('candidate','route','bestRoute'):code=code.replace(obj+'.',obj+'->')
code=code.replace('float length, projected, nearest', 'float length=0, projected=0, nearest').replace('float traversed;', 'float traversed=0;').replace('float offset;', 'float offset=0;')
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <vector>
#include <limits>
struct vector{float x=0,y=0,z=0;float& operator[](int i){return i==0?x:i==1?y:z;}vector operator+(vector b)const{return{x+b.x,y+b.y,z+b.z};}vector operator-(vector b)const{return{x-b.x,y-b.y,z-b.z};}vector operator*(float n)const{return{x*n,y*n,z*n};}float Length()const{return std::sqrt(x*x+y*y+z*z);}void Normalize(){float n=Length();x/=n;y/=n;z/=n;}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}static vector Lerp(vector a,vector b,float n){return a+(b-a)*n;}static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}};
vector Vector(float x,float y,float z){return{x,y,z};}
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count()const{return this->size();}bool IsEmpty()const{return this->empty();}void Insert(T v){this->push_back(v);}explicit operator bool()const{return true;}};
namespace Math{float Clamp(float x,float a,float b){return std::clamp(x,a,b);}int Min(int a,int b){return std::min(a,b);}}
namespace ERR_ForceRules{bool Finite(float n){return std::isfinite(n);}bool FiniteVector(vector v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}float Heading(vector a,vector b,float){return std::atan2(b.x-a.x,b.z-a.z)*180/3.141592653589793;}}
struct ERR_ForceRoute{Array<vector> points;bool sealed=false;float length=0;bool Add(vector v){if(sealed||points.Count()>=64||!ERR_ForceRules::FiniteVector(v))return false;points.Insert(v);return true;}bool Seal(){if(points.IsEmpty())return false;length=0;for(int i=1;i<points.Count();i++)length+=vector::DistanceXZ(points[i-1],points[i]);sealed=true;return true;}int Count(){return points.Count();}float Length(){return length;}vector At(int i){return points.at(i);}};
struct Plan{'''+code+r'''};
bool near(float a,float b){return std::abs(a-b)<.02;}
int main(){
Array<vector> positions,angles;ERR_ForceRoute* route=nullptr;float proximity;vector pos,dir;
assert(Plan::Sample({{0,0,0},{0,0,0},{100,0,0},{100,0,100}},150,pos,dir)&&near(pos.x,100)&&near(pos.z,50)&&near(dir.z,1));
assert(!Plan::Sample({{0,0,0},{100,0,0}},101,pos,dir));assert(!Plan::Sample({{0,0,0},{100,0,0}},-1,pos,dir));
assert(!Plan::Sample({{0,0,0},{100,0,0}},NAN,pos,dir));
assert(Plan::FromPolyline({{0,10,0},{390,10,0}},{100,10,20},positions,angles,route,proximity));
assert(positions.Count()==6&&angles.Count()==6&&route&&near(proximity,std::hypot(2.5f,20.0f)));assert(near(positions[0].x,190)&&near(positions[5].x,15));assert(near(route->Length(),200));
for(int i=1;i<6;i++)assert(near(vector::DistanceXZ(positions[i-1],positions[i]),35));
assert(Plan::FromPolyline({{0,10,0},{390,10,0}},{380,10,0},positions,angles,route,proximity));assert(near(positions[0].x,200)&&near(positions[5].x,375)&&near(angles[0].x,-90));
assert(Plan::FromPolyline({{0,10,0},{200,10,0},{200,10,200},{500,10,200}},{10,10,0},positions,angles,route,proximity));
assert(route->Count()==3&&near(route->At(1).x,200)&&near(route->At(1).z,0)&&near(route->At(2).z,200));
assert(Plan::FromPolyline({{0,10,0},{0,10,0},{600,10,0}},{102.5,10,490},positions,angles,route,proximity));
assert(!Plan::FromPolyline({{0,10,0},{600,10,0}},{30,10,500.1},positions,angles,route,proximity)&&route==nullptr);
assert(!Plan::FromPolyline({{0,10,0},{389.9,10,0}},{0,10,0},positions,angles,route,proximity));
assert(!Plan::FromPolyline({{0,10,0},{NAN,10,0}},{0,10,0},positions,angles,route,proximity));
assert(!Plan::FromPolyline({{0,10,0},{0,10,0}},{0,10,0},positions,angles,route,proximity));
Array<vector> dense;for(int i=0;i<=600;i++)dense.Insert({float(i),10,0});assert(!Plan::FromPolyline(dense,{0,10,0},positions,angles,route,proximity));
Array<vector> tooMany;for(int i=0;i<1025;i++)tooMany.Insert({float(i),10,0});assert(!Plan::FromPolyline(tooMany,{0,10,0},positions,angles,route,proximity));
// Tight hairpin folds the six intended starts onto each other: never accept it.
assert(!Plan::FromPolyline({{0,10,0},{100,10,0},{100,10,1},{0,10,1},{0,10,2},{500,10,2}},{0,10,0},positions,angles,route,proximity));
float start;
assert(Plan::CandidateStart(1200,500,0,start)&&near(start,412.5));
assert(Plan::CandidateStart(1200,500,1,start)&&near(start,447.5));
assert(Plan::CandidateStart(1200,500,2,start)&&near(start,377.5));
assert(!Plan::CandidateStart(390,100,1,start)&&!Plan::CandidateStart(390,100,2,start));
assert(!Plan::CandidateStart(1200,500,33,start)&&!Plan::CandidateStart(1200,500,-1,start));
assert(!Plan::CandidateStart(NAN,500,0,start)&&!Plan::CandidateStart(1200,NAN,0,start));
// All six chosen poses, not merely some remote part of the same road, stay near seed.
assert(!Plan::FromPolyline({{0,10,0},{2000,10,0}},{10,10,490},positions,angles,route,proximity));
assert(Plan::FromPolyline({{0,10,0},{2000,10,0}},{500,10,0},positions,angles,route,proximity,1));
assert(near(positions[0].x,622.5)&&near(positions[5].x,447.5)&&near(proximity,35));
assert(Plan::FromPolyline({{0,10,0},{2000,10,0}},{500,10,0},positions,angles,route,proximity,2));
assert(near(positions[0].x,552.5)&&near(positions[5].x,377.5)&&near(proximity,35));
assert(!Plan::FromPolyline({{0,10,0},{2000,10,0}},{500,10,0},positions,angles,route,proximity,31));
// Simulated native obstruction occupies initial lead around587.5; the first shifted
// formation is clear. A failed initial candidate must not discard the native road.
bool selected=false;int considered=0;Array<vector> first;
for(int index=0;index<33;index++){
if(!Plan::FromPolyline({{0,10,0},{2000,10,0}},{500,10,0},positions,angles,route,proximity,index))continue;
considered++;bool clear=true;for(auto point:positions)if(std::abs(point.x-587.5)<10)clear=false;
if(clear){selected=true;first=positions;break;}
}
assert(selected&&considered>1&&first.Count()==6);
ERR_ForceRoute a,b;a.Add({0,0,0});a.Add({1,0,0});a.Seal();b.Add({0,0,0});b.Add({2,0,0});b.Seal();
assert(Plan::Prefer(4,{{0,0,0}},&a,5,{{0,0,0}},&b));assert(!Plan::Prefer(6,{{0,0,0}},&a,5,{{0,0,0}},&b));
assert(Plan::Prefer(5,{{0,0,0}},&a,5,{{1,0,0}},&b));assert(Plan::Prefer(5,{{0,0,0}},&a,5,{{0,0,0}},&b));assert(!Plan::Prefer(5,{{0,0,0}},&b,5,{{0,0,0}},&a));
}
''')
build=body(p,'static bool Build(')
assert 'candidateIndex < 33' in build
assert build.index('Prefer(distance,') < build.index('ERR_ConvoyRules.GroundClear(')
print('PASS: actual road interpolation/projection/orientation, six35m starts,200m forward tail, preserved bends, short/far/degenerate/nonfinite/overdense/hairpin refusal and deterministic candidate ties')
+180
View File
@@ -0,0 +1,180 @@
"""Execute actual scoped skill and finite native-equipment policy with API doubles."""
import os,re
from pathlib import Path
from policy_harness import ROOT,run
source=Path(os.environ.get('ERR_EQUIPMENT_SOURCE',str(ROOT/'addons/ai/Scripts/Game/ERR/ERR_EnemyEquipment.c'))).read_text()
code=source[:source.index('class ERR_EnemyEquipmentGroup')]
code=re.sub(r'//[^\n]*','',code)
code=code.replace('modded class SCR_AICombatComponent','struct SCR_AICombatComponent : NativeCombat')
code=code.replace('class ERR_EnemyEquipmentCallback : ScriptedInventoryOperationCallback','struct ERR_EnemyEquipmentCallback')
code=code.replace('class ERR_EnemyEquipmentRules','struct ERR_EnemyEquipmentRules').replace('class ERR_EnemyEquipmentEntry','struct ERR_EnemyEquipmentEntry')
code=code.replace('protected ','').replace('override ','').replace('ref ','')
code=code.replace('static const ResourceName','static constexpr const char*').replace('static const int','static constexpr int')
code=code.replace('string result','std::string result').replace('array<IEntity>','std::vector<IEntity*>')
code=code.replace('BaseWeaponManagerComponent.Cast(Actor.FindComponent(BaseWeaponManagerComponent))','Actor->weapons')
code=code.replace('InventoryStorageManagerComponent.Cast(Actor.FindComponent(InventoryStorageManagerComponent))','Actor->inventory')
for cls in ('ERR_GameMode','SCR_AIGroup','SCR_AIGroupUtilityComponent','IEntity','SCR_AICombatComponent','ERR_EnemyEquipmentCallback','BaseWeaponManagerComponent','InventoryStorageManagerComponent'):
code=re.sub(r'\b'+cls+r' ([a-zA-Z_]\w*)',cls+r'* \1',code)
for obj in ('mode','group','utility','Actor','Group','Combat','Operation','item','weapons','inventory'):
code=code.replace(obj+'.',obj+'->')
for cls in ('Replication','SCR_Global','ERR_GameMode','ERR_Actors','ERR_EnemyEquipmentRules','EAISkill','ERR_HQ','Math','EStoragePurpose'):
code=code.replace(cls+'.',cls+'::')
code=code.replace('GetPrefabData().','GetPrefabData()->').replace('Settings().','Settings()->').replace('foreach (','for (').replace('null','nullptr').replace('super.','NativeCombat::')
code=code.replace('new ERR_EnemyEquipmentCallback()','NewOperation()')
code=re.sub(r'\b(bool|int|EAISkill|[A-Za-z_]\w*\*) ([A-Za-z_]\w*);',r'\1 \2 {};',code)
code=code.replace('\n}\n','\n};\n')
# Forward factory declaration permits bounded managed operation storage below.
code=code.replace('struct ERR_EnemyEquipmentEntry','ERR_EnemyEquipmentCallback* NewOperation();\nstruct ERR_EnemyEquipmentEntry')
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <memory>
enum class EAISkill {ROOKIE,REGULAR,EXPERT,CYLON};
namespace Replication {bool server=true;bool IsServer(){return server;}}
namespace SCR_Global {bool editor=false;bool IsEditMode(){return editor;}}
namespace EStoragePurpose {constexpr int PURPOSE_WEAPON_PROXY=7;}
namespace ERR_HQ {void Event(std::string,std::string){}}
struct SCR_AIGroupUtilityComponent {bool military=true;bool IsMilitary(){return military;}};
struct SCR_AIGroup {
SCR_AIGroupUtilityComponent utility;int players=0,faction=1;bool ready=true;
SCR_AIGroupUtilityComponent* GetGroupUtilityComponent(){return ready?&utility:nullptr;}
int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}
};
struct Settings {int oneIn=15;int Get(std::string key){assert(key=="stingerChanceOneIn");return oneIn;}};
struct ERR_GameMode {
static inline ERR_GameMode* current=nullptr;bool active=true;struct Settings values;
static ERR_GameMode* Get(){return current;}bool Active(){return active;}int EnemyFaction(){return 1;}
struct Settings* Settings(){return &values;}
};
namespace Math {int draw=0,calls=0;int RandomInt(int lo,int hi){assert(lo==0&&hi>0);calls++;return draw;}}
struct Prefab {std::string name;std::string GetPrefabName(){return name;}};
struct ERR_EnemyEquipmentCallback;
struct InventoryStorageManagerComponent {
bool accept=true;int requests=0;ERR_EnemyEquipmentCallback* callback=nullptr;
bool TrySpawnPrefabToStorage(const char* prefab,void* target,int slot,int purpose,ERR_EnemyEquipmentCallback* op){assert(std::string(prefab).find("3DF204EEE0534EF2")!=std::string::npos);assert(!target&&slot==-1&&purpose==7);requests++;callback=op;return accept;}
};
struct IEntity;
struct BaseWeaponManagerComponent {std::vector<IEntity*> items;void GetWeaponsList(std::vector<IEntity*>& out){out=items;}};
struct IEntity {
bool player=false,alive=true;SCR_AIGroup* group=nullptr;Prefab* prefab=nullptr;
BaseWeaponManagerComponent* weapons=nullptr;InventoryStorageManagerComponent* inventory=nullptr;
Prefab* GetPrefabData(){return prefab;}
};
namespace ERR_Actors {bool CanFight(IEntity* actor){return actor->alive;}bool Player(IEntity* actor){return actor->player;}SCR_AIGroup* Group(IEntity* actor){return actor->group;}}
struct NativeCombat {
IEntity* owner=nullptr;EAISkill skill=EAISkill::REGULAR;
IEntity* GetOwner(){return owner;}EAISkill GetAISkill(){return skill;}
virtual void SetAISkill(EAISkill value){skill=value;}virtual void ResetAISkill(){skill=EAISkill::ROOKIE;}
};
''' + code + r'''
std::vector<std::unique_ptr<ERR_EnemyEquipmentCallback>> operations;
ERR_EnemyEquipmentCallback* NewOperation(){operations.emplace_back(new ERR_EnemyEquipmentCallback());return operations.back().get();}
struct Soldier {
SCR_AIGroup group;BaseWeaponManagerComponent weapons;InventoryStorageManagerComponent inventory;IEntity actor;SCR_AICombatComponent combat;
Soldier(){actor.group=&group;actor.weapons=&weapons;actor.inventory=&inventory;combat.owner=&actor;}
ERR_EnemyEquipmentEntry Entry(){ERR_EnemyEquipmentEntry e;e.Actor=&actor;e.Group=&group;e.Combat=&combat;return e;}
};
int main(){
ERR_GameMode mode;ERR_GameMode::current=&mode;
// One draw per exact actor; accepted insertion cannot be retried, even after timeout.
for(int draw=0;draw<15;draw++){
Math::draw=draw;Math::calls=0;Soldier soldier;auto job=soldier.Entry();
for(int n=0;n<20;n++){job.Check(&mode);}
assert(job.Complete&&Math::calls==1&&soldier.inventory.requests==(draw==0?1:0));
}
Math::draw=0;mode.values.oneIn=0;Soldier off;auto disabled=off.Entry();Math::calls=0;
assert(disabled.Check(&mode)&&Math::calls==0&&off.inventory.requests==0);
mode.values.oneIn=1;
Soldier success;auto finished=success.Entry();assert(!finished.Check(&mode));
success.inventory.callback->OnComplete();success.inventory.callback->OnAllFailed();
assert(finished.Check(&mode)&&success.inventory.requests==1&&success.inventory.callback->Succeeded);
// A completed then removed launcher is never replenished.
for(int n=0;n<10;n++){finished.Check(&mode);}assert(success.inventory.requests==1);
Soldier rejected;rejected.inventory.accept=false;auto retries=rejected.Entry();
for(int n=0;n<10;n++){retries.Check(&mode);}assert(rejected.inventory.requests==4&&retries.Complete);
Soldier failed;auto again=failed.Entry();again.Check(&mode);failed.inventory.callback->OnFailed();again.Check(&mode);assert(failed.inventory.requests==2);
for(int n=0;n<10;n++){again.Check(&mode);}assert(failed.inventory.requests==2);
Soldier changed;auto captured=changed.Entry();captured.Check(&mode);changed.actor.player=true;assert(captured.Check(&mode));changed.actor.player=false;
for(int n=0;n<10;n++){captured.Check(&mode);}assert(changed.inventory.requests==1);
Soldier absent;absent.actor.inventory=nullptr;auto missing=absent.Entry();for(int n=0;n<8;n++){missing.Check(&mode);}assert(missing.Complete&&absent.inventory.requests==0);
Soldier equipped;Prefab prefab;prefab.name=ERR_EnemyEquipmentEntry::LAUNCHER;IEntity item;item.prefab=&prefab;equipped.weapons.items.push_back(&item);auto existing=equipped.Entry();assert(existing.Check(&mode)&&equipped.inventory.requests==0);
// The exact same eligibility policy protects skill binding and item submission.
for(int variant=0;variant<9;variant++){
Soldier soldier;ERR_GameMode other;auto job=soldier.Entry();Math::calls=0;
if(variant==0)Replication::server=false;
if(variant==1)SCR_Global::editor=true;
if(variant==2)mode.active=false;
if(variant==3)ERR_GameMode::current=&other;
if(variant==4)soldier.actor.player=true;
if(variant==5)soldier.actor.alive=false;
if(variant==6)soldier.group.players=1;
if(variant==7)soldier.group.faction=2;
if(variant==8)soldier.group.utility.military=false;
assert(job.Check(&mode)&&Math::calls==0&&soldier.inventory.requests==0);
soldier.combat.ERR_BindEnemySkill(&mode,&soldier.group);assert(soldier.combat.GetAISkill()==EAISkill::REGULAR);
Replication::server=true;SCR_Global::editor=false;mode.active=true;ERR_GameMode::current=&mode;
}
Soldier skill;skill.combat.ERR_BindEnemySkill(&mode,&skill.group);assert(skill.combat.GetAISkill()==EAISkill::EXPERT);
skill.combat.SetAISkill(EAISkill::ROOKIE);assert(skill.combat.GetAISkill()==EAISkill::EXPERT);
skill.combat.ResetAISkill();assert(skill.combat.GetAISkill()==EAISkill::EXPERT);
skill.combat.SetAISkill(EAISkill::CYLON);assert(skill.combat.GetAISkill()==EAISkill::CYLON);
skill.combat.ERR_BindEnemySkill(&mode,&skill.group);skill.combat.ERR_ReleaseEnemySkill(&mode);assert(skill.combat.GetAISkill()==EAISkill::REGULAR);
skill.combat.SetAISkill(EAISkill::ROOKIE);assert(skill.combat.GetAISkill()==EAISkill::ROOKIE);
Soldier moved;SCR_AIGroup replacement;moved.combat.ERR_BindEnemySkill(&mode,&moved.group);moved.actor.group=&replacement;
moved.combat.SetAISkill(EAISkill::ROOKIE);assert(moved.combat.GetAISkill()==EAISkill::ROOKIE);
Soldier stopped;auto terminal=stopped.Entry();terminal.Check(&mode);terminal.Stop(&mode);stopped.inventory.callback->OnComplete();
for(int n=0;n<10;n++){terminal.Check(&mode);}assert(stopped.inventory.requests==1&&terminal.Complete);
}
''')
print('PASS: actual enemy eligibility, scoped native skill floor/reset/restoration, one-roll inventory issuance, completion/failure, timeout quarantine, actor capture and terminal stop; native inventory runtime unproven')
coordinator=source[source.index('class ERR_EnemyEquipmentGroup'):]
coordinator=re.sub(r'//[^\n]*','',coordinator).replace('protected ','').replace('ref ','').replace('class ','struct ')
coordinator=coordinator.replace('array<ERR_EnemyEquipmentGroup>','Array<ERR_EnemyEquipmentGroup*>').replace('array<ERR_EnemyEquipmentEntry>','Array<ERR_EnemyEquipmentEntry*>').replace('array<AIAgent>','Array<AIAgent*>')
coordinator=coordinator.replace('SCR_AICombatComponent.Cast(actor.FindComponent(SCR_AICombatComponent))','actor->combat').replace('SCR_AICombatComponent.Cast(entry.Actor.FindComponent(SCR_AICombatComponent))','entry->Actor->combat')
for cls in ('ERR_GameMode','SCR_AIGroup','ERR_EnemyEquipmentGroup','ERR_EnemyEquipmentEntry','IEntity','AIAgent'):
coordinator=re.sub(r'\b'+cls+r' ([a-zA-Z_]\w*)',cls+r'* \1',coordinator)
for obj in ('m_Mode','existing','state','group','agent','entry','actor'):
coordinator=coordinator.replace(obj+'.',obj+'->')
coordinator=coordinator.replace('Group.GetAgents','Group->GetAgents').replace('Combat.ERR_','Combat->ERR_')
for cls in ('Replication','ERR_GameMode','ERR_EnemyEquipmentRules'):coordinator=coordinator.replace(cls+'.',cls+'::')
coordinator=coordinator.replace('void ERR_EnemyEquipment(', 'ERR_EnemyEquipment(')
coordinator=coordinator.replace('GetGame().GetWorld().GetWorldTime()','worldTime').replace('foreach (','for (').replace('null','nullptr')
coordinator=re.sub(r'\b(bool|int|float|[A-Za-z_]\w*\*) ([A-Za-z_]\w*);',r'\1 \2 {};',coordinator).replace('\n}\n','\n};\n')
run(r'''
#include <cassert>
#include <vector>
#include <algorithm>
float worldTime=0;
namespace Replication {bool IsServer(){return true;}}
template<typename T> struct Array:std::vector<T>{int Count(){return this->size();}void Insert(T value){this->push_back(value);}void Clear(){this->clear();}};
struct SCR_AIGroup;
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;bool active=true;bool Active(){return active;}static ERR_GameMode* Get(){return current;}};
struct SCR_AICombatComponent{int binds=0;void ERR_BindEnemySkill(ERR_GameMode*,SCR_AIGroup*){binds++;}};
struct IEntity{SCR_AICombatComponent* combat=nullptr;bool eligible=true;};
struct AIAgent{IEntity* actor=nullptr;IEntity* GetControlledEntity(){return actor;}};
struct SCR_AIGroup{Array<AIAgent*> agents;void GetAgents(Array<AIAgent*>& out){out=agents;}};
namespace ERR_EnemyEquipmentRules{bool Eligible(ERR_GameMode*,SCR_AIGroup*,IEntity* actor){return actor&&actor->eligible;}}
struct ERR_EnemyEquipmentEntry{
IEntity* Actor=nullptr;SCR_AIGroup* Group=nullptr;SCR_AICombatComponent* Combat=nullptr;bool Released=false;int SkillChecks=0,checks=0,stops=0;
void Check(ERR_GameMode*){checks++;}void Stop(ERR_GameMode*){stops++;Released=true;Combat=nullptr;}
};
''' + coordinator + r'''
int main(){
ERR_GameMode mode;ERR_GameMode::current=&mode;ERR_EnemyEquipment service(&mode);
SCR_AIGroup group;IEntity soldier;SCR_AICombatComponent combat;soldier.combat=&combat;AIAgent agent;agent.actor=&soldier;group.agents.Insert(&agent);
service.RegisterGroup(&group);service.RegisterGroup(&group);assert(service.m_Groups.Count()==1);
service.Tick(0);assert(service.m_Entries.Count()==0);service.Tick(3000);assert(service.m_Entries.Count()==1&&combat.binds==1);
service.Tick(3001);assert(service.m_Entries[0]->checks==1);service.Tick(6000);assert(service.m_Entries[0]->checks==2&&combat.binds==1);
service.UnregisterGroup(&group);service.Tick(9000);assert(service.m_Entries[0]->Released&&service.m_Entries[0]->checks==2);
service.RegisterGroup(&group);service.Tick(12000);assert(service.m_Entries.Count()==1&&combat.binds==1);
service.Stop();service.RegisterGroup(&group);service.Tick(50000);assert(service.m_Groups.Count()==0&&service.m_Stopped);
ERR_EnemyEquipment delayed(&mode);SCR_AIGroup late;delayed.RegisterGroup(&late);delayed.Tick(3000);late.agents.Insert(&agent);delayed.Tick(6000);assert(delayed.m_Entries.Count()==1);
ERR_EnemyEquipment expired(&mode);SCR_AIGroup empty;expired.RegisterGroup(&empty);for(int i=1;i<=6;i++){expired.Tick(i*3000);}empty.agents.Insert(&agent);expired.Tick(21000);assert(expired.m_Entries.Count()==0);
ERR_EnemyEquipment bounded(&mode);for(int i=0;i<140;i++){bounded.RegisterGroup(new SCR_AIGroup());}assert(bounded.m_Groups.Count()==128);
ERR_EnemyEquipment incomplete(&mode);SCR_AIGroup unresolved;IEntity body;AIAgent bodyAgent;bodyAgent.actor=&body;unresolved.agents.Insert(&bodyAgent);incomplete.RegisterGroup(&unresolved);
for(int i=1;i<=12;i++){incomplete.Tick(i*3000);}assert(incomplete.m_Entries.Count()==1&&incomplete.m_Entries[0]->SkillChecks==5);
incomplete.Stop();
}
''')
print('PASS: actual equipment coordinator registration dedupe, startup delay, five member scans, finite component retries, unregister/stop terminal behavior and 128-group bound')
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Exercise exact dependency closure, resource ownership and isolated profile inputs."""
import copy
import hashlib
import json
from pathlib import Path
import shutil
import sys
import tempfile
import unittest
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools'))
import external_addons as ext
ROOT=Path(__file__).resolve().parents[1]
class ExternalAddonsTests(unittest.TestCase):
def setUp(self):
self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup)
self.home=Path(self.temp.name);self.source=self.home/'installed';self.source.mkdir()
self.addons=self.home/'profile'/'addons';self.addons.mkdir(parents=True)
self.cache=self.home/'profile'/'cache.json';self.value=ext.policy(ROOT)
for guid,deps in ext.ALLOWED.items():
folder=self.source/guid;folder.mkdir()
manifest='GameProject { GUID "'+guid+'" Dependencies { '+' '.join('"'+d+'"' for d in sorted(deps))+' } }\n'
(folder/'addon.gproj').write_text(manifest)
self.value['addons'][guid]['project_sha256']=hashlib.sha256(manifest.encode()).hexdigest()
data=b'RDB synthetic fixture\0'
for path,entry in self.value['resources'].items():
if entry['addon']==guid:data+=path.encode()+b'\0\x06\0\0\0\0\0'+bytes.fromhex(entry['guid'])[::-1]
(folder/'resourceDatabase.rdb').write_bytes(data);(folder/'data.pak').write_bytes(b'fixture '+guid.encode())
def test_resource_identity_requires_visible_owner(self):
for path,item in self.value['resources'].items():
self.assertTrue(ext.resource_allowed(self.value,{item['addon']},item['guid'],path))
self.assertFalse(ext.resource_allowed(self.value,{'core'},item['guid'],path))
self.assertFalse(ext.resource_allowed(self.value,{item['addon']},'0000000000000000',path))
self.assertEqual(ext.direct_for('core'),set());self.assertEqual(ext.direct_for('chat'),set())
def test_policy_rejects_original_transitive_dependency(self):
root=self.home/'root';(root/'docs').mkdir(parents=True)
value=copy.deepcopy(self.value);value['addons'][ext.DIRECT[0]]['dependencies'].append('5F16D7E4A1CBE075')
(root/'docs/external-addons.json').write_text(json.dumps(value))
with self.assertRaisesRegex(ValueError,'transitive'):ext.policy(root)
def test_only_exact_six_are_exposed_and_hashed(self):
unrelated=self.source/'original';unrelated.mkdir();(unrelated/'addon.gproj').write_text('GameProject { GUID "5F16D7E4A1CBE075" }')
records=ext.prepare_profile(self.addons,self.source,self.value,self.cache)
self.assertEqual(set(records),set(ext.ALLOWED))
self.assertEqual({p.name for p in self.addons.iterdir()},{'ERRI_'+g for g in ext.ALLOWED})
self.assertTrue(all(p.is_symlink() for p in self.addons.iterdir()))
self.assertTrue(ext.verify_profile(records,self.source,self.value,self.cache,self.addons))
self.assertEqual(ext.prepare_profile(self.addons,self.source,self.value,self.cache),records)
(self.source/ext.DIRECT[0]/'data.pak').write_bytes(b'mutated runtime')
self.assertFalse(ext.verify_profile(records,self.source,self.value,self.cache,self.addons))
def test_missing_and_duplicate_identity_fail(self):
guid=ext.DIRECT[0];shutil.copytree(self.source/guid,self.source/'duplicate')
with self.assertRaisesRegex(ValueError,'Duplicate'):ext.installed(self.source,self.value)
shutil.rmtree(self.source/'duplicate');shutil.rmtree(self.source/guid)
with self.assertRaisesRegex(ValueError,'Missing installed'):ext.installed(self.source,self.value)
def test_manifest_and_database_mutations_fail(self):
guid=ext.DIRECT[0];manifest=self.source/guid/'addon.gproj';old=manifest.read_text();manifest.write_text(old+'// changed')
with self.assertRaisesRegex(ValueError,'manifest changed'):ext.installed(self.source,self.value)
manifest.write_text(old)
resource=next(iter(self.value['resources'].values()));(self.source/resource['addon']/'resourceDatabase.rdb').write_bytes(b'wrong GUID')
with self.assertRaisesRegex(ValueError,'resource identity'):ext.installed(self.source,self.value)
def test_unrelated_profile_addon_and_wrong_link_fail(self):
(self.addons/'original').mkdir()
with self.assertRaisesRegex(ValueError,'Unrelated'):ext.prepare_profile(self.addons,self.source,self.value,self.cache)
(self.addons/'original').rmdir();guid=ext.DIRECT[0]
(self.addons/('ERRI_'+guid)).symlink_to(self.source/ext.DIRECT[1],target_is_directory=True)
with self.assertRaisesRegex(ValueError,'Unexpected private'):ext.prepare_profile(self.addons,self.source,self.value,self.cache)
def test_symlinked_runtime_file_fails(self):
pak=self.source/ext.DIRECT[0]/'data.pak';pak.unlink();pak.symlink_to(self.source/ext.DIRECT[1]/'data.pak')
with self.assertRaisesRegex(ValueError,'regular file'):ext.installed(self.source,self.value)
if __name__=='__main__':unittest.main()
+50
View File
@@ -0,0 +1,50 @@
"""Actual external profile regression; all fixtures live in temporary directories."""
from pathlib import Path
import unittest,sys
sys.path[:0]=[str(Path(__file__).resolve().parent),str(Path(__file__).resolve().parents[1]/'tools')]
import test_external_addons as existing
import external_addons as ext
class ReviewTests(existing.ExternalAddonsTests):
# Parent tests stay in their separate unchanged module; only new cases run here.
def verify(self,records):return ext.verify_profile(records,self.source,self.value,self.cache,self.addons)
def test_review_retargeted_private_link_rejected(self):
records=ext.prepare_profile(self.addons,self.source,self.value,self.cache)
link=self.addons/('ERRI_'+ext.DIRECT[0]);link.unlink();link.symlink_to(self.source/ext.DIRECT[1],target_is_directory=True)
with self.assertRaisesRegex(ValueError,'link changed'):self.verify(records)
def test_review_missing_link_rejected(self):
records=ext.prepare_profile(self.addons,self.source,self.value,self.cache)
(self.addons/('ERRI_'+ext.DIRECT[0])).unlink()
with self.assertRaisesRegex(ValueError,'link changed'):self.verify(records)
def test_review_added_source_directory_rejected(self):
records=ext.prepare_profile(self.addons,self.source,self.value,self.cache)
source=self.source/ext.DIRECT[0]/'Scripts/Game/ForeignChange.c';source.parent.mkdir(parents=True);source.write_text('class ForeignChange {}')
with self.assertRaisesRegex(ValueError,'flat regular file'):self.verify(records)
def test_review_loose_root_resource_and_symlink_rejected(self):
records=ext.prepare_profile(self.addons,self.source,self.value,self.cache);p=self.source/ext.DIRECT[0]/'unaudited.et';p.write_text('Entity {}')
with self.assertRaisesRegex(ValueError,'Unreviewed'):self.verify(records)
p.unlink();p=self.source/ext.DIRECT[0]/'thumbnail.png';p.symlink_to(self.source/ext.DIRECT[0]/'data.pak')
with self.assertRaisesRegex(ValueError,'flat regular file'):self.verify(records)
def test_review_metadata_media_and_sidecars_remain_allowed_and_bound(self):
folder=self.source/ext.DIRECT[0]
for name in ('meta','ServerData.json','thumbnail.png','gallery0_776x436.jpg','addon.gproj_0.16.5208_manifest.json','data.pak_0.16.5208_manifest.json'):
(folder/name).write_bytes(b'metadata/media')
records=ext.prepare_profile(self.addons,self.source,self.value,self.cache)
self.assertTrue(self.verify(records));self.assertIn('thumbnail.png',records[ext.DIRECT[0]]['files'])
(folder/'thumbnail.png').write_bytes(b'changed');self.assertFalse(self.verify(records))
def test_review_added_allowed_media_invalidates_snapshot(self):
records=ext.prepare_profile(self.addons,self.source,self.value,self.cache)
(self.source/ext.DIRECT[0]/'gallery1_776x436.jpg').write_bytes(b'new');self.assertFalse(self.verify(records))
def test_review_package_guard_sees_new_scripts(self):
# Native package imports the shared implementation; exercise its real compare.
import native_package as n
from unittest.mock import patch
self.home.joinpath('docs').mkdir();import json
policy=self.home/'docs/external-addons.json';policy.write_text(json.dumps(self.value))
context=n.external_request('ai',self.source,self.home);session=self.home/'session';session.mkdir()
with patch.object(n,'external_installed',ext.installed):
n.capture_external(context,session)
folder=self.source/ext.DIRECT[0]/'Scripts';folder.mkdir()
with self.assertRaisesRegex(ValueError,'flat regular file'):n.verify_external(context)
if __name__=='__main__':
suite=unittest.TestSuite(ReviewTests(n) for n in unittest.defaultTestLoader.getTestCaseNames(ReviewTests) if n.startswith('test_review_'))
result=unittest.TextTestRunner(verbosity=2).run(suite);raise SystemExit(not result.wasSuccessful())
+224
View File
@@ -0,0 +1,224 @@
"""Execute actual bounded force ledger and dormant route bodies without engine claims."""
from policy_harness import ROOT, body, run
import re
L=ROOT/'addons/core/Scripts/Game/ERR/ERR_ForceLedger.c'
S=ROOT/'addons/core/Scripts/Game/ERR/ERR_ForceSimulation.c'
def cpp(s):
for old,new in [('protected ',''),('static ', 'static '),('Math.','Math::'),('vector.','vector::'),
('ERR_ForceRules.','ERR_ForceRules::'),('ERR_ForceKind.','ERR_ForceKind::'),
('ERR_ForceDuty.','ERR_ForceDuty::'),('Replication.','Replication::'),('out bool ','bool& '),
('sourceId.IsEmpty()','sourceId.empty()'),('sourceId.Length()','sourceId.size()'),
('string.Format(', 'Format('),('null','nullptr')]: s=s.replace(old,new)
for typ in ('ERR_ForceRecord','ERR_ForceRoute','ERR_ForceLedger','SCR_AIGroup'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for name in ('record','known','route','m_Ledger'):
s=s.replace(name+'.',name+'->')
s=s.replace('foreach (ERR_ForceRecord* record : m_Records)','for (ERR_ForceRecord* record : m_Records)')
s=s.replace('float length;','float length=0;').replace('int visited;','int visited=0;')
s=s.replace('int physical, dormant, eliminated, living, casualties;',
'int physical=0, dormant=0, eliminated=0, living=0, casualties=0;')
return s
rules='\n'.join(cpp(body(S,s)) for s in ('static bool Finite(', 'static bool FiniteVector(', 'static float Speed(',
'static int Advance(', 'static vector Step(', 'static bool Due(', 'static float Heading('))
route='\n'.join(cpp(body(L,s)) for s in ('int Count()', 'vector At(', 'bool Looped(', 'float Length(', 'bool Add(', 'bool Seal('))
ledger_source=L.read_text().split('class ERR_ForceLedger')[1]
# body() locates repeated Count/At unambiguously by extracting the ledger tail.
import tempfile
with tempfile.NamedTemporaryFile(mode='w',suffix='.c') as tail:
tail.write(ledger_source);tail.flush()
from pathlib import Path
ledger='\n'.join(cpp(body(Path(tail.name),s)) for s in ('int Count()', 'ERR_ForceRecord Find(',
'ERR_ForceRecord FindGroup(', 'ERR_ForceRecord At(', 'ERR_ForceRecord Register(', 'bool Unregister(',
'bool Eliminate(', 'bool RecordPhysicalState(', 'string Status(', 'void Clear('))
simulation='\n'.join(cpp(body(S,s)) for s in ('protected void Route(', 'void Tick('))
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <limits>
#include <map>
#include <vector>
#include <string>
#include <sstream>
using string=std::string;
struct vector{
float x=0,y=0,z=0;
vector()=default;vector(float a,float b,float c):x(a),y(b),z(c){}
vector(const char* value){std::istringstream(value)>>x>>y>>z;}
float& operator[](int i){return i==0?x:i==1?y:z;}
float operator[](int i)const{return i==0?x:i==1?y:z;}
vector operator+(vector b){return {x+b.x,y+b.y,z+b.z};}
vector operator-(vector b){return {x-b.x,y-b.y,z-b.z};}
vector operator*(float n){return {x*n,y*n,z*n};}
static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
};
struct Math{static float Floor(float n){return std::floor(n);}
static float Atan2(float a,float b){return std::atan2(a,b);}
static constexpr float RAD2DEG=180/3.14159265358979323846;
};
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
int logs=0;void Print(const char*){logs++;}
string Format(const char*,int a,int b,int c,int d,int e,int f){
return std::to_string(a)+","+std::to_string(b)+","+std::to_string(c)+","+
std::to_string(d)+","+std::to_string(e)+","+std::to_string(f);
}
template<class T>struct Array:std::vector<T>{
int Count(){return this->size();}bool IsEmpty(){return this->empty();}
void Insert(T n){this->push_back(n);}void Clear(){this->clear();}
void RemoveItem(T n){this->erase(std::remove(this->begin(),this->end(),n),this->end());}
};
template<class K,class V>struct Map:std::map<K,V>{
bool Contains(K k){return this->count(k);}V Get(K k){return this->count(k)?this->at(k):V{};}
int Count(){return this->size();}void Set(K k,V v){(*this)[k]=v;}
void Remove(K k){this->erase(k);}void Clear(){this->clear();}
};
enum class ERR_ForceKind { GARRISON, PATROL, CHECKPOINT, CONVOY };
enum class ERR_ForceDuty { HOLD, PATROL_ROUTE, MOVE_TO, SEARCH, RETURN };
struct ERR_ForceRules{
'''+rules+r'''
};
struct ERR_ForceRoute{
Array<vector> m_Points;bool m_Loop=false,m_Sealed=false;float m_Length=0;
ERR_ForceRoute(bool loop=false):m_Loop(loop){}
'''+route+r'''
};
struct SCR_AIGroup{};
struct ERR_ForceRecord{
int Id=0;string SourceId;ERR_ForceKind Kind=ERR_ForceKind::GARRISON;
vector Position,HomePosition,DutyTarget;float Heading=0;
ERR_ForceDuty HomeDuty=ERR_ForceDuty::HOLD,Duty=ERR_ForceDuty::HOLD;
ERR_ForceRoute* Route=nullptr;int RouteIndex=0;float RouteProgress=0;bool RouteFinished=false;
int InitialStrength=0,Strength=0,Casualties=0,Importance=1,RadioCapability=0;
SCR_AIGroup* Group=nullptr;bool Physical=false,Pinned=false,Eliminated=false;
float LastSimulatedAt=0,StateSince=0;
};
struct ERR_ForceLedger{
Array<ERR_ForceRecord*>m_Records;Map<int,ERR_ForceRecord*>m_ById;
Map<string,int>m_Sources;Map<SCR_AIGroup*,int>m_Groups;
int m_NextId=0;bool m_CapLogged=false;
'''+ledger+r'''
};
struct ERR_ForceSimulation{
ERR_ForceLedger* m_Ledger;ERR_ForceSimulation(ERR_ForceLedger* ledger):m_Ledger(ledger){}
'''+simulation+r'''
};
bool near(float a,float b,float e=.005){return std::abs(a-b)<=e;}
ERR_ForceRoute* square(bool loop){
auto r=new ERR_ForceRoute(loop);
assert(r->Add({0,10,0})&&r->Add({14,12,0})&&r->Add({14,14,14})&&r->Add({0,16,14}));
return r;
}
int main(){
using K=ERR_ForceKind;using D=ERR_ForceDuty;using R=ERR_ForceRules;
const float nan=std::numeric_limits<float>::quiet_NaN();
assert(R::Speed(K::GARRISON,D::HOLD)==0);
assert(R::Speed(K::CONVOY,D::MOVE_TO)==11&&near(R::Speed(K::PATROL,D::PATROL_ROUTE),1.4));
assert(R::Speed(static_cast<K>(-1),D::SEARCH)==0);
assert(R::Advance(3,4,false,true)==4&&R::Advance(4,4,false,true)==4);
assert(R::Advance(3,4,true,true)==0&&R::Advance(2,4,true,false)==2);
assert(R::Advance(-1,4,true,true)==-1&&R::Advance(0,65,true,true)==-1);
bool arrived=false;auto p=R::Step({0,10,0},{14,20,0},1.4,5,arrived);
assert(!arrived&&near(p.x,7)&&near(p.y,15));
p=R::Step(p,{14,20,0},1.4,999,arrived);assert(arrived&&p.x==14&&p.y==20);
p=R::Step({0,10,0},{14,20,0},-1,2,arrived);assert(!arrived&&p.x==0);
p=R::Step({0,10,0},{14,20,0},nan,2,arrived);assert(!arrived&&p.x==0);
int due=0;for(int id=0;id<8;id++)due+=R::Due(id,1000,0,2000);
assert(due>0&&due<8&&!R::Due(0,1000,2000,2000)&&!R::Due(0,nan,0,2000));
assert(near(R::Heading({0,0,0},{1,0,0},0),90));
assert(near(R::Heading({0,0,0},{-1,0,0},0),270));
ERR_ForceRoute empty;assert(!empty.Seal());
ERR_ForceRoute zero(true);zero.Add({0,0,0});zero.Add({0,0,0});assert(!zero.Seal());
ERR_ForceRoute cap;for(int i=0;i<64;i++)assert(cap.Add({float(i),0,0}));
assert(!cap.Add({0,0,0})&&cap.Seal()&&!cap.Add({0,0,0}));
ERR_ForceRoute invalid;assert(!invalid.Add({nan,0,0}));
ERR_ForceLedger ledger;
Replication::server=false;assert(!ledger.Register("client",K::PATROL,{0,0,0},4));
Replication::server=true;
assert(!ledger.Register("",K::PATROL,{0,0,0},4));
assert(!ledger.Register("bad",K::PATROL,{nan,0,0},4));
assert(!ledger.Register("bad",K::PATROL,{0,0,0},0));
assert(!ledger.Register("bad",K::PATROL,{0,0,0},33));
assert(!ledger.Register("bad",K::PATROL,{0,0,0},4,nullptr,nullptr,-1));
auto loop=square(true);auto force=ledger.Register("loop",K::PATROL,{0,10,0},4,loop);
assert(force&&loop->Length()==56&&loop->m_Sealed&&force->HomeDuty==D::PATROL_ROUTE);
assert(ledger.Register("loop",K::PATROL,{999,0,0},32,loop)==force&&force->Strength==4);
assert(!ledger.Register("loop",K::CONVOY,{0,0,0},4,loop));
ERR_ForceSimulation sim(&ledger);
// Four legs at 1.4 m/s: return after 40 seconds, with authored elevation along each segment.
for(int t=2000;t<=40000;t+=2000){
sim.Tick(t);
if(t==10000)assert(near(force->Position.x,14)&&near(force->Position.z,0));
if(t==20000)assert(near(force->Position.x,14)&&near(force->Position.z,14));
if(t==30000)assert(near(force->Position.x,0)&&near(force->Position.z,14));
}
assert(vector::DistanceXZ(force->Position,{0,10,0})<.01&&near(force->Position.y,10));
// Huge elapsed distance skips complete laps instead of looping per travelled metre.
sim.Tick(40004000); // total 1,000 laps plus one 4-second fragment
assert(near(force->Position.x,5.6,.02)&&near(force->Position.z,0));
auto line=square(false);auto terminal=ledger.Register("line",K::PATROL,{0,10,0},4,line,nullptr,40004000);
sim.Tick(40044000);assert(terminal->RouteFinished&&terminal->RouteIndex==4&&terminal->Duty==D::HOLD);
assert(near(terminal->Position.x,0)&&near(terminal->Position.z,14)&&terminal->Position.y==16);
sim.Tick(40046000);assert(terminal->RouteIndex==4&&terminal->Position.z==14);
// Movement orders clamp to destination; HOLD does not continue extrapolating.
auto mover=ledger.Register("move",K::CONVOY,{0,0,0},4,nullptr,nullptr,40046000);
mover->Duty=D::MOVE_TO;mover->DutyTarget={22,4,0};sim.Tick(40048000);
assert(mover->Position.x==22&&mover->Position.y==4&&mover->Duty==D::HOLD);
// Physical snapshots preserve losses and body position, including native dormant transitions.
SCR_AIGroup group;
auto physical=ledger.Register("physical",K::GARRISON,{10,0,10},4,nullptr,&group,40048000);
assert(physical&&ledger.FindGroup(&group)==physical);
assert(!ledger.Register("duplicate-group",K::GARRISON,{10,0,10},4,nullptr,&group));
physical->Duty=D::MOVE_TO;physical->DutyTarget={100,0,100};sim.Tick(40050000);
assert(physical->Position.x==10&&physical->LastSimulatedAt==40048000);
assert(ledger.RecordPhysicalState(physical->Id,{20,3,20},45,3,40050000));
assert(physical->Strength==3&&physical->Casualties==1&&physical->Position.x==20);
assert(!ledger.RecordPhysicalState(physical->Id,{30,3,30},45,4,40052000));
assert(!ledger.RecordPhysicalState(physical->Id,{30,3,30},45,3,40049990));
assert(ledger.RecordPhysicalState(physical->Id,{20,3,20},45,3,40052000,false));
assert(!physical->Physical&&physical->Group==&group&&physical->StateSince==40052000);
sim.Tick(40054000);assert(physical->Position.x>20&&physical->Casualties==1);
assert(ledger.RecordPhysicalState(physical->Id,physical->Position,45,3,40054000,true));
assert(physical->Strength==3&&physical->Physical);
assert(ledger.RecordPhysicalState(physical->Id,{30,0,30},0,0,40056000));
assert(physical->Eliminated&&physical->Strength==0&&physical->Casualties==4);
assert(!ledger.RecordPhysicalState(physical->Id,{30,0,30},0,3,40058000));
assert(ledger.Eliminate(physical->Id,40058000));sim.Tick(40060000);assert(physical->Position.x==30);
Replication::server=false;
assert(!ledger.Eliminate(force->Id,40060000)&&!ledger.Unregister(force->Id));
auto before=force->Position;sim.Tick(40062000);assert(force->Position.x==before.x);
Replication::server=true;
assert(ledger.Unregister(physical->Id)&&!ledger.FindGroup(&group));
assert(!ledger.Register("physical",K::GARRISON,{0,0,0},4));
ledger.Clear();assert(ledger.Count()==0&&ledger.m_Sources.Count()==0&&ledger.m_ById.Count()==0);
// All 128 formations remain bounded through a simulated hour; stagger only affects snapshot age.
ERR_ForceLedger hour;ERR_ForceSimulation hourSim(&hour);auto circuit=square(true);
for(int i=0;i<128;i++)assert(hour.Register(std::to_string(i),K::PATROL,{0,10,0},4,circuit));
assert(!hour.Register("overflow",K::PATROL,{0,0,0},4));
assert(!hour.Register("overflow2",K::PATROL,{0,0,0},4)&&logs==1);
for(int t=1000;t<=3600000;t+=1000)hourSim.Tick(t);
assert(hour.Count()==128&&hour.m_Sources.Count()==128&&hour.m_ById.Count()==128);
for(int i=0;i<128;i++){
auto r=hour.At(i);assert(!r->Eliminated&&r->Strength==4&&r->Casualties==0);
assert(3600000-r->LastSimulatedAt<=1000);
float distance=std::fmod(1.4*r->LastSimulatedAt/1000,56);
float actual=0;
if(r->Position.z<.01)actual=r->Position.x;
else if(r->Position.x>13.99)actual=14+r->Position.z;
else if(r->Position.z>13.99)actual=42-r->Position.x;
else actual=56-r->Position.z;
assert(std::min(std::abs(actual-distance),56-std::abs(actual-distance))<.1);
}
assert(hour.Status()=="128,0,128,0,512,0");
assert(hour.Unregister(hour.At(0)->Id));
assert(!hour.Register("replacement",K::PATROL,{0,0,0},4)); // retired identity consumes finite pool
}
''')
for path in (L,S):
text=path.read_text()
for forbidden in ('ERR_HQ', 'GetGame(', 'GetOrigin(', 'GetPlayers(', 'GetControlledEntity(',
'Ground(', 'Spawn(', 'CallLater(', 'EOnFrame(', 'GetWorld(', 'GetAgents('):
assert forbidden not in text,(path,forbidden)
assert all(len(line)<120 for line in text.splitlines()),path
for signature in ('protected void Route(', 'void Tick('):
assert 'new ' not in body(S,signature)
print('PASS: actual force ledger, finite casualties/identity/cap, routes/arrival/elevation/overshoot, server and physical guards, staggered 128-record simulated hour without world queries')
+105
View File
@@ -0,0 +1,105 @@
"""Execute actual HQ assignment ownership and failed-radio backoff bodies."""
from policy_harness import ROOT, body, run
import re
p=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c'
unit='\n'.join(body(p,s) for s in ('void CancelPending(', 'ERR_HQTask PlannedTask(', 'void ReleaseAssignment(', 'void Release()'))
lost='\n'.join(body(p,s) for s in ('protected void FinishWatch(', 'protected void LostOrder(')).replace('protected ', '')
def adapt(code):
code=code.replace('null','nullptr').replace('Math.','Math::').replace('ERR_GameMode.Get()', 'ERR_GameMode::Get()')
code=code.replace('SCR_EntityHelper.DeleteEntityAndChildren', 'SCR_EntityHelper::DeleteEntityAndChildren')
code=code.replace('ERR_HQUnit unit', 'ERR_HQUnit* unit')
code=code.replace('ERR_HQTask ', 'ERR_HQTask* ').replace('ERR_GameMode ', 'ERR_GameMode* ')
code=code.replace('ERR_ForceRecord record;', 'ERR_ForceRecord* record=nullptr;')
code=code.replace('ERR_HQHuntWindow hunt', 'ERR_HQHuntWindow* hunt')
code=code.replace('foreach (ERR_HQUnit other : m_Units)', 'for (ERR_HQUnit* other : m_Units)')
code=re.sub(r'\b(mode|record|Group|unit|other|task|planned|hunt)\.', r'\1->', code)
code=code.replace('mode->Forces().FindGroup', 'mode->Forces()->FindGroup')
return code
run(r'''
#include <cassert>
#include <vector>
#include <algorithm>
#include <string>
struct Math {static float Min(float a,float b){return std::min(a,b);}};
struct ERR_HQTask {bool ReleaseOnly=false;int BeliefId=0;std::string WatchSourceId;int WatchMessageId=0,WatchRequestId=0;bool Investigation(){return !WatchSourceId.empty();}};
struct ERR_HQReport {};
struct ERR_HQRoute {};
struct AIWaypoint {int deleted=0;};
struct SCR_AIGroup {int removals=0; AIWaypoint* last=nullptr;
void RemoveWaypoint(AIWaypoint* p){removals++;last=p;}};
struct ERR_ForceRecord {bool Eliminated=false;int Duty=2,HomeDuty=1,DutyTarget=200,HomePosition=100;};
struct Ledger {ERR_ForceRecord* record=nullptr;ERR_ForceRecord* FindGroup(SCR_AIGroup*){return record;}};
struct ERR_GameMode {Ledger ledger;static ERR_GameMode* current;
static ERR_GameMode* Get(){return current;} Ledger* Forces(){return &ledger;}};
ERR_GameMode* ERR_GameMode::current=nullptr;
struct SCR_EntityHelper {static void DeleteEntityAndChildren(AIWaypoint* p){p->deleted++;}};
struct ERR_HQHuntWindow {float NextHunt=0;};
struct ERR_HQUnit {
SCR_AIGroup* Group=nullptr;AIWaypoint* Waypoint=nullptr;
ERR_HQTask *Task=nullptr,*PendingTask=nullptr;
ERR_HQReport *Assignment=nullptr,*PendingOrder=nullptr;
ERR_HQRoute* Downlink=nullptr;bool PendingCheckin=false;
'''+adapt(unit)+r'''};
struct Watch {int calls=0;std::string source;int message=0,request=0;
bool FinishInvestigation(std::string s,int m,int r,float){calls++;source=s;message=m;request=r;return true;}};
struct HQ {Watch m_Watch;std::vector<ERR_HQUnit*> m_Units;ERR_HQHuntWindow window;
ERR_HQHuntWindow* Hunt(int){return &window;}
'''+adapt(lost)+r'''};
int main(){
ERR_GameMode mode;ERR_GameMode::current=&mode;ERR_ForceRecord record;mode.ledger.record=&record;
SCR_AIGroup group;ERR_HQReport oldReport,newReport;ERR_HQTask oldTask,newTask;ERR_HQRoute route;
oldTask.BeliefId=1;newTask.BeliefId=2;AIWaypoint oldWaypoint;
ERR_HQUnit u;u.Group=&group;u.Waypoint=&oldWaypoint;u.Assignment=&oldReport;u.Task=&oldTask;
u.PendingOrder=&newReport;u.PendingTask=&newTask;u.Downlink=&route;
assert(u.PlannedTask()==&newTask);
u.CancelPending();
assert(!u.PendingOrder&&!u.PendingTask&&!u.Downlink);
assert(u.Task==&oldTask&&u.Assignment==&oldReport&&u.Waypoint==&oldWaypoint);
assert(oldWaypoint.deleted==0&&group.removals==0&&record.Duty==2);
assert(u.PlannedTask()==&oldTask);
// Expiry of the current task must leave its replacement radio transaction intact.
u.PendingOrder=&newReport;u.PendingTask=&newTask;u.Downlink=&route;
u.ReleaseAssignment();
assert(!u.Assignment&&!u.Task&&!u.Waypoint);
assert(u.PendingOrder==&newReport&&u.PendingTask==&newTask&&u.Downlink==&route);
assert(oldWaypoint.deleted==1&&group.removals==1&&group.last==&oldWaypoint);
assert(record.Duty==record.HomeDuty&&record.DutyTarget==record.HomePosition);
u.ReleaseAssignment();assert(oldWaypoint.deleted==1&&group.removals==1);
u.Release();assert(!u.PendingOrder&&!u.PendingTask&&!u.Downlink);
// Elimination and absent group/mode cannot rewrite casualty state or double-delete.
AIWaypoint detached;u.Waypoint=&detached;u.Group=nullptr;record.Eliminated=true;record.Duty=9;
ERR_GameMode::current=nullptr;u.Release();assert(detached.deleted==1&&record.Duty==9);
ERR_GameMode::current=&mode;u.Group=&group;u.ReleaseAssignment();assert(record.Duty==9);
// A failed initial dispatch can retry after a short backoff.
HQ hq;hq.m_Units={&u};hq.window.NextHunt=300000;
u.PendingOrder=&newReport;u.PendingTask=&newTask;u.Downlink=&route;
hq.LostOrder(&u,1000);assert(hq.window.NextHunt==16000&&!u.PendingOrder);
// Failed retask preserves delivered assignment and its active hunt window.
u.Task=&newTask;u.Assignment=&oldReport;u.Waypoint=&oldWaypoint;
u.PendingOrder=&newReport;u.PendingTask=&newTask;u.Downlink=&route;hq.window.NextHunt=300000;
hq.LostOrder(&u,1000);assert(hq.window.NextHunt==300000&&u.Task==&newTask&&u.Waypoint==&oldWaypoint);
// Another group's pending hunt prevents duplicate retry commitments.
u.Task=nullptr;u.Assignment=nullptr;u.Waypoint=nullptr;
ERR_HQUnit peer;peer.PendingTask=&newTask;hq.m_Units.push_back(&peer);
u.PendingTask=&newTask;u.PendingOrder=&newReport;u.Downlink=&route;
hq.LostOrder(&u,2000);assert(hq.window.NextHunt==300000);
// A failed pending watch investigation completes only that pending lineage.
ERR_HQTask activeWatch,pendingWatch;activeWatch.WatchSourceId="patrol-A";activeWatch.WatchMessageId=41;activeWatch.WatchRequestId=1;
pendingWatch.WatchSourceId="patrol-B";pendingWatch.WatchMessageId=42;pendingWatch.WatchRequestId=2;
u.Task=&activeWatch;u.Waypoint=&oldWaypoint;u.Assignment=&oldReport;
u.PendingTask=&pendingWatch;u.PendingOrder=&newReport;u.Downlink=&route;
hq.LostOrder(&u,3000);assert(hq.m_Watch.calls==1&&hq.m_Watch.source=="patrol-B"&&hq.m_Watch.message==42&&hq.m_Watch.request==2);
assert(u.Task==&activeWatch&&u.Waypoint==&oldWaypoint&&u.Assignment==&oldReport);
assert(!u.PendingTask&&!u.PendingOrder&&!u.Downlink&&hq.window.NextHunt==300000);
// Dropped status-check-in transport cannot finish the delivered investigation.
u.PendingCheckin=true;u.Downlink=&route;hq.LostOrder(&u,4000);
assert(!u.PendingCheckin&&!u.Downlink&&u.Task==&activeWatch&&hq.m_Watch.calls==1);
ERR_HQTask recall;recall.ReleaseOnly=true;u.PendingTask=&recall;u.PendingOrder=&newReport;u.Downlink=&route;
hq.LostOrder(&u,5000);assert(!u.PendingTask&&u.Task==&activeWatch&&u.Waypoint==&oldWaypoint&&hq.m_Watch.calls==1);
u.Task=nullptr;u.Waypoint=nullptr;u.Assignment=nullptr;
// Unrelated belief cannot suppress bounded retry, and existing sooner retry remains sooner.
peer.PendingTask=&oldTask;u.PendingTask=&newTask;hq.window.NextHunt=5000;
hq.LostOrder(&u,2000);assert(hq.window.NextHunt==5000);
}
''')
print('PASS: actual assignment cancellation/release preserves radio ownership, native waypoint lifecycle and failed-order retry boundaries')
+150
View File
@@ -0,0 +1,150 @@
"""Execute actual bounded local body observer with a deterministic native API adapter."""
from policy_harness import ROOT, run
from pathlib import Path
import re, os
path=Path(os.environ.get('ERR_BODY_SOURCE', str(ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQBodyObserver.c')))
s=path.read_text()
assert 'GroupLookup' not in s and 'Spawn(' not in s and 'Accept(' not in s and 'm_Contacts' not in s
assert 'GetPlayerManager' not in s and 'DeleteEntity' not in s and 'GetTargets' not in s
code=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('ref ','')
for kind in ('ERR_HQBodyCandidate','ERR_HQBodyScan','ERR_HQReport'):
code=re.sub(r'\b'+kind+r' (?=\w)',kind+'* ',code)
code=code.replace('array<'+kind+'>','Array<'+kind+'*>')
code=code.replace('array<AIAgent>','Array<AIAgent>')
code=code.replace('out vector ','vector& ').replace('out ERR_HQReport* ','ERR_HQReport*& ')
code=code.replace('ERR_GameMode mode','GameMode* mode').replace('ERR_GameMode.Get()','GameMode::Get()')
code=code.replace('ChimeraCharacter character = ChimeraCharacter.Cast(actor);','Entity* character = CastCharacter(actor);')
code=code.replace('TraceParam trace','TraceParam* trace')
code=code.replace('foreach (','for (')
for name in ('Replication','ERR_HQMemoryRules','ERR_HQBodyRules','ERR_Actors','CompartmentAccessComponent','TraceFlags','ECharacterLifeState','vector'):
code=code.replace(name+'.',name+'::')
code=code.replace('null','nullptr')
for name in ('mode','group','force','character','scan','created','agent','actor','entry','candidate','observer','body','trace','state','report','selected'):
code=re.sub(r'\b'+name+r'\.',name+'->',code)
code=code.replace('scan->Group.GetPlayerCount', 'scan->Group->GetPlayerCount')
code=code.replace('mode->Forces().','mode->Forces()->').replace('character->GetCharacterController().','character->GetCharacterController()->')
code=code.replace('GetGame().GetWorld().','GetGame()->GetWorld()->')
code=re.sub(r'(m_(?:Bodies|Roster|Groups)\[[^\]]+\])\.',r'\1->',code)
code=code.replace('"0 1.5 0"','vector{0,1.5,0}').replace('"0 0.3 0"','vector{0,.3,0}')
code=re.sub(r'\b(bool|int|float) (\w+);',r'\1 \2=0;',code)
code=re.sub(r'\b(IEntity|SCR_AIGroup) (\w+);',r'\1 \2=nullptr;',code)
code=code.replace('ERR_HQBodyScan* state;','ERR_HQBodyScan* state=nullptr;').replace('ERR_HQBodyScan* selected;','ERR_HQBodyScan* selected=nullptr;')
code=code.replace('\n}\n','\n};\n').replace('class ', 'struct ')
run(r'''
#include <cassert>
#include <cmath>
#include <vector>
#include <algorithm>
#include <string>
struct vector {float x=0,y=0,z=0;float& operator[](int i){return i==0?x:i==1?y:z;}
vector operator-(vector v)const{return {x-v.x,y-v.y,z-v.z};}
vector operator+(vector v)const{return {x+v.x,y+v.y,z+v.z};}
float Length()const{return std::sqrt(x*x+y*y+z*z);}
vector Normalized()const{float n=Length();return {x/n,y/n,z/n};}
static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}};
template<class T> struct Array:std::vector<T>{using std::vector<T>::vector;
int Count(){return this->size();}bool IsEmpty(){return this->empty();}
void Clear(){this->clear();}void Insert(T v){this->push_back(v);}
void Remove(int n){this->erase(this->begin()+n);}
bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
struct ECharacterLifeState{static constexpr int DEAD=1;};
struct Entity {vector position,forward{0,0,1};bool dead=false,awake=true,inVehicle=false,character=true;int reads=0;
vector GetOrigin(){reads++;return position;}void GetWorldTransform(vector v[4]){v[2]=forward;}
Entity* GetCharacterController(){return character?this:nullptr;}
int GetLifeState(){return dead?1:0;}};
using IEntity=Entity*;Entity* CastCharacter(Entity* p){return p&&p->character?p:nullptr;}
struct Agent {Entity* actor=nullptr;Entity* GetControlledEntity(){return actor;}};using AIAgent=Agent*;
struct Group {Array<AIAgent> agents;int faction=1,players=0;
int GetFaction(){return faction;}int GetPlayerCount(bool){return players;}
void GetAgents(Array<AIAgent>& out){out=agents;}};using SCR_AIGroup=Group*;
struct ForceRecord {bool Physical=true,Eliminated=false;};using ERR_ForceRecord=ForceRecord*;
struct Ledger {ForceRecord force;ERR_ForceRecord FindGroup(Group*){return &force;}};
struct GameMode {Ledger ledger;static GameMode* current;
static GameMode* Get(){return current;}int EnemyFaction(){return 1;}Ledger* Forces(){return &ledger;}};
GameMode* GameMode::current=nullptr;
struct Replication {static bool server;static bool IsServer(){return server;}};bool Replication::server=true;
struct ERR_HQMemoryRules {static bool Finite(float v){return std::isfinite(v);}};
struct ERR_Actors {static bool CanFight(Entity* a){return a&&!a->dead&&a->awake;}};
struct CompartmentAccessComponent {static bool GetVehicleIn(Entity* a){return a->inVehicle;}};
struct TraceFlags {static constexpr int ENTS=1,WORLD=2;};
struct TraceParam {vector Start,End;IEntity Exclude=nullptr,TraceEnt=nullptr;int Flags=0;};
struct World {int traces=0;float fraction=1;IEntity hit=nullptr;
float TraceMove(TraceParam* trace,void*){traces++;trace->TraceEnt=hit;return fraction;}};
struct Game {World world;World* GetWorld(){return &world;}};Game game;Game* GetGame(){return &game;}
int logs=0;void Print(std::string){logs++;}
struct ERR_HQReport {IEntity Target=nullptr;Group* Source=nullptr;vector Position,Velocity;
float ObservedAt=-1;std::string TravelMode;int MessageId=0;bool Distress=false,HasVelocity=false;};
'''+code+r'''
int main(){
assert(ERR_HQBodyRules::InView(12,-1));assert(!ERR_HQBodyRules::InView(12.01,-1));
assert(ERR_HQBodyRules::InView(60,.5));assert(!ERR_HQBodyRules::InView(60.01,1));
assert(!ERR_HQBodyRules::InView(-1,1));assert(!ERR_HQBodyRules::InView(NAN,1));
assert(!ERR_HQBodyRules::Clear(.5,false));assert(ERR_HQBodyRules::Clear(.5,true));
assert(!ERR_HQBodyRules::Clear(NAN,true));
GameMode mode;GameMode::current=&mode;
Entity observer,corpse;corpse.position={0,0,20};corpse.dead=true;
Agent a,b;a.actor=&observer;b.actor=&corpse;Group group;group.agents={&a,&b};
ERR_HQBodyObserver service;service.RegisterGroup(&group);service.RegisterGroup(&group);
assert(service.m_Roster.Count()==2&&service.m_Groups.Count()==1);
service.TickBudget(0);assert(service.m_Bodies.Count()==1&&corpse.reads==0&&game.world.traces==0);
ERR_HQReport* report=nullptr;game.world.fraction=.2;
assert(!service.Observe(&group,0,report)&&!report&&game.world.traces==1);
// Fifteen-second group cadence and five-second global budget are independent.
assert(!service.Observe(&group,5000,report)&&game.world.traces==1);
game.world.fraction=1;assert(service.Observe(&group,15000,report));
assert(report->Target==&corpse&&report->Source==&group&&report->TravelMode=="body");
assert(report->Position.z==20&&report->ObservedAt==15000&&!report->MessageId);
assert(!report->HasVelocity&&!report->Distress);
corpse.position.z=30;assert(report->Position.z==20); // immutable output snapshot
assert(!service.Observe(&group,15001,report));
// Lost radio delivery receives no acknowledgement: fresh visibility permits retry.
assert(service.Observe(&group,30000,report)&&report->Position.z==30);
service.ConfirmDelivered(&corpse);assert(!service.Observe(&group,45000,report));
assert(service.m_Roster.Count()==2&&service.m_Bodies.IsEmpty());
service.Stop();assert(service.m_Roster.IsEmpty()&&service.m_Groups.IsEmpty()&&service.m_Traces==0);
// Unconscious/incapacitated is not a corpse; own physical awake observers only.
corpse.dead=false;corpse.awake=false;service.RegisterGroup(&group);service.TickBudget(0);
assert(service.m_Bodies.IsEmpty());corpse.dead=true;observer.awake=false;
assert(!service.Observe(&group,15000,report));observer.awake=true;observer.inVehicle=true;
assert(!service.Observe(&group,30000,report));observer.inVehicle=false;
mode.ledger.force.Physical=false;assert(!service.Observe(&group,45000,report));
mode.ledger.force.Physical=true;group.players=1;assert(!service.Observe(&group,45000,report));
group.players=0;group.faction=2;assert(!service.Observe(&group,45000,report));group.faction=1;
Replication::server=false;assert(!service.Observe(&group,45000,report));Replication::server=true;
// Twelve native LOS calls maximum across multiple groups in the same interval.
service.Stop();Entity dead[24];Agent da[24];Group many;many.agents={&a};
for(int i=0;i<24;i++){dead[i].dead=true;dead[i].position={0,0,float(i+15)};da[i].actor=&dead[i];many.agents.Insert(&da[i]);}
service.RegisterGroup(&many);game.world.fraction=.2;game.world.traces=0;
assert(!service.Observe(&many,0,report));assert(game.world.traces==12&&service.m_Traces==0);
service.TickBudget(4999);assert(service.m_Traces==0);service.TickBudget(5000);assert(service.m_Traces==12);
assert(service.m_Bodies.Count()==16);
service.TickBudget(4000);assert(service.m_Traces==12&&service.m_Now==5000);
// Candidate window rotates past the first16 undiscovered dead identities.
bool later=false;for(auto* candidate:service.m_Bodies)if(candidate->Corpse==&dead[23])later=true;
assert(later);
// Closed LOS can hit the corpse collider itself; close rear visibility remains trace-gated.
Entity nearBody;nearBody.dead=true;nearBody.position={0,0,-10};vector point;
game.world.hit=&nearBody;assert(service.Visible(&observer,&nearBody,point)&&point.z==-10);
nearBody.position.z=-13;int before=game.world.traces;assert(!service.Visible(&observer,&nearBody,point));
assert(game.world.traces==before);
// Oldest due group wins even when the first three callers exhaust every trace budget.
service.Stop();Group contenders[4];for(int i=0;i<4;i++){contenders[i].agents=many.agents;service.RegisterGroup(&contenders[i]);}
game.world.hit=nullptr;game.world.fraction=.2;
for(int t=0;t<=15000;t+=1000)for(int g=0;g<4;g++)service.Observe(&contenders[g],t,report);
assert(service.m_Groups[3]->NextScan==30000); // fourth group actually consumed the t15s turn
assert(service.m_Groups[0]->NextScan==15000); // first did not leapfrog never-scanned groups
// A caller that becomes inactive can delay others at most one budget window.
service.Stop();service.RegisterGroup(&contenders[0]);service.RegisterGroup(&contenders[1]);
service.m_Groups[0]->LastAsked=0;service.m_Groups[1]->NextScan=1;
assert(!service.Observe(&contenders[1],1000,report));
service.Observe(&contenders[1],6000,report);assert(service.m_Groups[1]->NextScan==21000);
// Registry cap is finite, logs once, and deleted identities free future slots.
service.Stop();logs=0;Entity actors[160];Agent agents[160];Group groups[5];
for(int g=0;g<5;g++){for(int i=0;i<32;i++){int n=g*32+i;agents[n].actor=&actors[n];groups[g].agents.Insert(&agents[n]);}service.RegisterGroup(&groups[g]);}
assert(service.m_Roster.Count()==128&&logs==1);service.RegisterGroup(&groups[4]);assert(logs==1);
for(int i=0;i<32;i++)service.m_Roster[i]->Corpse=nullptr;
service.RegisterGroup(&groups[4]);assert(service.m_Roster.Count()==128);
service.Stop();assert(service.m_Roster.IsEmpty());
}
''')
print('PASS: actual body observer enforces owned physical roster, local death-only candidates, native trace gating, budget/cadence/caps, immutable body-only snapshots, radio retries and confirmed suppression')
+183
View File
@@ -0,0 +1,183 @@
"""Actual HQ convoy wrappers and authenticated command dispatch; native deployment is a double."""
from policy_harness import ROOT,body,run
import re
H=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c'
P=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::'),
('ERR_GameMode.','ERR_GameMode::'),('ERR_ConvoyRoadPlan.','ERR_ConvoyRoadPlan::'),
('ERR_Settings.','ERR_Settings::'),('Math.','Math::'),('array<string>','Array<string>&')]:s=s.replace(old,new)
for typ in ('ERR_GameMode','ERR_ConvoyReceipt','ERR_ConvoyTask','ERR_SiteRuntime','ERR_HQUnit','ERR_AdminProvider'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for var in ('m_Mode','m_Convoys','m_ConvoyPool','m_ConvoyTask','m_HQ','site','unit','receipt','mode','provider'):
s=s.replace(var+'.',var+'->')
s=s.replace('site->Definition.', 'site->Definition->')
for name in ('GetGame()','GetWorld()','Forces()','Sites()','Settings()'):s=s.replace(name+'.',name+'->')
s=s.replace('foreach (ERR_HQUnit* unit : m_Units)','for (ERR_HQUnit* unit : m_Units)')
s=s.replace('foreach (ERR_AdminProvider* provider : m_Providers)','for (ERR_AdminProvider* provider : m_Providers)')
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;')
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('))
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.
admin=body(A,'void Execute(')
admin=cpp(admin[:admin.index(' array<string> parts = {};')]+'\n }')
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <algorithm>
#include <cctype>
#include <map>
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 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();}};
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;}};
struct Group{};struct Record{};
struct Ledger{std::map<Group*,Record*>records;Record* FindGroup(Group* g){return records.count(g)?records[g]:nullptr;}};
struct ERR_HQUnit{struct Group* Group=nullptr;};
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_GameMode{
inline static ERR_GameMode* current=nullptr;bool active=true,settingsReady=true;Ledger ledger;
struct Sites sites;ERR_Settings settings;bool sitesReady=true;
static ERR_GameMode* Get(){return current;}bool Active(){return active;}Ledger* Forces(){return &ledger;}
struct Sites* Sites(){return sitesReady?&sites:nullptr;}ERR_Settings* Settings(){return settingsReady?&settings:nullptr;}
string Status(){return "mission";}bool AbandonRound(){return false;}
};
struct Clock{float GetWorldTime(){return 123000;}Clock* GetWorld(){return this;}}clock_;
Clock* GetGame(){return &clock_;}
struct ERR_ForceRoute{};
struct ERR_ConvoyRoadPlan{
inline static bool allowed=true;inline static int calls=0;inline static vector lastSeed;
static bool Build(vector seed,Array<vector>& positions,Array<vector>& angles,ERR_ForceRoute*& route){
calls++;lastSeed=seed;assert(positions.IsEmpty()&&angles.IsEmpty()&&!route);
for(int i=0;i<6;i++){positions.Insert({float(i),0,0});angles.Insert({0,0,0});}
route=new ERR_ForceRoute();return allowed; // a rejected survey may still write all out parameters
}
};
struct ERR_ConvoyReceipt{bool Consumed=false;};
struct Pool{bool allowed=true;int calls=0,created=0;string source;ERR_ConvoyReceipt receipt;
ERR_ConvoyReceipt* Reserve(string id){calls++;source=id;if(!allowed)return nullptr;
if(created==0){created++;}return &receipt;}
};
struct ERR_ConvoyTask{bool Stopped=false;int retained=0;};
struct Service{
int calls=0,statusCalls=0;bool preflight=true,partial=false;ERR_ConvoyReceipt* last=nullptr;float lastTime=0;
ERR_ConvoyTask task;
ERR_ConvoyTask* Dispatch(ERR_ConvoyReceipt* receipt,Array<vector>& positions,Array<vector>& angles,
ERR_ForceRoute* route,float now){calls++;last=receipt;lastTime=now;
assert(positions.Count()==6&&angles.Count()==6&&route&&!receipt->Consumed);
if(!preflight)return nullptr;
receipt->Consumed=true;task.Stopped=partial;task.retained=partial?3:6;return &task;
}
string Status(){statusCalls++;return task.Stopped?"stopped; retained vehicles=3":"convoy service status";}
};
struct HQ{
ERR_GameMode* m_Mode;Pool* m_ConvoyPool;Service* m_Convoys;ERR_ConvoyTask* m_ConvoyTask=nullptr;
Array<vector>m_ConvoyPositions,m_ConvoyAngles;ERR_ForceRoute* m_ConvoyRoute=nullptr;string m_ConvoyFailure;
Array<ERR_HQUnit*>m_Units;
HQ(ERR_GameMode* mode,Pool* pool,Service* service):m_Mode(mode),m_ConvoyPool(pool),m_Convoys(service){}
string Status(){return "hq status";}void MemoryStatus(Array<string>& r){r.Insert("memory");}
'''+hq+r'''
};
struct ERR_AdminProvider{virtual bool Execute(ERR_GameMode*,string,Array<string>&)=0;virtual ~ERR_AdminProvider()=default;};
struct Provider:ERR_AdminProvider{
HQ* m_HQ;Provider(HQ* hq):m_HQ(hq){}int JournalSequence(){return 0;}
bool ReadJournal(int,Array<string>& r,int& cursor){cursor=0;r.Insert("journal");return true;}
'''+provider+r'''
};
struct Admin{Array<ERR_AdminProvider*>m_Providers;
'''+extension+admin+r'''
};
int main(){
ERR_GameMode mode;ERR_GameMode::current=&mode;Pool pool;Service service;HQ hq(&mode,&pool,&service);
Provider provider(&hq);Admin admin;admin.m_Providers.Insert(&provider);Array<string> replies;
Definition definition;ERR_SiteRuntime site;site.Definition=&definition;mode.sites.site=&site;
// Core authenticated dispatch: no provider call, route scan or reservation without authority/server.
replies.Insert("old");admin.Execute(&mode,false,"hq convoy start",replies);
assert(replies.Count()==1&&replies[0]=="Administrator access required."&&ERR_ConvoyRoadPlan::calls==0&&pool.calls==0);
Replication::server=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(replies.IsEmpty()&&ERR_ConvoyRoadPlan::calls==0&&pool.calls==0);
assert(!hq.PrepareConvoy({})&&!hq.StartConvoy());Replication::server=true;
admin.Execute(nullptr,true,"hq convoy start",replies);assert(replies.IsEmpty()&&pool.calls==0);
admin.Execute(&mode,true,string(161,'x'),replies);assert(replies[0]=="Command is too long."&&pool.calls==0);
mode.settingsReady=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(replies[0]=="Mission settings are not ready."&&pool.calls==0);mode.settingsReady=true;
mode.active=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(pool.calls==0&&ERR_ConvoyRoadPlan::calls==0);mode.active=true;
ERR_GameMode::current=nullptr;assert(!hq.PrepareConvoy({})&&!hq.StartConvoy());ERR_GameMode::current=&mode;
// Read-only status and unsupported commands cannot dispatch.
admin.Execute(&mode,true,"hq convoy show",replies);assert(replies.Count()==1&&pool.calls==0);
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.");
// 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;
site.Definition=nullptr;assert(!hq.StartScenarioConvoy());site.Definition=&definition;
ERR_ConvoyRoadPlan::allowed=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(pool.calls==0&&service.calls==0&&replies[0].find("No sufficiently long")!=string::npos);
assert(!hq.m_ConvoyRoute&&hq.m_ConvoyPositions.IsEmpty()&&hq.m_ConvoyAngles.IsEmpty());
assert(!hq.StartConvoy()&&pool.calls==0); // rejected builder output cannot become a deployment later
ERR_ConvoyRoadPlan::allowed=true;
// An asynchronously pending existing group blocks all new convoy reservations.
Group original;ERR_HQUnit unit;unit.Group=&original;hq.m_Units.Insert(&unit);
admin.Execute(&mode,true," HQ CONVOY START ",replies);
assert(pool.calls==0&&service.calls==0&&mode.sites.last=="everon-supply-1");
assert(ERR_ConvoyRoadPlan::lastSeed.x==12&&ERR_ConvoyRoadPlan::lastSeed.z==45);
assert(replies[0].find("Existing force admission is still pending")!=string::npos);
Record record;mode.ledger.records[&original]=&record;
// Pool refusal remains authoritative; the separate pool suite verifies the 59/60-body arithmetic.
pool.allowed=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(pool.calls==1&&pool.source=="everon-supply-convoy-1"&&service.calls==0&&!hq.m_ConvoyTask);
assert(replies[0].find("physical force budget is unavailable")!=string::npos);pool.allowed=true;
// Preflight failure retains one unconsumed canonical receipt; safe retry does not allocate a new allowance.
service.preflight=false;admin.Execute(&mode,true,"hq convoy start",replies);
assert(pool.created==1&&!pool.receipt.Consumed&&service.calls==1&&!hq.m_ConvoyTask);
assert(replies[0].find("Deployment preflight failed; no convoy created")!=string::npos);
service.preflight=true;admin.Execute(&mode,true,"hq convoy start",replies);
assert(service.calls==2&&pool.created==1&&pool.receipt.Consumed&&hq.m_ConvoyTask==&service.task);
assert(service.last==&pool.receipt&&service.lastTime==123000&&!service.task.Stopped);
int scans=ERR_ConvoyRoadPlan::calls,reserves=pool.calls;
for(int i=0;i<5;i++)admin.Execute(&mode,true,"hq convoy start",replies);
assert(service.calls==2&&pool.calls==reserves&&ERR_ConvoyRoadPlan::calls==scans);
// Partial native failure returns a stopped retained task. HQ must keep it, never retry/refund it.
Pool partialPool;Service partialService;partialService.partial=true;HQ partial(&mode,&partialPool,&partialService);
assert(!partial.StartScenarioConvoy()&&partial.m_ConvoyTask==&partialService.task);
assert(partialPool.receipt.Consumed&&partialService.task.retained==3&&partialService.calls==1);
assert(partial.ConvoyStatus().find("retained vehicles=3")!=string::npos);
scans=ERR_ConvoyRoadPlan::calls;
for(int i=0;i<5;i++)assert(!partial.StartScenarioConvoy()&&!partial.StartConvoy());
assert(partialService.calls==1&&partialPool.calls==1&&ERR_ConvoyRoadPlan::calls==scans);
// A preexisting consumed receipt is rejected even if the wrapper has no task handle.
Pool consumedPool;consumedPool.receipt.Consumed=true;consumedPool.created=1;
Service consumedService;HQ consumed(&mode,&consumedPool,&consumedService);
assert(!consumed.StartScenarioConvoy()&&!consumed.m_ConvoyTask&&consumedService.calls==0);
assert(consumed.ConvoyStatus().find("finite convoy allowance")!=string::npos);
}
''')
for sig in ('bool PrepareConvoy(', 'bool StartConvoy(', 'bool StartScenarioConvoy('):
source=body(H,sig)
for forbidden in ('ERR_Actors.Spawn(', 'DeleteEntity', 'DespawnMembers(', 'SpawnMembers('):assert forbidden not in source
assert body(H,'bool StartConvoy(').count('everon-supply-convoy-1')==1
assert 'm_Convoys.Stop()' in body(H,'override void Stop(')
print('PASS: actual authenticated convoy dispatch, active/ready admission gates, canonical receipt, safe unconsumed preflight retry, pool refusal, retained partial failure and consumed no-retry')
+174
View File
@@ -0,0 +1,174 @@
"""Actual bounded negative-search coverage rules; physical radio delivery is HQ's boundary."""
from policy_harness import ROOT, body, run
P=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQCoverage.c'
PURSUIT=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQPursuit.c'
def cpp(s):
return (s.replace('protected ', '').replace('Math.', 'Math::').replace('vector.', 'vector::')
.replace('ERR_HQPursuitRules.', 'ERR_HQPursuitRules::').replace('ERR_HQCoverageRules.', 'ERR_HQCoverageRules::')
.replace('out vector ', 'vector& ').replace('array<vector> reserved', 'Array<vector>* reserved')
.replace('ERR_HQSearchReport report', 'ERR_HQSearchReport* report').replace('report.', 'report->')
.replace('reserved.Count()', 'reserved->Count()').replace('= null', '= nullptr')
.replace('foreach (vector position : reserved)', 'for (vector position : *reserved)')
.replace('foreach (int known, float searched : m_Cells)', 'for (auto [known, searched] : m_Cells)')
.replace('int oldestKey;', 'int oldestKey=0;').replace('bool found;', 'bool found=false;'))
common='\n'.join(cpp(body(PURSUIT,s)) for s in ('static bool Finite(', 'static bool PositionValid('))
rules='\n'.join(cpp(body(P,s)) for s in ('static bool PositionValid(', 'static int Cell(', 'static int Key(',
'static bool Recent(', 'static bool ValidReport(', 'static bool Point('))
methods='\n'.join(cpp(body(P,s)) for s in ('int Count(', 'void Forget(', 'bool Recent(vector', 'bool Ingest(',
'protected bool Reserved(', 'bool Select(', 'void Clear('))
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <limits>
#include <map>
#include <vector>
struct vector{
float x=0,y=0,z=0;
float& operator[](int i){return i==0?x:i==1?y:z;}
float operator[](int i)const{return i==0?x:i==1?y:z;}
static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
};
vector Vector(float x,float y,float z){return {x,y,z};}
struct Math{
static float Clamp(float n,float lo,float hi){return std::clamp(n,lo,hi);}
static float Floor(float n){return std::floor(n);}
static float AbsFloat(float n){return std::abs(n);}
};
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}};
template<class K,class V>struct Map:std::map<K,V>{
bool Contains(K k){return this->count(k);}V Get(K k){return this->count(k)?this->at(k):V{};}
int Count(){return this->size();}void Set(K k,V v){(*this)[k]=v;}
void Remove(K k){this->erase(k);}void Clear(){this->clear();}
};
struct ERR_HQPursuitRules{
'''+common+r'''
};
struct ERR_HQCoverageRules{
'''+rules+r'''
};
struct ERR_HQSearchReport{
int SourceId=1,MessageId=1,BeliefId=1,BeliefRevision=1;
vector Position{250,12,250};float ObservedAt=0;
bool SweepCompleted=true,ContactFound=false;
};
struct Coverage{
Map<int,float>m_Cells;Map<int,int>m_Messages;
'''+methods+r'''
};
int main(){
using R=ERR_HQCoverageRules;
assert(R::Cell(0)==0&&R::Cell(499.9)==0&&R::Cell(500)==1);
assert(R::Cell(-.1)==-1&&R::Cell(-500)==-1&&R::Cell(-500.1)==-2);
assert(R::Key({-1,0,0})!=R::Key({0,0,-1}));
assert(R::Recent(0,900000)&&!R::Recent(0,900001)&&!R::Recent(1,0));
assert(R::ValidReport(1,1,1,1,true,false,0,120000));
assert(!R::ValidReport(1,1,1,1,true,false,0,120001));
assert(!R::ValidReport(1,1,1,1,false,false,0,1000));
assert(!R::ValidReport(1,1,1,1,true,true,0,1000));
assert(!R::ValidReport(1,1,0,1,true,false,0,1000));
assert(!R::ValidReport(1,1,1,0,true,false,0,1000));
float nan=std::numeric_limits<float>::quiet_NaN();
assert(!R::ValidReport(1,1,1,1,true,false,nan,1000));
assert(!R::PositionValid({nan,0,0})&&!R::PositionValid({1000000,0,0}));
vector point;
assert(R::Point(0,0,{0,12,0},0,point)&&point.x==0&&point.z==0);
assert(R::Point(0,0,{180,12,180},25,point)&&point.x==180&&point.z==180);
assert(!R::Point(2,2,{180,12,180},25,point));
// Undelivered reports remain inert objects. Only Ingest applies the delivered snapshot.
Coverage coverage;ERR_HQSearchReport report;
assert(coverage.Count()==0&&!coverage.Recent(report.Position,1000));
assert(coverage.Ingest(&report,1000)&&coverage.Count()==1);
assert(coverage.Recent(report.Position,900000)&&!coverage.Recent(report.Position,900001));
assert(!coverage.Ingest(&report,1000));
report.Position={750,12,250};report.BeliefRevision=2;
assert(!coverage.Ingest(&report,1000)&&!coverage.Recent(report.Position,1000));
report.MessageId=2;report.ObservedAt=1000;
assert(coverage.Ingest(&report,2000)&&coverage.Count()==2);
report.MessageId=1;report.Position={1250,12,250};
assert(!coverage.Ingest(&report,2000));
// Invalid/future/old/incomplete/positive observations never advance replay guards.
ERR_HQSearchReport invalid;invalid.SourceId=2;
for(int field=0;field<8;field++){
invalid=ERR_HQSearchReport{};invalid.SourceId=2;
if(field==0)invalid.ObservedAt=2001;
if(field==1)invalid.SweepCompleted=false;
if(field==2)invalid.ContactFound=true;
if(field==3)invalid.BeliefId=0;
if(field==4)invalid.BeliefRevision=0;
if(field==5)invalid.Position.x=nan;
if(field==6)invalid.MessageId=0;
if(field==7)invalid.ObservedAt=-1;
assert(!coverage.Ingest(&invalid,2000)&&!coverage.m_Messages.Contains(2));
}
assert(!coverage.Ingest(nullptr,2000));
// A fresh positive sighting may reopen a coarse cell, without replaying an old negative.
Coverage reopened;ERR_HQSearchReport searched;
assert(reopened.Ingest(&searched,1000));
reopened.Forget({1,12,1});assert(reopened.Count()==0);
assert(!reopened.Ingest(&searched,1000));
searched.MessageId=2;searched.ObservedAt=2000;
assert(reopened.Ingest(&searched,2000));
reopened.Forget({nan,0,0});assert(reopened.Count()==1);
// Delivery latency does not restart the 900-second coverage lifetime.
Coverage delayed;ERR_HQSearchReport late;
assert(delayed.Ingest(&late,120000));
assert(delayed.Recent(late.Position,900000)&&!delayed.Recent(late.Position,900001));
Coverage old;assert(!old.Ingest(&late,120001)&&old.Count()==0);
// Bounded cell retention evicts the oldest only for genuinely newer coverage.
Coverage bounded;ERR_HQSearchReport item;
for(int i=0;i<128;i++){
item.MessageId=i+1;item.Position={float(i*500+250),12,250};item.ObservedAt=1000+i;
assert(bounded.Ingest(&item,2000));
}
assert(bounded.Count()==128);
item.MessageId=129;item.Position={70000,12,250};item.ObservedAt=999;
assert(!bounded.Ingest(&item,2000)&&bounded.Count()==128);
item.MessageId=130;item.ObservedAt=1500;
assert(bounded.Ingest(&item,2000)&&bounded.Count()==128);
assert(!bounded.Recent({250,12,250},2000)&&bounded.Recent(item.Position,2000));
// Replay watermarks do not grow or evict an old source's identity under pressure.
Coverage sources;
for(int i=1;i<=128;i++){
item=ERR_HQSearchReport{};item.SourceId=i;item.ObservedAt=i;
assert(sources.Ingest(&item,1000));
}
item.SourceId=129;item.ObservedAt=129;
assert(!sources.Ingest(&item,1000)&&sources.m_Messages.Count()==128);
item.SourceId=1;item.MessageId=2;item.ObservedAt=130;
assert(sources.Ingest(&item,1000)&&sources.m_Messages.Count()==128);
// Nearest unswept candidates remain inside uncertainty, including a small circle.
Coverage search;vector center{250,12,250},destination;
assert(search.Select(center,25,{1000,0,250},1000,destination));
assert(R::Key(destination)==R::Key(center)&&vector::DistanceXZ(center,destination)<=25);
item=ERR_HQSearchReport{};assert(search.Ingest(&item,1000));
assert(!search.Select(center,25,{1000,0,250},1000,destination));
assert(search.Select(center,25,{1000,0,250},900001,destination));
assert(search.Select(center,600,{1000,0,250},1000,destination));
assert(destination.x==750&&destination.z==250);
Array<vector> reserved;reserved.push_back({501,0,1}); // different point, same reserved cell
assert(search.Select(center,600,{1000,0,250},1000,destination,&reserved));
assert(R::Key(destination)!=R::Key(reserved[0]));
assert(vector::DistanceXZ(center,destination)<=600);
Coverage tiny;reserved.clear();reserved.push_back({1,0,1});
assert(!tiny.Select(center,25,center,1000,destination,&reserved));
for(int i=0;i<7;i++)reserved.push_back(center);
assert(reserved.Count()==8&&tiny.Select(center,2000,center,1000,destination,&reserved));
reserved.push_back(center);
assert(!tiny.Select(center,2000,center,1000,destination,&reserved));
assert(!tiny.Select(center,2001,center,1000,destination));
assert(!tiny.Select(center,-1,center,1000,destination));
assert(!tiny.Select(center,nan,center,1000,destination));
assert(!tiny.Select(center,100,center,-1,destination));
assert(!tiny.Select({nan,0,0},100,center,1000,destination));
assert(!tiny.Select(center,100,{nan,0,0},1000,destination));
sources.Clear();assert(sources.Count()==0&&sources.m_Messages.Count()==0);
}
''')
source=P.read_text()
for forbidden in ('GetGame(', 'GetOrigin(', 'GetPlayers(', 'GetControlledEntity(', 'Spawn(', 'CallLater(', 'IEntity'):
assert forbidden not in source
assert all(len(line)<120 for line in source.splitlines())
print('PASS: actual bounded delivered-negative coverage, replay/freshness/expiry/invalid guards, oldest eviction, small-circle and nearest-unswept selection, reserved-cell exclusion and cleanup')
+53
View File
@@ -0,0 +1,53 @@
"""Execute the actual objective-inference rules; audit the HQ ingestion boundaries."""
from policy_harness import ROOT, body, run
P=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQDoctrine.c'
methods='\n'.join(body(P,x) for x in ('static float Threat(', 'static bool Forward(', 'static vector Approach('))
methods=methods.replace('Math.','Math::').replace('vector.','vector::').replace('ERR_HQPursuitRules.','ERR_HQPursuitRules::').replace('float alignment;', 'float alignment=0;')
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
struct vector {float x=0,y=0,z=0; vector()=default;vector(float a,float b,float c):x(a),y(b),z(c){}
float& operator[](int i){return i==0?x:i==1?y:z;}
vector operator-(vector b)const{return {x-b.x,y-b.y,z-b.z};}
vector operator+(vector b)const{return {x+b.x,y+b.y,z+b.z};}
vector operator*(float n)const{return {x*n,y*n,z*n};}
float Length()const{return std::sqrt(x*x+y*y+z*z);}
vector Normalized()const{return *this*(1/Length());}
static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}};
struct Math {static float Max(float a,float b){return std::max(a,b);}static float Clamp(float n,float a,float b){return std::clamp(n,a,b);}};
struct ERR_HQPursuitRules {
static bool Finite(float v){return std::isfinite(v);}
static bool PositionValid(vector v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}
static bool Heading(vector v,bool known){return known&&PositionValid(v)&&v.Length()>=.5&&v.Length()<=70;}
static vector Direction(vector v,bool){v.y=0;return v.Normalized();}
};
struct Doctrine {'''+methods+r'''};
int main(){
vector origin{1000,0,1000},north{1000,0,3000},south{1000,0,0},velocity{0,0,4};
assert(Doctrine::Forward(north,origin,velocity,true));
assert(!Doctrine::Forward(south,origin,velocity,true));
assert(!Doctrine::Forward(north,origin,velocity,false));
assert(!Doctrine::Forward({1000,0,1100},origin,velocity,true));
assert(Doctrine::Threat(north,origin,velocity,true,1,50)>Doctrine::Threat(south,origin,velocity,true,1,50));
assert(std::abs(Doctrine::Threat(north,origin,velocity,false,0,50)-.2)<.0001);
assert(Doctrine::Threat(north,origin,velocity,true,.3,500)<Doctrine::Threat(north,origin,velocity,true,1,500));
auto ahead=Doctrine::Approach(north,origin);assert(std::abs(ahead.z-1200)<.001&&ahead.x==1000);
auto close=Doctrine::Approach({1000,0,1300},origin);assert(std::abs(close.z-1100)<.001);
auto same=Doctrine::Approach(origin,origin);assert(same.x==origin.x&&same.z==origin.z);
}
''')
hq=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c'
a=body(hq,'protected void Accept(')
assert 'm_Memory.Ingest(report, now)' in a and 'm_Coverage.Forget(report.Position)' in a
sweep=body(hq,'protected void AcceptSweep(')
assert sweep.index('belief.BeliefRevision != report.BeliefRevision')<sweep.index('m_Coverage.Ingest')
observe=body(hq,'protected void Observe(')
assert 'm_Memory.' not in observe and 'Accept(' not in observe
assert 'GetPlayer' not in P.read_text() and 'GetOrigin' not in P.read_text()
delivery=body(hq,'protected void DeliverOrder(')
assert delivery.index('if (!waypoint)')<delivery.index('unit.Release();')
assert 'LostOrder(unit, now)' in delivery
unit=body(hq,'void CancelPending(')
assert 'Waypoint' not in unit and 'Assignment' not in unit
print('PASS: actual objective heading/threat/approach rules, persistent doctrine weight, fresh-contact coverage invalidation, stale-sweep rejection and atomic radio retask ownership')
+149
View File
@@ -0,0 +1,149 @@
"""Run the actual bounded memory implementation with value-only native adapters.
No fake world polling: identity stubs deliberately have no position/health API.
"""
import re
from policy_harness import ROOT, run
path = ROOT / 'addons/ai/Scripts/Game/ERR/ERR_HQMemory.c'
source = path.read_text()
for forbidden in ('GetOrigin(', 'GetControlledEntity(', 'GetPlayerManager(', 'GetDamageManager(', 'GetWorld('):
assert forbidden not in source
assert max(map(len, source.splitlines())) <= 120
code = source
for kind in ('ERR_HQReport', 'ERR_HQMemoryTrack'):
code = code.replace('ref array<ref ' + kind + '>', 'Array<' + kind + '*>')
for kind in ('int', 'float'):
code = code.replace('ref array<' + kind + '>', 'Array<' + kind + '>')
code = code.replace('protected ', '').replace('ref ERR_HQReport ', 'ERR_HQReport ')
code = re.sub(r'\b(ERR_HQReport|ERR_HQBelief|ERR_HQMemoryTrack) ([A-Za-z_]\w*)', r'\1* \2', code)
code = re.sub(r'class (\w+)\n\{', r'class \1\n{\npublic:', code)
code = re.sub(r'^}', '};', code, flags=re.M)
code = re.sub(r'\bvector\b', 'Vec', code)
for cls in ('ERR_HQMemoryRules', 'ERR_HQMemoryKind', 'Math', 'Vec'):
code = code.replace(cls + '.', cls + '::')
for name in ('report', 'last', 'belief', 'previous', 'track'):
code = re.sub(r'\b' + name + r'\.', name + '->', code)
code = re.sub(r'm_Tracks\[([^\]]+)\]\.', r'm_Tracks[\1]->', code)
code = code.replace('Latest().', 'Latest()->').replace('null', 'nullptr')
code = re.sub(r'^(\s+(?:IEntity|Vec|bool|float|int|string|ERR_HQReport\*|ERR_HQMemoryTrack\*) \w+);$',
r'\1{};', code, flags=re.M)
run(r'''
#include <cassert>
#include <cmath>
#include <limits>
#include <algorithm>
#include <vector>
#include <string>
#include <type_traits>
using string=std::string;
struct Identity {int id;};
using IEntity=Identity*;
struct Vec {
float v[3]={0,0,0};Vec()=default;Vec(float x,float y,float z):v{x,y,z}{}
float& operator[](int i){return v[i];}float operator[](int i)const{return v[i];}
Vec operator+(Vec b)const{return {v[0]+b[0],v[1]+b[1],v[2]+b[2]};}
Vec operator-(Vec b)const{return {v[0]-b[0],v[1]-b[1],v[2]-b[2]};}
Vec operator*(float b)const{return {v[0]*b,v[1]*b,v[2]*b};}
Vec operator/(float b)const{return {v[0]/b,v[1]/b,v[2]/b};}
float Length()const{return std::sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);}
Vec Normalized()const{return *this/Length();}
static float DistanceXZ(Vec a,Vec b){return std::hypot(a[0]-b[0],a[2]-b[2]);}
};
template<class T>struct Array:std::vector<T>{
void Clear(){this->clear();}int Count(){return this->size();}
void Insert(T v){this->push_back(v);}void Remove(int i){assert(i>=0&&i<Count());this->erase(this->begin()+i);}
bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}
};
namespace Math {
template<class A,class B>auto Max(A a,B b){using T=std::common_type_t<A,B>;return std::max(T(a),T(b));}
template<class A,class B,class C>auto Clamp(A a,B b,C c){using T=std::common_type_t<A,B,C>;return std::clamp(T(a),T(b),T(c));}
float AbsFloat(float v){return std::abs(v);}float Pow(float a,float b){return std::pow(a,b);}
float Atan2(float a,float b){return std::atan2(a,b);}constexpr float RAD2DEG=57.295779513f;
}
struct ERR_HQReport {
int MessageId=0;IEntity Target=nullptr,Source=nullptr;Vec Position,Velocity;
float ObservedAt=-1;bool Distress=false,HasVelocity=false;string TravelMode="unknown";
ERR_HQReport* Snapshot(){return new ERR_HQReport(*this);}
};
''' + code + r'''
ERR_HQReport Report(int id,IEntity target,float when,Vec pos={0,0,0},string mode="on foot"){
ERR_HQReport r;r.MessageId=id;r.Target=target;r.Source=target;r.ObservedAt=when;r.Position=pos;r.TravelMode=mode;return r;
}
bool near(float a,float b,float tolerance=.001){return std::abs(a-b)<tolerance;}
int main(){
using Rules=ERR_HQMemoryRules;
float nan=std::numeric_limits<float>::quiet_NaN(),inf=std::numeric_limits<float>::infinity();
assert(near(Rules::Confidence(0),1)&&near(Rules::Confidence(600),.5)&&near(Rules::Confidence(1200),.25));
assert(Rules::Confidence(-1)==0&&Rules::Confidence(1,0)==0&&Rules::Confidence(nan)==0&&Rules::Confidence(inf)==0);
assert(near(Rules::Radius(0,1.2,25),25)&&near(Rules::Radius(600,1.2,25),745));
assert(Rules::Radius(1,nan,25)>2000&&!Rules::Actionable(.5,2001)&&!Rules::Actionable(nan,20));
assert(Rules::RadiusGrowth(ERR_HQMemoryKind::SIGHTING,"vehicle")>Rules::RadiusGrowth(ERR_HQMemoryKind::SIGHTING,"on foot"));
assert(!Rules::DeliveryFresh(10,9)&&!Rules::DeliveryFresh(-1,9)&&!Rules::DeliveryFresh(0,120001));
assert(Rules::DeliveryFresh(0,120000)&&!Rules::DeliveryFresh(nan,0));
assert(!Rules::PositionValid({nan,0,0})&&!Rules::PositionValid({0,inf,0})&&!Rules::PositionValid({1000001,0,0}));
assert(Rules::MotionCredible(10,60,"on foot","on foot"));
for(float gap:{0.f,1.f,121.f,nan})assert(!Rules::MotionCredible(gap,10,"on foot","on foot"));
assert(!Rules::MotionCredible(10,130,"on foot","on foot"));
assert(!Rules::MotionCredible(10,20,"unknown","unknown")&&!Rules::MotionCredible(10,20,"on foot","vehicle"));
assert(near(Rules::Predict({0,40,0},{100,10,0},100)[0],300));
assert(near(Rules::Predict({0,40,0},{1,10,0},100)[0],30)&&near(Rules::Predict({0,40,0},{1,10,0},100)[1],40));
Identity a{1},b{2},friendly{3};ERR_HQMemory memory;
auto first=Report(1,&a,1000,{10,40,10});first.HasVelocity=true;first.Velocity={100,0,100};
assert(memory.Ingest(&first,2000));auto belief=memory.Get(&a);int beliefId=belief->BeliefId;
assert(belief->BeliefRevision==1&&belief->SampleCount==1&&!belief->HasVelocity&&belief->EvaluatedAt==2000);
first.Position={999,999,999};first.TravelMode="aircraft";belief->Position={777,0,777};belief->Report->Position={888,0,888};
assert(near(memory.Get(&a)->Position[0],10)&&memory.Get(&a)->TravelMode=="on foot");
auto next=Report(2,&a,11000,{70,80,10});assert(memory.Ingest(&next,16000));belief=memory.GetById(beliefId);
assert(belief->BeliefRevision==2&&belief->HasVelocity&&near(belief->Speed,6)&&near(belief->HeadingDegrees,90));
assert(near(belief->PredictedPosition[0],100)&&near(belief->PredictedPosition[1],80));
memory.Tick(611000);belief=memory.GetById(beliefId);
assert(belief&&near(belief->Confidence,.5)&&near(belief->Radius,745)&&near(belief->PredictedPosition[0],250));
assert(belief->PredictionSeconds==30&&belief->Actionable());
memory.Tick(610000);assert(memory.Get(&a)->EvaluatedAt==611000);memory.Tick(nan);assert(memory.Get(&a)->EvaluatedAt==611000);
assert(!memory.Ingest(&next,16000)&&!memory.Ingest(&next,611000));
assert(memory.NegativeSearch(beliefId,2,611000));belief=memory.Get(&a);
assert(near(belief->Confidence,.325)&&belief->ObservedAt==11000&&belief->SampleCount==2&&belief->BeliefRevision==2);
assert(!memory.NegativeSearch(beliefId,2,611000)&&!memory.NegativeSearch(beliefId,1,611000));
auto renewed=Report(3,&a,611000,{100,80,10});assert(memory.Ingest(&renewed,611000));
assert(near(memory.Get(&a)->Confidence,1)&&memory.Get(&a)->BeliefRevision==3&&!memory.Get(&a)->HasVelocity);
assert(!memory.NegativeSearch(beliefId,2,611000));
// Per-kind identity protects a moving contact from uncertain friendly/body information.
auto distress=Report(4,&a,611000,{500,0,500});distress.Distress=true;distress.Source=&friendly;
assert(memory.Ingest(&distress,612000)&&memory.Count()==2&&memory.Get(&a)->Kind==ERR_HQMemoryKind::SIGHTING);
auto distressBelief=memory.At(1);assert(distressBelief->Target==nullptr&&!distressBelief->HasVelocity&&!distressBelief->Actionable());
auto body=Report(5,&a,612000,{100,0,10},"body");assert(memory.Ingest(&body,612000));
assert(memory.At(2)->Kind==ERR_HQMemoryKind::BODY&&!memory.At(2)->HasVelocity&&memory.Get(&a)->BeliefRevision==3);
assert(!memory.NegativeSearch(memory.At(2)->BeliefId,1,612000));
// A deleted engine identity cannot erase or rebind an already numbered belief.
memory.m_Tracks[0]->Key=nullptr;memory.m_Tracks[0]->Latest()->Target=nullptr;
assert(memory.GetById(beliefId)&&near(memory.GetById(beliefId)->Position[0],100));
memory.Tick(2300000);assert(memory.Count()==0&&!memory.GetById(beliefId));
memory.Clear();auto restart=Report(1,&a,0);assert(memory.Ingest(&restart,0));assert(memory.Get(&a)->BeliefId>beliefId);
// Delayed legitimate deliveries for another track survive arbitrarily high fresh IDs.
memory.Clear();auto high=Report(100000,&a,10000);assert(memory.Ingest(&high,11000));
auto delayed=Report(2,&b,9000);assert(memory.Ingest(&delayed,12000));
auto replay=delayed;replay.ObservedAt=12000;replay.Position={500,0,0};assert(!memory.Ingest(&replay,12000));
auto stale=Report(200000,&a,0);assert(!memory.Ingest(&stale,130001));
auto low=Report(3,&b,130001);assert(memory.Ingest(&low,130001));
auto future=Report(999999,&a,130003);assert(!memory.Ingest(&future,130002));
auto good=Report(4,&b,130002);assert(memory.Ingest(&good,130002));
// Strictly chronological samples, limited history and conservative discontinuities.
memory.Clear();for(int i=1;i<=12;i++){auto r=Report(i,&a,i*2000,{float(i*2),0,0});assert(memory.Ingest(&r,i*2000));}
assert(memory.Get(&a)->SampleCount==8&&memory.Get(&a)->BeliefRevision==12&&memory.Get(&a)->HasVelocity);
auto old=Report(13,&a,23000);assert(!memory.Ingest(&old,25000)&&memory.Get(&a)->BeliefRevision==12);
auto jump=Report(14,&a,26000,{10000,0,0});assert(memory.Ingest(&jump,26000)&&!memory.Get(&a)->HasVelocity);
// No equal/weak burst can evict a stronger current sighting; later stronger reports can.
memory.Clear();Identity people[70];for(int i=0;i<70;i++)people[i].id=i+10;
for(int i=0;i<64;i++){auto r=Report(i+1,&people[i],0);assert(memory.Ingest(&r,0));}
auto over=Report(65,&people[64],0);assert(!memory.Ingest(&over,0)&&memory.Count()==64);
over.ObservedAt=10000;assert(memory.Ingest(&over,10000)&&memory.Count()==64);
auto weak=Report(66,&people[65],10000);weak.Distress=true;assert(!memory.Ingest(&weak,10000));
assert(!memory.At(-1)&&!memory.At(64)&&!memory.Get(nullptr));
// The global replay table is bounded even when valid observation IDs arrive rapidly.
memory.Clear();for(int i=1;i<=4096;i++){auto r=Report(i,&a,float(i));assert(memory.Ingest(&r,float(i)));}
auto full=Report(4097,&a,4097);assert(!memory.Ingest(&full,4097)&&memory.m_Seen.Count()==4096);
memory.Tick(124096);full.ObservedAt=124096;assert(memory.Ingest(&full,124096)&&memory.m_Seen.Count()==2);
auto expired=Report(1,&b,1);assert(!memory.Ingest(&expired,124096));
}
''')
print('PASS: actual HQ memory rules/service preserve delivered snapshots, chronology, replay bounds, decay, motion limits, identity and negative-search revisions')
+162
View File
@@ -0,0 +1,162 @@
"""Execute actual delivered-belief pursuit rules as C++; no engine/AI behavior claim."""
import re
from policy_harness import ROOT, body, run
P = ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQPursuit.c'
source = P.read_text()
methods = '\n'.join(body(P, signature) for signature in (
'static bool Finite(', 'static bool PositionValid(', 'static int Limit(', 'static bool Actionable(',
'static bool Heading(', 'static ERR_HQPursuitRole Role(', 'static float LifetimeSeconds(',
'static float TaskLifetimeSeconds(', 'static bool CanCommit(', 'static int Bearing(', 'static bool BearingAllowed(', 'static vector Direction(',
'static vector Objective(', 'static float Score(', 'static bool Better('))
methods = methods.replace('Math.', 'Math::').replace('vector.', 'vector::')
methods = methods.replace('ERR_HQPursuitRole.', 'ERR_HQPursuitRole::')
methods = methods.replace('vector offset;', 'vector offset{};')
run(r'''
#include <cassert>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <limits>
#include <vector>
#include <set>
enum class ERR_HQPursuitRole { SEARCH, INTERCEPT, CONTAIN };
struct Math {
static float Clamp(float n,float lo,float hi){return std::clamp(n,lo,hi);}
static float AbsFloat(float n){return std::abs(n);}
static float Min(float a,float b){return std::min(a,b);}
};
struct vector {
float x=0,y=0,z=0;
vector()=default;
vector(float a,float b,float c):x(a),y(b),z(c){}
vector(const char* text){sscanf(text,"%f %f %f",&x,&y,&z);}
float& operator[](int i){return i==0?x:i==1?y:z;}
float operator[](int i)const{return i==0?x:i==1?y:z;}
vector operator+(vector b)const{return {x+b.x,y+b.y,z+b.z};}
vector operator-(vector b)const{return {x-b.x,y-b.y,z-b.z};}
vector operator*(float v)const{return {x*v,y*v,z*v};}
float Length()const{return std::sqrt(x*x+y*y+z*z);}
vector Normalized()const{float n=Length();return {x/n,y/n,z/n};}
static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}
static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
};
vector Vector(float a,float b,float c){return {a,b,c};}
struct Rules {
'''+methods+r'''
};
bool near(float a,float b){return std::abs(a-b)<0.001f;}
int main(){
using Role=ERR_HQPursuitRole;
assert(Rules::Limit(-1)==0&&Rules::Limit(0)==0&&Rules::Limit(1)==1);
assert(Rules::Limit(3)==3&&Rules::Limit(8)==3);
assert(Rules::Actionable(.9,10,50)&&Rules::Actionable(.15,600,2000));
assert(!Rules::Actionable(.149,10,50)&&Rules::Actionable(.9,601,50));
assert(Rules::Actionable(.2,1200,1900)&&!Rules::Actionable(.2,1200,2001));
assert(!Rules::Actionable(.9,-1,50)&&!Rules::Actionable(.9,0,-1));
float nan=std::numeric_limits<float>::quiet_NaN();
float inf=std::numeric_limits<float>::infinity();
assert(!Rules::Actionable(nan,0,50)&&!Rules::Actionable(.9,inf,50));
assert(!Rules::Actionable(.9,0,nan)&&!Rules::Actionable(1.1,0,50));
vector north{0,0,5}, center{1000,12,1000};
assert(Rules::Heading(north,true)&&!Rules::Heading(north,false));
assert(!Rules::Heading({0,0,0},true)&&!Rules::Heading({0,0,71},true));
assert(!Rules::Heading({nan,0,5},true));
assert(Rules::Role(.9,10,50,true,0)==Role::INTERCEPT);
assert(Rules::Role(.9,10,50,true,1)==Role::SEARCH);
assert(Rules::Role(.9,10,50,true,2)==Role::CONTAIN);
for(int slot=0;slot<3;slot++){
assert(Rules::Role(.64,10,50,true,slot)==Role::SEARCH);
assert(Rules::Role(.9,46,50,true,slot)==Role::SEARCH);
assert(Rules::Role(.9,10,151,true,slot)==Role::SEARCH);
assert(Rules::Role(.9,10,50,false,slot)==Role::SEARCH);
}
assert(Rules::Role(.79,10,50,true,2)==Role::SEARCH);
assert(Rules::Role(.9,10,101,true,2)==Role::SEARCH);
assert(Rules::Role(.9,10,50,true,-1)==Role::SEARCH);
assert(Rules::LifetimeSeconds(Role::INTERCEPT)==60);
assert(Rules::LifetimeSeconds(Role::CONTAIN)==90);
assert(Rules::LifetimeSeconds(Role::SEARCH)==120);
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,0)==120);
assert(near(Rules::TaskLifetimeSeconds(Role::SEARCH,2000),2000.f/3+45));
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,5000)==900);
assert(Rules::TaskLifetimeSeconds(Role::INTERCEPT,2000)==60);
assert(Rules::TaskLifetimeSeconds(Role::CONTAIN,2000)==90);
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,-1)==0);
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,nan)==0);
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,inf)==0);
// The caller must already own a physical, eligible and unassigned formation.
assert(Rules::CanCommit(true,true,false,false,0,0,3));
assert(!Rules::CanCommit(false,true,false,false,0,0,3));
assert(!Rules::CanCommit(true,false,false,false,0,0,3));
assert(!Rules::CanCommit(true,true,true,false,0,0,3));
assert(!Rules::CanCommit(true,true,false,false,0,-1,3));
assert(!Rules::CanCommit(true,true,false,false,0,0,0));
assert(!Rules::CanCommit(true,true,false,false,0,3,99));
assert(!Rules::CanCommit(true,true,false,true,1,0,3));
int homeAvailable=3,assigned=0;
for(int i=0;i<5;i++)if(Rules::CanCommit(true,true,false,true,homeAvailable,assigned,3)){
homeAvailable--;assigned++;
}
assert(homeAvailable==1&&assigned==2);
// Pending orders spend the slot immediately, and each busy unit is ineligible.
bool busy=false;int commitments=0;
for(int i=0;i<5;i++)if(Rules::CanCommit(true,true,busy,false,1,commitments,3)){
busy=true;commitments++;
}
assert(commitments==1);
vector intercept=Rules::Objective(center,north,true,.9,10,50,Role::INTERCEPT,0);
assert(near(intercept.x,1000)&&near(intercept.z,1065)&&near(intercept.y,12));
vector contain=Rules::Objective(center,north,true,.9,10,50,Role::CONTAIN,2);
assert(near(contain.x,1080)&&near(contain.z,1080));
vector stale=Rules::Objective(center,north,true,.4,180,400,Role::INTERCEPT,0);
assert(near(stale.z,1250)); // degraded to a bounded search sector, no velocity lead
for(int slot=-8;slot<9;slot++)for(float radius:{0.f,50.f,200.f,2000.f}){
vector objective=Rules::Objective(center,north,true,.2,300,radius,Role::SEARCH,slot);
assert(vector::DistanceXZ(center,objective)<=300&&near(objective.y,center.y));
}
vector maximum=Rules::Objective(center,{0,0,70},true,.9,0,50,Role::INTERCEPT,0);
assert(near(maximum.z,1200)); // future lead capped even for a fast vehicle
vector noDoubleAge=Rules::Objective(center,north,true,.9,40,50,Role::INTERCEPT,0);
assert(near(noDoubleAge.z,1060)); // supplied position is already memory-predicted
vector first=Rules::Objective(center,north,false,.4,180,100,Role::SEARCH,0);
vector second=Rules::Objective(center,north,false,.4,180,100,Role::SEARCH,1);
vector third=Rules::Objective(center,north,false,.4,180,100,Role::SEARCH,2);
assert(vector::DistanceXZ(first,second)>100&&vector::DistanceXZ(second,third)>100);
// Nearby units already ahead win interception; nearby correct-flank units contain.
float ahead=Rules::Score(Role::INTERCEPT,center,north,{1000,0,1200},intercept);
float behind=Rules::Score(Role::INTERCEPT,center,north,{1000,0,800},intercept);
assert(ahead>=0&&behind<0);
float right=Rules::Score(Role::CONTAIN,center,north,{1100,0,1050},contain);
float left=Rules::Score(Role::CONTAIN,center,north,{900,0,1050},contain);
assert(right<left);
assert(Rules::Score(Role::INTERCEPT,center,north,{intercept.x+150,0,intercept.z},intercept)>=0);
assert(Rules::Score(Role::INTERCEPT,center,north,{intercept.x+151,0,intercept.z},intercept)<0);
assert(Rules::Score(Role::CONTAIN,center,north,{contain.x+240,0,contain.z},contain)>=0);
assert(Rules::Score(Role::CONTAIN,center,north,{contain.x+241,0,contain.z},contain)<0);
assert(Rules::Score(Role::SEARCH,center,north,{1000,0,1000},first)
<Rules::Score(Role::SEARCH,center,north,{2000,0,1000},first));
assert(Rules::Score(Role::SEARCH,{nan,0,0},north,center,first)<0);
assert(Rules::Better(100,2,100,5)&&!Rules::Better(100,5,100,2));
assert(Rules::Better(99,5,100,2)&&Rules::Better(100,1,-1,-1));
assert(!Rules::Better(-1,1,100,2)&&!Rules::Better(nan,1,100,2));
int firstBearing=-1,secondBearing=-1;std::set<int> bearings;
std::vector<vector> forces={{1000,0,1200},{1200,0,1000},{1000,0,800},{800,0,1000}};
for(vector force:forces){
int bearing=Rules::Bearing(force,center);
if(!Rules::BearingAllowed(bearing,firstBearing,secondBearing))continue;
bearings.insert(bearing);
if(firstBearing<0)firstBearing=bearing;
else if(bearing!=firstBearing&&secondBearing<0)secondBearing=bearing;
}
assert(bearings.size()==2);
assert(Rules::BearingAllowed(firstBearing,firstBearing,secondBearing));
assert(!Rules::BearingAllowed(2,0,1)&&!Rules::BearingAllowed(3,0,1));
assert(!Rules::BearingAllowed(-1,-1,-1)&&!Rules::BearingAllowed(2,-1,1));
assert(Rules::Bearing({nan,0,0},center)==-1);
}
''')
for forbidden in ('GetGame(', 'GetOrigin(', 'GetPlayers(', 'GetControlledEntity(',
'Spawn(', 'CallLater(', 'SCR_AIGroup', 'IEntity'):
assert forbidden not in source, f'pursuit must consume snapshots only: {forbidden}'
assert all(len(line)<120 for line in source.splitlines())
print('PASS: actual bounded pursuit roles/objectives/scores, stale search degradation, physical/busy/home guards, finite lifetime and two-bearing escape gap; no hidden player reads or spawning')
+126
View File
@@ -0,0 +1,126 @@
"""Execute actual own-roster status sampler against explicit native-state stand-ins."""
from policy_harness import ROOT,body,run
from pathlib import Path
import os,re
P=Path(os.environ.get('ERR_SAMPLER_SOURCE',ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQStatusSampler.c'))
U=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQPursuit.c'
def cpp(s):
s=s.replace('DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent))',
'(actor.hasDamage ? &actor.damage : null)')
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)
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)')
return s
methods='\n'.join(cpp(body(P,s)) for s in ('bool Bound()', 'protected bool Eligible(', 'bool Bind(',
'bool ObserveDeaths(', 'ERR_HQStatusReport Sample(', 'void Clear('))
common='\n'.join(cpp(body(U,s)) for s in ('static bool Finite(', 'static bool PositionValid('))
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <limits>
#include <vector>
#include <string>
using string=std::string;
struct vector{float x=0,y=0,z=0;float operator[](int i)const{return i==0?x:i==1?y:z;}};
struct Math{static float AbsFloat(float n){return std::abs(n);}};
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
template<class T>struct Array:std::vector<T>{
int Count(){return this->size();}void Insert(T n){this->push_back(n);}void Clear(){this->clear();}
bool Contains(T n){return std::find(this->begin(),this->end(),n)!=this->end();}
int Find(T n){auto i=std::find(this->begin(),this->end(),n);return i==this->end()?-1:i-this->begin();}
};
struct Faction{};struct SCR_AIGroup;
struct DamageManagerComponent{bool dead=false;bool IsDestroyed(){return dead;}};
struct IEntity{
SCR_AIGroup* group=nullptr;Faction* faction=nullptr;bool player=false,unconscious=false,radio=true,hasDamage=true;
DamageManagerComponent damage;
};
struct AIAgent{IEntity* actor=nullptr;IEntity* GetControlledEntity(){return actor;}};
struct SCR_AIGroup{
Array<AIAgent*>agents;Faction* faction=nullptr;int players=0;bool slave=false;vector position{100,5,300};
void GetAgents(Array<AIAgent*>& out){out=agents;}int GetPlayerCount(bool){return players;}
bool IsSlave(){return slave;}Faction* GetFaction(){return faction;}vector GetOrigin(){return position;}
};
struct ERR_Actors{
static bool Alive(IEntity* a){return a&&a->hasDamage&&!a->damage.dead;}
static bool CanFight(IEntity* a){return Alive(a)&&!a->unconscious;}
static bool Player(IEntity* a){return a&&a->player;}
static SCR_AIGroup* Group(IEntity* a){return a?a->group:nullptr;}
};
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{
string SourceId;int MessageId=0;vector Position;int Strength=0,Count=0,Fit=0,ConfirmedLosses=0;
float ObservedAt=-1;
};
struct ERR_HQPursuitRules{
'''+common+r'''
};
struct Sampler{
SCR_AIGroup* m_Group=nullptr;Faction* m_Faction=nullptr;string m_SourceId;
int m_Strength=0,m_Deaths=0;bool m_Bound=false;float m_LastSampleAt=-1;
Array<IEntity*>m_Members;Array<bool>m_Dead,m_Released;Array<AIAgent*>m_Agents;
'''+methods+r'''
};
int main(){
Faction enemy,other;SCR_AIGroup group;group.faction=&enemy;
IEntity members[4];AIAgent agents[4];
for(int i=0;i<4;i++){members[i].group=&group;members[i].faction=&enemy;agents[i].actor=&members[i];group.agents.Insert(&agents[i]);}
Sampler sample;assert(!sample.Bound());
Replication::server=false;assert(!sample.Bind(&group,"site",4,&enemy));Replication::server=true;
assert(!sample.Bind(&group,"site",33,&enemy)&&!sample.Bind(&group,"site",3,&enemy));
assert(!sample.Bind(&group,"site",4,&other));
group.players=1;assert(!sample.Bind(&group,"site",4,&enemy));group.players=0;
members[2].player=true;assert(!sample.Bind(&group,"site",4,&enemy));members[2].player=false;
members[2].damage.dead=true;assert(!sample.Bind(&group,"site",4,&enemy));members[2].damage.dead=false;
group.agents[3]=&agents[2];assert(!sample.Bind(&group,"site",4,&enemy));group.agents[3]=&agents[3];
assert(sample.Bind(&group,"site",4,&enemy)&&sample.Bound());
assert(sample.Bind(&group,"site",4,&enemy)&&!sample.Bind(&group,"other-source",4,&enemy));
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");
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);
// A retained explicit destroyed body is evidence even after its agent detaches.
members[1].damage.dead=true;members[1].group=nullptr;group.agents.erase(group.agents.begin()+1);
assert(sample.ObserveDeaths()&&sample.m_Deaths==1);
assert(sample.ObserveDeaths()&&sample.m_Deaths==1); // counted once
sample.m_Members[1]=nullptr; // simulate native weak-reference invalidation after corpse cleanup
auto losses=sample.Sample(3000,3,&members[0]);assert(losses&&losses->Count==3&&losses->ConfirmedLosses==1);
// Deleted unknown actor and missing damage component cannot be manufactured into deaths.
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);
members[3].hasDamage=true;members[3].player=true;assert(sample.ObserveDeaths());
members[3].player=false;members[3].damage.dead=true;assert(sample.ObserveDeaths()&&sample.m_Deaths==1);
// 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));
auto original=sample.Sample(5000,5,&members[0]);assert(original&&original->Strength==4&&original->Count==1);
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;
group.faction=&other;assert(!sample.Bind(&group,"site",4,&enemy)&&!sample.Sample(6000,6,&members[0]));group.faction=&enemy;
group.slave=true;assert(!sample.ObserveDeaths());group.slave=false;
Replication::server=false;assert(!sample.ObserveDeaths()&&!sample.Sample(6000,6,&members[0]));Replication::server=true;
assert(!sample.Sample(4999,6,&members[0])&&!sample.Sample(6000,0,&members[0]));
assert(!sample.Sample(std::numeric_limits<float>::quiet_NaN(),6,&members[0]));
// No sender survives: local evidence may update, but a radio report cannot be fabricated.
members[0].damage.dead=true;assert(sample.ObserveDeaths()&&sample.m_Deaths==2);
assert(!sample.Sample(7000,7,&members[0])&&!sample.Sample(7000,7,nullptr));
sample.Clear();assert(!sample.Bound()&&sample.m_Members.Count()==0&&sample.m_Deaths==0);
}
''')
source=P.read_text()
for forbidden in ('ERR_HQWatch ', 'ERR_HQMemory', 'GetPlayers(', 'GetWorld(', 'GetControlledEntity() == target',
'Spawn(', 'CallLater(', 'Ground('):assert forbidden not in source,forbidden
assert 'new ' not in body(P,'bool ObserveDeaths(')
assert 'GetAgents(' not in body(P,'bool ObserveDeaths(')
assert all(len(line)<120 for line in source.splitlines())
print('PASS: own initial roster, explicit retained corpse evidence, unconscious/missing/ref transfer guards, fixed strength, no replacement adoption, detached samples and surviving radio sender requirement')
+139
View File
@@ -0,0 +1,139 @@
"""Actual HQ report methods, with deterministic radio/perception boundary doubles.
Native radio and perception remain explicit boundary doubles.
"""
from pathlib import Path
import os,re
from policy_harness import ROOT,body,run
P=Path(os.environ.get('ERR_HQ_SOURCE',str(ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c')))
original=(ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c').read_text()
legacy='Observe(unit, now);\n ObserveBody(unit, now);\n StatusReport(unit, now);\n Sweep(unit, now);'
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'):
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'):
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_HQObservation.','ERR_HQObservation::').replace('vector.DistanceXZ','vector::DistanceXZ').replace('string.Format','Format')
return s
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>
#include <cmath>
#include <vector>
#include <string>
using string=std::string;
template<class... T>string Format(T... args){return "diagnostic";}
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);}};
template<class T>struct Array:std::vector<T>{bool IsEmpty(){return this->empty();}};
struct Group{vector position;vector GetOrigin(){return position;}};
struct Settings{int interval=5;int Get(string){return interval;}};
struct Mode{struct Settings settings;struct Settings* Settings(){return &settings;}};
struct ERR_HQVisualContact{void* Entity=this;vector Position;float ObservedAt=0;};
struct Utility{bool visible=true;};
struct ERR_HQObservation{static void Gather(Utility* u,float now,Array<ERR_HQVisualContact*>& a){
static ERR_HQVisualContact contact;if(u->visible){contact.ObservedAt=now;a.push_back(&contact);}}
template<class T>static void Describe(T*,Array<ERR_HQVisualContact*>&,float){}
};
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 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;
string WatchSourceId;vector Position;float ObservedAt=0;bool SweepCompleted=false,ContactFound=false;};
struct ERR_HQTask{bool SweepQueued=false;float ArrivedAt=-1;vector Destination;
string WatchSourceId;int WatchMessageId=0,WatchRequestId=0,BeliefId=1,BeliefRevision=2;};
struct ERR_HQRoute{int MessageId=0;string Payload;};
struct Network{bool available=true;int routes=0;ERR_HQRoute* Route(void*){routes++;return available?new ERR_HQRoute:nullptr;}};
struct Bodies{bool visible=false;bool Observe(Group*,float now,ERR_HQReport*& r){if(!visible)return false;r=new ERR_HQReport;r->ObservedAt=now;return true;}};
struct Journal{int seq=0;int Message(){return ++seq;}void Write(string,string,int){}};
struct ERR_HQUnit{int Id=1;struct Group ownGroup;struct Utility ownUtility;Selection selection;Sampler sampler;
struct 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;
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;}
};
struct HQ{Mode mode;Mode* m_Mode=&mode;Network m_Network;Bodies m_Bodies;Journal m_Journal;
bool Fighting(ERR_HQUnit* u){return u->fighting;}
'''+methods+r'''
void Legacy(ERR_HQUnit* unit,float now){
'''+legacy+r'''
}
};
struct Stats{int sight=0,status=0,sweep=0;};
void Finish(ERR_HQUnit& u,Stats& s,bool failed=false){
assert(u.Uplink);if(u.PendingStatus){if(!failed)s.status++;}
else if(u.PendingSweep){if(!failed)s.sweep++;else u.Task->SweepQueued=false;}
else {assert(u.PendingReport);if(!failed)s.sight++;}
u.Uplink=nullptr;u.PendingStatus=nullptr;u.PendingSweep=nullptr;u.PendingReport=nullptr;
}
Stats Simulate(bool legacy){HQ h;ERR_HQUnit u;Stats counts;float due=-1;
// Model three native two-second legs; real transport is intentionally outside this harness.
for(int now=0;now<=600000;now+=1000){
if(u.Uplink&&now>=due)Finish(u,counts);
bool busy=u.Uplink;if(legacy)h.Legacy(&u,now);else h.QueueReports(&u,now);
if(!busy&&u.Uplink)due=now+6000;
}
assert(u.sampler.observations==601);return counts;
}
int main(){
auto before=Simulate(true);assert(before.sight==100&&before.status==0);
auto after=Simulate(false);assert(after.status>=4&&after.sight>=95);
HQ h;ERR_HQUnit u;Stats counts;
// Queueing creates no delivered roster knowledge; exact sampled time stays on the pending report.
h.QueueReports(&u,0);assert(u.PendingStatus&&u.PendingStatus->ObservedAt==0&&counts.status==0);
auto original=u.Uplink;h.QueueReports(&u,1000);assert(u.Uplink==original&&u.sampler.observations==2);
Finish(u,counts);assert(counts.status==1);h.QueueReports(&u,6000);assert(u.PendingReport);
// HQ check-in may be delivered while sighting uplink is busy; next free slot must carry status.
u.NextStatus=7000;h.QueueReports(&u,7000);assert(u.PendingReport&&!u.PendingStatus);
Finish(u,counts);h.QueueReports(&u,12000);assert(u.PendingStatus&&u.PendingStatus->ObservedAt==12000);
// Quiet-but-visible contact must suppress negative evidence, even though Fighting is false.
Finish(u,counts);ERR_HQTask task;task.ArrivedAt=0;u.Task=&task;u.Assignment=&task;u.Waypoint=&task;
h.QueueReports(&u,30000);assert(u.PendingReport&&!u.PendingSweep&&task.ArrivedAt==-1&&!u.fighting);
// Start dwell only once native perception sees no actual contact.
Finish(u,counts);u.ownUtility.visible=false;h.QueueReports(&u,36000);
assert(task.ArrivedAt==36000&&!u.Uplink);
// While a status frame is busy, newly visible contact still resets the dwell clock.
u.NextStatus=37000;h.QueueReports(&u,37000);assert(u.PendingStatus);
u.ownUtility.visible=true;h.QueueReports(&u,38000);
assert(task.ArrivedAt==-1&&u.PendingStatus&&!u.PendingReport);
// Clearing vision begins a fresh 30s dwell even while the existing uplink remains occupied.
u.ownUtility.visible=false;h.QueueReports(&u,39000);assert(task.ArrivedAt==39000);
Finish(u,counts);h.QueueReports(&u,43000);assert(!u.Uplink);
h.QueueReports(&u,68000);assert(!u.PendingSweep);
h.QueueReports(&u,69000);assert(u.PendingSweep&&u.PendingSweep->ObservedAt==69000&&task.SweepQueued);
// Delivered search is never queued a second time.
Finish(u,counts);h.QueueReports(&u,75000);assert(!u.Uplink&&counts.sweep==1);
// A failed negative retry remains subordinate to current positive observation.
task.SweepQueued=false;u.ownUtility.visible=true;h.QueueReports(&u,80000);
assert(u.PendingReport&&!u.PendingSweep&&task.ArrivedAt==-1);
// Quiet vision is insufficient while Fighting: combat and movement each reset uninterrupted dwell.
Finish(u,counts);u.ownUtility.visible=false;u.fighting=true;task.ArrivedAt=0;
h.QueueReports(&u,90000);assert(task.ArrivedAt==-1&&!u.PendingSweep);
u.fighting=false;h.QueueReports(&u,91000);assert(task.ArrivedAt==91000);
u.ownGroup.position.x=100;h.QueueReports(&u,92000);assert(task.ArrivedAt==-1&&!u.PendingSweep);
u.ownGroup.position.x=0;h.QueueReports(&u,93000);assert(task.ArrivedAt==93000);
h.QueueReports(&u,122000);assert(!u.PendingSweep);h.QueueReports(&u,123000);assert(u.PendingSweep);
// Route loss or an unavailable status sampler must not fabricate a report or block useful contacts.
HQ lost;ERR_HQUnit v;lost.m_Network.available=false;lost.QueueReports(&v,0);
assert(!v.Uplink&&v.NextStatus==0&&v.selection.attempts==0);
lost.m_Network.available=true;v.sampler.valid=false;lost.QueueReports(&v,6000);assert(v.PendingReport&&!v.PendingStatus);
// An unavailable radio cannot manufacture a frame or a delivery.
HQ quiet;ERR_HQUnit q;q.Radio=nullptr;quiet.QueueReports(&q,48000);assert(!q.Uplink);
// Body evidence still gets its physical radio frame ahead of a completed negative sweep.
ERR_HQTask qt;q.Radio=&q;q.NextStatus=999999;q.Task=&qt;q.Assignment=&qt;q.Waypoint=&qt;
qt.ArrivedAt=0;q.ownUtility.visible=false;quiet.m_Bodies.visible=true;
quiet.QueueReports(&q,50000);assert(q.PendingReport&&!q.PendingSweep);
}
''')
print('PASS: actual report methods reproduce old starvation; status-first scheduler delivers check-ins and requires 30s without combat or actual visible contacts before negative reports')
+157
View File
@@ -0,0 +1,157 @@
"""Actual delivered-only roster, missing-patrol and cumulative confirmed-loss rules."""
from policy_harness import ROOT, body, run
from pathlib import Path
import os,re
P=Path(os.environ.get('ERR_WATCH_SOURCE',ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQWatch.c'))
U=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQPursuit.c'
def cpp(s):
for old,new in [('protected ',''),('Math.','Math::'),('ERR_HQPursuitRules.','ERR_HQPursuitRules::'),
('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'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for var in ('entry','report'):s=s.replace(var+'.',var+'->')
return s
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)', 'bool Ingest(', '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('))
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <limits>
#include <map>
#include <vector>
#include <string>
using string=std::string;
struct vector{float x=0,y=0,z=0;float operator[](int i)const{return i==0?x:i==1?y:z;}};
struct Math{static int Clamp(int n,int lo,int hi){return std::clamp(n,lo,hi);}
static int Min(int a,int b){return std::min(a,b);}static float AbsFloat(float n){return std::abs(n);}};
int logs=0;void Print(const char*){logs++;}
template<class T>struct Array:std::vector<T>{
int Count(){return this->size();}void Insert(T n){this->push_back(n);}void Clear(){this->clear();}
};
template<class K,class V>struct Map:std::map<K,V>{
V Get(K k){return this->count(k)?this->at(k):V{};}void Set(K k,V v){(*this)[k]=v;}
void Clear(){this->clear();}
};
struct ERR_HQPursuitRules{
'''+common+r'''
};
struct ERR_HQStatusReport{
string SourceId="patrol-1";int MessageId=1;vector Position{100,20,300};
int Strength=4,Count=4,Fit=4,ConfirmedLosses=0;float ObservedAt=0;
};
struct ERR_HQWatchEntry{
string SourceId;int MessageId=0,RequestId=0;vector Position;
int Strength=0,Count=0,Fit=0,ConfirmedLosses=0;
float ObservedAt=-1,LastDeliveredAt=-1,AskedAt=-1,NextAttempt=0;
bool Investigating=false,Closed=false;
};
struct ERR_HQWatchRules{
'''+rules+r'''
};
struct Watch{
Array<ERR_HQWatchEntry*>m_Entries;Map<string,ERR_HQWatchEntry*>m_BySource;
int m_ConfirmedLosses=0;bool m_CapLogged=false;
'''+methods+r'''
};
int main(){
using R=ERR_HQWatchRules;
assert(R::SquadLimit(0,100)==0&&R::SquadLimit(-1,100)==0);
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 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));
assert(watch.Ingest(&report,5000)&&watch.Count()==1);
auto entry=watch.Find(report.SourceId);
report.Position={999,40,999}; // immutable intake: changing sender object cannot relocate HQ's snapshot
assert(entry->Position.x==100&&entry->LastDeliveredAt==5000);
assert(!watch.RequestDue(report.SourceId,359999));
assert(watch.RequestDue(report.SourceId,360000));
assert(!watch.MarkRequested(report.SourceId,2,360000));
assert(watch.MarkRequested(report.SourceId,1,360000));
assert(!watch.MarkRequested(report.SourceId,1,360001));
assert(!watch.InvestigationDue(report.SourceId,389999));
assert(watch.InvestigationDue(report.SourceId,390000));
assert(watch.MarkInvestigating(report.SourceId,1,1,390000));
assert(!watch.MarkInvestigating(report.SourceId,1,1,390000));
assert(watch.ConfirmedLosses()==0&&entry->Position.x==100); // silence proves neither casualties nor target position
assert(!watch.FinishInvestigation(report.SourceId,2,1,391000));
assert(watch.FinishInvestigation(report.SourceId,1,1,391000));
assert(!watch.FinishInvestigation(report.SourceId,1,1,391000));
assert(!watch.RequestDue(report.SourceId,570999)&&watch.RequestDue(report.SourceId,571000));
assert(watch.MarkRequested(report.SourceId,1,571000));
assert(watch.MarkInvestigating(report.SourceId,1,2,601000));
assert(!watch.FinishInvestigation(report.SourceId,1,1,601001)); // late completion from first request
// A fresh delayed check-in cancels the old missing episode, never refreshes an enemy contact.
report.MessageId=2;report.ObservedAt=600000;report.Fit=2;
assert(watch.Ingest(&report,602000));
assert(!entry->Investigating&&entry->AskedAt==-1&&entry->Position.x==999);
assert(watch.ConfirmedLosses()==0); // two unconscious members still alive
assert(!watch.FinishInvestigation(report.SourceId,1,1,603000));
assert(!watch.Ingest(&report,603000));
report.MessageId=3;assert(!watch.Ingest(&report,603000)); // same observation relabelled cannot renew status
report.ObservedAt=601000;report.Count=3;report.Fit=2;report.ConfirmedLosses=1;
assert(watch.Ingest(&report,603000)&&watch.ConfirmedLosses()==1);
report.MessageId=4;report.ObservedAt=602000;
assert(watch.Ingest(&report,604000)&&watch.ConfirmedLosses()==1); // cumulative, not additive
report.MessageId=5;report.ObservedAt=603000;report.Count=2;report.ConfirmedLosses=2;
assert(watch.Ingest(&report,605000)&&watch.ConfirmedLosses()==2&&watch.SquadLimit(2)==3);
report.MessageId=6;report.ObservedAt=604000;report.ConfirmedLosses=1;
assert(!watch.Ingest(&report,606000)&&watch.ConfirmedLosses()==2);
report.ConfirmedLosses=2;report.Strength=5;
assert(!watch.Ingest(&report,606000));report.Strength=4;
// Missing members cannot be counted as dead. A delivered explicit whole-formation loss can close it.
report.Count=0;report.Fit=0;
assert(watch.Ingest(&report,606000)&&!entry->Closed&&watch.ConfirmedLosses()==2);
report.MessageId=7;report.ObservedAt=605000;report.ConfirmedLosses=4;
assert(watch.Ingest(&report,607000)&&entry->Closed&&watch.ConfirmedLosses()==4);
assert(!watch.RequestDue(report.SourceId,2000000));
report.MessageId=8;report.ObservedAt=606000;report.ConfirmedLosses=0;report.Count=4;report.Fit=4;
assert(!watch.Ingest(&report,608000)); // source cannot resurrect or erase counted deaths
// Validation precedes allocation/watermarks; malformed deliveries cannot poison future genuine reports.
float nan=std::numeric_limits<float>::quiet_NaN();
for(int field=0;field<13;field++){
ERR_HQStatusReport bad;bad.SourceId="invalid";
if(field==0)bad.SourceId="";
if(field==1)bad.MessageId=0;
if(field==2)bad.Position.x=nan;
if(field==3)bad.ObservedAt=1001;
if(field==4)bad.ObservedAt=-1;
if(field==5)bad.ObservedAt=nan;
if(field==6)bad.Strength=33;
if(field==7)bad.Count=5;
if(field==8)bad.Fit=5;
if(field==9)bad.Fit=-1;
if(field==10)bad.ConfirmedLosses=1;
if(field==11)bad.ConfirmedLosses=-1;
if(field==12)bad.SourceId=string(97,'a');
assert(!watch.Ingest(&bad,1000)&&watch.Count()==1);
}
ERR_HQStatusReport late;late.SourceId="late";
assert(!watch.Ingest(nullptr,1000)&&!watch.Ingest(&late,nan));
assert(!watch.Ingest(&late,120001)&&watch.Ingest(&late,120000));
assert(!watch.RequestDue("late",nan));
Watch cap;ERR_HQStatusReport item;item.Count=0;item.Fit=0;item.ConfirmedLosses=4;
for(int i=0;i<128;i++){item.SourceId=std::to_string(i);assert(cap.Ingest(&item,1000));}
assert(cap.Count()==128&&cap.ConfirmedLosses()==512);
item.SourceId="overflow";assert(!cap.Ingest(&item,1000));
item.SourceId="overflow2";assert(!cap.Ingest(&item,1000)&&logs==1);
item.SourceId="0";item.MessageId=2;item.ObservedAt=2000;
assert(cap.Ingest(&item,2000)&&cap.ConfirmedLosses()==512&&cap.Count()==128);
cap.Clear();assert(cap.Count()==0&&cap.ConfirmedLosses()==0&&!cap.Find("0"));
}
''')
source=P.read_text()
for forbidden in ('IEntity','SCR_AIGroup','GetGame(','GetOrigin(','GetPlayers(','GetAgents(',
'GetControlledEntity(','Spawn(','CallLater(','ERR_HQMemory','ERR_HQReport '):
assert forbidden not in source,forbidden
assert all(len(line)<120 for line in source.splitlines())
print('PASS: delivered-only status snapshots, two missed check-ins/reply deadline, last reported coordinates, episode/replay guards, confirmed-loss idempotence and 128-record cap')
@@ -28,6 +28,7 @@ struct World {float now=0;float GetWorldTime(){return now;}} world;
struct Service {void Tick(float=0){}void Maintain(float){}void Invoke(float){}};
struct Exhausted {std::map<int,Controller*> values;void Remove(int id){values.erase(id);}void Set(int id,Controller* c){values[id]=c;}bool Contains(int id){return values.count(id);}Controller* Get(int id){return values.at(id);}};
struct Mode {
void TickForces(float){}
bool m_StartedPlayers=false;int m_Phase=1;float m_NoSurvivorsSince=-1;std::string m_EndReason;
Exhausted m_ExhaustedInitialAdmissions;Roster roster;Roster* m_Roster=&roster;
Service service,m_OnTick;Service *m_Extraction=&service,*m_RadioDefence=&service,*m_Recovery=&service,*m_Command=&service;
+23 -8
View File
@@ -11,9 +11,12 @@ def adapt(s):
s=re.sub(r'foreach \((\w+) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
for n in ('Replication','ERR_Actors','Math','ERR_Resources','ERR_DiscoveryService','SCR_InventoryStorageManagerComponent','SCR_AIGroupUtilityComponent','ERR_MortarSupplyState'):
s=s.replace(n+'.',n+'::')
for n in ('group','member','actor','request','inventory','known','unit','m_Mode','m_Battery','mode','m_Reroll'):
for n in ('group','member','actor','request','inventory','known','unit','m_Mode','m_Battery','mode','m_Reroll','home','sites','record'):
s=s.replace(n+'.',n+'->')
s=s.replace('Actor.FindComponent(', 'Actor->FindComponent(').replace('SCR_InventoryStorageManagerComponent inventory', 'SCR_InventoryStorageManagerComponent* inventory').replace('SCR_AIGroupUtilityComponent utility', 'SCR_AIGroupUtilityComponent* utility')
s=s.replace('home->Definition.', 'home->Definition->').replace('Patrol.IsEmpty()', 'Patrol->IsEmpty()').replace('unit->StatusSampler.Bind(', 'unit->StatusSampler.Bind(')
s=s.replace('ERR_SiteRuntime home;', 'ERR_SiteRuntime home=nullptr;')
s=s.replace('Forces().', 'Forces()->')
s=s.replace('Settings().','Settings()->').replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->')
s=s.replace('FindComponent(SCR_InventoryStorageManagerComponent)','FindComponent()').replace('FindComponent(SCR_AIGroupUtilityComponent)','FindComponent()')
s=s.replace('purpose: EStoragePurpose.PURPOSE_ANY','EStoragePurpose::PURPOSE_ANY').replace('cb: ', '').replace('Operation.', 'Operation->').replace('new ERR_IntelInsertCallback()', 'new Callback()')
@@ -21,7 +24,7 @@ def adapt(s):
s=s.replace('ERR_HQUnit unit = new ERR_HQUnit();','ERR_HQUnit unit = new Unit();')
s=re.sub(r'^.*(?:Print|m_Journal.Write)\([^\n]*$', ';',s,flags=re.M)
return s
register=adapt(body(intel,'void RegisterGroup('));tick=adapt(body(intel,'void Tick('));hqregister=adapt(body(hq,'override void RegisterGroup('))
register=adapt(body(intel,'void RegisterGroup('));tick=adapt(body(intel,'void Tick('));hqregister=adapt(body(hq,'override void RegisterGroup('));hqbind=adapt(body(hq,'protected void BindPhysical('))
observation=adapt(body(intel,'bool ERR_ProbeWasRolled('))
poll=body(ready,'ERR_MortarSupplyState Poll(')
# Execute exact early stopped/ready guards and final qualified handoff. Native
@@ -62,36 +65,48 @@ namespace ERR_DiscoveryService{void Folders(Inventory* i,Array<IEntity>& a){a.cl
struct Utility{static Utility* Cast(void* p){return static_cast<Utility*>(p);}};using SCR_AIGroupUtilityComponent=Utility;
struct Group{Array<AIAgent> members;Utility utility;int faction=1;void GetAgents(Array<AIAgent>& out){out=members;}void* FindComponent(){return &utility;}int GetFaction(){return faction;}};using SCR_AIGroup=Group*;
struct Settings{int chance=100;int Get(const char*){return chance;}};
struct Mode{struct Settings settings;int publications=0;struct Settings* Settings(){return &settings;}int EnemyFaction(){return 1;}void RegisterGroup(SCR_AIGroup);};
struct Definition{string Id="site-a";Array<int>* Patrol=nullptr;};
struct Site{struct Definition data;struct Definition* Definition=&data;};using ERR_SiteRuntime=Site*;
struct Sites{Site home;Site* FindGroup(SCR_AIGroup){return &home;}};using ERR_SiteService=Sites*;
struct ForceRecord{string SourceId="site-a";int InitialStrength=4;bool Physical=true,Eliminated=false;};using ERR_ForceRecord=ForceRecord*;
struct Forces{ForceRecord record;bool available=false;ForceRecord* FindGroup(SCR_AIGroup){return available?&record:nullptr;}};
struct Mode{struct Sites sites;struct Forces forces;struct Sites* Sites(){return &sites;}struct Forces* Forces(){return &forces;}struct Settings settings;int publications=0;struct Settings* Settings(){return &settings;}int EnemyFaction(){return 1;}void RegisterGroup(SCR_AIGroup);};
struct Request{Callback* Operation=nullptr;IEntity Actor=nullptr;int Attempts=0;bool Accepted=false;};using ERR_IntelRequest=Request*;
struct Reroll{void Tick(){}};
struct Intel{Mode* m_Mode;Array<ERR_IntelRequest> m_Requests;Set<IEntity> m_Rolled;bool m_Stopped=false;Reroll* m_Reroll=nullptr;void Unsettled(IEntity){}
'''+register+tick+observation+r'''
};
struct Clock{float GetWorldTime(){return 10;}Clock* GetWorld(){return this;}}clock_;Clock* GetGame(){return &clock_;}
struct Unit{int Id=0;SCR_AIGroup Group=nullptr;struct Utility* Utility=nullptr;float NextReport=0;};using ERR_HQUnit=Unit*;
struct HQ{Mode* m_Mode;int m_NextId=0;Array<ERR_HQUnit> m_Units;
'''+hqregister+r'''
struct Sampler{bool Bound(){return bindings>0;}int bindings=0,strength=0;SCR_AIGroup group=nullptr;string source;void Bind(SCR_AIGroup g,string id,int n,int faction){assert(faction==1);bindings++;group=g;source=id;strength=n;}};
struct GroupService{int registrations=0;Set<SCR_AIGroup> seen;void RegisterGroup(SCR_AIGroup g){if(!seen.Contains(g)){seen.Insert(g);registrations++;}}};
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 HQ{Mode* m_Mode;GroupService m_Equipment,m_Bodies;int m_NextId=0;Array<ERR_HQUnit> m_Units;
'''+hqbind+hqregister+r'''
};
Intel* intel_;HQ* hq_;
void Mode::RegisterGroup(SCR_AIGroup group){publications++;hq_->RegisterGroup(group);intel_->RegisterGroup(group);}
void Mode::RegisterGroup(SCR_AIGroup group){forces.available=true;publications++;hq_->RegisterGroup(group);intel_->RegisterGroup(group);}
namespace ERR_MortarSupplyState{const int FAILED=-1,PENDING=0,READY=1;}
struct Battery{bool stopped=false;int shells=27;bool Stopped(){return stopped;}int CountShells(){return shells;}};
struct Ready{bool m_Stopped=false,m_Ready=false;Battery* m_Battery;int Finish(Mode* mode,SCR_AIGroup group){
'''+handoff+r'''
}};
int main(){
Mode mode;Intel intel{&mode,{},{} };HQ hq{&mode,0,{}};intel_=&intel;hq_=&hq;Group group;
Mode mode;Intel intel{&mode,{},{} };HQ hq{&mode,{},{},0,{}};intel_=&intel;hq_=&hq;Group group;
hq.RegisterGroup(&group);assert(hq.m_Units.Count()==1&&mode.publications==0&&Math::rolls==0);
assert(!hq.m_Units[0]->StatusSampler.Bound()&&hq.m_Units[0]->ForceSourceId.empty()&&hq.m_Bodies.registrations==0);
Actor actors[4];Agent agents[4];for(int i=0;i<4;i++){agents[i].actor=&actors[i];group.members.Insert(&agents[i]);}
Battery battery;Ready ready{false,false,&battery};battery.shells=26;
assert(ready.Finish(&mode,&group)==0&&mode.publications==0&&Math::rolls==0);
battery.shells=27;assert(ready.Finish(&mode,&group)==1&&mode.publications==1&&Math::rolls==4&&intel.m_Requests.Count()==4);
assert(hq.m_Units.Count()==1&&hq.m_NextId==1);
assert(hq.m_Equipment.registrations==1&&hq.m_Bodies.registrations==1);
assert(hq.m_Units[0]->HomeGuard&&hq.m_Units[0]->HomeId=="site-a"&&hq.m_Units[0]->ForceSourceId=="site-a");
assert(hq.m_Units[0]->StatusSampler.bindings==1&&hq.m_Units[0]->StatusSampler.group==&group&&hq.m_Units[0]->StatusSampler.strength==4);
for(Actor& a:actors){assert(intel.ERR_ProbeWasRolled(&a));}assert(!intel.ERR_ProbeWasRolled(nullptr));Actor unrelated;assert(!intel.ERR_ProbeWasRolled(&unrelated));
Replication::server=false;assert(!intel.ERR_ProbeWasRolled(&actors[0]));Replication::server=true; // actual HQ method deduplicates readiness publication
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);
intel.Tick(1);for(Actor& a:actors)assert(a.inventory.requests==1);
for(int n=0;n<5;n++){intel.Tick(2+n);}for(Actor& a:actors)assert(a.inventory.requests==1); // accepted asynchronous spawn never reissued
for(Actor& a:actors){a.inventory.folder=true;}for(auto request:intel.m_Requests){request->Operation->Finished=true;request->Operation->Succeeded=true;}intel.Tick(10);assert(intel.m_Requests.Count()==0);
+39
View File
@@ -0,0 +1,39 @@
"""Exercise actual native convoy proof threshold and reject unsafe runner variants."""
from policy_harness import ROOT, body, run
import subprocess, sys
p=ROOT/'addons/chat/Scripts/Game/ERR/ERR_NativeConvoyProbe.c'
s=p.read_text()
method=body(p,'static bool MovementComplete(').replace('ERR_ForceRules.','ERR_ForceRules::')
run('''
#include <cassert>
#include <cmath>
struct ERR_ForceRules {static bool Finite(float n){return std::isfinite(n);}};
'''+method+'''
int main(){
assert(MovementComplete(100,20));assert(MovementComplete(200,30));
assert(!MovementComplete(99.99,20));assert(!MovementComplete(100,19.99));
assert(!MovementComplete(NAN,20));assert(!MovementComplete(INFINITY,20));
assert(!MovementComplete(100,NAN));assert(!MovementComplete(100,-20));
}
''')
assert s.startswith('#ifdef WORKBENCH') and 'outcome == "convoy"' in s
for forbidden in ('SetOrigin(', 'SetAngles(', 'ERR_Actors.Spawn(', 'SpawnCharacterInCompartment(',
'TrySpawnPrefabToStorage(', 'SetPermanentLOD(', 'ActivateAI(', 'SetCruiseSpeed(',
'Ready = true', 'ForceUnconsciousness(', 'SetHealth(', 'PrepareConvoy("'):
assert forbidden not in s, forbidden
assert 'hq.PrepareConvoy(seed)' in s and 'hq.StartConvoy()' in s
assert 'seat.GetOccupant() == actor' in s and 'agents.Count() == expected' in s
assert 'record.Id == m_Task.Receipt.Records[i]' in s
assert '!operation.Succeeded' in s and 'm_CargoBefore.Contains(item)' in s
assert 'rifles == 8 && magazines >= 48' in s and 'CommittedBodies() <= 60' in s
assert 'm_InitialLedger + 6' in s and s.count('!hq.StartConvoy()')==2
smoke=(ROOT/'addons/chat/Scripts/Game/ERR/ERR_NativeSmoke.c').read_text()
assert 'm_ERRTestConvoy = outcome == "convoy"' in smoke and 'm_ERRSmokeStep = 106' in smoke
runner=ROOT/'tools/headless_workbench.py'
for extra in (['--project','core','--rounds','1','--timeout','600'],
['--project','chat','--rounds','2','--timeout','600'],
['--project','chat','--rounds','1','--timeout','599']):
result=subprocess.run([sys.executable,str(runner),'--smoke','convoy','--compat','/tmp/erri-unused-convoy-compat',
'--profile','/tmp/erri-unused-convoy-profile',*extra],capture_output=True,text=True)
assert result.returncode==2 and 'convoy requires Chat' in result.stderr,result.stderr
print('PASS: actual convoy motion thresholds, native-only proof boundaries, physical cargo/crew ownership and pre-launch runner restrictions')
+4 -1
View File
@@ -31,7 +31,10 @@ struct ERR_Actors {static int deleted;static void DeleteSpawned(int e){assert(e=
struct Command {int stops=0;explicit operator bool()const{return true;}void Stop(){stops++;}};
struct PhaseEvent {int count=0,oldPhase=-1,newPhase=-1;std::function<void()> callback;void Invoke(int old,int next){count++;oldPhase=old;newPhase=next;if(callback)callback();}};
struct StopEvent {int count=0;std::function<void()> callback;void Invoke(){count++;if(callback)callback();}};
struct ForceBoundary {int clears=0;void Clear(){clears++;}};
struct Mode {
ForceBoundary m_Forces,m_ForceAgents;
int forceTicks=0;void TickForces(float){forceTicks++;}
bool m_Stopped=false,m_NativeEndHandled=false;
int m_Phase=Phase::PRISON,StartScenario=1,ServerTick=2,BindLocalObserver=3;
string m_EndReason;string m_PreparationError;
@@ -54,7 +57,7 @@ int main(){
assert(m.m_OnPhaseChanged.count==1&&m.m_OnPhaseChanged.oldPhase==phase);
assert(m.m_OnStop.count==1&&m.m_Command.stops==1&&ERR_Actors::deleted==deleted+1);
assert(game.queue.removed.size()==removed+4&&game.queue.removed[removed]==m.BindLocalObserver&&game.queue.removed[removed+1]==m.StartScenario&&game.queue.removed[removed+2]==m.ServerTick&&game.queue.removed[removed+3]==m.BindLocalObserver);
assert(m.nativeEnds==1&&m.receivedData==314&&m.escapeSnapshot==77&&m.published==1);
assert(m.nativeEnds==1&&m.receivedData==314&&m.escapeSnapshot==77&&m.published==1&&m.m_Forces.clears==1&&m.m_ForceAgents.clears==1);
m.OnGameModeEnd(999);m.StopServices();
assert(m.nativeEnds==1&&m.receivedData==314&&m.m_OnStop.count==1&&m.m_OnPhaseChanged.count==1&&m.published==1);
}
+14 -3
View File
@@ -23,7 +23,7 @@ class NativePackageTests(unittest.TestCase):
self.source = self.home/'original'
shutil.copytree(ROOT/'addons', self.source/'addons')
(self.source/'docs').mkdir()
for name in ('addons.json', 'vanilla-resources.json', 'acceptance.json'):
for name in ('addons.json', 'vanilla-resources.json', 'acceptance.json', 'external-addons.json'):
shutil.copy2(ROOT/'docs'/name, self.source/'docs'/name)
self.session = self.home/'session'
self.compat = self.home/'compat'
@@ -36,6 +36,15 @@ class NativePackageTests(unittest.TestCase):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text('synthetic installed-file double; never executed')
def external_fixture(self):
# Synthetic exact closure for orchestration doubles; never an installed addon or engine.
import test_external_addons
fixture = test_external_addons.ExternalAddonsTests()
fixture.setUp()
self.addCleanup(fixture.doCleanups)
(self.source/'docs/external-addons.json').write_text(json.dumps(fixture.value))
return fixture.source
def test_closure_order(self):
self.assertEqual(n.project_order('core'), ['core'])
self.assertEqual(n.project_order('ai'), ['core', 'ai'])
@@ -267,7 +276,7 @@ class NativePackageTests(unittest.TestCase):
import io
fresh = Path('/tmp')/('erri-package-orchestration-'+uuid.uuid4().hex)
self.addCleanup(lambda: shutil.rmtree(fresh, ignore_errors=True))
args = argparse.Namespace(steam=self.steam, compat=self.compat, session=fresh, project='chat', timeout=30)
args = argparse.Namespace(steam=self.steam, compat=self.compat, session=fresh, project='chat', timeout=30, external_addons=self.external_fixture())
with patch.object(n, 'assert_no_game'), patch.object(n, 'assert_prefix_idle'), patch.object(n.shutil, 'which', return_value='/mock'), \
patch.object(n, 'run_project', return_value={'candidate_created':False}) as run, patch.object(n.subprocess, 'Popen') as child, contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(n.run(args, self.source), 1)
@@ -298,7 +307,8 @@ class NativePackageTests(unittest.TestCase):
fresh = Path('/tmp')/('erri-package-orchestration-'+uuid.uuid4().hex)
self.addCleanup(lambda: shutil.rmtree(fresh, ignore_errors=True))
args = argparse.Namespace(steam=self.steam, compat=self.compat, session=fresh,
project='chat', timeout=30, continue_on_failure=continuation, exit_after_init=exit_after_init)
project='chat', timeout=30, continue_on_failure=continuation, exit_after_init=exit_after_init,
external_addons=self.external_fixture())
with patch.object(n, 'assert_no_game'), patch.object(n, 'assert_prefix_idle', side_effect=idle), \
patch.object(n.shutil, 'which', return_value='/mock'), \
patch.object(n, 'run_project', side_effect=reports) as run, \
@@ -311,6 +321,7 @@ class NativePackageTests(unittest.TestCase):
def completed_report(success=False, **changes):
report = {'candidate_created': success, 'cleanup_confirmed': True,
'runner_error': None, 'copied_authored_unchanged': True,
'profile_addons_unchanged': True, 'external_addons_unchanged_after_run': True,
'completion_observed': 'native-child-exited',
'errors': [] if success else ['RESOURCES (E): synthetic shutdown leak']}
report.update(changes)
+190
View File
@@ -0,0 +1,190 @@
"""Exact synthetic external closure and packaging orchestration; never launch native executables."""
import argparse
import contextlib
import io
import json
from pathlib import Path
import shutil
import sys
import unittest
import uuid
from unittest.mock import Mock,patch
import test_native_package as base
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools'))
import native_package as n
import external_addons as ext
class ExternalPackageTests(unittest.TestCase):
def setUp(self):
self.fixture=base.NativePackageTests();self.fixture.setUp();self.addCleanup(self.fixture.doCleanups)
self.source=self.fixture.external_fixture()
self.root=self.fixture.source;self.session=self.fixture.session
self.steam=self.fixture.steam;self.compat=self.fixture.compat
self.context=n.external_request('chat',self.source,self.root)
def prepare(self):
self.authored=n.copy_payload(self.root,self.session)
n.capture_external(self.context,self.session)
def profile(self,project):
profile=self.session/'profiles'/project;(profile/'addons').mkdir(parents=True)
(profile/'addons/ArmaReforger').symlink_to(self.steam/'steamapps/common/Arma Reforger/addons/data',target_is_directory=True)
return profile
def native_double(self,project='ai',mutation=None,error='',native_exit=0,cleanup=True,write_status=True):
child=Mock();child.poll.return_value=0
def spawn(command,**kwargs):
self.assertNotIn('WINEPREFIX',kwargs['env'])
logs=self.session/'profiles'/project/'logs/logs_synthetic';logs.mkdir(parents=True)
(logs/'console.log').write_text('SYNTHETIC PACKAGE DOUBLE; NOT NATIVE EVIDENCE\n'+error)
(self.session/'packages'/project/'data.pak').write_bytes(b'SYNTHETIC SUCCESSOR OUTPUT')
native=n.command_for(self.steam,self.session,project);native=native[native.index('--')+1:]
if write_status:n.save(self.session/'evidence'/project/'native-exit.json',{'command':native,'exit_code':native_exit})
if mutation:mutation()
return child
with patch.object(n,'assert_no_game'),patch.object(n,'assert_prefix_idle'), \
patch.object(n.subprocess,'Popen',side_effect=spawn) as launch, \
patch.object(n.subprocess,'check_output',side_effect=lambda args,**kw:'[]' if 'sink-inputs' in args else '7'), \
patch.object(n.subprocess,'run'),patch.object(n,'cleanup_owned_run',return_value={'cleanup_confirmed':cleanup,'errors':[]}) as cleaned:
report=n.run_project(self.steam,self.compat,self.session,project,30,self.authored,
external=self.context if project in {'ai','chat'} else None)
launch.assert_called_once();cleaned.assert_called_once()
return report
def args(self,project='chat',external=True,continuation=True):
session=Path('/tmp')/('erri-external-package-'+uuid.uuid4().hex)
self.addCleanup(lambda:shutil.rmtree(session,ignore_errors=True))
return argparse.Namespace(project=project,external_addons=self.source if external else None,
session=session,steam=self.steam,compat=self.compat,timeout=30,continue_on_failure=continuation)
def run_double(self,args,callback):
with patch.object(n,'assert_no_game'),patch.object(n,'assert_prefix_idle'), \
patch.object(n.shutil,'which',return_value='/mock'),patch.object(n,'run_project',side_effect=callback) as runs, \
patch.object(n.subprocess,'Popen') as child,contextlib.redirect_stdout(io.StringIO()):
code=n.run(args,self.root)
child.assert_not_called()
return code,json.loads((args.session/'package-report.json').read_text()),runs.call_args_list
def test_option_is_required_only_for_external_projects_before_native_work(self):
for project in ('ai','chat'):
with self.subTest(project=project),patch.object(n,'assert_no_game') as game,patch.object(n,'copy_payload') as copy:
with self.assertRaisesRegex(ValueError,'require --external-addons'):n.run(self.args(project,False),self.root)
game.assert_not_called();copy.assert_not_called()
for project in ('core','intel','map'):
with self.subTest(project=project),patch.object(n,'assert_no_game') as game:
with self.assertRaisesRegex(ValueError,'vanilla-only'):n.run(self.args(project),self.root)
game.assert_not_called()
self.assertIsNone(n.external_request(project,None,self.root))
def test_cli_passes_explicit_installed_path_without_launch(self):
with patch.object(n,'run',return_value=1) as run:
self.assertEqual(n.main(['--project','ai','--session','/tmp/not-created','--compat',str(self.compat),
'--external-addons',str(self.source)]),1)
self.assertEqual(run.call_args.args[0].external_addons,self.source)
def test_exact_profiles_expose_only_reviewed_six_not_workshop_parent(self):
unrelated=self.source/'original';unrelated.mkdir();(unrelated/'addon.gproj').write_text('GameProject { GUID "5F16D7E4A1CBE075" }')
self.prepare()
for project in ('core','ai','intel','map','chat'):
profile=self.profile(project);context=self.context if project in ('ai','chat') else None
records=n.expose_external(project,profile,self.steam,context)
expected={'ArmaReforger'}|({'ERRI_'+guid for guid in ext.ALLOWED} if context else set())
self.assertEqual({p.name for p in (profile/'addons').iterdir()},expected)
self.assertEqual(len(records),6 if context else 0)
for link in (profile/'addons').iterdir():
self.assertTrue(link.is_symlink());self.assertNotEqual(link.resolve(),self.source)
self.assertNotEqual(link.resolve(),unrelated)
command=n.command_for(self.steam,self.session,project)
self.assertEqual(command[command.index('-addonsDir')+1],'Z:'+str(self.session/'source/addons'))
self.assertNotIn(str(self.source),command)
self.assertFalse((self.session/'source/addons'/ext.DIRECT[0]).exists())
self.assertEqual(json.loads((self.session/'external-addons-before.json').read_text()),self.context['records'])
def test_native_helper_rejects_missing_or_wrong_context_before_creating_profile(self):
self.prepare()
with patch.object(n.subprocess,'Popen') as child:
with self.assertRaises(ValueError):n.run_project(self.steam,self.compat,self.session,'ai',30,self.authored)
with self.assertRaises(ValueError):n.run_project(self.steam,self.compat,self.session,'core',30,self.authored,external=self.context)
child.assert_not_called()
self.assertFalse((self.session/'profiles').exists())
def test_missing_manifest_changed_policy_or_foreign_dependency_refuse(self):
manifest=self.source/ext.DIRECT[0]/'addon.gproj';manifest.write_text(manifest.read_text()+'// altered')
self.session.mkdir()
with self.assertRaisesRegex(ValueError,'manifest changed'):n.capture_external(self.context,self.session)
self.assertFalse((self.session/'profiles').exists())
def test_missing_closure_and_resource_identity_refuse_before_exposure(self):
database=self.source/ext.DIRECT[1]/'resourceDatabase.rdb';database.write_bytes(b'wrong resource identity')
self.session.mkdir()
with self.assertRaisesRegex(ValueError,'resource identity'):n.capture_external(self.context,self.session)
shutil.rmtree(self.source/ext.DIRECT[0])
with self.assertRaisesRegex(ValueError,'Missing installed'):n.capture_external(self.context,self.session)
def test_unrelated_addon_wrong_link_and_whole_tree_link_are_refused(self):
self.prepare();profile=self.profile('ai');records=n.expose_external('ai',profile,self.steam,self.context)
addon=profile/'addons';extra=addon/'Original';extra.symlink_to(self.source,target_is_directory=True)
with self.assertRaisesRegex(ValueError,'unrelated'):n.verify_package_profile(profile,self.steam,records)
extra.unlink();link=addon/('ERRI_'+ext.DIRECT[0]);link.unlink();link.symlink_to(self.source,target_is_directory=True)
with self.assertRaisesRegex(ValueError,'link changed'):n.verify_package_profile(profile,self.steam,records)
link.unlink();link.mkdir()
with self.assertRaisesRegex(ValueError,'link changed'):n.verify_package_profile(profile,self.steam,records)
def test_native_clean_exit_binds_before_after_external_fingerprints(self):
self.prepare();report=self.native_double()
self.assertTrue(report['candidate_created']);self.assertTrue(report['external_addons_unchanged_after_run'])
self.assertTrue(report['profile_addons_unchanged']);self.assertEqual(report['external_addon_count'],6)
evidence=self.session/'evidence/ai'
before=json.loads((evidence/'external-addons-before.json').read_text())
after=json.loads((evidence/'external-addons-after.json').read_text())
self.assertTrue(after['unchanged']);self.assertEqual(after['records'],before)
self.assertEqual(json.loads((evidence/'launch.json').read_text())['external_policy_sha256'],report['external_policy_sha256'])
for key in ('release_qualified','package_contents_verified','native_consumption_verified','tool_completion_semantics_verified'):
self.assertFalse(report[key])
def test_external_payload_change_cannot_qualify_after_clean_native_exit(self):
self.prepare();pak=self.source/ext.DIRECT[0]/'data.pak'
report=self.native_double(mutation=lambda:pak.write_bytes(b'changed installed payload'))
self.assertFalse(report['candidate_created']);self.assertFalse(report['external_addons_unchanged_after_run'])
self.assertTrue(report['copied_authored_unchanged']);self.assertTrue(report['cleanup_confirmed'])
self.assertTrue(any('External input validation failed' in e for e in report['errors']))
after=json.loads((self.session/'evidence/ai/external-addons-after.json').read_text())
self.assertFalse(after['unchanged']);self.assertNotEqual(after['records'],self.context['records'])
def test_cache_tampering_cannot_relabel_changed_payload_as_unchanged(self):
self.prepare()
self.context['cache_path'].write_text('{}')
with self.assertRaisesRegex(ValueError,'fingerprint cache changed'):n.verify_external(self.context)
def test_policy_mutation_or_deleted_input_cannot_qualify(self):
self.prepare();path=self.context['policy_path']
report=self.native_double(mutation=lambda:path.write_text(path.read_text()+'\n'))
self.assertFalse(report['candidate_created']);self.assertFalse(report['external_addons_unchanged_after_run'])
after=json.loads((self.session/'evidence/ai/external-addons-after.json').read_text());self.assertIsNone(after['records'])
def test_relinked_private_dependency_fails_without_altering_installed_inputs(self):
self.prepare()
def mutate():
link=self.session/'profiles/ai/addons'/('ERRI_'+ext.DIRECT[0]);link.unlink();link.symlink_to(self.source,target_is_directory=True)
report=self.native_double(mutation=mutate)
self.assertFalse(report['candidate_created']);self.assertFalse(report['profile_addons_unchanged'])
self.assertTrue(report['external_addons_unchanged_after_run'])
def test_unchanged_external_inputs_do_not_suppress_native_errors(self):
self.prepare();report=self.native_double(error='RESOURCES (E): retained upstream localization error')
self.assertFalse(report['candidate_created']);self.assertTrue(report['external_addons_unchanged_after_run'])
self.assertTrue(any('retained upstream localization error' in e for e in report['errors']))
def test_missing_native_exit_or_failed_cleanup_remain_disqualifying(self):
self.prepare();report=self.native_double(write_status=False,cleanup=False)
self.assertFalse(report['candidate_created']);self.assertIsNone(report['native_exit_code'])
self.assertFalse(report['cleanup_confirmed']);self.assertTrue(report['external_addons_unchanged_after_run'])
def test_context_only_passed_to_ai_and_chat_in_full_build(self):
args=self.args()
def result(*pos,**kw):
self.assertEqual(kw['external'] is not None,pos[3] in ('ai','chat'))
return base.NativePackageTests.completed_report(True)
code,summary,calls=self.run_double(args,result)
self.assertEqual(code,0);self.assertEqual([c.args[3] for c in calls],['core','ai','intel','map','chat'])
self.assertEqual(summary['external_addon_count'],6);self.assertTrue(summary['external_addons_unchanged_after_run'])
self.assertFalse(summary['release_qualified']);self.assertFalse(summary['published'])
self.assertTrue((args.session/'external-addons-after.json').is_file())
def test_diagnostic_continuation_stops_on_mid_batch_external_mutation(self):
args=self.args();seen=[]
def result(*pos,**kw):
seen.append(pos[3]);(self.source/ext.DIRECT[0]/'data.pak').write_bytes(b'changed between projects')
return base.NativePackageTests.completed_report(True)
code,summary,calls=self.run_double(args,result)
self.assertEqual(code,1);self.assertEqual(seen,['core']);self.assertEqual(len(calls),1)
self.assertFalse(summary['external_addons_unchanged_after_run']);self.assertFalse(summary['all_candidates_created'])
self.assertIn('external dependency inputs changed',summary['stopped_reason'])
self.assertEqual(summary['skipped_projects'],['ai','intel','map','chat'])
def test_diagnostic_continuation_cannot_ignore_unverified_external_report(self):
args=self.args();reports=[base.NativePackageTests.completed_report(True),
base.NativePackageTests.completed_report(True,external_addons_unchanged_after_run=False)]
code,summary,calls=self.run_double(args,lambda *a,**k:reports.pop(0))
self.assertEqual(code,1);self.assertEqual(len(calls),2);self.assertIn('external dependency',summary['stopped_reason'])
self.assertFalse(summary['all_candidates_created'])
if __name__=='__main__':unittest.main()
+3 -1
View File
@@ -4,8 +4,10 @@ from policy_harness import ROOT,body,run
spec=importlib.util.spec_from_file_location('import_settings',ROOT/'tools/import_settings.py')
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
result,deferred=m.merge({'configVersion':1,'reportIntervalSeconds':20.0,'supportSquadLimit':2,'responseRadiusMetres':1500,'convoyCount':3},{'aiIntelChancePercent':0,'intelRevealChancePercent':100})
assert result==dict(reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0)
assert result==dict(reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,stingerChanceOneIn=15)
assert deferred==['ai:convoyCount']
assert m.merge(ai={'stingerChanceOneIn':37})[0]['stingerChanceOneIn']==37
assert m.merge(ai={'stingerChanceOneIn':0})[0]['stingerChanceOneIn']==0
assert m.merge()[0]=={k:v[0] for k,v in m.SCHEMA.items()}
for key,(_,lo,hi) in m.SCHEMA.items():
if key not in m.AI_KEYS and key not in m.INTEL_KEYS:continue
+1 -1
View File
@@ -26,7 +26,7 @@ class SourceBundleTests(unittest.TestCase):
self.root = self.home / 'source'
shutil.copytree(ROOT / 'addons', self.root / 'addons')
(self.root / 'docs').mkdir()
for name in ('addons.json', 'vanilla-resources.json', 'acceptance.json'):
for name in ('addons.json', 'vanilla-resources.json', 'acceptance.json', 'external-addons.json'):
shutil.copy2(ROOT / 'docs' / name, self.root / 'docs' / name)
self.output = self.home / 'bundle.zip'