Integrate delivered HQ memory, finite physical convoy and RHS WCS dependencies
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
@@ -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):
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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"}));}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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(¬Native.mode,¬Native.route,1000);notNative.unit.Tick(¬Native.mode,¬Native.route,2000);assert(notNative.unit.RouteIndex==0&¬Native.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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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')
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user