Build independent Everon successor foundation and native regressions
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
from pathlib import Path
|
||||
from policy_harness import body,run
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
s=body(ROOT/'addons/ai/Scripts/Game/ERR/ERR_HelicopterAvailability.c','string Check(')
|
||||
run('#include <string>\n#include <cassert>\nusing std::string;\nstruct Availability {float m_fGroundedSince=-1;\n'+s+'''};
|
||||
int main(){
|
||||
Availability a;
|
||||
assert(a.Check(0,0,false,false,true,false,false,0,0).empty());
|
||||
assert(!a.Check(0,0,true,false,true,false,false,0,0).empty());
|
||||
assert(a.Check(20000,0,false,false,true,false,false,0,0).empty());
|
||||
assert(a.Check(79999,0,false,false,true,false,false,0,0).empty());
|
||||
assert(!a.Check(80000,0,false,false,true,false,false,0,0).empty());
|
||||
assert(a.Check(81000,0,false,false,true,true,false,0,0).empty());
|
||||
assert(a.Check(82000,0,false,false,true,false,true,0,0).empty());
|
||||
assert(!a.Check(82000,0,false,true,true,false,false,0,0).empty());
|
||||
assert(!a.Check(82000,0,false,false,false,false,false,0,0).empty());
|
||||
}
|
||||
''')
|
||||
print('PASS: migrated aircraft availability policy (controller migration still pending)')
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute production bounded radio graph and hop-delivery state machine."""
|
||||
from pathlib import Path
|
||||
import re, subprocess, tempfile
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
source=(ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQNetwork.c').read_text()
|
||||
def extract(signature):
|
||||
a=source.index(signature);b=source.index('{',a);end=b+1;depth=1
|
||||
while depth:depth+=(source[end]=='{')-(source[end]=='}');end+=1
|
||||
return source[a:end]
|
||||
def trans(s):
|
||||
s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('null','nullptr')
|
||||
s=s.replace('string.Format','Format').replace('string ', 'std::string ')
|
||||
s=s.replace('Math.Round(distance).ToString()', 'std::to_string(int(distance))')
|
||||
s=re.sub(r'\b(\w+)\.ToString\(\)',r'std::to_string(\1)',s)
|
||||
for name in ['Math','vector']:s=s.replace(name+'.',name+'::')
|
||||
for typ in ['IEntity','SCR_AIGroup','ERR_HQRoute','ERR_HQRadioNode','ERR_HQJournal']:
|
||||
s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
|
||||
s=s.replace('array<int> queue = {}','Arr<int> queue').replace('array<int> cell','Arr<int>* cell').replace('array<int> neighbors','Arr<int>* neighbors')
|
||||
s=s.replace('cell = {};','cell = new Arr<int>();')
|
||||
s=re.sub(r'foreach\s*\(int (\w+) : neighbors\)',r'for (auto \1 : *neighbors)',s)
|
||||
for name in ['carrier','caller','node','route','journal','cell']:
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=re.sub(r'(Nodes|m_Nodes)\[([^\]]+)\]\.',r'\1[\2]->',s)
|
||||
s=s.replace('Carrier.GetOrigin()', 'Carrier->GetOrigin()')
|
||||
s=re.sub(r'\b(int|float|bool) (\w+);',r'\1 \2{};',s)
|
||||
return s
|
||||
headers=r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
struct vector{float v[3]{};float operator[](int i)const{return v[i];}static float DistanceXZ(vector a,vector b){return std::hypot(a[0]-b[0],a[2]-b[2]);}};
|
||||
vector V(float x,float z=0){return {{x,0,z}};}
|
||||
namespace Math{int Floor(float x){return std::floor(x);}}
|
||||
std::string Format(const char*,int x,int z){return std::to_string(x)+":"+std::to_string(z);}
|
||||
template<class T>struct Arr:std::vector<T>{int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Clear(){this->clear();}void Insert(T x){this->push_back(x);}void InsertAt(T x,int i){this->insert(this->begin()+i,x);}};
|
||||
template<class K,class V>struct Map:std::map<K,V>{void Clear(){this->clear();}bool Contains(K k){return this->count(k);}bool Find(K k,V& v){if(!Contains(k))return false;v=this->at(k);return true;}V Get(K k){if(!Contains(k))return {};return this->at(k);}void Insert(K k,V v){(*this)[k]=v;}};
|
||||
struct SCR_AIGroup{bool enemy=true;};struct IEntity{vector pos;bool alive=true;int radio=1;bool captured=false;vector GetOrigin(){return pos;}};
|
||||
struct ERR_HQRadioNode{IEntity* Carrier{};SCR_AIGroup* Group{};int UnitId{},Capability{};bool Valid(bool manpack=false){return Carrier&&Carrier->alive&&!Carrier->captured&&Carrier->radio>=(manpack?2:1);}std::string Label(){return "unit "+std::to_string(UnitId);}};
|
||||
struct ERR_HQJournal{Arr<std::string> events;void Write(std::string kind,std::string text,int){events.Insert(kind+" "+text);}};
|
||||
'''
|
||||
routefields='Arr<ERR_HQRadioNode*> Nodes;bool Downlink{},Sending{},Failed{};int Cursor{},MessageId{};float Due{},StartedAt=-1;vector SentFrom;std::string Payload;'
|
||||
networkfields='Arr<ERR_HQRadioNode*> m_Nodes;Arr<int> m_Parents,m_Depths;Map<IEntity*,int> m_Indices;Map<std::string,Arr<int>*> m_Cells;bool Overflow{};'
|
||||
route='struct ERR_HQRoute{'+routefields+'\n'+'\n'.join(trans(extract(x)) for x in ['int Advance(', 'protected string SenderLabel(', 'protected int Fail('])+'};\n'
|
||||
network='struct Network{'+networkfields+'\n'+'\n'.join(trans(extract(x)) for x in ['void Clear(', 'protected string Cell(', 'void Add(', 'void Build(', 'ERR_HQRoute Route('])+'};\n'
|
||||
tests=r'''
|
||||
int main(){
|
||||
Network network;SCR_AIGroup group;IEntity c[5];for(int i=0;i<5;i++)c[i].pos=V(i*500);
|
||||
c[0].radio=2;for(int i=0;i<5;i++)network.Add(&c[i],&group,37+i,c[i].radio);network.Build();
|
||||
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
|
||||
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);
|
||||
auto down=network.Route(&c[2],true);assert(down->Nodes[0]->Carrier==&c[0]);assert(down->Advance(0,&journal)==0);
|
||||
assert(down->Advance(2000,&journal)==0);assert(down->Advance(4000,&journal)==0);assert(down->Advance(6000,&journal)==1);
|
||||
auto captured=network.Route(&c[2],true);captured->Advance(0,&journal);c[0].captured=true;assert(captured->Advance(2000,&journal)==-1);c[0].captured=false;
|
||||
auto timeout=network.Route(&c[2]);timeout->Advance(0,&journal);assert(timeout->Advance(30001,&journal)==-1);
|
||||
Network disconnected;disconnected.Add(&c[1],&group,1,1);disconnected.Build();assert(!disconnected.Route(&c[1]));
|
||||
Network boundary;c[1].pos=V(501);boundary.Add(&c[0],&group,1,2);boundary.Add(&c[1],&group,2,1);boundary.Build();assert(!boundary.Route(&c[1]));
|
||||
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";
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='eai-hq-network-') as tmp:
|
||||
cpp=Path(tmp)/'test.cpp';exe=Path(tmp)/'test';cpp.write_text(headers+route+network+tests)
|
||||
subprocess.run(['g++','-std=c++17','-Wall','-Wextra',str(cpp),'-o',str(exe)],check=True)
|
||||
subprocess.run([str(exe)],check=True)
|
||||
@@ -0,0 +1,21 @@
|
||||
from pathlib import Path
|
||||
from policy_harness import body,run
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
s=body(ROOT/'addons/ai/Scripts/Game/ERR/ERR_MortarSalvo.c','static vector Offset(').replace('Math.','Math::')
|
||||
run('''#include <cassert>
|
||||
#include <cmath>
|
||||
struct vector { float x,y,z;};
|
||||
vector Vector(float x,float y,float z){return {x,y,z};}
|
||||
namespace Math {constexpr float DEG2RAD=0.01745329252;float Sqrt(float n){return std::sqrt(n);}float Sin(float n){return std::sin(n);}float Cos(float n){return std::cos(n);}}
|
||||
constexpr int ROUNDS=8;constexpr float RADIUS_METRES=40;
|
||||
'''+s+'''
|
||||
int main(){for(int site=0;site<4;site++)for(int order=0;order<10;order++){
|
||||
float previous=0;
|
||||
for(int shot=0;shot<8;shot++){
|
||||
auto a=Offset(site,order,shot),b=Offset(site,order,shot);
|
||||
assert(a.x==b.x&&a.z==b.z&&a.y==0);
|
||||
float radius=std::hypot(a.x,a.z);assert(radius>previous&&radius<=40.0001);previous=radius;
|
||||
}
|
||||
}}
|
||||
''')
|
||||
print('PASS: migrated salvo offsets are deterministic, spread and bounded (firing controller pending)')
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Execute actual Enforce policy bodies as C++, without claiming engine coverage."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile,re
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
def body(path,signature):
|
||||
s=path.read_text();a=s.index(signature);b=s.index('{',a);i=b+1;depth=1
|
||||
while depth:
|
||||
depth+=(s[i]=='{')-(s[i]=='}');i+=1
|
||||
return s[a:i]
|
||||
def run(code):
|
||||
with tempfile.TemporaryDirectory(prefix='err-policy-') as tmp:
|
||||
p=Path(tmp);(p/'check.cpp').write_text(code)
|
||||
subprocess.run(['g++','-std=c++17','-Wall','-Werror',str(p/'check.cpp'),'-o',str(p/'check')],check=True)
|
||||
subprocess.run([str(p/'check')],check=True)
|
||||
@@ -0,0 +1,29 @@
|
||||
from policy_harness import ROOT,body,run
|
||||
source=ROOT/'addons/core/Scripts/Game/ERR/ERR_MissionRules.c'
|
||||
rules='\n'.join(body(source,x) for x in ['static bool Terminal(', 'static bool CanTransition(', 'static bool DefenceComplete(', 'static bool ContactFresh(', 'static int Discovery(']).replace('ERR_Phase.', 'ERR_Phase::')
|
||||
rules += '\n' + body(ROOT/'addons/core/Scripts/Game/ERR/ERR_PlayerRoster.c', 'static bool CanEnter(').replace('ERR_Phase.', 'ERR_Phase::')
|
||||
run('''#include <cassert>
|
||||
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
|
||||
'''+rules+'''
|
||||
int main(){
|
||||
for(int phase=-1;phase<=7;phase++){
|
||||
assert(!CanEnter(true,phase));
|
||||
assert(CanEnter(false,phase)==(phase>=1&&phase<=3));
|
||||
}
|
||||
for(int from=0;from<=6;from++)for(int to=0;to<=6;to++){
|
||||
bool expected=(from<5&&to==from+1)||(from>=1&&from<=4&&to==6);
|
||||
assert(CanTransition(from,to)==expected);
|
||||
}
|
||||
assert(!DefenceComplete(100,0,180,true));assert(DefenceComplete(180,0,180,true));
|
||||
assert(!DefenceComplete(180,0,180,false));assert(!DefenceComplete(300,-1,180,true));
|
||||
assert(!DefenceComplete(10,20,180,true));assert(!DefenceComplete(300,0,0,true));
|
||||
assert(ContactFresh(0,120000));assert(!ContactFresh(0,120001));
|
||||
assert(!ContactFresh(100,99));assert(!ContactFresh(-1,0));
|
||||
for(int roll=0;roll<100;roll++){
|
||||
assert(Discovery(0,roll,0)==1);assert(Discovery(0,roll,100)==2);
|
||||
assert(Discovery(1,roll,0)==2);assert(Discovery(2,roll,0)==2);
|
||||
assert(Discovery(0,roll,15)==(roll<15?2:1));
|
||||
}
|
||||
}
|
||||
''')
|
||||
print('PASS: all phase transitions, terminal states, defence boundaries, stale/future observations, intel discovery')
|
||||
@@ -0,0 +1,31 @@
|
||||
from pathlib import Path
|
||||
import importlib.util,json,tempfile
|
||||
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)
|
||||
assert deferred==['ai:convoyCount']
|
||||
assert m.merge()[0]=={k:v[0] for k,v in m.SCHEMA.items()}
|
||||
for key,(_,lo,hi) in m.SCHEMA.items():
|
||||
if key=='radioDefenceSeconds':continue
|
||||
for invalid in (lo-1,hi+1,True,False,None,'10',lo+0.5,float('nan'),float('inf')):
|
||||
try:m.merge(**({'ai':{key:invalid}} if key in m.AI_KEYS else {'intel':{key:invalid}}))
|
||||
except ValueError:pass
|
||||
else:raise AssertionError((key,invalid))
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
dest=Path(tmp)/'rereintroduced.json'
|
||||
m.write_new(dest,json.dumps(result));before=dest.read_bytes()
|
||||
try:m.write_new(dest,'overwrite')
|
||||
except FileExistsError:pass
|
||||
else:raise AssertionError('Existing destination overwritten')
|
||||
assert dest.read_bytes()==before
|
||||
assert sorted(p.name for p in Path(tmp).iterdir())==['rereintroduced.json']
|
||||
# Test the actual runtime validator against the migration schema at each boundary.
|
||||
validator=body(ROOT/'addons/core/Scripts/Game/ERR/ERR_Settings.c','static bool Valid(').replace('string key','std::string key')
|
||||
checks=[]
|
||||
for key,(_,lo,hi) in m.SCHEMA.items():
|
||||
for value,expected in [(lo-1,False),(lo,True),(hi,True),(hi+1,False)]:
|
||||
checks.append(f'assert(Valid("{key}",{value})=={str(expected).lower()});')
|
||||
run('#include <cassert>\n#include <string>\n'+validator+'\nint main(){'+''.join(checks)+'assert(!Valid("unknown",1));}\n')
|
||||
print('PASS: legacy values preserved, unsupported fields reported, invalid data rejected, existing files protected, runtime bounds aligned')
|
||||
Reference in New Issue
Block a user