Add mission customization, HQ support and aggression-driven AI

Integrate faction and random settings menus, generated objectives, scouts, convoys, air support, mortar stations and body-intel recovery. Add identified-body aggression, finite hunter UAZ deployment, dedicated objective defence and maximum AI skill with separate reduced aim accuracy. Include policy tests, native probes, evidence and documentation.
This commit is contained in:
2026-09-23 15:07:14 +02:00
parent 04bd19327b
commit 1b1a8b5173
242 changed files with 12153 additions and 875 deletions
+6 -4
View File
@@ -1,20 +1,22 @@
from pathlib import Path
from policy_harness import body,run
ROOT=Path(__file__).resolve().parents[1]
import re
production=(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_MortarSalvo.c').read_text()
constants='\n'.join(re.findall(r'static const (?:int|float) [^;]+;',production)).replace('static const','constexpr')
s=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/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++){
'''+constants+s+'''
int main(){assert(ROUNDS>=8&&RADIUS_METRES>=80);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;
float radius=std::hypot(a.x,a.z);assert(radius>previous&&radius<=80.0001);previous=radius;
}
}}
''')
+8 -6
View File
@@ -10,7 +10,7 @@ source = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/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(BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c', 'static array<string> Keys('))
assert len(keys) == 10 and set(settings) - set(keys) == {"stingerChanceOneIn", "automaticConvoyEnabled"}
assert len(keys) == 11 and set(settings) - set(keys) == {"stingerChanceOneIn", "automaticConvoyEnabled", "heliTimerMinutes"}
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.
@@ -44,9 +44,10 @@ except AssertionError:
else:
raise AssertionError('Malformed child structure must be rejected')
names = re.findall(r'\bName "([^"]+)"', layout.read_text())
assert 'RandomDay_startTimeMinutes' in names
assert len(names) == len(set(names)), 'Menu widget names must be unique'
for key in keys:
assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key))
assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key, 'Random_' + key))
for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'):
assert name in names
for name in re.findall(r'"([A-Za-z]+)"', body(source, 'override void OnMenuOpen(').split('foreach')[0]):
@@ -90,9 +91,10 @@ int main() {
{"intelRevealChancePercent","intel reveal"},{"startTimeMinutes","mission time"},
{"timeMultiplier","mission speed"},{"startDateCode","mission date"},
{"weatherProfile","mission weather"},{"reportIntervalSeconds","hq interval"},
{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"}};
{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"},{"convoyCount","hq convoy count"}};
for(auto pair:cases){
assert(Fields::Command(pair.first," 42 ")==pair.second+" set 42");
assert(Fields::Command(pair.first,"Random")==pair.second+" set Random");
assert(Fields::Command(pair.first,"").empty());
assert(Fields::Command(pair.first," \t ").empty());
assert(Fields::Command(pair.first,string(33,'x')).empty());
@@ -101,10 +103,10 @@ int main() {
assert(Fields::Command("startTimeMinutes","14:25")=="mission time set 14:25");
assert(Fields::Command("startDateCode","2026-09-19")=="mission date set 2026-09-19");
assert(Fields::Command("weatherProfile","authored")=="mission weather set authored");
for(string key:{"heliTimer","convoyCount","prisonDistance","stingerChance","mission abandon","","bad"}){
for(string key:{"heliTimer","prisonDistance","stingerChance","mission abandon","","bad"}){
assert(Fields::Command(key,"1").empty());assert(!Fields::Available(key,true,true));
}
for(string key:{"reportIntervalSeconds","supportSquadLimit","responseRadiusMetres"}){
for(string key:{"reportIntervalSeconds","supportSquadLimit","responseRadiusMetres","convoyCount"}){
assert(!Fields::Available(key,false,true));assert(Fields::Available(key,true,false));
}
assert(!Fields::Available("aiIntelChancePercent",true,false));
@@ -112,4 +114,4 @@ int main() {
for(string key:{"intelRevealChancePercent","radioDefenceSeconds","startTimeMinutes","timeMultiplier","startDateCode","weatherProfile"}) assert(Fields::Available(key,false,false));
}
''')
print('PASS: actual menu command routing covers all ten settings, rejects unsupported keys, gates companions, and matches unique authored widgets')
print('PASS: actual menu command routing covers all eleven settings, rejects unsupported keys, gates companions, and matches unique authored widgets')
+9 -1
View File
@@ -15,7 +15,7 @@ def adapt(s):
s=s.replace('FindComponent(DamageManagerComponent)', 'FindDamage()').replace('FindComponent(SCR_InventoryStorageManagerComponent)', 'FindInventory()')
s=s.replace('DamageManagerComponent.Cast(', 'Damage::Cast(').replace('SCR_InventoryStorageManagerComponent.Cast(', 'Inventory::Cast(')
s=s.replace('purpose: EStoragePurpose.PURPOSE_DEPOSIT, cb: m_Grants->Operation', '1, m_Grants->Operation')
s=s.replace('String item;', 'String item;')
s=s.replace('String item;', 'String item;').replace('string.Format(', 'String::Format(').replace('ERR_DebugAI.', 'DebugAI::').replace('ERR_DebugIntel.', 'DebugIntel::')
return s
binding=adapt(body(source,'void Bind('))
constructor=adapt(body(source,'void ERR_AdminPlayerTools(')).replace('void ERR_AdminPlayerTools(ERR_AdminGrantState grants = nullptr)', 'Tools(GrantState* grants = nullptr)')
@@ -30,6 +30,7 @@ run(r'''
#include <vector>
struct String:std::string {
using std::string::string;String()=default;String(const std::string& s):std::string(s){}
static String Format(String text,int n){text.replace(text.find("%1"),2,std::to_string(n));return text;}
String Trim(){auto a=find_first_not_of(" \t\n");return a==npos?String():String(substr(a,find_last_not_of(" \t\n")-a+1));}
void ToLower(){std::transform(begin(),end(),begin(),[](unsigned char c){return std::tolower(c);});}
bool StartsWith(const char* p){return rfind(p,0)==0;}bool IsEmpty(){return empty();}
@@ -51,6 +52,8 @@ struct Game{float now=0;Manager manager;Manager* GetPlayerManager(){return &mana
namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace Global{bool admin=true;bool IsAdmin(int){return admin;}}
namespace Actors{bool Alive(Entity* a){return a&&a->alive;}}
namespace DebugAI {int calls=0;int KillNearby(Mode* m,Entity* e){assert(m&&e);calls++;return 3;}}
namespace DebugIntel {int calls=0;bool success=true;bool SpawnSix(Mode* m,Entity* e){assert(m&&e);calls++;return success;}}
struct GrantState{IEntity Actor=nullptr;ERR_AdminItemCallback* Operation=nullptr;String Item;float Deadline=0;bool TimedOut=false;
'''+binding+r'''
};
@@ -65,6 +68,11 @@ struct Fixture{
bool command(String args){replies.clear();return tools.Execute(&mode,&controller,args,replies);}void tick(){replies.clear();tools.Tick(replies);}
};
int main(){
{Fixture f;int before=DebugIntel::calls;f.command("cheat intel");assert(DebugIntel::calls==before+1&&f.replies[0].find("Spawned 6")!=String::npos&&f.actor.inventory.submissions==0);Global::admin=false;f.command("cheat intel");assert(DebugIntel::calls==before+1);}
{Fixture f;DebugIntel::success=false;f.command("cheat intel");assert(f.replies[0].find("Could not place")!=String::npos);DebugIntel::success=true;}
{Fixture f;int before=DebugAI::calls;f.command("killais");assert(DebugAI::calls==before+1&&f.replies[0].find("Killed 3")!=String::npos);Global::admin=false;f.command("killais");assert(DebugAI::calls==before+1);}
{Fixture f;assert(!f.command("mission time show")&&f.replies.empty());assert(f.command(" GOD SET ON ")&&!f.actor.damage.enabled);assert(f.tools.m_God);f.command("god");assert(f.actor.damage.changes==1);f.command("god show");assert(f.replies[0].find("on")!=String::npos);f.command("ungod");assert(f.actor.damage.enabled&&f.actor.damage.changes==2);f.tools.Stop();assert(f.actor.damage.changes==2);}
{Fixture f;f.actor.damage.enabled=false;f.command("god");f.command("god set off");assert(!f.actor.damage.enabled);}
{Fixture f;f.command("god");Global::admin=false;f.tick();assert(f.actor.damage.enabled&&f.tools.m_Stopped&&f.replies.empty());Global::admin=true;f.command("god");assert(f.actor.damage.enabled);}
+1 -1
View File
@@ -65,7 +65,7 @@ int main(){
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);
HQHelp hh;IntelHelp ih;lines.clear();assert(hh.Help("hq",lines)&&lines.size()<=5);lines.clear();assert(ih.Help("intel",lines)&&lines.size()<=4);
lines.clear();assert(!hh.Help("unknown",lines)&&!ih.Help("unknown",lines)&&lines.empty());
}
''')
+55
View File
@@ -0,0 +1,55 @@
"""Execute production air-resupply reservation and native-operation quarantine rules."""
from policy_harness import ROOT, body, run
reserve = body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_AirfieldSupport.c', 'bool ReserveAirAmmo(')
reserve = reserve.replace('ERR_HQUnit unit', 'ERR_HQUnit* unit').replace('unit.', 'unit->').replace('Replication.', 'Replication::')
pending = body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_AirAmmoMission.c', 'bool PendingNative(')
pending = pending.replace('foreach (', 'for (').replace('ERR_ConvoyCargoCallback loading', 'ERR_ConvoyCargoCallback* loading').replace('ERR_InfantryAmmoTransfer operation', 'ERR_InfantryAmmoTransfer* operation').replace('loading.', 'loading->').replace('operation.', 'operation->')
run(r'''
#include <cassert>
#include <vector>
struct Replication {inline static bool server=true;static bool IsServer(){return server;}};
struct ERR_HQUnit {
bool eligible=true,HomeGuard=false,Radio=true,PendingOrder=false,Downlink=false,fighting=false;
int LogisticsReservation=0,Assignment=42;
bool Eligible(int){return eligible&&LogisticsReservation==0;}
};
struct HQ {
int m_Mode=0;bool active=true;
bool MissionActive(){return active;}
bool Fighting(ERR_HQUnit* unit){return unit->fighting;}
'''+reserve+r'''
};
struct ERR_ConvoyCargoCallback {bool Finished=false;};
struct ERR_InfantryAmmoTransfer {bool unresolved=false;bool Unresolved(){return unresolved;}};
struct Mission {
std::vector<ERR_ConvoyCargoCallback*> m_Loads;
std::vector<ERR_InfantryAmmoTransfer*> m_Operations;
'''+pending+r'''
};
int main(){
HQ hq;ERR_HQUnit unit;
// A quiet unit may be reserved while retaining its existing delivered search.
assert(hq.ReserveAirAmmo(&unit,200001));assert(unit.LogisticsReservation==200001&&unit.Assignment==42);
assert(!hq.ReserveAirAmmo(&unit,200002));
for(int invalid=0;invalid<9;invalid++){
unit={};hq.active=true;Replication::server=true;
if(invalid==0)Replication::server=false;
if(invalid==1)hq.active=false;
if(invalid==2)unit.eligible=false;
if(invalid==3)unit.HomeGuard=true;
if(invalid==4)unit.Radio=false;
if(invalid==5)unit.PendingOrder=true;
if(invalid==6)unit.Downlink=true;
if(invalid==7)unit.fighting=true;
int token=invalid==8?0:200001;
assert(!hq.ReserveAirAmmo(&unit,token)&&unit.Assignment==42&&unit.LogisticsReservation==0);
}
assert(!hq.ReserveAirAmmo(nullptr,200001));
Mission mission;ERR_ConvoyCargoCallback loading;ERR_InfantryAmmoTransfer move;
mission.m_Loads={&loading};mission.m_Operations={&move};
assert(mission.PendingNative());loading.Finished=true;assert(!mission.PendingNative());
move.unresolved=true;assert(mission.PendingNative());move.unresolved=false;assert(!mission.PendingNative());
}
''')
print('PASS: air logistics reservation preserves delivered search until radio handoff, excludes guards/combat/in-flight orders, and quarantines unresolved native loads/moves')
+64
View File
@@ -0,0 +1,64 @@
"""Production recon timing, orbit and standoff geometry; no native flight claim."""
import re
from policy_harness import ROOT,body,run
path=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_AirReconSystem.c'
schedule=path.read_text().split('class ERR_AirReconSystem')[0]
schedule=schedule[schedule.index('class ERR_ReconSchedule'):].replace('class ERR_ReconSchedule','struct ERR_ReconSchedule').replace('\n}\n','\n};\n')
flight=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ReconFlightComponent.c'
rules=body(flight,'static vector Cruise(')+'\n'+body(flight,'static vector StandOff(')
rules=rules.replace('Math.','Math::').replace('vector.','vector::')
run(r'''
#include <cassert>
#include <algorithm>
#include <cmath>
#include <cstdio>
struct Math{static float Min(float a,float b){return std::min(a,b);}static float Max(float a,float b){return std::max(a,b);}static float Clamp(float a,float lo,float hi){return std::clamp(a,lo,hi);}};
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*s){sscanf(s,"%f %f %f",&x,&y,&z);}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 f)const{return{x*f,y*f,z*f};}vector&operator-=(vector b){x-=b.x;y-=b.y;z-=b.z;return*this;}float Length()const{return std::sqrt(x*x+y*y+z*z);}vector Normalized()const{float f=Length();if(f==0)return{};return{x/f,y/f,z/f};}void Normalize(){*this=Normalized();}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};}
'''+schedule+rules+r'''
int main(){
ERR_ReconSchedule s{};assert(!s.Due(1000000,false));s.Begin(1000);assert(s.Deadline==901000);
s.Begin(2000);assert(s.Deadline==901000);assert(!s.Due(900999,false));assert(s.Due(901000,false));assert(!s.Due(901000,true));
s.Launched();assert(s.Deadline<0&&!s.Due(2000000,false));s.Change(2,2000,true);assert(s.Deadline<0&&s.Minutes==2);
s.Lost(3000);assert(s.Deadline==123000&&!s.Due(122999,false));s.Change(15,4000,false);assert(s.Deadline==904000);
s.Stop();s.Begin(100);s.Lost(200);s.Change(1,300,false);assert(s.Deadline<0&&!s.Due(1e8,false));
vector orbit=Cruise({300,120,0},{0,0,0},false);assert(std::abs(orbit.x)<.001&&std::abs(orbit.z+28)<.001);
vector far=Cruise({1000,120,0},{0,0,0},false);assert(far.x==-40&&far.y==0);
vector near=Cruise({100,120,0},{0,0,0},false);assert(near.x>0); // push back out toward 300m orbit
vector home=Cruise({20,120,0},{0,0,0},true);assert(home.x==-8); // return approaches pad instead of orbiting
vector away=StandOff({100,120,0},{0,0,0},{-40,0,0});assert(away.x>0);
vector clear=StandOff({300,120,0},{0,0,0},{-40,0,0});assert(clear.x==-40);
vector coincident=StandOff({0,120,0},{0,0,0},{0,0,0});assert(std::isfinite(coincident.x)&&coincident.x>0);
}
''')
print('PASS: original 15-minute launch/loss cadence, active/captured timer disarm, saved delay reset, stop, 300m orbit and close-contact standoff')
# Explicit entity deletion must not leave an inactive launch deadline behind.
removed=body(path,'void Removed(')
for old,new in [('ERR_ReconFlightComponent flight','Flight* flight'),('Replication.','Replication::'),('ERR_HQ.','ERR_HQ::'),('m_Mode.','m_Mode->'),('null','nullptr'),('GetGame().GetWorld().GetWorldTime()','clockNow')]:
removed=removed.replace(old,new)
run(r'''
#include <cassert>
#include <string>
using string=std::string;
struct Flight{};struct Mode{bool active=true;bool Active(){return active;}};
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
struct ERR_HQ{inline static int events=0;static void Event(string,string){events++;}};
float clockNow=5000;
'''+schedule+r'''
struct System{
ERR_ReconSchedule m_Schedule;Flight* m_Current=nullptr;Mode* m_Mode=nullptr;int m_PadAttempts=3;string m_Reason;
string Status(){return m_Reason;}
'''+removed+r'''
};
int main(){
Mode mode;Flight owned,other;System s{};s.m_Mode=&mode;s.m_Current=&owned;s.m_Schedule.Begin(0);s.m_Schedule.Launched();
s.Removed(nullptr);s.Removed(&other);assert(s.m_Current==&owned&&s.m_Schedule.Deadline<0&&ERR_HQ::events==0);
Replication::server=false;s.Removed(&owned);assert(s.m_Current==&owned);Replication::server=true;
mode.active=false;s.Removed(&owned);assert(s.m_Current==&owned);mode.active=true;
s.Removed(&owned);assert(!s.m_Current&&s.m_PadAttempts==0&&s.m_Schedule.Deadline==905000&&ERR_HQ::events==1);
clockNow=9000;s.Removed(&owned);assert(s.m_Schedule.Deadline==905000&&ERR_HQ::events==1);
s.m_Current=&other;s.m_Schedule.Stop();s.Removed(&other);assert(s.m_Current==&other&&s.m_Schedule.Deadline<0);
}
''')
print('PASS: actual aircraft-removal notification guards owner/server/active/stop and schedules one full delay without replay extension')
+57
View File
@@ -0,0 +1,57 @@
"""Execute production HQ reinforcement policy against confidence and force scenarios."""
from policy_harness import ROOT, run
import re
source=(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_AirSupportPolicy.c').read_text().split('// One immutable')[0]
source=source.replace('class ', 'struct ').replace('\n}', '\n};').replace('ERR_AirSupportSituation s','ERR_AirSupportSituation* s').replace('s.', 's->').replace('ERR_HQMemoryRules.','ERR_HQMemoryRules::').replace('Math.','Math::')
source=source.replace('ERR_HQMemoryRules->', 'ERR_HQMemoryRules::')
source=re.sub(r'\b(bool|int|float) (\w+);',r'\1 \2{};',source)
run(r'''
#include <cassert>
#include <string>
#include <cmath>
#include <algorithm>
#include <limits>
struct string:std::string{using std::string::string;bool IsEmpty(){return empty();}};
struct Math {static int Clamp(int x,int a,int b){return std::clamp(x,a,b);}static float Max(float a,float b){return std::max(a,b);}};
struct ERR_HQMemoryRules{static bool Finite(float v){return std::isfinite(v);}};
'''+source+r'''
int main(){
ERR_AirSupportSituation s{};s.AircraftReady=true;s.BeliefId=1;s.Samples=3;s.Spotted=3;s.Age=5;s.Confidence=.9;s.Radius=20;s.MotionKnown=true;s.Infantry=true;s.Speed=1;
ERR_AirSupportPolicy p;assert(!p.Consider(&s,0));assert(!p.Consider(&s,44999));assert(p.Consider(&s,45000));
p.Committed(45000);assert(!p.Consider(&s,50000));assert(!p.Consider(&s,525000));assert(p.Consider(&s,570000));
s.CommittedFit=6;assert(!p.Consider(&s,600000));s.CommittedFit=0;s.GroundFit=6;assert(!p.Consider(&s,600000));
s.ConfirmedLosses=2;assert(!p.Consider(&s,600000));assert(p.Consider(&s,620000));
s.ArtilleryBusy=true;assert(!p.Consider(&s,630000));s.ArtilleryBusy=false;assert(!p.Consider(&s,640000));assert(p.Consider(&s,660000));
s.BeliefId=2;assert(!p.Consider(&s,670000));assert(!p.Consider(&s,689999));assert(p.Consider(&s,690000));
auto good=s;
s.Age=91;assert(!p.Consider(&s,700000));s=good;s.Confidence=.69;assert(!p.Consider(&s,700000));
s=good;s.Radius=161;assert(!p.Consider(&s,700000));s=good;s.Samples=1;assert(!p.Consider(&s,700000));
s=good;s.Infantry=false;assert(!p.Consider(&s,700000));s=good;s.Speed=7;assert(!p.Consider(&s,700000));
s=good;s.MotionKnown=false;assert(!p.Consider(&s,700000));s=good;s.AircraftReady=false;assert(!p.Consider(&s,700000));
s=good;s.Age=std::numeric_limits<float>::quiet_NaN();assert(!p.Consider(&s,700000));
assert(!p.Consider(nullptr,700000));assert(!p.Consider(&good,std::numeric_limits<float>::infinity()));
}
''')
print('PASS: production airborne memory confidence, persistence, ground coverage, confirmed losses, artillery veto, cooldown and invalid-input rules')
from policy_harness import body
load=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_AirAssaultLoad.c'
cover=body(load,'bool Cover(').replace('out vector center','int& center').replace('m_Order.Target','m_Order->Target')
service=body(load,'bool NeedsService(')
run(r'''
#include <cassert>
struct Order {int Target=730;};
struct Load {
Order* m_Order=nullptr;bool m_Used=false,m_Approaching=false;float m_CoverUntil=300000;
'''+cover+service+r'''
};
int main(){
Load load;Order order;int center=-1;
assert(!load.Cover(0,center)&&center==-1&&!load.NeedsService(0));
load.m_Order=&order;load.m_Used=true;load.m_Approaching=true;
assert(!load.Cover(1,center)&&!load.NeedsService(300001));
load.m_Approaching=false;assert(load.Cover(299999,center)&&center==730&&!load.NeedsService(299999));
center=-1;assert(!load.Cover(300000,center)&&center==-1&&load.NeedsService(300000));
}
''')
print('PASS: production cover retains the carried HQ target until its deadline; airport reload waits until the drop pass and cover have finished')
+48
View File
@@ -0,0 +1,48 @@
"""Exercise production content selection bodies with small engine boundary doubles.
Native preference I/O, group membership and UI have separate Workbench probes.
"""
from policy_harness import ROOT, body, run
import re
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ContentPolicy.c'
def cpp(sig):
s=body(p,sig).replace('ResourceName ', 'string ').replace('Faction faction','Faction* faction')
s=s.replace('ERR_ContentRegistry.Definition().Find(pack)','registry.Find(pack)')
s=s.replace('ERR_ContentRegistry.Definition().Options','registry.Options')
s=s.replace('ERR_ContentOption option', 'Option* option').replace('foreach (','for (')
s=s.replace('array<ResourceName> groups', 'Array<string> groups')
s=s.replace('option.', 'option->').replace('faction.', 'faction->')
return s
run(r'''
#include <cassert>
#include <map>
#include <string>
#include <vector>
struct string:std::string {using std::string::string;string(std::string s):std::string(s){} bool IsEmpty(){return empty();}};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}bool IsEmpty(){return this->empty();}int Count(){return this->size();}};
struct Flags:std::map<string,bool>{using std::map<string,bool>::operator=;bool Contains(string k){return count(k);}bool Get(string k){return (*this)[k];}};
struct Option{string Id,Parent,NativeFactionKey,GuardGroup;};
struct Registry{std::vector<Option*> Options;Option* Find(string id){for(auto*o:Options)if(o->Id==id)return o;return nullptr;}};
struct Faction{string key;string GetFactionKey(){return key;}};
struct Policy{int m_GuardOffset=0;Flags m_Active;Registry registry;
'''+'\n'.join(cpp(sig) for sig in ('bool Enabled(', 'ResourceName GuardGroup(', 'bool Enemy('))+r'''
};
int main(){
Option pack{"rhs","","",""}, a{"usmc","rhs","RHS_USAF","usmc-group"}, b{"afrf","rhs","RHS_AFRF","afrf-group"};
Policy p;p.registry.Options={&pack,&a,&b};p.m_Active={{"rhs",true},{"usmc",true},{"afrf",true}};
Faction usa{"RHS_USAF"},ru{"RHS_AFRF"},other{"US"};
assert(p.Enabled("")&&!p.Enabled("unknown"));assert(p.Enemy(&usa)&&p.Enemy(&ru)&&!p.Enemy(&other)&&!p.Enemy(nullptr));
assert(p.GuardGroup(0)=="usmc-group"&&p.GuardGroup(1)=="afrf-group"&&p.GuardGroup(2)=="usmc-group");
p.m_GuardOffset=1;assert(p.GuardGroup(0)=="afrf-group"&&p.GuardGroup(1)=="usmc-group");
p.m_Active["rhs"]=false;assert(!p.Enabled("rhs")&&!p.Enabled("usmc")&&!p.Enabled("afrf"));assert(p.GuardGroup(0).empty()&&!p.Enemy(&usa));
p.m_Active["rhs"]=true;p.m_Active["usmc"]=false;assert(!p.Enabled("usmc")&&p.Enabled("afrf"));
assert(p.GuardGroup(0)=="afrf-group"&&p.GuardGroup(10)=="afrf-group"&&!p.Enemy(&usa)&&p.Enemy(&ru));
p.m_Active["afrf"]=false;assert(p.GuardGroup(0).empty());
}
''')
# Config-driven labels, IDs and exact source resources; no fixed RHS row in UI.
registry=(ROOT/'addons/core/Configs/ERR/Content/Registry.conf').read_text()
assert re.findall(r'\bId "([^"]+)"',registry)==['rhs','rhs_usmc','rhs_afrf','rhs_ion']
assert len(re.findall(r'\bGuardGroup ',registry))==3
assert 'ERR_ContentRegistry.Ids()' in (ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ContentMenu.c').read_text()
assert 'PrisonBackpack.et' in (ROOT/'addons/core/Configs/ERR/EveronSites.conf').read_text()
print('PASS: actual content policy gates parent packs, individual factions, faction identity and finite group rotation with vanilla fallback')
+2 -2
View File
@@ -5,7 +5,7 @@ from policy_harness import ROOT,body,run
BASE=Path(os.environ.get('ERR_CONVOY_SETTINGS_ROOT',ROOT))
P=BASE/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c'
keys=body(P,'static array<string> Keys(').replace('array<string>','std::vector<string>')
valid=body(P,'static bool Valid(')
valid='\n'.join(body(P,sig) for sig in ('static bool Valid(', 'static bool Randomizable(', 'static bool ValidSelection('))
default=body(P,'static int AddedDefault(')
read=body(P,'protected bool Read(').replace('protected ','').replace('out map<string, int> values','Map*& values').replace('new map<string, int>()','new Map()')
read=read.replace('JsonLoadContext json','JsonLoadContext* json').replace('json.','json->').replace('values.','values->').replace('foreach (string key : Keys())','for (string key : Keys())').replace('Math.Round','std::round')
@@ -53,5 +53,5 @@ for forbidden in ('StartConvoy','StartScenarioConvoy','Spawn','m_HQ.'):
# Do not advertise this command-only setting as an editable field absent from the actual fixed menu.
menu=body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c','static array<string> Keys(')
assert 'automaticConvoyEnabled' not in menu
assert body(provider,'override bool Help(').count('replies.Insert(')<=4
assert body(provider,'override bool Help(').count('replies.Insert(')<=5
print('PASS: actual read migrates old valid settings, preserves explicit disabled state, rejects invalid data; next-round feedback and fixed menu remain honest')
+14
View File
@@ -44,6 +44,20 @@ int main(){
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"));
// Production configuration reserves every full troop truck: 14 passengers + 11 crew.
for(int target:{0,1,3,20}){
ERR_GameMode supply;ERR_GameMode::current=&supply;ERR_ForceRecord baseline;baseline.Strength=60;supply.ledger.records.Insert(&baseline);ERR_ConvoyPool full(&supply,target,true);
for(int i=0;i<target;i++){
auto ticket=full.Reserve("supply-"+std::to_string(i));assert(ticket&&ticket->OutstandingBodies==25&&ticket->OutstandingGroups==8&&ticket->Records.Count()==8);assert(full.Consume(ticket));
for(int group=0;group<8;group++){
auto r=new ERR_ForceRecord();r->Id=1+supply.ledger.records.Count();r->SourceId=ticket->SourceId+"-vehicle-"+std::to_string(group);r->InitialStrength=full.PlannedCount(group);r->Strength=r->InitialStrength;supply.ledger.records.Insert(r);assert(full.BindRecord(ticket,group,r));
}
assert(ticket->OutstandingBodies==0&&ticket->OutstandingGroups==0&&full.CommittedBodies()==60+25*(i+1));
}
assert(!full.Reserve("excess"));
for(auto r:supply.ledger.records)r->Strength=0;
assert(!full.Reserve("replacement"));
}
ERR_GameMode finite;ERR_GameMode::current=&finite;ERR_ConvoyPool one(&finite,1);assert(one.Reserve("first"));assert(!one.Reserve("second"));
}
''')
+3 -2
View File
@@ -41,6 +41,7 @@ namespace ERR_ConvoyResources{bool available=true;bool Available(){return availa
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;
ERR_ConvoyTask* NewTask(){return new ERR_ConvoyTask();} ERR_ConvoyVehicle* NewVehicle(){return new ERR_ConvoyVehicle();}
bool Current(){return current;}bool FillCrew(ERR_ConvoyVehicle* unit){crews++;return unit->Index!=failCrew;}
''' + code + r'''
};
@@ -49,7 +50,7 @@ int main(){
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";
enemy.key="US"; // A selected non-USSR enemy uses the same finite dispatch transaction.
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++){
@@ -103,7 +104,7 @@ int main(){
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')
print('PASS: actual convoy dispatch validates before receipt consumption, consumes once before native mutation, retains all12 partial-failure paths, accepts the selected non-USSR enemy, 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.
+4 -3
View File
@@ -8,7 +8,7 @@ s=s.replace('bool m_Enabled;', 'bool m_Enabled=false;').replace('bool m_Finished
s=s.replace('class ERR_ConvoyStartup', 'struct ERR_ConvoyStartup').rstrip()+';'
h=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c','protected void AutomaticConvoy(')
h=h.replace('protected ','').replace('null','nullptr').replace('Replication.','Replication::').replace('ERR_GameMode.','ERR_GameMode::')
for name in ('m_Mode','m_ConvoyStartup'):h=h.replace(name+'.',name+'->')
for name in ('m_Mode','m_ConvoyStartup','m_SupplyConvoys','m_Convoys'):h=h.replace(name+'.',name+'->')
run(r'''
#include <cassert>
#include <cmath>
@@ -20,7 +20,8 @@ struct ERR_ForceRules {static bool Finite(float n){return std::isfinite(n);}};
struct Replication {inline static bool server=true;static bool IsServer(){return server;}};
struct ERR_GameMode {inline static ERR_GameMode* current;bool active=true;static ERR_GameMode* Get(){return current;}bool Active(){return active;}};
struct Journal {int count=0;void Write(string,string){count++;}};
struct HQ {ERR_GameMode* m_Mode;ERR_ConvoyStartup* m_ConvoyStartup;void* m_ConvoyTask=nullptr;string m_ConvoyFailure;Journal m_Journal;bool ready=true,accepted=true;int calls=0;bool ConvoyAdmissionReady(){return ready;}bool StartScenarioConvoy(){calls++;return accepted;}
struct Supply {void Tick(float,bool){}}; struct Service {int Count(){return 0;}void* At(int){return nullptr;}};
struct HQ {Supply* m_SupplyConvoys=nullptr; Service* m_Convoys=nullptr;ERR_GameMode* m_Mode;ERR_ConvoyStartup* m_ConvoyStartup;void* m_ConvoyTask=nullptr;string m_ConvoyFailure;Journal m_Journal;bool ready=true,accepted=true;int calls=0;bool ConvoyAdmissionReady(){return ready;}bool StartScenarioConvoy(){calls++;return accepted;}
'''+h+r'''
};
int main(){
@@ -31,7 +32,7 @@ int main(){
ERR_ConvoyStartup manual;manual.Initialize(true);assert(!manual.Decide(0,true,true)&&manual.Finished());assert(!manual.Decide(60000,false,true));
ERR_ConvoyStartup invalid;invalid.Initialize(true);assert(!invalid.Decide(std::numeric_limits<float>::quiet_NaN(),false,true));assert(!invalid.Decide(-1,false,true));invalid.Decide(10000,false,false);assert(!invalid.Decide(0,false,true));assert(invalid.Decide(40000,false,true));
ERR_ConvoyStartup stopped;stopped.Initialize(true);stopped.Decide(0,false,false);stopped.Stop();assert(!stopped.Decide(30000,false,true));
ERR_GameMode mode;ERR_GameMode::current=&mode;ERR_ConvoyStartup startup;startup.Initialize(true);HQ hq{&mode,&startup};
ERR_GameMode mode;ERR_GameMode::current=&mode;ERR_ConvoyStartup startup;startup.Initialize(true);HQ hq;hq.m_Mode=&mode;hq.m_ConvoyStartup=&startup;
Replication::server=false;hq.AutomaticConvoy(0);assert(hq.calls==0&&hq.m_Journal.count==0);Replication::server=true;
ERR_GameMode::current=nullptr;hq.AutomaticConvoy(0);assert(hq.m_Journal.count==0);ERR_GameMode::current=&mode;
mode.active=false;hq.AutomaticConvoy(0);assert(hq.m_Journal.count==0);mode.active=true;
+18
View File
@@ -0,0 +1,18 @@
"""Execute production kill eligibility, including players, retained bodies and radius."""
from policy_harness import ROOT,body,run
s=body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_DebugAI.c','static bool Eligible(')
s=s.replace('IEntity target','IEntity* target').replace('IEntity center','IEntity* center').replace('array<IEntity> protectedBodies','Array protectedBodies').replace('target.','target->').replace('center.','center->').replace('ChimeraCharacter.Cast','ChimeraCharacter::Cast').replace('ERR_Actors.','ERR_Actors::').replace('vector.','vector::')
run(r'''
#include <cassert>
#include <vector>
#include <algorithm>
#include <cmath>
struct vector{float x=0,y=0,z=0;static float Distance(vector a,vector b){return std::sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)+(a.z-b.z)*(a.z-b.z));}};
struct IEntity{vector position;bool alive=true,player=false,character=true;vector GetOrigin(){return position;}};
struct Array:std::vector<IEntity*>{using std::vector<IEntity*>::vector;bool Contains(IEntity* p){return std::find(begin(),end(),p)!=end();}};
namespace ChimeraCharacter{bool Cast(IEntity* e){return e&&e->character;}}
namespace ERR_Actors{bool Alive(IEntity* e){return e->alive;}bool Player(IEntity* e){return e->player;}}
'''+s+r'''
int main(){IEntity center,target;Array empty;target.position.x=1000;assert(Eligible(&target,&center,empty));target.position.x=1000.1;assert(!Eligible(&target,&center,empty));target.position={0,1001,0};assert(!Eligible(&target,&center,empty));target.position={10,0,0};assert(Eligible(&target,&center,empty));target.player=true;assert(!Eligible(&target,&center,empty));target.player=false;assert(!Eligible(&target,&center,Array{&target}));target.character=false;assert(!Eligible(&target,&center,empty));target.character=true;target.alive=false;assert(!Eligible(&target,&center,empty));assert(!Eligible(nullptr,&center,empty));assert(!Eligible(&center,&center,empty));}
''')
print('PASS: actual killais eligibility includes the 1000m boundary, excludes players/retained bodies/dead/non-character entities and checks full 3D distance')
+1 -1
View File
@@ -76,7 +76,7 @@ class DedicatedNativeTests(unittest.TestCase):
self.assertEqual(args[args.index('-worldSystemsConfig')+1], '{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf')
definition = (root/'addons/core/addon.gproj').read_text()
dependencies = set(re.findall(r'"([A-F0-9]{16})"', re.search(r'Dependencies\s*\{([^}]+)', definition)[1]))
self.assertEqual(dependencies, {'58D0FB3206B6F859', '595F2BF2F44836FB', '629B2BA37EFFD577', '5E524E4FEECCA92B'})
self.assertEqual(dependencies, {'58D0FB3206B6F859', '595F2BF2F44836FB', '629B2BA37EFFD577', '5E524E4FEECCA92B', '65930CB4CD0237B2'})
def test_invalid_project_route_combinations_fail_before_files_or_processes(self):
for route, project in [('vanilla-everon', 'chat'), ('unknown', 'core'), ('header', 'unknown')]:
+41
View File
@@ -0,0 +1,41 @@
"""Execute production aim/perception overrides with native API doubles."""
from policy_harness import ROOT, body, run
path=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EnemySkillAwareness.c'
def native(signature):
s=body(path,signature).replace('override ','').replace('super.', 'Native::').replace('EAISkill.', 'EAISkill::')
s=s.replace('PerceptionComponent perceptionComp','PerceptionComponent* perceptionComp').replace('SCR_AIThreatSystem threatSystem','SCR_AIThreatSystem* threatSystem')
return s.replace('perceptionComp.', 'perceptionComp->').replace('m_CombatComponent.', 'm_CombatComponent->')
run(r'''
#include <cassert>
enum class EAISkill{REGULAR,CYLON};
struct SCR_AIThreatSystem{};
struct PerceptionComponent{float factor=0;void SetPerceptionFactor(float f){factor=f;}};
struct Native{
int calls=0;EAISkill sampled=EAISkill::CYLON;
float GetRandomFactor(EAISkill s,float mu){calls++;sampled=s;return mu+1;}
void UpdatePerceptionFactor(PerceptionComponent* p,SCR_AIThreatSystem*){calls++;p->factor=.4f;}
};
struct Combat:Native{
bool bound=false;float m_fEquipmentPerceptionFactor=1,m_fPerceptionFactor=1;
static constexpr float PERCEPTION_FACTOR_ALERTED=2.5f;
bool ERR_EnhancedEnemy(){return bound;}
'''+native('override void UpdatePerceptionFactor(')+r'''
};
struct Aim:Native{
Combat* m_CombatComponent=nullptr;
'''+native('override float GetRandomFactor(')+r'''
};
int main(){
Aim aim;Combat combat;PerceptionComponent perception;SCR_AIThreatSystem threat;
aim.GetRandomFactor(EAISkill::CYLON,0);assert(aim.sampled==EAISkill::CYLON);
aim.m_CombatComponent=&combat;aim.GetRandomFactor(EAISkill::CYLON,0);assert(aim.sampled==EAISkill::CYLON);
combat.bound=true;aim.GetRandomFactor(EAISkill::CYLON,0);assert(aim.sampled==EAISkill::REGULAR);
combat.UpdatePerceptionFactor(&perception,&threat);assert(perception.factor==2.5f&&combat.calls==0);
combat.m_fEquipmentPerceptionFactor=3;combat.m_fPerceptionFactor=.5f;
combat.UpdatePerceptionFactor(&perception,&threat);assert(perception.factor==3.75f);
combat.UpdatePerceptionFactor(nullptr,&threat);assert(combat.calls==0);
combat.bound=false;combat.UpdatePerceptionFactor(&perception,&threat);assert(perception.factor==.4f&&combat.calls==1);
aim.GetRandomFactor(EAISkill::CYLON,0);assert(aim.sampled==EAISkill::CYLON);
}
''')
print('PASS: actual scoped REGULAR aim error and alert perception; preserves equipment multiplier and restores vanilla behavior when unbound')
+7 -7
View File
@@ -44,7 +44,7 @@ struct SCR_AIGroup {
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;}
static ERR_GameMode* Get(){return current;}bool Active(){return active;}int EnemyFaction(){return 1;}bool IsMissionEnemy(int f){return f==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;}}
@@ -70,7 +70,7 @@ struct NativeCombat {
namespace ERR_EnemyGunnerSkill {
bool CapturedGroup(ERR_GameMode*,SCR_AIGroup*){return false;}
bool OriginalGunner(ERR_GameMode*,SCR_AIGroup*,IEntity*){return false;}
EAISkill Profile(EAISkill requested,bool){return requested<EAISkill::EXPERT?EAISkill::EXPERT:requested;}
EAISkill Profile(EAISkill requested,bool){return EAISkill::CYLON;}
}
struct ERR_EnemyOptics{int stops=0;void Stop(){stops++;}};
''' + code + r'''
@@ -121,9 +121,9 @@ int main(){
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);
Soldier skill;skill.combat.ERR_BindEnemySkill(&mode,&skill.group);assert(skill.combat.GetAISkill()==EAISkill::CYLON);
skill.combat.SetAISkill(EAISkill::ROOKIE);assert(skill.combat.GetAISkill()==EAISkill::CYLON);
skill.combat.ResetAISkill();assert(skill.combat.GetAISkill()==EAISkill::CYLON);
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);
@@ -187,7 +187,7 @@ int main(){
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 bounded(&mode);for(int i=0;i<270;i++){bounded.RegisterGroup(new SCR_AIGroup());}assert(bounded.m_Groups.Count()==256);
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);
auto incompleteOptics=incomplete.m_Entries[0]->Optics;int checksBeforeStop=incompleteOptics->checks;
@@ -199,4 +199,4 @@ int main(){
capturedBody.eligible=true;capture.Tick(9000);assert(capturedOptics->checks==1);
}
''')
print('PASS: actual equipment coordinator registration dedupe, startup delay, five member scans, finite component retries, unregister/stop terminal behavior and 128-group bound')
print('PASS: actual equipment coordinator registration dedupe, startup delay, five member scans, finite component retries, unregister/stop terminal behavior and 256-group bound')
+14 -13
View File
@@ -9,11 +9,11 @@ def convert(s):
s=s.replace('ERR_HQ.Cast(mode.Command())','mode->Command()').replace('TurretCompartmentSlot.Cast(slot)','dynamic_cast<TurretCompartmentSlot*>(slot)')
for cls in ('ERR_GameMode','SCR_AIGroupUtilityComponent','SCR_AIGroup','IEntity','ERR_HQ','ERR_ConvoyTask','ERR_ConvoyVehicle','ERR_ForceRecord','BaseCompartmentSlot'):
s=re.sub(r'\b'+cls+r' ([a-zA-Z_]\w*)',cls+r'* \1',s)
for obj in ('mode','group','actor','utility','hq','task','unit','record','slot'):
for obj in ('mode','group','actor','utility','hq','task','unit','vehicle','record','slot'):
s=s.replace(obj+'.',obj+'->')
for field in ('Receipt','Entity'):
s=s.replace(field+'.',field+'->')
s=s.replace('Forces().','Forces()->')
s=s.replace('Forces().','Forces()->').replace('Convoys().','Convoys()->')
for cls in ('ERR_EnemyEquipmentRules','ERR_EnemyGunnerSkill','ERR_ConvoyRules','ERR_Actors','Replication','SCR_Global','ERR_GameMode','EAISkill','ERR_ForceKind','ECompartmentType','CompartmentAccessComponent'):
s=s.replace(cls+'.',cls+'::')
s=s.replace('super.','NativeCombat::').replace('null','nullptr').replace('foreach (','for (')
@@ -22,8 +22,8 @@ def convert(s):
rules=equipment[equipment.index('class ERR_EnemyEquipmentRules'):equipment.index('class ERR_EnemyEquipmentCallback')]
rules=convert(rules.replace('class ERR_EnemyEquipmentRules','struct ERR_EnemyEquipmentRules').replace('modded class SCR_AICombatComponent','struct SCR_AICombatComponent : NativeCombat')).replace('\n}\n','\n};\n')
# Declare helper before rules, then define its real method bodies after rules.
helper=helper[helper.index(' static bool CapturedGroup'):helper.rfind('}')]
helper=convert(helper).replace('static bool CapturedGroup','bool ERR_EnemyGunnerSkill::CapturedGroup').replace('static bool OriginalGunner','bool ERR_EnemyGunnerSkill::OriginalGunner').replace('static EAISkill Profile','EAISkill ERR_EnemyGunnerSkill::Profile')
helper=helper[helper.index(' static ERR_ConvoyTask TaskForGroup'):helper.rfind('}')]
helper=convert(helper).replace('static ERR_ConvoyTask* TaskForGroup','ERR_ConvoyTask* ERR_EnemyGunnerSkill::TaskForGroup').replace('static bool CapturedGroup','bool ERR_EnemyGunnerSkill::CapturedGroup').replace('static bool OriginalGunner','bool ERR_EnemyGunnerSkill::OriginalGunner').replace('static EAISkill Profile','EAISkill ERR_EnemyGunnerSkill::Profile')
run(r'''
#include <cassert>
#include <vector>
@@ -44,12 +44,12 @@ struct Ledger{ERR_ForceRecord* record=nullptr;auto FindGroup(SCR_AIGroup*){retur
struct ERR_ConvoyReceipt{bool Consumed=true;Array<int> Records={8};};
struct ERR_ConvoyVehicle{SCR_AIGroup* Group=nullptr;Vehicle* Entity=nullptr;bool Registered=true,Released=false;int Index=0;Array<IEntity*> Members;Array<BaseCompartmentSlot*> Seats;};
struct ERR_ConvoyTask{bool Ready=true,Stopped=false;ERR_ConvoyReceipt* Receipt=nullptr;Array<ERR_ConvoyVehicle*> Vehicles;};
struct ERR_HQ{ERR_ConvoyTask* task=nullptr;auto ConvoyTask(){return task;}};
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;bool active=true;ERR_HQ* hq=nullptr;Ledger* ledger=nullptr;static auto Get(){return current;}bool Active(){return active;}int EnemyFaction(){return 1;}auto Command(){return hq;}auto Forces(){return ledger;}};
struct ERR_HQ{ERR_ConvoyTask* task=nullptr;ERR_ConvoyTask* extra=nullptr;auto Convoys(){return this;}int Count(){return extra?2:1;}auto At(int index){return index==0?task:extra;}auto ConvoyTask(){return task;}};
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;bool active=true;ERR_HQ* hq=nullptr;Ledger* ledger=nullptr;static auto Get(){return current;}bool Active(){return active;}int EnemyFaction(){return 1;}bool IsMissionEnemy(int f){return f==1;}auto Command(){return hq;}auto Forces(){return ledger;}};
namespace ERR_Actors{bool CanFight(IEntity* a){return a->alive;}bool Alive(IEntity* a){return a->alive;}bool Player(IEntity* a){return a->player;}auto Group(IEntity* a){return a->group;}}
namespace ERR_ConvoyRules{bool capture=false;bool Captured(Vehicle*,SCR_AIGroup* group){return capture||group->players>0;}bool PartOf(IEntity* a,IEntity* b){return a==b;}}
namespace CompartmentAccessComponent{auto GetVehicleIn(IEntity* actor){return actor->vehicle;}}
struct ERR_EnemyGunnerSkill{static bool CapturedGroup(ERR_GameMode*,SCR_AIGroup*);static bool OriginalGunner(ERR_GameMode*,SCR_AIGroup*,IEntity*);static EAISkill Profile(EAISkill,bool);};
struct ERR_EnemyGunnerSkill{static ERR_ConvoyTask* TaskForGroup(ERR_HQ*,SCR_AIGroup*);static bool CapturedGroup(ERR_GameMode*,SCR_AIGroup*);static bool OriginalGunner(ERR_GameMode*,SCR_AIGroup*,IEntity*);static EAISkill Profile(EAISkill,bool);};
struct NativeCombat{IEntity* owner=nullptr;EAISkill skill=EAISkill::REGULAR;int writes=0;auto GetOwner(){return owner;}auto GetAISkill(){return skill;}virtual void SetAISkill(EAISkill value){writes++;skill=value;}virtual void ResetAISkill(){skill=EAISkill::ROOKIE;}};
''' + rules + helper + r'''
int main(){
@@ -59,6 +59,7 @@ int main(){
ERR_ConvoyReceipt receipt;ERR_ConvoyTask task;task.Receipt=&receipt;task.Vehicles={&unit};ERR_HQ hq;hq.task=&task;mode.hq=&hq;
ERR_ForceRecord record;Ledger ledger;ledger.record=&record;mode.ledger=&ledger;
assert(ERR_EnemyGunnerSkill::OriginalGunner(&mode,&group,&actor));
ERR_ConvoyTask empty;hq.task=&empty;hq.extra=&task;assert(ERR_EnemyGunnerSkill::OriginalGunner(&mode,&group,&actor));hq.task=&task;hq.extra=nullptr;
// Every native/canonical ownership fact independently matters.
for(int i=0;i<25;i++){
auto savedActor=actor;auto savedVehicle=vehicle;auto savedGroup=group;auto savedUnit=unit;auto savedTask=task;auto savedReceipt=receipt;auto savedRecord=record;auto savedSlot=slot;
@@ -96,9 +97,9 @@ int main(){
SCR_AICombatComponent combat;combat.owner=&actor;combat.ERR_BindEnemySkill(&mode,&group);assert(combat.skill==EAISkill::CYLON);
combat.ResetAISkill();assert(combat.skill==EAISkill::CYLON);
combat.SetAISkill(static_cast<EAISkill>(90));assert(combat.skill==EAISkill::CYLON);
actor.vehicle=nullptr;slot.occupant=nullptr;combat.ERR_RefreshEnemySkill();assert(static_cast<int>(combat.skill)==90);
actor.vehicle=nullptr;slot.occupant=nullptr;combat.ERR_RefreshEnemySkill();assert(combat.skill==EAISkill::CYLON);
actor.vehicle=&vehicle;slot.occupant=&actor;combat.ERR_RefreshEnemySkill();assert(combat.skill==EAISkill::CYLON);
combat.SetAISkill(EAISkill::ROOKIE);slot.occupant=nullptr;combat.ERR_RefreshEnemySkill();assert(combat.skill==EAISkill::EXPERT);
combat.SetAISkill(EAISkill::ROOKIE);slot.occupant=nullptr;combat.ERR_RefreshEnemySkill();assert(combat.skill==EAISkill::CYLON);
slot.occupant=&actor;combat.ERR_RefreshEnemySkill();assert(combat.skill==EAISkill::CYLON);
// Captured group refresh cannot mutate any soldier.
ERR_ConvoyRules::capture=true;int writes=combat.writes;combat.ERR_RefreshEnemySkill();assert(combat.writes==writes);ERR_ConvoyRules::capture=false;
@@ -106,10 +107,10 @@ int main(){
// A legitimately high underlying setting stays high after leaving a gunner seat.
combat.SetAISkill(EAISkill::CYLON);combat.ERR_BindEnemySkill(&mode,&group);slot.occupant=nullptr;combat.ERR_RefreshEnemySkill();assert(combat.skill==EAISkill::CYLON);
combat.ERR_ReleaseEnemySkill(&mode);assert(combat.skill==EAISkill::CYLON);
// Ordinary original infantry retain the existing floor without convoy identity.
hq.task=nullptr;combat.SetAISkill(EAISkill::REGULAR);combat.ERR_BindEnemySkill(&mode,&group);assert(combat.skill==EAISkill::EXPERT);
combat.SetAISkill(static_cast<EAISkill>(90));assert(static_cast<int>(combat.skill)==90);
// Ordinary original infantry also receive maximum skill without convoy identity.
hq.task=nullptr;combat.SetAISkill(EAISkill::REGULAR);combat.ERR_BindEnemySkill(&mode,&group);assert(combat.skill==EAISkill::CYLON);
combat.SetAISkill(static_cast<EAISkill>(90));assert(combat.skill==EAISkill::CYLON);
combat.ERR_ReleaseEnemySkill(&mode);assert(combat.skill==EAISkill::REGULAR);
}
''')
print('PASS: actual original turret identity, native/canonical guards, scoped >=80/>=100 profile, reset, dismount/reboard, requested-skill preservation and restoration')
print('PASS: actual original turret identity, native/canonical guards, scoped maximum-skill profile, reset, dismount/reboard, requested-skill preservation and restoration')
+4 -1
View File
@@ -27,6 +27,7 @@ code=code.replace('EWeaponType type','int type').replace('foreach (','for (').re
for name in ('ERR_EnemyEquipmentRules','ERR_HQ','Math','EStoragePurpose','EWeaponType'):code=code.replace(name+'.',name+'::')
for name in ('entity','child','weapon','sights','slot','job','manager','inventory','storage','inspection'):
code=re.sub(r'\b'+name+r'\.',name+'->',code)
code=code.replace('mode.Content().Allows(prefab)', 'mode->Content()->Allows(prefab)').replace('mode.Content()', 'mode->Content()')
code=code.replace('job->Operation.','job->Operation->')
code=re.sub(r'\b(bool|int|IEntity|SCR_AIGroup) (\w+);',r'\1 \2{};',code)
code=re.sub(r'\b(ERR_EnemyEquipmentCallback\*) (\w+);',r'\1 \2{};',code)
@@ -38,7 +39,7 @@ preamble=r'''
#include <vector>
using string=std::string;using ResourceName=std::string;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Insert(T t){this->push_back(t);}void Remove(int index){this->erase(this->begin()+index);}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}};
struct Entity;struct Group;struct Weapon;struct Manager;struct Inventory;struct Storage;struct Slot;struct Sights;struct ERR_GameMode{};
struct Entity;struct Group;struct Weapon;struct Manager;struct Inventory;struct Storage;struct Slot;struct Sights;struct ContentPolicy{bool rhs=true;bool Allows(string p){return rhs||p.find("{C850A33226B8F9C1}")!=string::npos||p.find("{D2018EDB1BBF4C88}")!=string::npos||p.find("{BD496EE1B40DC510}")!=string::npos;}};struct ERR_GameMode{ContentPolicy content;ContentPolicy* Content(){return &content;}};
using IEntity=Entity*;using SCR_AIGroup=Group*;using BaseWeaponComponent=Weapon*;using BaseWeaponManagerComponent=Manager*;
using InventoryStorageManagerComponent=Inventory*;using WeaponAttachmentsStorageComponent=Storage*;using InventoryStorageSlot=Slot*;using AttachmentSlotComponent=Slot*;using BaseSightsComponent=Sights*;
struct ScriptedInventoryOperationCallback{virtual void OnComplete(){}virtual void OnFailed(){}virtual void OnAllComplete(){}virtual void OnAllFailed(){}virtual ~ScriptedInventoryOperationCallback()=default;};
@@ -68,6 +69,8 @@ struct Fixture{ERR_GameMode mode;Group group,other;Entity actor,rifle;Weapon wea
Fixture(){actor.group=&group;actor.manager=&manager;actor.inventory=&inv;manager.weapons={&rifle};rifle.weapon=&weapon;rifle.storage=&storage;weapon.owner=&rifle;storage.slots={&slot};}
};
int main(){
{Fixture f;f.mode.content.rhs=false;f.inv.fit=false;ERR_EnemyOptics s(&f.actor,&f.group);for(int i=0;i<5;i++)s.Check(&f.mode);assert(f.inv.submits==0&&f.inv.checks==12);}
// Existing mounted/nested/sights magnified optics are preserved. Occupied red dots aren't overwritten.
for(int style=0;style<3;style++){Fixture f;Entity optic,adapter;Sights sights;Slot attach;
optic.scope2D=style==0;optic.scope2DPip=style==1;optic.scopePip=style==2;if(style==0){f.rifle.child=&adapter;adapter.child=&optic;}
+1 -1
View File
@@ -38,7 +38,7 @@ class ExternalAddonsTests(unittest.TestCase):
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):
def test_only_exact_authorized_closure_is_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))
+50
View File
@@ -0,0 +1,50 @@
"""Execute role resolution: reserve explicit choices, then sample without replacement."""
import re
from policy_harness import ROOT, body, run
path = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_FactionSelection.c'
code = '\n'.join(body(path, signature) for signature in ['static bool Resolve(', 'static bool Distinct('])
code = code.replace('array<string> available', 'Array<string> available').replace('array<string> fixed', 'Array<string> fixed')
code = code.replace('map<string, string>', 'Map&').replace('foreach (', 'for (').replace('Math.RandomInt', 'RandomInt')
run(r'''
#include <cassert>
#include <map>
#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()const{return this->size();} bool IsEmpty(){return this->empty();}
bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}
void Insert(T v){this->push_back(v);} void Remove(int i){this->erase(this->begin()+i);}
void RemoveItem(T v){auto i=std::find(this->begin(),this->end(),v);if(i!=this->end())this->erase(i);}
};
struct Map:std::map<string,string>{bool Contains(string k){return count(k);}string Get(string k){return at(k);}void Set(string k,string v){(*this)[k]=v;}void Clear(){clear();}};
struct Rules {
static Array<string> Roles(){return {"escapists","enemy","independent"};}
static bool Find(string id){return id=="us"||id=="ussr"||id=="fia"||id=="rhs";}
static int RandomInt(int,int end){static unsigned x=17;x=x*1664525u+1013904223u;return x%end;}
''' + code + r'''
};
int main(){
Map saved,resolved;
for(auto role:Rules::Roles())saved.Set(role,"random");
for(int n=0;n<1000;n++){
assert(Rules::Resolve({"us","ussr","fia","rhs"},saved,resolved));
assert(Rules::Distinct(resolved)&&resolved.size()==3);
}
saved.Set("enemy","us");saved.Set("independent","fia");
for(int n=0;n<100;n++){
assert(Rules::Resolve({"us","ussr","fia"},saved,resolved));
assert(resolved.Get("escapists")=="ussr"&&resolved.Get("enemy")=="us"&&resolved.Get("independent")=="fia");
}
saved.Set("enemy","rhs");
assert(Rules::Resolve({"us","ussr","fia"},saved,resolved));
assert(resolved.Get("enemy")!="rhs"&&resolved.Get("independent")=="fia");
saved.Set("enemy","fia");assert(!Rules::Resolve({"us","ussr","fia"},saved,resolved));assert(resolved.empty());
saved.Set("enemy","unknown");assert(!Rules::Resolve({"us","ussr","fia"},saved,resolved));
saved.Set("enemy","us");assert(!Rules::Resolve({"us","fia"},saved,resolved));
saved.erase("escapists");assert(!Rules::Resolve({"us","ussr","fia"},saved,resolved));
}
''')
print('PASS: actual faction resolution reserves explicit roles, samples without replacement, substitutes disabled selections, and rejects duplicate/unknown/missing choices and insufficient pools')
+6 -6
View File
@@ -190,14 +190,14 @@ int main(){
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.
// All 256 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));
for(int i=0;i<256;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++){
assert(hour.Count()==256&&hour.m_Sources.Count()==256&&hour.m_ById.Count()==256);
for(int i=0;i<256;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);
@@ -208,7 +208,7 @@ int main(){
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.Status()=="256,0,256,0,1024,0");
assert(hour.Unregister(hour.At(0)->Id));
assert(!hour.Register("replacement",K::PATROL,{0,0,0},4)); // retired identity consumes finite pool
}
@@ -221,4 +221,4 @@ for path in (L,S):
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')
print('PASS: actual force ledger, finite casualties/identity/cap, routes/arrival/elevation/overshoot, server and physical guards, staggered 256-record simulated hour without world queries')
+36
View File
@@ -0,0 +1,36 @@
"""Run the actual bounded placement scheduler with terrain and clock adapters."""
import re
from policy_harness import ROOT,body,run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_GeneratedSites.c'
s=body(p,'ERR_PrepareResult Tick(')
s=s.replace('ERR_SiteDefinition site','Site* site').replace('site.','site->').replace('m_Terrain.Site','m_Terrain.SiteAt')
s=s.replace('System.GetTickCount','Clock::Now').replace('Replication.IsServer','Server').replace('Math.RandomFloat','RandomFloat').replace('ERR_PrepareResult.','Result::')
s=s.replace('float floor;','float floor=0;')
run(r'''
#include <cassert>
#include <string>
#include <vector>
using string=std::string;
struct vector{float a[3];float& operator[](int i){return a[i];}};vector Vector(float x,float y,float z){return {{x,y,z}};}
namespace Result{enum Value{FAILED=-1,PENDING,READY};}using ERR_PrepareResult=Result::Value;
namespace Clock {int now=0,step=1;int Now(){int t=now;now+=step;return t;}}
bool Server(){return true;}float RandomFloat(float a,float b){return (a+b)/2;}template<class...T>void PrintFormat(T...){}
struct Site{vector Position=Vector(10,0,20);float Heading=0;bool PlacementResolved=false;string Id="site";};
template<class T>struct Array:std::vector<T>{using std::vector<T>::operator=;int Count(){return this->size();}};
struct Terrain{bool clear=true;int queries=0;bool SiteAt(Site*,vector,float,float& height){queries++;height=42;return clear;}};
struct Scheduler{
bool m_Started=true;ERR_PrepareResult m_Result=Result::PENDING;int m_Index=0,m_Attempts=0,m_Began=0;
vector m_Low=Vector(0,0,0),m_High=Vector(10000,0,10000);Array<Site*>m_Pending;Terrain m_Terrain;string m_Error;
bool Separated(Site*,vector,bool){return true;}
'''+s+r'''
};
int main(){
{Scheduler s;s.m_Started=false;assert(s.Tick()==Result::FAILED);}
{Scheduler s;assert(s.Tick()==Result::READY);assert(s.m_Terrain.queries==0);}
{Clock::now=0;Scheduler s;Site a,b;s.m_Pending={&a,&b};while(s.Tick()==Result::PENDING){}assert(s.m_Result==Result::READY&&a.PlacementResolved&&b.PlacementResolved);assert(a.Position[1]>42&&a.Position[1]<42.1);int calls=s.m_Terrain.queries;assert(s.Tick()==Result::READY&&s.m_Terrain.queries==calls);}
{Clock::now=0;Scheduler s;Site a;s.m_Pending={&a};s.m_Terrain.clear=false;assert(s.Tick()==Result::PENDING);assert(s.m_Terrain.queries<10&&!a.PlacementResolved);Clock::now=30001;assert(s.Tick()==Result::FAILED&&!s.m_Error.empty()&&!a.PlacementResolved);int calls=s.m_Terrain.queries;assert(s.Tick()==Result::FAILED&&s.m_Terrain.queries==calls);}
{Clock::now=0;Scheduler s;Site a;s.m_Pending={&a};s.m_Attempts=12000;assert(s.Tick()==Result::READY&&a.Position[0]==10&&a.Position[2]==20&&a.PlacementResolved);}
{Clock::now=30001;Scheduler s;Site a;s.m_Pending={&a};assert(s.Tick()==Result::READY&&a.Position[0]==10&&a.Position[2]==20);}
}
''')
print('PASS: actual generation scheduler bounds each tick, enforces deadline/candidate ceiling, validates fallback, commits only accepted positions, and terminates without repeated searches')
+2 -2
View File
@@ -7,14 +7,14 @@ for method in ('protected void Investigate(', 'protected void Retask(', 'protect
# Execute the production delivery admission before unrelated native waypoint creation.
s=body(H,'protected void DeliverOrder(')
s=s[:s.index(' if (task.Investigation())')]+' admitted++;\n }'
s=s.replace('protected ','').replace('null','nullptr').replace('ERR_HQUnit unit','ERR_HQUnit* unit')
s=s.replace('protected ','').replace('null','nullptr').replace('.IsEmpty()', '.empty()').replace('ERR_HQUnit unit','ERR_HQUnit* unit')
s=s.replace('ERR_HQReport order','ERR_HQReport* order').replace('ERR_HQTask task','ERR_HQTask* task')
for name in ('unit','task','order'):s=s.replace(name+'.',name+'->')
run(r'''
#include <cassert>
#include <string>
struct ERR_HQReport{};
struct ERR_HQTask{float ExpiresAt=100000;bool ReleaseOnly=false;ERR_HQTask* ReplacesTask=nullptr;};
struct ERR_HQTask{std::string SecuritySite;float ExpiresAt=100000;bool ReleaseOnly=false;ERR_HQTask* ReplacesTask=nullptr;};
struct ERR_HQUnit{ERR_HQReport* PendingOrder=nullptr;ERR_HQTask* PendingTask=nullptr;ERR_HQTask* Task=nullptr;
std::string ForceSourceId="original-1";bool eligible=true;int cancellations=0;
bool Eligible(void*){return eligible;}void CancelPending(){cancellations++;PendingOrder=nullptr;PendingTask=nullptr;}};
+5 -1
View File
@@ -5,6 +5,7 @@ p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/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('.IsEmpty()', '.empty()')
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')
@@ -21,7 +22,7 @@ run(r'''
#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_HQTask {bool ReleaseOnly=false;int BeliefId=0;std::string WatchSourceId,SecuritySite;int WatchMessageId=0,WatchRequestId=0;bool Investigation(){return !WatchSourceId.empty();}};
struct ERR_HQReport {};
struct ERR_HQRoute {};
struct AIWaypoint {int deleted=0;};
@@ -98,6 +99,9 @@ int main(){
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;
ERR_HQTask security;security.SecuritySite="radio-1";u.PendingTask=&security;u.PendingOrder=&newReport;u.Downlink=&route;
hq.window.NextHunt=300000;hq.LostOrder(&u,5000);
assert(!u.PendingTask&&!u.Downlink&&hq.window.NextHunt==300000&&hq.m_Watch.calls==1);
// 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);
+4 -4
View File
@@ -157,11 +157,11 @@ int main(){
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],Roster(&groups[g]));}
assert(service.m_Roster.Count()==128&&logs==1);service.RegisterGroup(&groups[4],Roster(&groups[4]));assert(logs==1);
service.Stop();logs=0;Entity actors[4128];Agent agents[4128];Group groups[129];
for(int g=0;g<129;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],Roster(&groups[g]));}
assert(service.m_Roster.Count()==4096&&logs==1);service.RegisterGroup(&groups[128],Roster(&groups[128]));assert(logs==1);
for(int i=0;i<32;i++)service.m_Roster[i]->Corpse=nullptr;
service.RegisterGroup(&groups[4],Roster(&groups[4]));assert(service.m_Roster.Count()==128);
service.RegisterGroup(&groups[128],Roster(&groups[128]));assert(service.m_Roster.Count()==4096);
service.Stop();assert(service.m_Roster.IsEmpty());
}
''')
+10 -9
View File
@@ -6,9 +6,9 @@ H = ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'
def cpp(s):
s=s.replace('protected ', '').replace('null','nullptr').replace('foreach (','for (')
for x in ('ERR_HQPursuitRules','vector','string'): s=s.replace(x+'.',x+'::')
for typ in ('ERR_HQUnit','ERR_HQReport','ERR_HQWatchEntry','ERR_HQRoute','ERR_ConvoyCombatOrder','ERR_ConvoyVehicle'):
for typ in ('ERR_HQUnit','ERR_HQReport','ERR_HQWatchEntry','ERR_HQRoute','ERR_ConvoyCombatOrder','ERR_ConvoyVehicle','ERR_ConvoyTask'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for x in ('entry','unit','best','report','vehicle','contact','route','order','m_ConvoyTask'):
for x in ('entry','unit','best','report','vehicle','contact','route','order','task','m_ConvoyTask'):
s=s.replace(x+'.',x+'->')
s=s.replace('->Combat.', '->Combat->').replace('->Receipt.', '->Receipt->')
s=s.replace('unit->Downlink.', 'unit->Downlink->')
@@ -16,7 +16,7 @@ def cpp(s):
s=s.replace('bool accepted;', 'bool accepted = false;')
return s
methods='\n'.join(cpp(body(H,s)) for s in ('protected bool ConvoyPositionKnown(',
'protected ERR_HQReport ConvoyContact(', 'protected void PlanConvoy(', 'protected void DeliverConvoyOrder('))
'protected ERR_HQReport ConvoyContact(', 'protected void PlanConvoy(', 'protected void PlanConvoyTask(', 'protected void DeliverConvoyOrder('))
downlink=cpp(body(H,'if (unit.Downlink)\n'))
cancel=cpp(body(H,'void CancelPending('))
src=H.read_text()
@@ -40,20 +40,21 @@ struct Watch {std::map<string,ERR_HQWatchEntry*> entries;ERR_HQWatchEntry* Find(
struct Journal {int seq=100;int Message(){return ++seq;}void Write(string,string,int){}};
struct ERR_HQRoute {int MessageId=0;string Payload;int result=0;int Advance(float,Journal&){return result;}};
struct Network {bool available=true;int calls=0;ERR_HQRoute* Route(void*,bool down){assert(down);calls++;return available?new ERR_HQRoute:nullptr;}};
struct ERR_ConvoyCombatOrder {int CommandId=0,ReportId=0,VehicleIndex=0;string SourceId;SCR_AIGroup* Group=nullptr;bool Hold=false;float ObservedAt=-1,IssuedAt=0,ExpiresAt=0;};
struct ERR_ConvoyCombatOrder {int CommandId=0,ReportId=0,VehicleIndex=0;string SourceId;SCR_AIGroup* Group=nullptr;vector Position;bool Hold=false;float ObservedAt=-1,IssuedAt=0,ExpiresAt=0;};
struct CombatState {bool Held=false;int LastCommandId=0,ReportId=0;float ObservedAt=-1;};
struct ERR_ConvoyVehicle {int SupplyToken=0,Index=0;SCR_AIGroup* Group=nullptr;bool Released=false,Arrived=false,Registered=true;CombatState state;CombatState* Combat=&state;};
struct Receipt {string SourceId="convoy-1";};
struct ConvoyTask {bool Stopped=false,Ready=true;struct Receipt receipt;struct Receipt* Receipt=&receipt;std::vector<ERR_ConvoyVehicle*> Vehicles;};
struct Service {int calls=0;bool valid=true;ERR_ConvoyCombatOrder* last=nullptr;
bool ApplyCombatOrder(ConvoyTask* task,ERR_ConvoyCombatOrder* o,float){calls++;last=o;if(!task||!valid)return false;
struct ERR_ConvoyTask {bool Stopped=false,Ready=true;struct Receipt receipt;struct Receipt* Receipt=&receipt;std::vector<ERR_ConvoyVehicle*> Vehicles;};
struct Service {ERR_ConvoyTask* task=nullptr;int Count(){return task?1:0;}ERR_ConvoyTask* At(int){return task;}ERR_ConvoyTask* Find(string id){return task&&task->Receipt->SourceId==id?task:nullptr;}
int calls=0;bool valid=true;ERR_ConvoyCombatOrder* last=nullptr;
bool ApplyCombatOrder(ERR_ConvoyTask* task,ERR_ConvoyCombatOrder* o,float){calls++;last=o;if(!task||!valid)return false;
auto s=task->Vehicles.at(o->VehicleIndex)->Combat;s->Held=o->Hold;s->LastCommandId=o->CommandId;s->ReportId=o->ReportId;s->ObservedAt=o->ObservedAt;return true;}};
struct ERR_HQUnit {SCR_AIGroup* Group=nullptr;bool ReservedForSupport=true;void* Radio=this;string ForceSourceId="vehicle-0";
ERR_HQRoute* Downlink=nullptr;ERR_ConvoyCombatOrder* PendingConvoyOrder=nullptr;void* PendingOrder=nullptr;void* PendingTask=nullptr;
bool PendingCheckin=false;float NextStatus=0;
'''+cancel+r'''
};
struct HQ {Watch m_Watch;Network m_Network;Journal m_Journal;Service m_Convoys;ConvoyTask* m_ConvoyTask=nullptr;
struct HQ {Watch m_Watch;Network m_Network;Journal m_Journal;Service m_Convoys;ERR_ConvoyTask* m_ConvoyTask=nullptr;
std::vector<ERR_HQUnit*> m_Units;std::vector<ERR_HQReport*> m_Contacts;int ordinary=0,lost=0;
void DeliverOrder(ERR_HQUnit*,float){ordinary++;}void LostOrder(ERR_HQUnit* u,float){lost++;u->CancelPending();}
'''+methods+r'''
@@ -62,7 +63,7 @@ struct HQ {Watch m_Watch;Network m_Network;Journal m_Journal;Service m_Convoys;C
}
};
int main(){
HQ h;SCR_AIGroup g;ERR_HQUnit u;u.Group=&g;h.m_Units={&u};ConvoyTask t;h.m_ConvoyTask=&t;
HQ h;SCR_AIGroup g;ERR_HQUnit u;u.Group=&g;h.m_Units={&u};ERR_ConvoyTask t;h.m_ConvoyTask=&t;h.m_Convoys.task=&t;
ERR_ConvoyVehicle v;v.Group=&g;t.Vehicles={&v};ERR_HQWatchEntry e;e.ObservedAt=1000;e.Position={0,0};
ERR_HQReport r;r.MessageId=1;r.ObservedAt=2000;r.Position={200,0};
h.PlanConvoy(3000);assert(!u.Downlink); // no delivered own status or target report
+24 -7
View File
@@ -12,23 +12,24 @@ def cpp(s):
('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_ConvoyStartup','m_Mode','m_Convoys','m_ConvoyPool','m_ConvoyTask','m_HQ','site','unit','receipt','mode','provider'):
for var in ('m_SupplyConvoys','m_ConvoyStartup','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+'->')
for name in ('GetGame()','GetWorld()','Forces()','Sites()','Settings()','Recon()','AirAssault()','Airfield()','Recovery()'):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;')
s=s.replace('Array<string>& parts = {};','Array<string> parts = {};')
s=s.replace('Array<string>& parts = {};','Array<string> parts = {};').replace('Array<string>& countParts = {};','Array<string> countParts = {};').replace('ERR_CommandArguments.Selection','ERR_CommandArguments::Selection')
s=s.replace('Array<string>& timerParts = {};','Array<string> timerParts = {};')
s=s.replace('out int ', 'int& ').replace('ERR_CommandArguments.Integer', 'ERR_CommandArguments::Integer')
s=s.replace('value.ToString()', 'string(std::to_string(value))')
s=s.replace('"0123456789".IndexOf(', 'string("0123456789").IndexOf(')
return s
hq='\n'.join(cpp(body(H,s)) for s in ('static bool MissionActive(', 'bool PrepareConvoy(', 'bool StartConvoy(',
'bool StartScenarioConvoy(', 'string ConvoyStatus('))
provider=cpp(body(P,'protected void ConvoyAutoSetting('))+cpp(body(P,'protected void StingerSetting('))+cpp(body(P,'override bool Execute('))
provider=cpp(body(P,'protected void CountSetting('))+cpp(body(P,'protected void ConvoyAutoSetting('))+cpp(body(P,'protected void StingerSetting('))+cpp(body(P,'override bool Execute('))
integer=cpp(body(A,'static bool Integer('))
validator=cpp(body(BASE/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c','static bool Valid('))
extension=cpp(body(A,'protected bool Extension('))
@@ -65,6 +66,7 @@ void string::Split(const char* separator,Array<string>& parts,bool skipEmpty)con
}
}
struct ERR_CommandArguments{
static bool Selection(string key,string text,int& value){if(key!="convoyCount")return false;if(text=="random"){value=-1;return true;}return Integer(text,value)&&value>=0&&value<=20;}
'''+integer+r'''
};
struct vector{float x=0,y=0,z=0;};
@@ -78,10 +80,12 @@ 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{
int automatic=1,stinger=15,writes=0;bool saveFails=false;
static Array<string> Keys(){return {}; }int Get(string key){return key=="stingerChanceOneIn"?stinger:automatic;}
int automatic=1,stinger=15,convoy=3,writes=0;bool saveFails=false;
static Array<string> Keys(){return {}; }int Get(string key){if(key=="convoyCount")return convoy;return key=="stingerChanceOneIn"?stinger:automatic;}
'''+validator+r'''
bool Set(string key,int value){writes++;if(saveFails||!Valid(key,value))return false;
static string SelectionText(int value){return value==-1?string("random"):string(std::to_string(value));}
bool Set(string key,int value){writes++;if(saveFails||(!Valid(key,value)&&!(key=="convoyCount"&&value==-1)))return false;
if(key=="convoyCount"){convoy=value;return true;}
if(key=="automaticConvoyEnabled"){automatic=value;return true;}
assert(key=="stingerChanceOneIn");stinger=value;return true;}
};
@@ -118,10 +122,15 @@ struct Service{
if(!preflight)return nullptr;
receipt->Consumed=true;task.Stopped=partial;task.retained=partial?3:6;return &task;
}
int Count(){return 0;}ERR_ConvoyTask* At(int){return &task;}
string Status(){statusCalls++;return task.Stopped?"stopped; retained vehicles=3":"convoy service status";}
};
struct Supply{bool StartOne(){return false;}string Status(){return "supply";}};
struct ERR_ConvoyStartup{string Status(){return "startup snapshot";}};
struct ReconService{string Status(){return "recon";}void Return(){}bool SetDelay(int n){return n>=1&&n<=1440;}};
struct HQ{
ReconService recon;ReconService* Recon(){return &recon;}ReconService* AirAssault(){return &recon;}ReconService* Airfield(){return &recon;}ReconService* Recovery(){return &recon;}
Supply* m_SupplyConvoys=nullptr;bool ConvoyAdmissionReady(){return true;}
ERR_ConvoyStartup* m_ConvoyStartup=nullptr;
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;
@@ -246,6 +255,14 @@ int main(){
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(string command:{"convoys set 0","hq convoy count set 20","convoys set random"}){
int calls=service.calls;replies.Clear();admin.Execute(&mode,true,command,replies);assert(replies.Count()==1&&replies[0].find("saved for next round")!=std::string::npos&&service.calls==calls);
}
assert(mode.settings.convoy==-1);writes=mode.settings.writes;admin.Execute(&mode,false,"convoys set 5",replies);assert(mode.settings.convoy==-1&&mode.settings.writes==writes);
for(string invalid:{"convoys set 21","convoys set -2","convoys set 1 extra"}){admin.Execute(&mode,true,invalid,replies);assert(mode.settings.convoy==-1);}
mode.settings.saveFails=true;admin.Execute(&mode,true,"convoys set 4",replies);assert(mode.settings.convoy==-1);mode.settings.saveFails=false;
admin.Execute(&mode,true,"convoys show",replies);assert(replies.Count()==1&&replies[0].find("random")!=std::string::npos);
}
''')
for sig in ('bool PrepareConvoy(', 'bool StartConvoy(', 'bool StartScenarioConvoy('):
+5 -5
View File
@@ -131,14 +131,14 @@ int main(){
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++){
for(int i=1;i<=256;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);
item.SourceId=257;item.ObservedAt=257;
assert(!sources.Ingest(&item,1000)&&sources.m_Messages.Count()==256);
item.SourceId=1;item.MessageId=2;item.ObservedAt=258;
assert(sources.Ingest(&item,1000)&&sources.m_Messages.Count()==256);
// 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));
+7 -2
View File
@@ -13,7 +13,7 @@ def cpp(s):
s=re.sub(r'foreach \((ERR_HQUnit\*) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
for name in ('unit','other','task','planned','order','belief','record','mode','Group','hunt','m_Mode'):
s=s.replace(name+'.',name+'->')
s=s.replace('ResourceName ', 'int ')
s=s.replace('ResourceName ', 'int ').replace('.IsEmpty()', '.empty()')
s=s.replace('PendingTask.', 'PendingTask->').replace('Task.', 'Task->')
s=s.replace('Downlink.', 'Downlink->').replace('Forces().','Forces()->').replace('ERR_ForceRecord* record;','ERR_ForceRecord* record=nullptr;')
return s
@@ -39,7 +39,7 @@ struct ERR_HQPursuitRules{
'''+rules+r'''
};
struct Math{static float Min(float a,float b){return std::min(a,b);}};
struct ERR_HQTask{bool RoadIntercept=false,ReleaseOnly=false;ERR_HQTask* ReplacesTask=nullptr;string WatchSourceId;int WatchMessageId=0,WatchRequestId=0;
struct ERR_HQTask{string SecuritySite;bool RoadIntercept=false,ReleaseOnly=false;ERR_HQTask* ReplacesTask=nullptr;string WatchSourceId;int WatchMessageId=0,WatchRequestId=0;
int BeliefId=1,BeliefRevision=2,Role=0;vector Destination{40,0,50};float QueuedAt=0,ExpiresAt=60000;bool Investigation(){return !WatchSourceId.empty();}string Name(){return "search";}};
struct ERR_HQReport{int MessageId=10;float ObservedAt=0;};
struct ERR_HQRoute{int MessageId=20;};
@@ -116,6 +116,11 @@ int main(){
assert(ERR_Actors::spawns==1&&f.unit.Waypoint&&f.unit.Waypoint->resource==2&&f.unit.Task==&f.next&&f.previous.deletes==1);
f.hq.DeliverOrder(&f.unit,45001);assert(ERR_Actors::spawns==1&&f.group.additions==1);
f.hq.Cleanup(60000,1,4);assert(!f.unit.Task&&!f.unit.Waypoint&&f.mode.ledger.record.Duty==f.mode.ledger.record.HomeDuty);}
// Security orders carry an inspected-site hypothesis, not a fabricated enemy track.
{Fixture f;f.next.SecuritySite="mortar-1";f.next.BeliefId=999;
f.hq.DeliverOrder(&f.unit,1000);assert(f.unit.Task==&f.next&&f.unit.Waypoint->resource==2);
f.hq.Cleanup(1001,1,4);assert(f.unit.Task==&f.next);
f.hq.Cleanup(60000,1,4);assert(!f.unit.Task&&!f.unit.Waypoint);}
// Cancelling a mismatched recall never releases a replacement task which arrived first.
{Fixture f;f.next.ReleaseOnly=true;f.next.ReplacesTask=nullptr;f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
{Fixture f;f.next.ReleaseOnly=true;f.next.ReplacesTask=&f.active;f.hq.DeliverOrder(&f.unit,1000);assert(!f.unit.Task&&!f.unit.Assignment&&!f.unit.Waypoint&&f.previous.deletes==1&&ERR_Actors::spawns==0);}
+2 -2
View File
@@ -11,7 +11,7 @@ for name in ('unit','other','route','own','seen','report'):
s=s.replace(name+'.',name+'->')
for name in ('Uplink','Task'):
s=s.replace(name+'.',name+'->')
s=s.replace('Nodes[0].','Nodes[0]->').replace('m_Mode.Settings().Get','m_Mode->Settings()->Get').replace('ERR_HQObservation.','ERR_HQObservation::').replace('null','nullptr')
s=s.replace('Nodes[0].','Nodes[0]->').replace('m_Mode.Settings().Runtime','m_Mode->Settings()->Runtime').replace('ERR_HQObservation.','ERR_HQObservation::').replace('null','nullptr')
s=s.replace('ERR_HQRoute* own;','ERR_HQRoute* own=nullptr;').replace('ERR_HQRoute* route;','ERR_HQRoute* route=nullptr;')
run(r'''
#include <cassert>
@@ -30,7 +30,7 @@ struct ERR_HQObservation{static void Gather(Utility*u,float,Arr<ERR_HQVisualCont
struct Selector{int queues=0;ERR_HQVisualContact*Select(Arr<ERR_HQVisualContact*>&s){return s[0];}void Queued(IEntity){queues++;}};
struct History{IEntity target=nullptr;float time=-1;bool CanQueue(IEntity t,float at,float){return t&&!(t==target&&at<=time);}void Queued(IEntity t,float at,float){target=t;time=at;}};
struct ERR_HQReport{int MessageId=0,Position=0;IEntity Target=nullptr;Group*Source=nullptr;float ObservedAt=0;string Description(float){return "unaltered source sighting";}};
struct Settings{int Get(string){return 5;}};
struct Settings{int Runtime(string){return 5;}};
struct Mode{struct Settings values;struct Settings*Settings(){return &values;}};
struct Task{float ArrivedAt=0;};
struct ERR_HQUnit{struct Group*Group=nullptr;struct Utility*Utility=nullptr;IEntity Radio=nullptr;float NextReport=0;ERR_HQRoute*Uplink=nullptr;ERR_HQReport*PendingReport=nullptr;Selector ReportSelection;History SentObservations;int StatusSampler=1;struct Task*Task=nullptr;};
+4 -4
View File
@@ -19,10 +19,10 @@ def cpp(s):
s=s.replace(v+'.',v+'->')
for v in ('Utility','ReportSelection','StatusSampler','Group','Task'):
s=s.replace('->'+v+'.','->'+v+'->')
s=s.replace('m_Mode.Settings().Get(', 'm_Mode->Settings()->Get(')
s=s.replace('m_Mode.Settings().Runtime(', 'm_Mode->Settings()->Runtime(')
s=s.replace('ERR_HQPursuitRules.', 'ERR_HQPursuitRules::')
s=s.replace('ERR_HQObservation.','ERR_HQObservation::').replace('vector.DistanceXZ','vector::DistanceXZ').replace('string.Format','Format')
return s.replace('AmmoState.', 'AmmoState->')
return s.replace('AmmoState.', 'AmmoState->').replace('SecuritySite.IsEmpty()', 'SecuritySite.empty()')
policy_path=Path(os.environ.get('ERR_AMMO_REPORTING_ROOT', ROOT))/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoReporting.c'
policy=cpp(policy_path.read_text()).replace('class ERR_HQAmmoReporting','struct ERR_HQAmmoReporting').rstrip()+';'
methods='\n'.join(cpp(body(P,s)) for s in ('protected void Observe(', 'protected void ObserveBody(', 'protected void StatusReport(', 'protected void Sweep(', 'protected void QueueReports('))
@@ -36,7 +36,7 @@ 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 Settings{int interval=5;int Runtime(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;};
@@ -55,7 +55,7 @@ struct Sampler{int observations=0,samples=0;bool valid=true;void ObserveDeaths()
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 RoadIntercept=false,SweepQueued=false;float ArrivedAt=-1;vector Destination;
struct ERR_HQTask{std::string SecuritySite;bool RoadIntercept=false,SweepQueued=false;float ArrivedAt=-1;vector Destination;
string WatchSourceId;int WatchMessageId=0,WatchRequestId=0,BeliefId=1,BeliefRevision=2;};
struct ERR_HQRoute{Group* LocalSource=nullptr;int MessageId=0;string Payload;};
struct Network{bool available=true;int routes=0;ERR_HQRoute* Route(void*){routes++;return available?new ERR_HQRoute:nullptr;}};
+19 -12
View File
@@ -30,9 +30,9 @@ ammo=ammo.replace('class ','struct ').replace('ERR_HQAmmoSnapshot Snapshot(', 'E
ammo=ammo.replace('ERR_HQAmmoSnapshot copy', 'ERR_HQAmmoSnapshot* copy').replace('copy.', 'copy->')
ammo=re.sub(r'\b(bool|int) (\w+);',r'\1 \2=0;',ammo).replace('\n}\n','\n};\n')
rules='\n'.join(cpp(body(P,s)) for s in ('static bool Valid(', 'static bool RequestDue(',
'static bool InvestigationDue(', 'static int SquadLimit('))
'static bool InvestigationDue(', 'static int SquadLimit(', 'static int Aggression(', 'static int Hunters('))
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 ArmedSearchBlocked(', 'protected ERR_HQWatchEntry Create(',
'int IdentifiedBodies()', 'int Aggression()', 'int HunterTarget()', 'int ConfirmedLosses()', 'int SquadLimit(int baseline)', 'bool ArmedSearchBlocked(', 'protected ERR_HQWatchEntry Create(',
'protected void ApplyLosses(', 'bool Ingest(', 'bool IngestBody(', 'bool RequestDue(string',
'bool MarkRequested(', 'bool InvestigationDue(string', 'bool MarkInvestigating(', 'bool FinishInvestigation(', 'void Clear('))
common='\n'.join(cpp(body(U,s)) for s in ('static bool Finite(', 'static bool PositionValid('))
@@ -72,7 +72,7 @@ struct ERR_HQPursuitRules{
};
'''+helper+ammo+r'''
struct ERR_HQReport{
string TravelMode="body",CasualtySourceId="patrol-1";bool Distress=false;int MessageId=99,CasualtySlot=0,CasualtyStrength=4;
string RoadStatus="road unknown"; string TravelMode="body",CasualtySourceId="patrol-1";bool Distress=false;int MessageId=99,CasualtySlot=0,CasualtyStrength=4;
vector Position{900,30,900},Velocity;float ObservedAt=1000,DetailsObservedAt=-1;
void* Target=nullptr;void* Source=nullptr;bool HasVelocity=false,Engaged=false;
int SeenEscapists=-1;string SeenWeapons="unknown";
@@ -86,7 +86,7 @@ struct ERR_HQStatusReport{
struct ERR_HQWatchEntry{
string SourceId;int MessageId=0,RequestId=0;vector Position;
ERR_HQAmmoSnapshot* AmmoState=nullptr;
int Strength=0,Count=-1,Fit=-1,ConfirmedLosses=0;
int Strength=0,Count=-1,Fit=-1,ConfirmedLosses=0;Array<int> IdentifiedSlots;
ERR_HQDeliveredCasualties* Casualties=new ERR_HQDeliveredCasualties();float LastBodyDeliveredAt=-1;
float ObservedAt=-1,LastDeliveredAt=-1,AskedAt=-1,NextAttempt=0;
bool Investigating=false,Closed=false;
@@ -96,7 +96,7 @@ struct ERR_HQWatchRules{
};
struct Watch{
Array<ERR_HQWatchEntry*>m_Entries;Map<string,ERR_HQWatchEntry*>m_BySource;
int m_ConfirmedLosses=0;bool m_CapLogged=false;
int m_ConfirmedLosses=0,m_IdentifiedBodies=0;bool m_CapLogged=false;
'''+methods+r'''
};
struct ERR_MissionRules{static bool ContactFresh(float t,float now){return std::isfinite(t)&&std::isfinite(now)&&t>=0&&now>=t&&now-t<=120000;}};
@@ -106,9 +106,9 @@ struct MemoryAdapter{int attempts=0;bool Ingest(ERR_HQReport* r,float){attempts+
struct BodyAdapter{int confirmations=0;void ConfirmDelivered(void*){confirmations++;}};
struct CoverageAdapter{int forgotten=0;void Forget(vector){forgotten++;}};
struct JournalAdapter{void Write(string,string,int){}};
string Format(string,int,int){return "delivered";}
template<class... Args> string Format(string,Args...){return "delivered";}
struct HQDelivery{
Watch m_Watch;MemoryAdapter m_Memory;BodyAdapter m_Bodies;CoverageAdapter m_Coverage;JournalAdapter m_Journal;
Watch m_Watch;MemoryAdapter m_Memory;BodyAdapter m_Bodies;struct RecoveryAdapter{int calls=0;void ReportBody(ERR_HQReport*){calls++;}} m_Recovery;CoverageAdapter m_Coverage;JournalAdapter m_Journal;
Array<ERR_HQReport*>m_Contacts;
'''+accept+r'''
};
@@ -212,12 +212,12 @@ int main(){
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;item.DeathSlots={0,1,2,3};
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);
for(int i=0;i<256;i++){item.SourceId=std::to_string(i);assert(cap.Ingest(&item,1000));}
assert(cap.Count()==256&&cap.ConfirmedLosses()==1024);
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);
assert(cap.Ingest(&item,2000)&&cap.ConfirmedLosses()==1024&&cap.Count()==256);
cap.Clear();assert(cap.Count()==0&&cap.ConfirmedLosses()==0&&!cap.Find("0"));
// Delivered bodies merge exact original roster slots, never aggregate totals or body positions.
Watch merged;ERR_HQStatusReport status;status.Count=3;status.Fit=2;status.ConfirmedLosses=1;status.DeathSlots={0};
@@ -234,9 +234,16 @@ int main(){
body.CasualtySlot=3;assert(merged.IngestBody(&body,4001));
assert(known->Closed&&known->Count==0&&known->Fit==0&&merged.ConfirmedLosses()==4);
assert(known->MessageId==2&&known->ObservedAt==3000&&known->Position.x==200);
assert(merged.IngestBody(&body,4002)&&merged.ConfirmedLosses()==4); // replay or different observer
assert(!merged.IngestBody(&body,4002)&&merged.ConfirmedLosses()==4); // replay or different observer
status.MessageId=3;status.ObservedAt=5000;assert(merged.Ingest(&status,5000)&&known->Closed);
assert(!merged.RequestDue("patrol-1",999999));
assert(merged.IdentifiedBodies()==4&&merged.Aggression()==1&&merged.HunterTarget()==1);
for(int bodies=0;bodies<=300;bodies++){
assert(ERR_HQWatchRules::Aggression(bodies)==std::min(30,bodies/3));
assert(ERR_HQWatchRules::Hunters(ERR_HQWatchRules::Aggression(bodies))==std::min(15,(bodies/3+1)/2));
}
assert(ERR_HQWatchRules::Aggression(-10)==0&&ERR_HQWatchRules::Hunters(-10)==0);
merged.Clear();assert(merged.IdentifiedBodies()==0&&merged.HunterTarget()==0);
// Exercise the actual HQ delivered-intake method and actual Report.Snapshot, including corpse deletion.
HQDelivery intake;ERR_HQReport local;local.CasualtySourceId="deleted-during-uplink";
local.CasualtyStrength=1;local.CasualtySlot=0;local.Target=&local;local.MessageId=501;
@@ -279,4 +286,4 @@ for forbidden in ('IEntity','SCR_AIGroup','GetGame(','GetOrigin(','GetPlayers(',
'GetControlledEntity(','Spawn(','CallLater(','ERR_HQMemory'):
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')
print('PASS: delivered-only status snapshots, two missed check-ins/reply deadline, last reported coordinates, episode/replay guards, confirmed-loss idempotence and 256-record cap')
+44
View File
@@ -0,0 +1,44 @@
"""Production hunter release and objective-role policy, without native spawning."""
from policy_harness import ROOT, body, run
source=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'
s=body(source,'override void Release(ERR_GameMode mode)')
s=s.replace('override ','').replace('super.','Native::').replace('null','nullptr')
s=s.replace('ERR_GameMode mode','ERR_GameMode* mode').replace('ERR_ForceRecord record','ERR_ForceRecord* record')
s=s.replace('ERR_HQ hq = ERR_HQ.Cast(mode.Command())', 'ERR_HQ* hq = mode->Command()')
s=s.replace('array<AIAgent> agents = {};','Array<AIAgent*> agents;').replace('foreach (AIAgent agent : agents)','for (AIAgent* agent : agents)')
for name in ('mode','record','Group','agent','hq'):s=s.replace(name+'.',name+'->')
s=s.replace('Forces().','Forces()->').replace('ERR_ConvoyRules.','ERR_ConvoyRules::')
objective=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ObjectiveDefence.c','static bool Objective(').replace('ERR_LocationKind.','ERR_LocationKind::')
run(r'''
#include <cassert>
#include <vector>
template<class T>using Array=std::vector<T>;
struct AIAgent{int lod=0,writes=0;void SetPermanentLOD(int x){lod=x;writes++;}};
struct GroupType{int lod=0;AIAgent agent;void SetPermanentLOD(int x){lod=x;}void GetAgents(Array<AIAgent*>& a){a={&agent};}};
struct ERR_ForceRecord{bool Pinned=true;};
struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(GroupType*){return &record;}};
struct ERR_HQ{int releases=0;void ReleaseRecovery(GroupType*){releases++;}};
struct ERR_GameMode{bool active=true;Ledger ledger;ERR_HQ hq;bool Active(){return active;}Ledger* Forces(){return &ledger;}ERR_HQ* Command(){return &hq;}};
namespace ERR_ConvoyRules{bool captured=false;bool Captured(void*,GroupType*){return captured;}}
struct Native{bool Released=false;int delegated=0,clears=0;GroupType* Group=nullptr;void* Entity=nullptr;void* Downlink=nullptr;
void Release(ERR_GameMode*){delegated++;Released=true;}void ClearOrder(){clears++;}
};
struct Hunter:Native{
'''+s+r'''
};
namespace ERR_LocationKind{enum{PRISON,RADIO,EXTRACTION,PATROL,DEPOT,OUTPOST,MORTAR};}
'''+objective+r'''
int main(){
ERR_GameMode mode;GroupType group;Hunter unit;unit.Group=&group;unit.Entity=&group;unit.Downlink=&mode;
unit.Release(&mode);assert(unit.Released&&!unit.Downlink&&unit.clears==1&&unit.delegated==0);
assert(!mode.ledger.record.Pinned&&mode.hq.releases==1&&group.lod==-1&&group.agent.writes==1);
assert(unit.Entity==&group&&unit.Group==&group); // living and dead actors/vehicle are preserved
unit.Release(&mode);assert(unit.clears==1&&mode.hq.releases==1&&group.agent.writes==1);
Hunter captured;ERR_ConvoyRules::captured=true;captured.Release(&mode);assert(captured.delegated==1&&mode.hq.releases==1);
ERR_ConvoyRules::captured=false;Hunter stopped;mode.active=false;stopped.Release(&mode);assert(stopped.delegated==1);
Hunter missing;missing.Release(nullptr);assert(missing.delegated==1);
assert(Objective(ERR_LocationKind::RADIO)&&Objective(ERR_LocationKind::DEPOT)&&Objective(ERR_LocationKind::OUTPOST)&&Objective(ERR_LocationKind::EXTRACTION));
assert(!Objective(ERR_LocationKind::PRISON)&&!Objective(ERR_LocationKind::PATROL)&&!Objective(ERR_LocationKind::MORTAR));
}
''')
print('PASS: actual hunter release preserves survivors/HQ, releases driving ownership once, delegates capture/teardown, and excludes prison/patrol/mortar from generic objective defence')
+6 -4
View File
@@ -56,15 +56,17 @@ int main(){
storage.owner=&otherTruck;assert(!access.Check(&truck,&actor));storage.owner=&truck;
BaseUserAction door;door.owner=&truck;context.actions={&door};assert(!access.Check(&truck,&actor));context.actions={&storage};
manager.enabled=false;assert(!access.Check(&truck,&actor));manager.enabled=true;
// Stock Ural defines 36 root contexts and ammo adds one: a valid storage action near the end must remain discoverable.
for(int count:{37,64}){
// Stock Ural has 37 contexts and Mi-8 has 82: a valid storage action near the end must remain discoverable.
for(int count:{37,64,82,128}){
std::vector<UserActionContext> stockContexts(count);manager.contexts.clear();
for(auto& authored:stockContexts){authored.actions={&door};manager.contexts.push_back(&authored);}
stockContexts.back().actions={&storage};
assert(ERR_InfantryAmmoAccess::Points(&truck,points)&&points.Count()==1&&access.Check(&truck,&actor));
}
manager.contexts.clear();manager.contexts.resize(65,&context);assert(!access.Check(&truck,&actor));manager.contexts={&context};
context.actions.resize(17,&storage);assert(!access.Check(&truck,&actor));context.actions={&storage};
manager.contexts.clear();manager.contexts.resize(129,&context);assert(!access.Check(&truck,&actor));manager.contexts={&context};
// Native helicopter door contexts include 38 actions, most unrelated to storage.
for(int count:{33,38,64}){context.actions.clear();context.actions.resize(count,&door);context.actions.back()=&storage;assert(access.Check(&truck,&actor));}
context.actions.resize(65,&storage);assert(!access.Check(&truck,&actor));context.actions={&storage};
manager.contexts={nullptr};assert(!access.Check(&truck,&actor));manager.contexts={&context};
assert(!access.Check(&truck,&wall));assert(!access.Check(nullptr,&actor));
// A blocked context never skips an intervening external entity; another independently clear authored context may qualify.
+2 -1
View File
@@ -10,6 +10,7 @@ callback=s[s.index('class ERR_InfantryAmmoCallback'):s.index('// One exact physi
code=s[s.index('class ERR_InfantryAmmoTransfer\n'):]
code=re.sub(r'//[^\n]*','',code).replace('protected ','').replace('class ','struct ')
for src,dst in {
'm_Donor.FindComponent(VehicleHelicopterSimulation)':'m_Donor->helicopter',
'BaseMagazineComponent.Cast(exactMagazine.FindComponent(BaseMagazineComponent))':'exactMagazine->magazine',
'BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent))':'actor->weapons',
'BaseWeaponComponent.Cast(rifle.FindComponent(BaseWeaponComponent))':'rifle->weapon',
@@ -78,7 +79,7 @@ struct Usage{Slot*pilot=nullptr;Slot*GetPilotCompartmentSlot(){return pilot;}};
struct VehicleManager{IEntity vehicle=nullptr;IEntity FindVehicle(IEntity v){return vehicle==v?v:nullptr;}};
struct Utility{VehicleManager*m_VehicleMgr=nullptr;bool military=true;bool IsMilitary(){return military;}};
struct Group{int faction=1,players=0;bool slave=false;Utility*utility=nullptr;int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}bool IsSlave(){return slave;}Utility*GetGroupUtilityComponent(){return utility;}};
struct Entity{bool alive=true,awake=true,player=false,captured=false;int faction=1;Group*group=nullptr;IEntity vehicle=nullptr,parent=nullptr;
struct Entity{bool helicopter=false;bool alive=true,awake=true,player=false,captured=false;int faction=1;Group*group=nullptr;IEntity vehicle=nullptr,parent=nullptr;
Inventory*inventory=nullptr;Magazine*magazine=nullptr;Manager*weapons=nullptr;Weapon*weapon=nullptr;Item*item=nullptr;Usage*usage=nullptr;Physics_*physics=nullptr;vector position;
IEntity GetParent(){return parent;}Physics_*GetPhysics(){return physics;}vector GetOrigin(){return position;}};
struct Record{SCR_AIGroup Group=nullptr;bool Physical=true,Eliminated=false;};
+88
View File
@@ -0,0 +1,88 @@
"""Execute the production physical body-inspection gate; engine traces are boundary doubles."""
from policy_harness import ROOT, body, run
import re
p=ROOT/'addons/core/Scripts/Game/ERR/20_Intel/ERR_IntelEvidence.c'
s='\n'.join(body(p,sig) for sig in ('bool Inspect(', 'bool RetainedWithin(', 'void Delivered(', 'void Release('))
for old,new in [('out array<string> missing','Array<string>& missing'),('array<IEntity>','Array<IEntity>'),('array<string>','Array<string>'),('foreach (','for ('),('ERR_BodyEvidence record','ERR_BodyEvidence* record'),('ERR_IssuedIntel issued','ERR_IssuedIntel* issued'),('TraceParam trace','TraceParam* trace'),('InventoryStorageManagerComponent inventory','InventoryStorageManagerComponent* inventory'),('SCR_GarbageSystem garbage','SCR_GarbageSystem* garbage'),('null','nullptr')]:s=s.replace(old,new)
s=s.replace('InventoryStorageManagerComponent.Cast(body.FindComponent(InventoryStorageManagerComponent))','body->inventory')
for cls in ('Replication','ERR_GameMode','ERR_Actors','CompartmentAccessComponent','vector','TraceFlags','ERR_DiscoveryService','SCR_GarbageSystem'):
s=s.replace(cls+'.',cls+'::')
s=s.replace('parent.GetParent()', 'parent->GetParent()')
for v in ('body','observer','record','issued','trace','garbage'):
s=s.replace(v+'.',v+'->')
s=s.replace('ERR_GameMode::Get().','ERR_GameMode::Get()->').replace('GetGame().GetWorld().','GetGame()->GetWorld()->')
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <algorithm>
#include <cmath>
using string=std::string;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Clear(){this->clear();}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}void Insert(T t){this->push_back(t);}};
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*){}vector operator+(vector v){return {x+v.x,y+v.y,z+v.z};}static float Distance(vector a,vector b){return std::hypot(std::hypot(a.x-b.x,a.y-b.y),a.z-b.z);}};
struct Entity;using IEntity=Entity*;
struct InventoryStorageManagerComponent{Array<IEntity> folders;};
struct Entity{IEntity parent=nullptr;IEntity GetParent(){return parent;}bool alive=true,fit=true,player=false,seated=false;InventoryStorageManagerComponent* inventory=nullptr;vector position;vector GetOrigin(){return position;}};
struct ERR_Actors{static bool Alive(IEntity e){return e&&e->alive;}static bool CanFight(IEntity e){return e&&e->alive&&e->fit;}static bool Player(IEntity e){return e&&e->player;}};
struct CompartmentAccessComponent{static bool GetVehicleIn(IEntity e){return e->seated;}};
struct TraceFlags{enum{ENTS=1,WORLD=2};};struct TraceParam{vector Start,End;IEntity Exclude=nullptr,TraceEnt=nullptr;int Flags=0;};
struct World{float fraction=1;IEntity hit=nullptr;float TraceMove(TraceParam* t,void*){t->TraceEnt=hit;return fraction;}};
struct Game{World world;World* GetWorld(){return &world;}} game;Game* GetGame(){return &game;}
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
struct ERR_GameMode{bool active=true;static ERR_GameMode* Get(){static ERR_GameMode mode;return &mode;}bool Active(){return active;}};
struct ERR_DiscoveryService{static void Folders(InventoryStorageManagerComponent* inv,Array<IEntity>& folders){folders=inv->folders;}};
struct ERR_IssuedIntel{IEntity Folder;string SiteId;};
struct ERR_BodyEvidence{IEntity Actor;Array<ERR_IssuedIntel*> Issued;bool Searched=false;bool Reported=false;};
struct SCR_GarbageSystem {
inline static int releases=0,inserts=0;inline static bool blacklisted=true;inline static float delay=0;
static SCR_GarbageSystem* GetByEntityWorld(IEntity){static SCR_GarbageSystem g;return &g;}
void UpdateBlacklist(IEntity,bool value){blacklisted=value;releases++;}
void Insert(IEntity,float value){assert(!blacklisted);inserts++;delay=value;}
};
struct Evidence{Array<ERR_BodyEvidence*> m_Roster;bool m_Stopped=false;ERR_BodyEvidence* record=nullptr;ERR_BodyEvidence* Find(IEntity b){return record&&record->Actor==b?record:nullptr;}
'''+s+r'''
};
int main(){
for(int invalid=0;invalid<9;invalid++){
Entity body,observer,folder;body.alive=false;InventoryStorageManagerComponent inventory;body.inventory=&inventory;
ERR_IssuedIntel issued{&folder,"radio-1"};ERR_BodyEvidence record{&body,{&issued},false};Evidence service;service.record=&record;Array<string> result;
game.world.fraction=1;Replication::server=true;ERR_GameMode::Get()->active=true;
if(invalid==0)Replication::server=false;
if(invalid==1)service.m_Stopped=true;
if(invalid==2)body.alive=true;
if(invalid==3)observer.player=true;
if(invalid==4)observer.fit=false;
if(invalid==5)observer.seated=true;
if(invalid==6)observer.position={3.01f,0,0};
if(invalid==7)game.world.fraction=.5;
if(invalid==8)body.inventory=nullptr;
assert(!service.Inspect(&body,&observer,result)&&!record.Searched&&result.empty());
}
Replication::server=true;game.world.fraction=1;
Entity body,observer,present,missing;body.alive=false;InventoryStorageManagerComponent inv;body.inventory=&inv;inv.folders.Insert(&present);
ERR_IssuedIntel a{&present,"radio-1"},b{&missing,"mortar-3"},c{nullptr,"depot-1"};ERR_BodyEvidence record{&body,{&a,&b,&c},false};Evidence service;service.record=&record;Array<string> result;
observer.position={3,0,0};assert(service.Inspect(&body,&observer,result));assert(result.size()==2&&result.Contains("mortar-3")&&result.Contains("depot-1")&&!result.Contains("radio-1"));
assert(!service.Inspect(&body,&observer,result));
Entity wreck,seat,unrelated;body.parent=&seat;seat.parent=&wreck;service.m_Roster={&record};
assert(service.RetainedWithin(&body)&&service.RetainedWithin(&seat)&&service.RetainedWithin(&wreck));
assert(!service.RetainedWithin(&unrelated)&&!service.RetainedWithin(nullptr));
body.alive=true;assert(!service.RetainedWithin(&wreck));body.alive=false;
record.Reported=true;assert(!service.RetainedWithin(&wreck));record.Reported=false;
service.m_Stopped=true;assert(!service.RetainedWithin(&wreck));service.m_Stopped=false;
// Searching protects the body until HQ receives the report. Premature,
// client-side, stopped and repeated deliveries never release/renew cleanup.
record.Searched=false;service.Delivered(&body);assert(!record.Reported&&SCR_GarbageSystem::inserts==0);
record.Searched=true;Replication::server=false;service.Delivered(&body);assert(!record.Reported);
Replication::server=true;service.m_Stopped=true;service.Delivered(&body);assert(!record.Reported);
service.m_Stopped=false;service.Delivered(&unrelated);assert(!record.Reported);
assert(service.RetainedWithin(&body));service.Delivered(&body);
assert(record.Reported&&!service.RetainedWithin(&body));
assert(SCR_GarbageSystem::inserts==1&&SCR_GarbageSystem::releases==1&&SCR_GarbageSystem::delay==300);
service.Delivered(&body);assert(SCR_GarbageSystem::inserts==1&&SCR_GarbageSystem::releases==1);
record.Reported=false;
// Corrupt/cyclic parent links cannot cause unbounded cleanup traversal.
seat.parent=&seat;assert(!service.RetainedWithin(&wreck));
}
''')
print('PASS: physical inspection and parent-wreck retention; server-only searched/report delivery unlocks native cleanup once with a 300-second delay')
+11 -9
View File
@@ -15,28 +15,28 @@ def cpp(s):
s=s.replace('OnTick().Remove(ERR_FlushDiscoveryFeedback);','OnTick().Remove(0);')
s=s.replace('string.Format(', 'Format(')
return s
methods='\n'.join(cpp(body(P,s)) for s in ('protected void ERR_QueueDiscoveryFeedback(', 'protected void ERR_FlushDiscoveryFeedback(', 'protected void ERR_ReceiveDiscoveryFeedback(', 'protected void ERR_StopDiscoveryFeedback('))
methods='\n'.join(cpp(body(P,s)) for s in ('protected void ERR_QueueDiscoveryFeedback(', 'protected void ERR_FlushDiscoveryFeedback(', 'protected void ERR_ReceiveDiscoveryFeedback(', 'protected void ERR_StopDiscoveryFeedback(', 'void ERR_IntelExhausted('))
run(r'''
#include <cassert>
#include <string>
#include <map>
#include <vector>
struct string:std::string {using std::string::string;using std::string::operator=;string(std::string s):std::string(s){}bool IsEmpty()const{return empty();}};
struct string:std::string {using std::string::string;using std::string::operator=;string(std::string s):std::string(s){}bool IsEmpty()const{return empty();}bool Contains(const char* text)const{return find(text)!=npos;}};
string Format(string text,int identified,int rough){text.replace(text.find("%1"),2,std::to_string(identified));text.replace(text.find("%2"),2,std::to_string(rough));return text;}
struct Replication {static bool server;static bool IsServer(){return server;}};bool Replication::server=true;
struct SCR_Global {static bool edit;static bool IsEditMode(){return edit;}};bool SCR_Global::edit=false;
struct SCR_ChatComponent {static std::vector<string> messages;static void RadioProtocolMessage(string text){messages.push_back(text);}};std::vector<string> SCR_ChatComponent::messages;
struct Math {static float Max(float a,float b){return a>b?a:b;}};
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,EXTRACTION};}
namespace ERR_LocationKind {enum {PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT};}
struct ERR_LocationComponent {int kind=ERR_LocationKind::OUTPOST,knowledge=1;string label="Hidden outpost";int Kind(){return kind;}int Knowledge(){return knowledge;}string Label(){return label;}};
namespace ERR_LocationKind {enum {PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR};}
struct ERR_LocationComponent {int kind=ERR_LocationKind::OUTPOST,knowledge=1;string label="Hidden outpost";int Kind(){return kind;}int Knowledge(){return knowledge;}string Label(){return label;}string Id(){return "outpost-id";}};
template<class K,class V> struct Map {std::map<K,V> values;bool Find(K k,V& v){auto i=values.find(k);if(i==values.end())return false;v=i->second;return true;}bool Contains(K k){return values.count(k);}int Count(){return values.size();}bool IsEmpty(){return values.empty();}void Set(K k,V v){values[k]=v;}void Clear(){values.clear();}};
struct World {float time=0;float GetWorldTime(){return time;}} world;
struct Game {bool controller=true;bool GetPlayerController(){return controller;}} game;
Game* currentGame=&game;Game* GetGame(){return currentGame;}
struct Events {int removes=0;void Remove(int){removes++;}};
struct ERR_IntelDiscoveryText {static
'''+cpp(body(P,'static string Message('))+r'''
'''+cpp(body(P,'static string Type('))+'static '+cpp(body(P,'static string Message('))+r'''
};
struct ERR_GameMode {
static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}
@@ -57,14 +57,14 @@ int main(){
m.ERR_QueueDiscoveryFeedback(nullptr,0,1);m.ERR_QueueDiscoveryFeedback(&site,1,1);m.ERR_QueueDiscoveryFeedback(&site,0,2);m.ERR_QueueDiscoveryFeedback(&site,-1,1);assert(m.m_ERRIntelPending.IsEmpty());
// Rough clues do not disclose labels, kinds or coordinates. Listen callbacks dedupe.
m.ERR_QueueDiscoveryFeedback(&site,0,1);m.ERR_FlushDiscoveryFeedback(1499);assert(SCR_ChatComponent::messages.empty());
m.ERR_FlushDiscoveryFeedback(1500);assert(m.rpcCount==1&&SCR_ChatComponent::messages.size()==1);assert(SCR_ChatComponent::messages.back().find("Hidden")==string::npos);assert(SCR_ChatComponent::messages.back().find("500 m")!=string::npos);
m.ERR_FlushDiscoveryFeedback(1500);assert(m.rpcCount==1&&SCR_ChatComponent::messages.size()==1);assert(SCR_ChatComponent::messages.back().find("Hidden")==string::npos);assert(SCR_ChatComponent::messages.back().find("New location marked. Type unknown.")!=string::npos);
m.ERR_QueueDiscoveryFeedback(&site,0,1);assert(m.m_ERRIntelPending.IsEmpty());
site.knowledge=2;world.time=2000;m.ERR_QueueDiscoveryFeedback(&site,1,2);m.ERR_FlushDiscoveryFeedback(6499);assert(m.rpcCount==1);
m.ERR_FlushDiscoveryFeedback(6500);assert(m.rpcCount==2&&SCR_ChatComponent::messages.back().find("Hidden outpost")!=string::npos);
m.ERR_FlushDiscoveryFeedback(6500);assert(m.rpcCount==2&&SCR_ChatComponent::messages.back()=="Hey! A outpost was identified in the intel!");
// A same-batch rough→exact update becomes one final identified location.
ERR_GameMode batch;ERR_GameMode::current=&batch;ERR_LocationComponent a,b;world.time=0;
batch.ERR_QueueDiscoveryFeedback(&a,0,1);a.knowledge=2;batch.ERR_QueueDiscoveryFeedback(&a,1,2);batch.ERR_QueueDiscoveryFeedback(&b,0,1);batch.ERR_FlushDiscoveryFeedback(1500);
assert(SCR_ChatComponent::messages.back().find("1 identified locations and 1 search areas")!=string::npos);
assert(SCR_ChatComponent::messages.back().find("Hey! A outpost was identified in the intel!")!=string::npos && SCR_ChatComponent::messages.back().find("New location marked. Type unknown.")!=string::npos);
// End/restart cancels pending output and removes both hooks, new mode is clean.
world.time=2000;b.knowledge=2;batch.ERR_QueueDiscoveryFeedback(&b,1,2);batch.ERR_StopDiscoveryFeedback();assert(batch.m_ERRIntelSeen.IsEmpty()&&batch.m_ERRIntelPending.IsEmpty()&&batch.discovered.removes==1&&batch.tick.removes==1);
int calls=batch.rpcCount;batch.ERR_FlushDiscoveryFeedback(99999);batch.ERR_QueueDiscoveryFeedback(&b,0,2);assert(batch.rpcCount==calls&&batch.m_ERRIntelPending.IsEmpty());
@@ -76,7 +76,9 @@ int main(){
next.ERR_ReceiveDiscoveryFeedback(2,"new");assert(SCR_ChatComponent::messages.size()==shown+2);
// Queue is bounded even for malformed/unusually large authored scenarios.
Replication::server=true;ERR_GameMode bounded;ERR_GameMode::current=&bounded;ERR_LocationComponent many[65];for(auto& item:many)bounded.ERR_QueueDiscoveryFeedback(&item,0,1);assert(bounded.m_ERRIntelSeen.Count()==64&&bounded.m_ERRIntelPending.Count()==64);
assert(ERR_IntelDiscoveryText::Message(0,0,"secret").empty());assert(ERR_IntelDiscoveryText::Message(1,0,"")=="A location identified. Check your field map.");
assert(ERR_IntelDiscoveryText::Type(ERR_LocationKind::MORTAR,"any")=="mortar site");
ERR_GameMode::current=&m;m.ERR_IntelExhausted();assert(SCR_ChatComponent::messages.back()=="Every known location has already been identified. This intel doesn't help us.");
assert(ERR_IntelDiscoveryText::Message(0,0,"secret").empty());assert(ERR_IntelDiscoveryText::Message(1,0,"")=="Hey! A location was identified in the intel!");
}
''')
for required in ('OnLocationDiscovered().Insert(ERR_QueueDiscoveryFeedback)', 'OnTick().Insert(ERR_FlushDiscoveryFeedback)', 'OnStop().Insert(ERR_StopDiscoveryFeedback)', '[RplRpc(RplChannel.Reliable, RplRcver.Broadcast)]'):
+19 -9
View File
@@ -10,9 +10,9 @@ def adapt(s):
s=s.replace('override ', '').replace('protected ', '').replace('null','nullptr')
s=re.sub(r'array<([^>]+)> (\w+) = \{\};',r'Array<\1> \2;',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'):
for n in ('Replication','ERR_Actors','Math','ERR_Resources','ERR_DiscoveryService','SCR_InventoryStorageManagerComponent','SCR_AIGroupUtilityComponent','ERR_MortarSupplyState','ERR_LocationKind','ERR_ObjectiveDefence'):
s=s.replace(n+'.',n+'::')
for n in ('group','member','actor','request','inventory','known','unit','m_Mode','m_Battery','mode','m_Reroll','home','sites','record','m_Mortars'):
for n in ('group','member','actor','request','inventory','known','unit','m_Mode','m_Battery','mode','m_Reroll','home','sites','record','m_Mortars','m_Scouts','m_Recon','m_Airfield'):
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(')
@@ -24,7 +24,7 @@ def adapt(s):
s=s.replace('ERR_IntelRequest request = new ERR_IntelRequest();','ERR_IntelRequest request = new Request();')
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
return s.replace('Evidence().','Evidence()->').replace('Cargo().','Cargo()->')
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(')
@@ -37,7 +37,7 @@ assert poll.index('if (m_Ready) return') < poll.index('mode.RegisterGroup(group)
for condition in ('if (!operation.Finished)', 'if (!operation.Succeeded)', 'if (population == ERR_MortarSupplyState.FAILED)', 'if (population == ERR_MortarSupplyState.PENDING)'):
assert poll.index(condition)<poll.index('mode.RegisterGroup(group)')
assert poll.count('mode.RegisterGroup(group)')==1
prepare=body(system,'bool Prepare(')
prepare=body(system,'protected bool CreateBattery(')
assert 'mode.RegisterGroup(group)' not in prepare
assert prepare.index('mode.Own(group)')<prepare.index('m_HQ.RegisterGroup(group)')<prepare.index('m_HQ.ReserveGroup(group)')
site=body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c','void Activate(')
@@ -65,13 +65,16 @@ namespace ERR_Resources{int IntelFolder(){return 1;}}
namespace ERR_DiscoveryService{void Folders(Inventory* i,Array<IEntity>& a){a.clear();if(i->folder)a.Insert(nullptr);}}
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 Definition{string Id="site-a";Array<int>* Patrol=nullptr;};
struct Settings{int chance=100;int Runtime(const char*){return chance;}};
namespace ERR_LocationKind {enum {RADIO=1};}
namespace ERR_ObjectiveDefence {bool Objective(int kind){return kind==1;}}
struct Definition{int Kind=3;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 EvidenceService{void Capture(IEntity){}void Retire(IEntity){}};
struct Mode{EvidenceService evidence;EvidenceService* Evidence(){return &evidence;}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;}bool IsMissionEnemy(int f){return f==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){}
@@ -82,10 +85,13 @@ struct Sampler{bool Bound(){return bindings>0;}int bindings=0,strength=0;SCR_AIG
struct GroupService{int registrations=0;Set<SCR_AIGroup> seen;Array<int> ids;Array<bool> fits;
void RegisterGroup(SCR_AIGroup g,int radioId=0,bool fitRadios=true){ids.Insert(radioId);fits.Insert(fitRadios);if(!seen.Contains(g)){seen.Insert(g);registrations++;}}};
struct Mortars{SCR_AIGroup owned=nullptr;bool FindGroup(SCR_AIGroup group){return group&&group==owned;}};
struct Scouts{SCR_AIGroup owned=nullptr;bool Contains(SCR_AIGroup group){return group&&group==owned;}};
struct CargoFlight{SCR_AIGroup crew=nullptr;SCR_AIGroup Crew(){return crew;}};struct Airfield{CargoFlight* flight=nullptr;CargoFlight* Cargo(){return flight;}};
struct Recon{SCR_AIGroup owned=nullptr;bool Owns(SCR_AIGroup group){return group&&group==owned;}};
struct BodyService:GroupService{void RegisterGroup(SCR_AIGroup g,Sampler& s){assert(s.Bound()&&s.group==g);GroupService::RegisterGroup(g);}};
struct Reporting{int initializations=0;void Initialize(int id,float now){assert(id>0&&now==10);initializations++;}};
struct Unit{Reporting AmmoReporting;string HomeId,ForceSourceId;bool HomeGuard=false;float NextStatus=0;Sampler StatusSampler;int Id=0;SCR_AIGroup Group=nullptr;struct Utility* Utility=nullptr;float NextReport=0;};using ERR_HQUnit=Unit*;
struct HQ{Mode* m_Mode;GroupService m_Equipment;BodyService m_Bodies;int m_NextId=0;Array<ERR_HQUnit> m_Units;Mortars* m_Mortars=nullptr;
struct Unit{Reporting AmmoReporting;string HomeId,ForceSourceId;bool ReservedForSupport=false;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{void BindSpecialSkills(SCR_AIGroup){}void AuditGroup(ERR_HQUnit){}Mode* m_Mode;GroupService m_Equipment;BodyService m_Bodies;int m_NextId=0;Array<ERR_HQUnit> m_Units;Mortars* m_Mortars=nullptr;Scouts* m_Scouts=nullptr;Recon* m_Recon=nullptr;Airfield* m_Airfield=nullptr;
'''+hqbind+hqregister+r'''
};
Intel* intel_;HQ* hq_;
@@ -107,6 +113,7 @@ int main(){
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]->ReservedForSupport);
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));
@@ -133,6 +140,9 @@ int main(){
assert(hq.m_Equipment.ids.Count()==3&&hq.m_Equipment.ids[2]==hq.m_Units[2]->Id&&hq.m_Equipment.ids[2]==3&&hq.m_Equipment.fits[2]);
// A subsequent missing mortar registry cannot reroll an already bound group's opt-out.
hq.RegisterGroup(&group);assert(hq.m_Equipment.ids.Count()==3&&!hq.m_Equipment.fits[0]&&hq.m_NextId==3);
Group radioGuards;mode.sites.home.data.Kind=ERR_LocationKind::RADIO;hq.RegisterGroup(&radioGuards);
assert(hq.m_Units[3]->ReservedForSupport && hq.m_Units[3]->HomeGuard);
hq.RegisterGroup(&radioGuards);assert(hq.m_Units.Count()==4&&hq.m_Units[3]->ReservedForSupport);
}
''')
print('PASS: actual Intel/HQ methods and one-shot qualified readiness handoff deliver late actors once; duplicate readiness, accepted spawn, pickup/drop, death and original negative rolls do not mint assignments')
+4 -3
View File
@@ -23,7 +23,7 @@ def adapt(text,cls):
text=re.sub(r'^(\w+) (\w+)\(',lambda m:m[1]+' '+cls+'::'+m[2]+'(',text)
# Defaults belong only to declarations in C++.
text=text.replace('bool failed = false, bool skipped = false','bool failed, bool skipped')
return text
return text.replace('Evidence().','Evidence()->')
service_methods=[('void RegisterGroup(', 'void RegisterGroup(SCR_AIGroup group);'),('void Tick(', 'void Tick(float now);'),('bool BeginReroll(', 'bool BeginReroll();'),('bool HasPendingInsertion(', 'bool HasPendingInsertion(IEntity actor);'),('void Unsettled(', 'void Unsettled(IEntity actor);'),('bool Settled(', 'bool Settled(IEntity actor);'),('void Stop(', 'void Stop();')]
batch_methods=[('void Add(', 'void Add(IEntity actor);'),('bool Running(', 'bool Running();'),('void Tick(', 'void Tick();'),('protected void Finish(', 'void Finish(ERR_IntelRerollEntry entry,bool failed=false,bool skipped=false);'),('protected void Fail(', 'void Fail(ERR_IntelRerollEntry entry,string reason);'),('protected void Process(', 'void Process(ERR_IntelRerollEntry entry);'),('void Stop(', 'void Stop();')]
code=r'''
@@ -48,8 +48,9 @@ struct Group{Array<AIAgent> agents;void GetAgents(Array<AIAgent>& out){out=agent
namespace ERR_Actors{bool Alive(IEntity a){return a&&a->alive;}bool Player(IEntity a){return a&&a->player;}}
namespace ERR_Resources{int IntelFolder(){return 1;}}
struct Discovery{Set<IEntity> spent;bool capacity=true;static void Folders(Inventory* i,Array<IEntity>& out){out=i->folders;}bool IsSpent(IEntity a){return spent.Contains(a);}bool CanMarkSpent(IEntity a){return capacity;}bool MarkSpent(IEntity a){if(!capacity)return false;spent.Insert(a);return true;}};using ERR_DiscoveryService=Discovery;
struct Settings{int chance=100;int Get(const char*){return chance;}};
struct Mode{bool active=true;struct Settings settings;struct Discovery discovery;Set<IEntity> owned;bool Active(){return active;}struct Settings* Settings(){return &settings;}struct Discovery* Discovery(){return &discovery;}void Own(IEntity a){owned.Insert(a);}};
struct Settings{int chance=100;int Runtime(const char*){return chance;}};
struct EvidenceService{void Capture(IEntity){}void Retire(IEntity){}};
struct Mode{EvidenceService evidence;EvidenceService* Evidence(){return &evidence;}bool active=true;struct Settings settings;struct Discovery discovery;Set<IEntity> owned;bool Active(){return active;}struct Settings* Settings(){return &settings;}struct Discovery* Discovery(){return &discovery;}void Own(IEntity a){owned.Insert(a);}};
struct Entry{string WaitingFor="inventory";Callback* Operation=nullptr;IEntity Actor=nullptr,Removing=nullptr;int Attempts=0;bool GiveIntel=false,Inserting=false,Accepted=false,Done=false;};using ERR_IntelRerollEntry=Entry*;
struct Request{Callback* Operation=nullptr;IEntity Actor=nullptr;int Attempts=0;bool Accepted=false;};using ERR_IntelRequest=Request*;
struct Service;struct Batch;
+4 -2
View File
@@ -10,7 +10,8 @@ def adapt(signature):
s=s.replace('SCR_AIActionBase action','Action* action').replace('EAIActionState ', 'State ').replace('EAIActionState.', 'State::')
s=s.replace('ERR_MortarSupplyState ', 'Supply ').replace('ERR_MortarSupplyState.', 'Supply::').replace('ERR_MortarSalvo.', 'Salvo::')
s=s.replace('string reason', 'std::string reason')
for name in ['Activity','m_ObservedActivity','m_Transfer','m_Readiness']:
s=s.replace('new ERR_MortarStation(this)', 'new Station(this)')
for name in ['Activity','m_ObservedActivity','m_Transfer','m_Readiness','m_Station']:
s=s.replace(name+'.',name+'->')
for name in ['OnActivityCompleted','OnActivityFailed']:
s=s.replace('.Insert('+name+')', '.Insert(this, &Battery::'+name+')').replace('.Remove('+name+')', '.Remove(this, &Battery::'+name+')')
@@ -28,6 +29,7 @@ enum class Supply {FAILED=-1,PENDING,READY};
struct Salvo {static constexpr float TIMEOUT_MS=120000;};
struct Text:std::string {using std::string::string;bool IsEmpty(){return empty();}};
struct Action;struct Battery;
struct Station {Station(Battery*){} void Update(float){} void Stop(){}};
struct Invoker {Battery* owner=nullptr;void(Battery::*callback)(Action*)=nullptr;int inserts=0,removes=0,calls=0;
void Insert(Battery* b,void(Battery::*f)(Action*)){owner=b;callback=f;inserts++;}
void Remove(Battery* b,void(Battery::*f)(Action*)){if(owner==b&&callback==f){owner=nullptr;callback=nullptr;removes++;}}
@@ -44,7 +46,7 @@ struct Battery {
bool m_Stopped=false,CommandOpen=true,StartupActivated=true,operational=true,RefreshRejected=false,Released=false;
Action* Activity=nullptr;Action* m_ObservedActivity=nullptr;int OrderId=1,m_ObservedActivityOrder=0,OrderShots=8,TotalShots=8;
State m_ActivityTerminal=State::EVALUATED;float OrderedAt=0,AimedAt=100,AimValidatedAt=100;
Text SpawnFailure;std::string lastCancel;Readiness readiness;Readiness* m_Readiness=&readiness;Transfer transfer;Transfer* m_Transfer=&transfer;
Text SpawnFailure;std::string lastCancel;Readiness readiness;Readiness* m_Readiness=&readiness;Transfer transfer;Transfer* m_Transfer=&transfer;Station* m_Station=nullptr;
void* RadioOrder=nullptr;void* PendingRadioReport=nullptr;void* PendingSupplyReport=nullptr;void* Refresh=nullptr;void* Report=this;void* AimSolver=this;void* AimSolution=this;
bool Operational(){return operational;}std::string OperationalReason(){return "physical crew unavailable";}void SpawnFailed(const char*){}void UpdateCrewDetail(float){}void FlushTraces(){}
'''+methods+cancel+r'''
+45
View File
@@ -128,3 +128,48 @@ int main(){int faction,group,foreign;Usage usage;Actor gun{&faction,&group};gun.
}
''')
print('PASS: actual native-gun ownership gate rejects player/foreign/replaced/dead occupant, faction change, missing seat and missing registry')
# Exercise the production between-trip target selector with four batteries.
select = body(p, 'protected void SelectBattery(')
select = select.replace('protected ', '').replace('ERR_MortarLogisticsRules.', 'Rules::').replace('vector.DistanceXZ', 'Distance')
select = select.replace('array<ref ERR_MortarBattery> batteries = {};', 'Array<Battery*> batteries;').replace('ERR_MortarBattery selected;', 'Battery* selected=nullptr;')
select = select.replace('foreach (ERR_MortarBattery candidate : batteries)', 'for (Battery* candidate : batteries)').replace('float.MAX', '999999')
select = select.replace('m_HQ.Mortars().Batteries(batteries)', 'system.Batteries(batteries)')
for name in ('m_Battery', 'candidate', 'selected', 'm_Cache'):
select = select.replace(name+'.', name+'->')
run(r'''
#include <cassert>
#include <cmath>
#include <vector>
using std::abs;
struct Battery { bool StartupActivated=true, stopped=false, operational=true; int shells=0, Gun=1, Group=1; float Position=0;
bool Stopped(){return stopped;} bool Operational(){return operational;} int CountShells(){return shells;} };
template<class T> struct Array:std::vector<T>{using std::vector<T>::operator=;void Clear(){this->clear();}};
struct System {Array<Battery*> all; void Batteries(Array<Battery*>& out){out=all;}};
struct Cache {float GetOrigin(){return 0;}};
float Distance(float a,float b){return abs(a-b);}
namespace Rules {bool Need(int n){return n>=0&&n<=6;}}
struct Courier {int m_State=1;bool m_Report=false,m_Move=false,m_Order=false; Battery* m_Battery=nullptr;
int m_OriginalGun=0,m_OriginalBatteryGroup=0;Array<int> m_BatteryMembers;float m_NextReport=99;
Cache cache;Cache* m_Cache=&cache;System system;
bool TargetAvailable(){return m_Battery&&m_Battery->StartupActivated&&!m_Battery->Stopped()&&m_Battery->Operational();}
''' + select + r'''
};
int main(){
Battery a,b,c,d;a.Position=200;b.Position=100;c.Position=300;d.Position=400;
Courier s;s.system.all={&a,&b,&c,&d};s.SelectBattery();assert(s.m_Battery==&b);
// Keep an active low-ammo recipient; a new nearer request cannot retarget it.
a.Position=1;s.SelectBattery();assert(s.m_Battery==&b);
// Once replenished, choose another depleted battery and reset only binding.
b.shells=12;s.m_BatteryMembers={9};s.SelectBattery();assert(s.m_Battery==&a&&s.m_BatteryMembers.empty()&&s.m_NextReport==0);
a.operational=false;s.SelectBattery();assert(s.m_Battery==&c);
c.stopped=true;d.StartupActivated=false;s.SelectBattery();assert(s.m_Battery==&c);
d.StartupActivated=true;
for(int gate=0;gate<4;gate++){
s.m_State=gate==0?3:1;s.m_Report=gate==1;s.m_Move=gate==2;s.m_Order=gate==3;
s.SelectBattery();assert(s.m_Battery==&c);
}
s.m_Order=false;s.SelectBattery();assert(s.m_Battery==&d);
}
''')
print('PASS: four-battery courier selection preserves pending trip/report/transfer identity, skips unavailable crews, and selects the next depleted battery without new stock')
+35
View File
@@ -0,0 +1,35 @@
"""Run actual station scope predicates; native boarding/movement is tested separately."""
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_MortarStation.c'
s='\n'.join(body(p,name) for name in ('bool Holds(', 'bool HoldsSeat(', 'bool HoldsPosition('))
s=(s.replace('IEntity actor','IEntity* actor').replace('IEntity vehicle','IEntity* vehicle')
.replace('m_Battery.', 'm_Battery->').replace('Replication.', 'Replication::')
.replace('ERR_HQ.', 'ERR_HQ::').replace('ERR_Actors.', 'ERR_Actors::')
.replace('CompartmentAccessComponent.', 'CompartmentAccessComponent::'))
run(r'''
#include <cassert>
struct IEntity {bool player=false, fighting=true; int group=1; IEntity* vehicle=nullptr;};
struct Battery {int Group=1; IEntity* Gun=nullptr;};
namespace Replication {bool server=true;bool IsServer(){return server;}}
namespace ERR_HQ {bool active=true;bool MissionActive(){return active;}}
namespace ERR_Actors {bool Player(IEntity* a){return a->player;}int Group(IEntity* a){return a->group;}bool CanFight(IEntity* a){return a&&a->fighting;}}
namespace CompartmentAccessComponent {IEntity* GetVehicleIn(IEntity* a){return a->vehicle;}}
struct Station {Battery* m_Battery;IEntity* m_Gunner=nullptr;bool m_Stopped=false,intact=true;bool GunIntact(){return intact;}
''' + s + r'''
};
int main(){
IEntity gun,other,actor;Battery b;b.Gun=&gun;Station s{&b};actor.vehicle=&gun;
assert(s.Holds(&actor)&&s.HoldsPosition(&actor)&&s.HoldsSeat(&actor,&gun));
assert(!s.HoldsSeat(&actor,&other));actor.vehicle=nullptr;assert(!s.HoldsSeat(&actor,&gun));
s.m_Gunner=&actor;assert(s.Holds(&actor)&&!s.HoldsPosition(&actor)); // gunner may walk to board
s.m_Gunner=nullptr;actor.fighting=false;assert(!s.HoldsPosition(&actor));actor.fighting=true;
actor.player=true;assert(!s.Holds(&actor));actor.player=false;
actor.group=2;assert(!s.Holds(&actor));actor.group=1;
s.intact=false;assert(!s.Holds(&actor)&&!s.HoldsPosition(&actor));s.intact=true;
s.m_Stopped=true;assert(!s.Holds(&actor));s.m_Stopped=false;
Replication::server=false;assert(!s.Holds(&actor));Replication::server=true;
ERR_HQ::active=false;assert(!s.Holds(&actor));ERR_HQ::active=true;
assert(!s.Holds(nullptr));
}
''')
print('PASS: mortar duty scope excludes players, other crews, stopped/client/inactive states; only destruction releases an active assignment, while the gunner may approach the gun')
+1 -1
View File
@@ -9,7 +9,7 @@ def cpp(s):
s=s.replace('GetGame().','GetGame()->').replace('GetCallqueue().','GetCallqueue()->').replace('GetPlayerManager().','GetPlayerManager()->')
return s
methods='\n'.join(cpp(body(SOURCE, signature)) for signature in ('bool Active(', 'protected override void OnGameModeEnd(', 'protected void StopServices('))
startGuard=body(SOURCE,'protected void StartScenario(').split('if (!m_Factions',1)[0].replace('protected ','')
startGuard=body(SOURCE,'protected void StartScenario(').split('if (!m_FactionSelection',1)[0].replace('protected ','')
run(r'''#include <cassert>
#include <string>
#include <vector>
+3 -3
View File
@@ -79,7 +79,7 @@ class ExternalPackageTests(unittest.TestCase):
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)
self.assertEqual(len(records),len(ext.ALLOWED) 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)
@@ -117,7 +117,7 @@ class ExternalPackageTests(unittest.TestCase):
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)
self.assertTrue(report['profile_addons_unchanged']);self.assertEqual(report['external_addon_count'],len(ext.ALLOWED))
evidence=self.session/'evidence/core'
before=json.loads((evidence/'external-addons-before.json').read_text())
after=json.loads((evidence/'external-addons-after.json').read_text())
@@ -164,7 +164,7 @@ class ExternalPackageTests(unittest.TestCase):
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'])
self.assertEqual(summary['external_addon_count'],6);self.assertTrue(summary['external_addons_unchanged_after_run'])
self.assertEqual(summary['external_addon_count'],len(ext.ALLOWED));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):
+2 -2
View File
@@ -77,7 +77,7 @@ class ExternalPackedTests(unittest.TestCase):
def test_private_copy_exact_six_links_and_schema(self):
unrelated=self.source/'protected';unrelated.mkdir();(unrelated/'addon.gproj').write_text('GameProject {GUID "5F16D7E4A1CBE075"}')
self.prepare();raw=self.snapshot();value=json.loads(raw)
self.assertEqual(value['schema_version'],4);self.assertEqual(value['kind'],'packed-chat-external-runtime-input');self.assertEqual(len(value['external_addons']),6)
self.assertEqual(value['schema_version'],4);self.assertEqual(value['kind'],'packed-chat-external-runtime-input');self.assertEqual(len(value['external_addons']),len(ext.ALLOWED))
self.assertEqual({p.name for p in (self.profile/'addons').iterdir()},{'ArmaReforger',*('ERRI_'+g for g in ext.ALLOWED)})
self.assertEqual(self.seal(raw)['packed_inputs_unchanged_after_run'],True)
self.assertNotEqual((self.candidate/'core/data.pak').stat().st_ino,(self.packed['input']/'core/data.pak').stat().st_ino)
@@ -113,7 +113,7 @@ class ExternalPackedTests(unittest.TestCase):
self.prepare();raw=self.snapshot();(self.session/v4.MANIFEST).write_bytes(raw+b' ')
self.assertFalse(self.seal(raw)['packed_inputs_unchanged_after_run'])
def test_valid_full_nine_mount_database_proof(self):
result=self.evidence();self.assertTrue(result['ready'],result);self.assertFalse(result['release_qualified']);self.assertEqual(result['external_addon_count'],6)
result=self.evidence();self.assertTrue(result['ready'],result);self.assertFalse(result['release_qualified']);self.assertEqual(result['external_addon_count'],len(ext.ALLOWED))
def test_runtime_evidence_rejects_redirects_extra_missing_and_loose(self):
content=self.content();guid=ext.DIRECT[0];name=self.packed['external_names'][guid];folder=self.profile/'addons'/('ERRI_'+guid)
variants=[content.replace(str(folder),'/unreviewed'),content.replace('(pak count: 1) to filesystem under name '+name,'(pak count: 2) to filesystem under name '+name),content+'00 ENGINE : dir: "ignored"\n'+f"00 ENGINE : dir: '{self.source}'\n",content+f"00 ENGINE : FileSystem: Adding relative directory '{folder}/Scripts' to filesystem under name {name}\n",re.sub(r'[^\n]*gproj:[^\n]*'+guid+r'[^\n]*\n','',content),re.sub(r'[^\n]*ResourceDB: loading cache[^\n]*name='+name+r' [^\n]*\n','',content),content.replace('PLATFORM_WINDOWS','PLATFORM_WINDOWS,WORKBENCH')]
+2 -2
View File
@@ -42,8 +42,8 @@ namespace CompartmentAccessComponent{IEntity GetVehicleIn(IEntity a){return a?a-
struct Roster{std::map<int,ERR_PlayerLife> lives;ERR_PlayerLife Get(int id){auto p=lives.find(id);return p==lives.end()?nullptr:p->second;}
'''+roster+r'''
};
struct ERR_GameMode{static ERR_GameMode* current;Roster* roster;int phase=2,faction=1;bool active=true;static ERR_GameMode* Get(){return current;}bool Active(){return active;}int Phase(){return phase;}int PlayerFaction(){return faction;}int PrisonPosition(){return 0;}Roster* Players(){return roster;}};ERR_GameMode* ERR_GameMode::current=nullptr;
struct Placement{int nearCalls=0,prisonCalls=0;IEntity last=nullptr;bool clear=true;bool Near(IEntity a,int,vector& p){nearCalls++;last=a;p=a->origin;return clear;}bool Prison(int,int,vector& p){prisonCalls++;p=0;return true;}};
struct ERR_GameMode{static ERR_GameMode* current;Roster* roster;int phase=2,faction=1;bool active=true;static ERR_GameMode* Get(){return current;}bool Active(){return active;}int Phase(){return phase;}int PlayerFaction(){return faction;}int PrisonPosition(){return 0;}float PrisonHeading(){return 90;}Roster* Players(){return roster;}};ERR_GameMode* ERR_GameMode::current=nullptr;
struct Placement{int nearCalls=0,prisonCalls=0;IEntity last=nullptr;bool clear=true;bool Near(IEntity a,int,vector& p){nearCalls++;last=a;p=a->origin;return clear;}bool Prison(int,int,vector& p,float heading){assert(heading==90);prisonCalls++;p=0;return true;}};
struct Spawn{Placement m_Placement;int m_AuthorizedPositions=0;
'''+spawn+r'''
};
+1 -1
View File
@@ -26,7 +26,7 @@ namespace ERR_Actors {bool Player(Actor* a){return a&&a->player;}bool CanFight(A
namespace SCR_FactionManager {int SGetFaction(Actor* a){return a->faction;}}
struct Hook {std::set<std::string> callbacks;int inserted=0;void Insert(std::string c){callbacks.insert(c);inserted++;}void Remove(std::string c){callbacks.erase(c);}};
struct ERR_GameMode {static ERR_GameMode* current;int phase=0,enemy=2;Hook phases,stops;
static ERR_GameMode* Get(){return current;}int Phase(){return phase;}int EnemyFaction(){return enemy;}
static ERR_GameMode* Get(){return current;}int Phase(){return phase;}int EnemyFaction(){return enemy;}bool IsMissionEnemy(int f){return enemy&&f>=enemy;}
Hook* OnPhaseChanged(){return &phases;}Hook* OnStop(){return &stops;}};
ERR_GameMode* ERR_GameMode::current=nullptr;
struct Agent {ERR_GameMode* m_ERRPrisonMode=nullptr;Actor* actor=nullptr;bool pending=false,active=true;int starts=0;
+37 -1
View File
@@ -4,7 +4,7 @@ This verifies bounded search order/termination, not terrain or body clearance.
from policy_harness import ROOT,body,run
source=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnPlacement.c'
search=(body(source,'bool Prison(').replace('map<int, vector> reservations','map<int, vector>* reservations')
.replace('out vector position','vector& position').replace('ERR_Actors.', 'ERR_Actors::').replace('Math.', 'Math::'))
.replace('out vector position','vector& position').replace('ERR_Actors.', 'ERR_Actors::').replace('Math.', 'Math::').replace('ERR_CampTerrain.', 'ERR_CampTerrain::'))
run('''#include <cassert>
#include <cmath>
#include <vector>
@@ -16,6 +16,7 @@ vector Vector(float x,float y,float z){return {x,y,z};}
template<class K,class V> struct map {};
namespace ERR_Actors {vector Ground(vector value){return value;}}
namespace Math {int AbsInt(int value){return std::abs(value);}}
namespace ERR_CampTerrain {vector Rotate(vector v,float a){float r=a*3.14159265359f/180;return {v.x*std::cos(r)+v.z*std::sin(r),v.y,-v.x*std::sin(r)+v.z*std::cos(r)};}}
struct Search {
std::vector<vector> visited;
int accept=-1;
@@ -23,6 +24,7 @@ struct Search {
''' + search + '''
};
int main(){
Search rotated;rotated.accept=0;vector turned;assert(rotated.Prison(Vector(0,0,0),nullptr,turned,90));assert(std::abs(turned.x+1.5)<.001&&std::abs(turned.z-1.5)<.001);
const vector center={6090,100,7160};
vector result={-1,-1,-1};
Search blocked;
@@ -57,3 +59,37 @@ int main(){
}
''')
print('PASS: prison search packs 16 unique 3m-spaced inside-wall centers, prioritizes inner/edge positions and never bypasses clearance')
occupancy = (body(source, 'protected bool Occupied(').replace('protected ', '')
.replace('IEntity entity', 'IEntity* entity').replace('entity.', 'entity->')
.replace('ChimeraCharacter.Cast', 'ChimeraCharacter::Cast')
.replace('vector.', 'vector::').replace('Math.', 'Math::'))
run(r'''
#include <cassert>
#include <cmath>
#include <initializer_list>
struct vector {
float x=0,y=0,z=0;
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);}
};
struct IEntity {vector position;bool character=true;vector GetOrigin(){return position;}};
namespace ChimeraCharacter {bool Cast(IEntity* e){return e&&e->character;}}
namespace Math {float AbsFloat(float f){return std::abs(f);}}
struct Placement {
IEntity* m_Exclude=nullptr; vector m_Candidate; bool m_Blocked=false;
''' + occupancy + r'''
};
int main(){
IEntity body;
for(float distance : {0.f,1.199f,1.2f,2.12f,3.f}) {
Placement p;body.position={distance,0,0};
assert(p.Occupied(&body)==(distance>=1.2f));
}
Placement vertical;body.position={0,2,0};assert(vertical.Occupied(&body));
Placement excluded;excluded.m_Exclude=&body;body.position={0,0,0};assert(excluded.Occupied(&body));
Placement scenery;body.character=false;assert(scenery.Occupied(&body)); // geometry uses the separate native trace
assert(scenery.Occupied(nullptr));
}
''')
print('PASS: physical body occupancy rejects overlap, permits diagonal neighboring slots, and excludes own body and unrelated broad-phase bounds')
+47
View File
@@ -0,0 +1,47 @@
"""Exercise concrete bounds, saved modes, calendar mapping and daylight edges."""
from datetime import date, timedelta
from policy_harness import ROOT, body, run
core = ROOT/'addons/core/Scripts/Game/ERR/00_Core'
rules = '\n'.join(body(core/file, sig) for file, sig in (
('ERR_Settings.c', 'static bool Valid('),
('ERR_Settings.c', 'static bool Randomizable('),
('ERR_Settings.c', 'static bool ValidSelection('),
('ERR_Settings.c', 'static bool RandomBounds('),
('ERR_EnvironmentService.c', 'static int DateFromOrdinal('),
('ERR_TimeService.c', 'static bool DaylightBounds('),
))
rules = rules.replace('string ', 'std::string ').replace('out int ', 'int& ').replace('Math.Floor', 'std::floor').replace('Math.Ceil', 'std::ceil')
calendar = []
for year in (1900, 1989, 2000, 2024, 2050):
first = date(year, 1, 1)
days = (date(year+1,1,1)-first).days
for ordinal in range(days):
day = first+timedelta(days=ordinal)
code = (year-1900)*10000+day.month*100+day.day
calendar.append(f'assert(DateFromOrdinal({year},{ordinal})=={code});')
calendar.append(f'assert(DateFromOrdinal({year},{days})==-1);')
run('#include <cassert>\n#include <string>\n#include <cmath>\n' + rules + r"""
int main() {
int lo=-1,hi=-1;
for (std::string key:{"reportIntervalSeconds","supportSquadLimit","responseRadiusMetres","aiIntelChancePercent","intelRevealChancePercent","radioDefenceSeconds","startTimeMinutes","timeMultiplier","startDateCode","weatherProfile","convoyCount","heliTimerMinutes"}) {
assert(ValidSelection(key,-1)); assert(!Valid(key,-1));
assert(ValidSelection(key,-2)==(key=="startTimeMinutes")); assert(!ValidSelection(key,-3));
if(key=="startDateCode")continue;
assert(RandomBounds(key,lo,hi));
assert(lo<=hi);
for(int value=lo;value<=hi;value++) assert(Valid(key,value));
}
for(std::string key:{"unknown","","stingerChanceOneIn","automaticConvoyEnabled"}) assert(!ValidSelection(key,-1));
assert(RandomBounds("convoyCount",lo,hi)&&lo==0&&hi==20);
assert(RandomBounds("weatherProfile",lo,hi)&&lo==1&&hi==3);
assert(RandomBounds("startTimeMinutes",lo,hi)&&lo==0&&hi==1439);
assert(DaylightBounds(6.0,18.0,lo,hi)&&lo==361&&hi==1080);
assert(DaylightBounds(6.125,18.125,lo,hi)&&lo==368&&hi==1088);
for(int minute=lo;minute<hi;minute++) assert(minute/60.0>6.125&&minute/60.0<18.125);
assert(!DaylightBounds(18,6,lo,hi)); assert(!DaylightBounds(12,12,lo,hi));
assert(!DaylightBounds(-1,18,lo,hi));assert(!DaylightBounds(6,25,lo,hi));
assert(!DaylightBounds(6.0,6.001,lo,hi));
assert(DaylightBounds(0,24,lo,hi)&&lo==1&&hi==1440);
assert(DateFromOrdinal(1899,0)==-1&&DateFromOrdinal(2051,0)==-1&&DateFromOrdinal(2000,-1)==-1);
""" + ''.join(calendar) + '}')
print('PASS: random mode restrictions, full numeric ranges, independent calendar dates and strict daylight boundaries')
+2 -2
View File
@@ -46,8 +46,8 @@ int main(){
assert(has(unknown,"Choose either marked supply cache")&&!has(unknown,"Intel recovered"));
radio.knowledge=1;
string rough=Actual::PhaseDescription(&mode,true);
assert(rough!=unknown&&has(rough,"Intel recovered")&&has(rough,"500 m search square"));
assert(has(rough,"carried field map")&&has(rough,"different intelligence folder"));
assert(rough!=unknown&&has(rough,"Intel recovered")&&has(rough,"question mark"));
assert(!has(rough,"search square")&&has(rough,"different intelligence folder"));
assert(has(rough,"already recovered cannot provide another clue"));
assert(Actual::PhaseDescription(&mode,false)==rough); // Same task and guide copy.
for(int i=0;i<10;i++) assert(Actual::PhaseDescription(&mode,true)==rough);
+79
View File
@@ -0,0 +1,79 @@
"""Execute production hill/road geometry and finite admission state machine."""
import re
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ScoutSystem.c'
hill=body(p,'static bool Hill(')
road=body(p,'static bool OnSegment(')
prepare=body(p,'ERR_PrepareResult Prepare(')
for name in ('Math','vector','ERR_PrepareResult','Replication'):
hill=hill.replace(name+'.',name+'::');road=road.replace(name+'.',name+'::');prepare=prepare.replace(name+'.',name+'::')
prepare=prepare.replace('ERR_ScenarioDefinition definition','ERR_ScenarioDefinition* definition').replace('foreach (ERR_ScoutPost post : m_Posts)','for (ERR_ScoutPost* post : m_Posts)').replace('post.','post->').replace('bool pending;','bool pending=false;').replace('GetGame().GetWorld().','clock.').replace('GetGame().GetWorldEntity().','world.').replace('System.GetTickCount()','0')
motion=body(p,'static void Estimate(').replace('ERR_HQReport report','ERR_HQReport* report').replace('ERR_HQReport previous','ERR_HQReport* previous')
for name in ('report','previous'): motion=motion.replace(name+'.',name+'->')
for name in ('vector','ERR_HQMemoryRules'): motion=motion.replace(name+'.',name+'::')
memory=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMemory.c'
credible=body(memory,'static float SpeedLimit(')+'\n'+body(memory,'static bool MotionCredible(')
credible=credible.replace('string ', 'std::string ')
run(r'''
#include <cassert>
#include <algorithm>
#include <cmath>
#include <vector>
#include <string>
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);}static float AbsFloat(float n){return std::abs(n);}};
struct vector{float v[3]={};vector()=default;vector(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];}vector operator-(vector b)const{return{v[0]-b[0],v[1]-b[1],v[2]-b[2]};}vector operator/(float f)const{return{v[0]/f,v[1]/f,v[2]/f};}static const vector Zero;static float Dot(vector a,vector b){return a[0]*b[0]+a[1]*b[1]+a[2]*b[2];}static vector Lerp(vector a,vector b,float f){return{a[0]+(b[0]-a[0])*f,a[1]+(b[1]-a[1])*f,a[2]+(b[2]-a[2])*f};}static float DistanceXZ(vector a,vector b){return std::hypot(a[0]-b[0],a[2]-b[2]);}};
const vector vector::Zero{};
'''+hill+'\n'+road+r'''
struct ERR_HQReport{void* Target=nullptr;void* Source=nullptr;bool Distress=false,HasVelocity=false;vector Position,Velocity;float ObservedAt=0;std::string TravelMode="on foot";};
struct ERR_HQMemoryRules{static bool Finite(float n){return std::isfinite(n);}
'''+credible+r'''};
'''+motion+r'''
enum class ERR_PrepareResult{PENDING,READY,FAILED};
struct Replication{static bool server;static bool IsServer(){return server;}};bool Replication::server=true;
struct ERR_ScenarioDefinition{};struct ERR_ScoutPost{bool Ready=false,Group=false;};
struct Clock{float now=0;float GetWorldTime(){return now;}};
struct World{void GetWorldBounds(vector&,vector&) {}};
struct Population{
bool m_Stopped=false,m_Ready=false,m_Begun=false,m_Surveyed=false;ERR_ScenarioDefinition*m_Definition=nullptr;int m_Started=0,spawns=0,stocks=0,surveys=0;float m_Deadline=10;vector m_Low,m_High;Clock clock;World world;
std::vector<ERR_ScoutPost*>m_Posts;
ERR_PrepareResult survey=ERR_PrepareResult::READY,stock=ERR_PrepareResult::READY;
ERR_PrepareResult Survey(){surveys++;m_Surveyed=survey==ERR_PrepareResult::READY;return survey;}
ERR_PrepareResult Fail(const char*){m_Stopped=true;return ERR_PrepareResult::FAILED;}
bool Spawn(ERR_ScoutPost*p){spawns++;p->Group=true;return true;}
ERR_PrepareResult Stock(ERR_ScoutPost*p){stocks++;p->Ready=stock==ERR_PrepareResult::READY;return stock;}
'''+prepare+r'''
};
int main(){
assert(Hill(90,200,6,4));assert(!Hill(34,100,30,8));assert(!Hill(35,200,30,8));assert(!Hill(100,200,5.9,8));assert(!Hill(100,200,30,3));
vector a{0,10,0},b{100,20,0};
assert(OnSegment({50,15,2},a,b,4));assert(!OnSegment({50,15,2.01},a,b,4));
assert(!OnSegment({50,25,0},a,b,4)); // aircraft/overpass != ground road
assert(!OnSegment({105,20,0},a,b,4)); // no infinite extension past end
assert(!OnSegment({50,15,0},a,a,4));assert(!OnSegment({50,15,0},a,b,0));
// A bend is tested as its real segments, not a diagonal shortcut.
assert(!OnSegment({50,10,50},{0,10,0},{100,10,0},4));assert(!OnSegment({50,10,50},{100,10,0},{100,10,100},4));
ERR_HQReport previous,current;previous.Target=&previous;current.Target=&previous;previous.Source=&current;current.Source=&current;
previous.ObservedAt=1000;previous.Position={0,0,0};current.ObservedAt=11000;current.Position={20,5,30};
Estimate(&current,&previous);assert(current.HasVelocity&&current.Velocity[0]==2&&current.Velocity[1]==0&&current.Velocity[2]==3);
current.Target=&current;Estimate(&current,&previous);assert(!current.HasVelocity);current.Target=&previous;
current.Source=nullptr;Estimate(&current,&previous);assert(!current.HasVelocity);current.Source=&current;
current.ObservedAt=1000;Estimate(&current,&previous);assert(!current.HasVelocity);
current.ObservedAt=33000;Estimate(&current,&previous);assert(!current.HasVelocity);
current.ObservedAt=11000;current.Position={500,0,0};Estimate(&current,&previous);assert(!current.HasVelocity);
current.TravelMode="in vehicle";Estimate(&current,&previous);assert(!current.HasVelocity);
previous.TravelMode="in vehicle";Estimate(&current,&previous);assert(current.HasVelocity&&current.Velocity[0]==50);
previous.Distress=true;Estimate(&current,&previous);assert(!current.HasVelocity);
ERR_ScenarioDefinition def;Population pop;ERR_ScoutPost first,second;pop.m_Posts={&first,&second};
pop.survey=ERR_PrepareResult::PENDING;assert(pop.Prepare(&def)==ERR_PrepareResult::PENDING&&pop.spawns==0);
pop.survey=ERR_PrepareResult::READY;assert(pop.Prepare(&def)==ERR_PrepareResult::PENDING&&pop.spawns==1);
pop.stock=ERR_PrepareResult::PENDING;assert(pop.Prepare(&def)==ERR_PrepareResult::PENDING&&pop.spawns==2);
assert(pop.Prepare(&def)==ERR_PrepareResult::PENDING&&pop.spawns==2);
pop.stock=ERR_PrepareResult::READY;assert(pop.Prepare(&def)==ERR_PrepareResult::READY);
int stocks=pop.stocks;first.Group=false;first.Ready=false;
assert(pop.Prepare(&def)==ERR_PrepareResult::READY&&pop.spawns==2&&pop.stocks==stocks); // dead original never replaced/refilled
Population timeout;timeout.m_Begun=true;timeout.m_Surveyed=true;timeout.clock.now=11;assert(timeout.Prepare(&def)==ERR_PrepareResult::FAILED&&timeout.spawns==0);
Population stopped;stopped.m_Stopped=true;assert(stopped.Prepare(&def)==ERR_PrepareResult::FAILED&&stopped.surveys==0);
Replication::server=false;Population client;assert(client.Prepare(&def)==ERR_PrepareResult::FAILED&&client.surveys==0);
}
''')
print('PASS: hill prominence/elevation, actual road edges/heights/bends and server-only finite scout admission, pending stock, timeout and no replacements/refills')
+4 -4
View File
@@ -5,12 +5,12 @@ from policy_harness import ROOT,body,run
spec=importlib.util.spec_from_file_location('import_settings',BASE/'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,stingerChanceOneIn=15,automaticConvoyEnabled=1)
assert deferred==['ai:convoyCount=3 mapped to automaticConvoyEnabled=1; successor capped at one initial convoy']
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,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
assert deferred==[]
for count in (0,1,3,20):
values,notes=m.merge(ai={'convoyCount':count})
assert values['automaticConvoyEnabled']==int(count>0)
assert bool(notes)==(count>1)
assert values['convoyCount']==count and not notes
for invalid in (-1,21,True,False,None,'1',1.5,float('nan'),float('inf')):
try:m.merge(ai={'convoyCount':invalid})
except ValueError:pass
@@ -38,7 +38,7 @@ with tempfile.TemporaryDirectory() as tmp:
legacy=Path(tmp)/'legacy-ai.json';legacy.write_text('{"convoyCount":3}');original=legacy.read_bytes()
preview=subprocess.run([sys.executable,str(BASE/'tools/import_settings.py'),'--ai',str(legacy)],capture_output=True,text=True,check=True)
assert json.loads(preview.stdout)['automaticConvoyEnabled']==1
assert 'successor capped at one initial convoy' in preview.stderr and legacy.read_bytes()==original
assert json.loads(preview.stdout)['convoyCount']==3 and not preview.stderr and legacy.read_bytes()==original
# Test the actual runtime validator against the migration schema at each boundary.
validator=body(BASE/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c','static bool Valid(').replace('string key','std::string key')
+2 -1
View File
@@ -6,7 +6,7 @@ methods = '\n'.join(body(source, sig) for sig in (
'static float RadioRadiusLimit(', 'static bool ValidRadius(', 'static bool InRange('
)).replace('ERR_LocationKind.', 'ERR_LocationKind::')
run('''#include <cassert>
enum class ERR_LocationKind { PRISON, RADIO, EXTRACTION, OUTPOST, DEPOT };
enum class ERR_LocationKind { PRISON, RADIO, EXTRACTION, OUTPOST, DEPOT, MORTAR };
''' + methods.replace('int kind,', 'ERR_LocationKind kind,') + '''
int main() {
using K=ERR_LocationKind;
@@ -14,6 +14,7 @@ int main() {
assert(!ValidRadius(K::RADIO,8.01f) && !ValidRadius(K::RADIO,-1));
assert(ValidRadius(K::DEPOT,60) && ValidRadius(K::OUTPOST,60));
assert(!ValidRadius(K::DEPOT,60.01f));
assert(ValidRadius(K::MORTAR,35) && !ValidRadius(K::MORTAR,61));
assert(ValidRadius(K::PRISON,0) && ValidRadius(K::EXTRACTION,0));
assert(!ValidRadius(K::PRISON,1) && !ValidRadius(K::EXTRACTION,1));
assert(!ValidRadius(static_cast<K>(42),0));
+251
View File
@@ -0,0 +1,251 @@
"""Execute production supply-circuit and combat timing policies with native boundaries doubled."""
from policy_harness import ROOT, body, run
import re
D=ROOT/'addons/core/Scripts/Game/ERR/10_AI'
def cpp(s):
s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('override ','').replace('foreach (','for (').replace('null','nullptr')
s=s.replace('m_Task.', 'm_Task->').replace('m_Mode.', 'm_Mode->').replace('m_Service.', 'm_Service->')
for typ in ('ERR_SupplyConvoyTask','ERR_ConvoyVehicle','ERR_SupplyTeam','ERR_ForceRoute','IEntity'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for obj in ('task','vehicle','departure','team','lost','boarding','stranded','leg','infantryTeam','member','delayed'):
s=s.replace(obj+'.',obj+'->')
for cls in ('Math','ERR_SupplyConvoyRules','ERR_HQ','ERR_ForceRules','ERR_Actors','vector','string'):
s=s.replace(cls+'.',cls+'::')
s=s.replace('bool surviving, pending;','bool surviving=false, pending=false;')
s=s.replace('float nearest = 400; bool found;', 'float nearest = 400; bool found=false;')
s=s.replace('m_Support[released].', 'm_Support[released]->').replace('Vehicles[released].', 'Vehicles[released]->').replace('infantryTeam->Group.', 'infantryTeam->Group->')
s=s.replace('->Receipt.', '->Receipt->').replace('Vehicles[2].', 'Vehicles[2]->')
s=s.replace('.Entity.', '.Entity->').replace('->Entity.', '->Entity->')
return s
common=r'''
#include <cassert>
#include <cmath>
#include <limits>
#include <vector>
#include <string>
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}template<class...T>static string Format(T...){return "event";}};
struct vector{float x=0;static float Distance(vector a,vector b){return std::abs(a.x-b.x);}static float DistanceXZ(vector a,vector b){return Distance(a,b);}};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}};
namespace Math{int RandomInt(int low,int){return low;}float RandomFloat(float low,float){return low;}}
namespace ERR_HQ{void Event(string,string){}}
namespace ERR_ForceRules{bool Finite(float v){return std::isfinite(v);}bool FiniteVector(vector v){return Finite(v.x);}}
'''
next_base=cpp(body(D/'ERR_SupplyConvoySystem.c','static int NextBase('))
run(common+'struct ERR_SupplyConvoyRules{'+next_base+r'''};
struct Entity{vector position;vector GetOrigin(){return position;}};
struct ERR_ForceRoute{Array<vector> points;void Add(vector p){points.push_back(p);}void Seal(){}};
struct Receipt{string SourceId="test";};
struct ERR_ConvoyVehicle{bool Released=false,Arrived=false;int cleared=0,RouteIndex=3;struct Entity entity;struct Entity* Entity=&entity;
void ClearOrder(){cleared++;}void Tick(void*,ERR_ForceRoute*,float){}};
struct ERR_SupplyConvoyTask{Array<ERR_ConvoyVehicle*> Vehicles;float TravelDeadline=1000,Departure=0;bool Waiting=false,stopped=false;int LastBase=-1,Legs=0;
Array<vector> Bases={{0},{1000},{2000}};ERR_ForceRoute* Route=nullptr;struct Receipt receipt;struct Receipt* Receipt=&receipt;
void Stop(void*,string){stopped=true;}};
struct Service{void* m_Mode=nullptr;
'''+cpp(body(D/'ERR_SupplyConvoyService.c','protected void Circuit('))+r'''};
int main(){
for(int last=0;last<5;last++)for(int roll=0;roll<4;roll++){int next=ERR_SupplyConvoyRules::NextBase(5,last,roll);assert(next>=0&&next<5&&next!=last);}
assert(ERR_SupplyConvoyRules::NextBase(1,0,0)==-1);assert(ERR_SupplyConvoyRules::NextBase(3,3,0)==-1);
Service s;ERR_SupplyConvoyTask t;ERR_ConvoyVehicle a,b;t.Vehicles={&a,&b};
s.Circuit(&t,999);assert(!t.Waiting&&!a.Arrived);s.Circuit(&t,1000);assert(a.Arrived&&b.Arrived&&t.Waiting&&t.Departure==46000);
s.Circuit(&t,45999);assert(t.Legs==0);s.Circuit(&t,46000);assert(t.Legs==1&&!t.Waiting&&!a.Arrived&&!b.Arrived&&t.LastBase==0&&t.TravelDeadline==646000);
a.entity.position={250};b.entity.position={251};s.Circuit(&t,47000);assert(a.Arrived&&!b.Arrived&&!t.Waiting);
b.entity.position={249};s.Circuit(&t,48000);assert(t.Waiting);s.Circuit(&t,93000);assert(t.LastBase==1&&t.Legs==2);
a.Released=true;b.Released=true;s.Circuit(&t,94000);assert(t.stopped); // no replacements
}
''')
run(common+r'''
struct Group{int GetFaction(){return 1;}};struct IEntity{};struct Mode{int EnemyFaction(){return 1;}};
namespace ERR_Actors{bool Alive(IEntity*){return true;}struct Group* Group(IEntity*){return nullptr;}}
struct ERR_ConvoyVehicle{bool Released=false;void* Group=nullptr;void* Entity=nullptr;};
struct ERR_SupplyTeam{bool Released=false,aboard=true,living=true;struct Group nativeGroup;struct Group* Group=&nativeGroup;Array<IEntity*> Members;int orders=0;void Clear(){}
bool Aboard(void*,bool required){return aboard==required;}bool Living(){return living;}void Release(void*){Released=true;}void Add(int){orders++;}};
struct Receipt{string SourceId="test";};
struct ERR_SupplyConvoyTask{Array<ERR_ConvoyVehicle*> Vehicles;Array<ERR_SupplyTeam*> Teams;struct Receipt receipt;struct Receipt* Receipt=&receipt;};
struct Tactics{ERR_SupplyConvoyTask* m_Task;Mode mode;Mode* m_Mode=&mode;Array<ERR_SupplyTeam*>m_Support;bool m_Stopped=false,m_TruckWithdrawn=false,seeing=false,truck=true;
int m_State=0,starts=0,reorders=0,withdraws=0,finishes=0,m_DismountAttempts=0,flanks=0;float m_NextDismount=0;void Flank(int,bool){flanks++;}float m_LastContact=-1,m_NextOrders=0,m_BoardDeadline=0;vector m_Contact;
bool Observe(void*,vector& p,float&){p={100};return seeing;}void Start(vector p,float){m_Contact=p;m_State=1;m_TruckWithdrawn=false;starts++;}
void Orders(bool,float){reorders++;}bool InfantryAboard(bool required){for(auto t:m_Task->Teams)if(!t->Released&&!t->Aboard(nullptr,required))return false;return true;}
void Support(int i,bool hold){if(i==2&&!hold)withdraws++;}bool Usable(int){return truck;}void Clear(){}int Board(void*,bool){return 1;}
void Finish(float){m_State=0;finishes++;}
'''+cpp(body(D/'ERR_SupplyConvoyTactics.c','void Report('))+cpp(body(D/'ERR_SupplyConvoyTactics.c','bool Tick('))+r'''};
int main(){
ERR_SupplyConvoyTask task;ERR_ConvoyVehicle driver;task.Vehicles={&driver,&driver,&driver};ERR_SupplyTeam left,right;task.Teams={&left,&right};Tactics t{&task};
t.Report({100},std::numeric_limits<float>::quiet_NaN(),0);assert(t.starts==0);
t.Report({100},-1,0);t.Report({100},10,0);t.Report({100},0,60001);assert(t.starts==0);
t.Report({100},1000,1000);assert(t.starts==1&&t.m_State==1);t.Report({0},999,1001);assert(t.m_LastContact==1000);
assert(t.Tick(60999)&&t.m_State==1&&t.withdraws==0);left.aboard=right.aboard=false;
assert(t.Tick(61000)&&t.m_State==2&&t.withdraws==1&&left.orders==1&&right.orders==1&&t.m_BoardDeadline==181000);
assert(t.Tick(180999));left.aboard=true;assert(!t.Tick(181000)&&t.finishes==1&&!left.Released&&right.Released);
right.Released=false;t.Report({100},200000,200000);t.Tick(260000);assert(t.m_State==2);
t.seeing=true;t.Tick(260001);assert(t.m_State==1&&t.starts==3); // fresh contact interrupts reboarding
t.seeing=false;t.truck=false;t.Tick(320001);assert(t.m_State==0&&left.Released&&right.Released); // truck lost: release surviving infantry
t.m_Stopped=true;t.Report({100},400000,400000);assert(t.starts==3&&!t.Tick(400000));
left.Released=right.Released=false;left.aboard=right.aboard=true;Tactics retry{&task};retry.Report({100},0,0);
for(int i=1;i<=9;i++){retry.Report({100},10000*i,10000*i);retry.Tick(10000*i);}
assert(retry.m_DismountAttempts==6&&retry.flanks==12); // six attempts per still-mounted team, no infinite retry
}
''')
startup=cpp(body(D/'ERR_SupplyConvoySystem.c','void Tick('))
run(common+r'''
struct Service{int count=0;int Count(){return count;}};
struct Startup{Service* m_Service;bool m_Stopped=false,m_Automatic=true,create=false;int m_Target=3,m_Attempts=0,calls=0;
float m_Began=-1,m_Next=0;string m_Reason;
bool StartOne(){calls++;if(create)m_Service->count++;return create;}
'''+startup+r'''};
int main(){
Service service;Startup s{&service};s.Tick(1000,true);s.Tick(30999,true);assert(!s.calls);s.Tick(31000,false);assert(!s.calls);s.Tick(36000,true);assert(s.calls==1);
s.create=true;s.Tick(41000,true);s.Tick(46000,true);s.Tick(51000,true);assert(service.count==3&&s.calls==4);s.Tick(56000,true);assert(s.calls==4);
Service noRoad;Startup fail{&noRoad};fail.Tick(0,true);for(int i=0;i<200;i++)fail.Tick(30000+5000*i,true);assert(fail.calls==120&&fail.m_Attempts==120);
Startup off{&noRoad};off.m_Target=0;off.Tick(0,true);off.Tick(30000,true);assert(!off.calls);
off.m_Target=3;off.m_Automatic=false;off.Tick(90000,true);assert(!off.calls);
}
''')
retain=body(D/'ERR_SupplyConvoyTactics.c','static bool Retain(')
retain=retain.replace('IEntity actor','IEntity* actor').replace('IEntity vehicle','IEntity* vehicle')
retain=retain.replace('SCR_VehicleDamageManagerComponent damage = SCR_VehicleDamageManagerComponent.Cast(vehicle.FindComponent(SCR_VehicleDamageManagerComponent));','SCR_VehicleDamageManagerComponent* damage = vehicle->damage;')
for name in ('ERR_Actors','CompartmentAccessComponent','EDamageState'):retain=retain.replace(name+'.',name+'::')
retain=retain.replace('damage.', 'damage->')
run(r"""
#include <cassert>
#include <map>
enum class EDamageState{UNDAMAGED,DAMAGED,DESTROYED};
struct SCR_VehicleDamageManagerComponent{float health=1;bool engine=true,gear=true;EDamageState state=EDamageState::UNDAMAGED;float GetHealthScaled(){return health;}bool GetEngineFunctional(){return engine;}bool GetGearboxFunctional(){return gear;}EDamageState GetState(){return state;}};
struct IEntity{bool player=false,fit=true;IEntity* vehicle=nullptr;SCR_VehicleDamageManagerComponent* damage=nullptr;};
namespace ERR_Actors{bool Player(IEntity* a){return a->player;}bool CanFight(IEntity* a){return a->fit;}}
namespace CompartmentAccessComponent{IEntity* GetVehicleIn(IEntity* a){return a->vehicle;}}
struct Map:std::map<IEntity*,IEntity*>{bool Contains(IEntity* a){return count(a);}IEntity* Get(IEntity* a){return at(a);}}s_Crews;
"""+retain+r"""
int main(){
IEntity driver,truck,other;SCR_VehicleDamageManagerComponent health;truck.damage=&health;driver.vehicle=&truck;
assert(!Retain(&driver,&truck));s_Crews[&driver]=&truck;assert(Retain(&driver,&truck));assert(!Retain(&driver,&other));
driver.player=true;assert(!Retain(&driver,&truck));driver.player=false;driver.fit=false;assert(!Retain(&driver,&truck));driver.fit=true;
health.state=EDamageState::DAMAGED;health.health=.9;assert(Retain(&driver,&truck));health.health=.25;assert(!Retain(&driver,&truck));health.health=1;
health.engine=false;assert(!Retain(&driver,&truck));health.engine=true;health.gear=false;assert(!Retain(&driver,&truck));health.gear=true;
health.state=EDamageState::DESTROYED;assert(!Retain(&driver,&truck));health.state=EDamageState::UNDAMAGED;driver.vehicle=nullptr;assert(!Retain(&driver,&truck));
}
""")
boarding=body(D/'ERR_SupplyConvoyTactics.c','static AIWaypoint Board(')
boarding=boarding.replace('AIWaypoint Board','AIWaypoint* Board').replace('Vehicle truck','Vehicle* truck').replace('truck.', 'truck->').replace('waypoint.', 'waypoint->').replace('specific.', 'specific->').replace('null','nullptr')
boarding=boarding.replace('SCR_BoardingWaypoint waypoint','SCR_BoardingWaypoint* waypoint').replace('SCR_BoardingEntityWaypoint specific','SCR_BoardingEntityWaypoint* specific')
boarding=boarding.replace('SCR_BoardingWaypoint.Cast(', 'dynamic_cast<SCR_BoardingWaypoint*>(').replace('SCR_BoardingEntityWaypoint.Cast(', 'dynamic_cast<SCR_BoardingEntityWaypoint*>(').replace('ERR_Actors.Spawn','ERR_Actors::Spawn').replace('EAIWaypointCompletionType.All','EAIWaypointCompletionType::All')
run(r"""
#include <cassert>
#include <string>
using ResourceName=std::string;
enum class EAIWaypointCompletionType{Leader,All};
struct Vehicle{int GetOrigin(){return 7;}};
struct AIWaypoint{virtual ~AIWaypoint(){}int radius=9;EAIWaypointCompletionType completion=EAIWaypointCompletionType::Leader;void SetCompletionRadius(int n){radius=n;}void SetCompletionType(EAIWaypointCompletionType type){completion=type;}};
struct SCR_BoardingWaypoint:AIWaypoint{bool driver=true,gunner=true,cargo=false;void SetAllowance(bool d,bool g,bool c){driver=d;gunner=g;cargo=c;}};
struct SCR_BoardingEntityWaypoint:SCR_BoardingWaypoint{Vehicle* vehicle=nullptr;void SetEntity(Vehicle* v){vehicle=v;}};
namespace ERR_Actors{bool fail=false;AIWaypoint* Spawn(ResourceName resource,int){if(fail)return nullptr;if(resource.find("GetIn.et")!=std::string::npos)return new SCR_BoardingEntityWaypoint();return new SCR_BoardingWaypoint();}}
"""+boarding+r"""
int main(){
Vehicle truck;assert(!Board(nullptr,false));
auto out=dynamic_cast<SCR_BoardingWaypoint*>(Board(&truck,false));assert(out&&out->completion==EAIWaypointCompletionType::All&&out->radius==100&&!out->driver&&!out->gunner&&out->cargo);
auto in=dynamic_cast<SCR_BoardingEntityWaypoint*>(Board(&truck,true));assert(in&&in->radius==100&&in->completion==EAIWaypointCompletionType::All&&in->vehicle==&truck&&!in->driver&&!in->gunner&&in->cargo);
delete out;delete in;ERR_Actors::fail=true;assert(!Board(&truck,true));
}
""")
delivery=body(D/'ERR_SupplyConvoyService.c','override bool ApplyCombatOrder(')
delivery=delivery.replace('override ','').replace('ERR_SupplyConvoyTask.Cast(task)','dynamic_cast<ERR_SupplyConvoyTask*>(task)')
for typ in ('ERR_ConvoyTask','ERR_SupplyConvoyTask','ERR_ConvoyCombatOrder'):delivery=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',delivery)
for name in ('supply','order'):delivery=delivery.replace(name+'.',name+'->')
delivery=delivery.replace('Tactics.','Tactics->').replace('super.ApplyCombatOrder','Base::ApplyCombatOrder')
run(r"""
#include <cassert>
struct ERR_ConvoyTask{virtual ~ERR_ConvoyTask(){}};
struct ERR_ConvoyCombatOrder{bool Hold=true;int Position=3;float ObservedAt=100;};
struct Tactics{int state=0,reports=0;int State(){return state;}void Report(int,float,float){reports++;}};
struct ERR_SupplyConvoyTask:ERR_ConvoyTask{struct Tactics tactics;struct Tactics* Tactics=&tactics;};
struct Base{bool accept=true;int applied=0;bool ApplyCombatOrder(ERR_ConvoyTask*,ERR_ConvoyCombatOrder*,float){applied++;return accept;}};
struct Service:Base{
"""+delivery+r"""
};
int main(){
Service service;ERR_SupplyConvoyTask task;ERR_ConvoyCombatOrder order;
for(int state=0;state<3;state++){task.tactics.state=state;assert(service.ApplyCombatOrder(&task,&order,100));}
assert(task.tactics.reports==3);order.Hold=false;assert(!service.ApplyCombatOrder(&task,&order,100));assert(service.applied==3);
task.tactics.state=0;assert(service.ApplyCombatOrder(&task,&order,100));assert(task.tactics.reports==3);
service.accept=false;order.Hold=true;assert(!service.ApplyCombatOrder(&task,&order,100)&&task.tactics.reports==3);
assert(!service.ApplyCombatOrder(nullptr,&order,100));assert(!service.ApplyCombatOrder(&task,nullptr,100));
}
""")
print('PASS: actual supply route cycling/arrival/stops, finite startup bounds, contact freshness, dismount hold, quiet interval, interrupted boarding, timeout and lost-truck release')
# Mounted groups retain their native spawn origin; sighting distance must follow vehicles.
distance=cpp(body(D/'ERR_SupplyConvoyTactics.c','protected float DistanceToConvoy(')).replace('Math.Min','Math::Min')
observe=cpp(body(D/'ERR_SupplyConvoyTactics.c','protected bool Observe('))
for typ in ('SCR_AIGroup','SCR_AIGroupPerception','PerceptionManager','SCR_AITargetInfo'):
observe=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',observe)
for obj in ('group','perception','manager','target'):observe=observe.replace(obj+'.',obj+'->')
observe=observe.replace('out vector position','vector& position').replace('inout float nearest','float& nearest').replace('bool found;','bool found=false;')
observe=observe.replace('GetGroupUtilityComponent().','GetGroupUtilityComponent()->').replace('GetFaction().','GetFaction()->').replace('GetGame().','GetGame()->').replace('EAITargetInfoCategory.','EAITargetInfoCategory::')
run(common+r"""
namespace Math{float Min(float a,float b){return a<b?a:b;}}
struct Entity{vector position;vector GetOrigin(){return position;}};
struct ERR_ConvoyVehicle{bool Released=false;struct Entity* Entity=nullptr;};
struct ERR_SupplyConvoyTask{Array<ERR_ConvoyVehicle*> Vehicles;};
struct Faction{bool hostile=true;bool IsFactionEnemy(Faction* other){return other->hostile;}};
enum class EAITargetInfoCategory{IDENTIFIED,SUSPECTED};
struct SCR_AITargetInfo{void* m_Entity=this;Faction* m_Faction=nullptr;float m_fTimestamp=10;EAITargetInfoCategory m_eCategory=EAITargetInfoCategory::IDENTIFIED;vector m_vWorldPos={5100};};
struct SCR_AIGroupPerception{Array<SCR_AITargetInfo*> m_aTargets;};
struct Utility{SCR_AIGroupPerception* m_Perception=nullptr;bool allowed=true;bool IsPositionAllowed(vector){return allowed;}};
struct SCR_AIGroup{Faction faction;Utility utility;Utility* GetGroupUtilityComponent(){return &utility;}Faction* GetFaction(){return &faction;}vector GetOrigin(){return {0};}};
struct PerceptionManager{float now=10;float GetTime(){return now;}};
struct Game{PerceptionManager manager;PerceptionManager* GetPerceptionManager(){return &manager;}}game;
Game* GetGame(){return &game;}
struct Observer{ERR_SupplyConvoyTask* m_Task;
"""+distance+observe+r"""
};
int main(){
Entity car{{5000}},lost{{0}};ERR_ConvoyVehicle live{false,&car},released{true,&lost};ERR_SupplyConvoyTask task{{&live,&released}};Observer observer{&task};
Faction enemy;SCR_AITargetInfo target;target.m_Faction=&enemy;SCR_AIGroupPerception perception{{&target}};SCR_AIGroup group;group.utility.m_Perception=&perception;
vector position;float nearest=400;assert(observer.Observe(&group,position,nearest)&&position.x==5100&&nearest==100);
target.m_vWorldPos={100};nearest=400;assert(!observer.Observe(&group,position,nearest)); // stale group/spawn origin is irrelevant
target.m_vWorldPos={5401};assert(!observer.Observe(&group,position,nearest));target.m_vWorldPos={5100};
target.m_eCategory=EAITargetInfoCategory::SUSPECTED;assert(!observer.Observe(&group,position,nearest));target.m_eCategory=EAITargetInfoCategory::IDENTIFIED;
target.m_fTimestamp=4;assert(!observer.Observe(&group,position,nearest));target.m_fTimestamp=11;assert(!observer.Observe(&group,position,nearest));target.m_fTimestamp=5;
assert(observer.Observe(&group,position,nearest));nearest=400;enemy.hostile=false;assert(!observer.Observe(&group,position,nearest));enemy.hostile=true;
group.utility.allowed=false;assert(!observer.Observe(&group,position,nearest));group.utility.allowed=true;
live.Released=true;assert(!observer.Observe(&group,position,nearest));assert(!observer.Observe(nullptr,position,nearest));
}
""")
retain_group=body(D/'ERR_SupplyConvoyTactics.c','static bool RetainGroup(')
retain_group=retain_group.replace('SCR_AIGroup group','SCR_AIGroup* group').replace('IEntity vehicle','IEntity* vehicle').replace('IEntity leader','IEntity* leader').replace('group.','group->').replace('CompartmentAccessComponent.','CompartmentAccessComponent::')
source=(D/'ERR_SupplyConvoyTactics.c').read_text()
overrides=[]
for cls in ('SCR_AIGetOutVehicle','SCR_AIGetOutActivity'):
section=source[source.index('modded class '+cls):];section=section[:section.index('\nclass ') if '\nclass ' in section else len(section)]
for method in ('CustomEvaluate','EvaluatePriorityLevel'):
# Each method's exact production body, selecting only this class's first occurrence.
a=section.index('override float '+method+'(');b=section.index('\n }',a)+3
method_source=section[a:b].replace('override ','').replace('m_Utility.','m_Utility->').replace('ERR_ConvoyMountedCrew.','ERR_ConvoyMountedCrew::').replace('super.','Base::')
overrides.append(method_source)
run(r"""
#include <cassert>
struct IEntity{bool held=false;IEntity* vehicle=nullptr;};
struct SCR_AIGroup{int players=0;IEntity* leader=nullptr;int GetPlayerCount(bool){return players;}IEntity* GetLeaderEntity(){return leader;}};
namespace CompartmentAccessComponent{IEntity* GetVehicleIn(IEntity* actor){return actor->vehicle;}}
struct ERR_ConvoyMountedCrew{static bool Retain(IEntity* actor,IEntity* vehicle){return actor&&actor->held&&vehicle&&actor->vehicle==vehicle;}
"""+retain_group+r"""
};
struct Utility{IEntity* m_OwnerEntity;SCR_AIGroup* m_Owner;};
struct Base{float CustomEvaluate(){return 1162;}float EvaluatePriorityLevel(){return 1000;}};
struct Individual:Base{Utility* m_Utility=nullptr;struct{IEntity* m_Value=nullptr;}m_Vehicle;
"""+''.join(overrides[:2])+r"""
};
struct Activity:Base{Utility* m_Utility=nullptr;struct{IEntity* m_Value=nullptr;}m_Vehicle;
"""+''.join(overrides[2:])+r"""
};
int main(){
IEntity truck,driver{true,&truck};SCR_AIGroup group{0,&driver};Utility utility{&driver,&group};Individual individual;Activity activity;
individual.m_Utility=&utility;individual.m_Vehicle.m_Value=&truck;activity.m_Utility=&utility;activity.m_Vehicle.m_Value=&truck;
assert(individual.CustomEvaluate()==0&&individual.EvaluatePriorityLevel()==0);assert(activity.CustomEvaluate()==0&&activity.EvaluatePriorityLevel()==0);
assert(ERR_ConvoyMountedCrew::RetainGroup(&group,nullptr));group.players=1;assert(activity.CustomEvaluate()==1162&&activity.EvaluatePriorityLevel()==1000);group.players=0;
driver.held=false;assert(individual.CustomEvaluate()==1162&&individual.EvaluatePriorityLevel()==1000);assert(activity.CustomEvaluate()==1162&&activity.EvaluatePriorityLevel()==1000);
group.leader=nullptr;assert(!ERR_ConvoyMountedCrew::RetainGroup(&group,&truck));assert(!ERR_ConvoyMountedCrew::RetainGroup(nullptr,&truck));
}
""")
print('PASS: moving-convoy perception boundaries and scoped individual/group abandonment priorities')
+8 -5
View File
@@ -21,7 +21,7 @@ for name,site in sites.items():
stock[name].append((ref[1],ref[2],count,item))
assert sum(item[2] for items in stock.values() for item in items)<=256
# Both established routes to radio intel and the prison field map remain usable.
for name in ('everon-supply-1','everon-supply-2'):
for name in ('everon-supply-1','everon-medical-1'):
assert 'RevealOnEscape 1' in sites[name]
clues=[i for i in stock[name] if i[1]=='Prefabs/ERR/ObjectiveDocument.et']
assert sum(i[2] for i in clues)>=2 and all('TargetId "everon-radio-1"' in i[3] for i in clues)
@@ -29,8 +29,11 @@ assert any(i[1].endswith('PaperMap_01_folded.et') for i in stock['everon-prison-
kitpath='Prefabs/ERR/Items/EmergencyRepairKit.et'
canpath='Prefabs/Items/Fuel/Jerrycan_01/Jerrycan_01_item.et'
maintenance=[(name,i) for name,items in stock.items() for i in items if i[1] in (kitpath,canpath)]
assert len(maintenance)==2 and all(name=='everon-supply-2' for name,_ in maintenance)
assert {i[1]:i[2] for _,i in maintenance}=={kitpath:1,canpath:2}
assert 'everon-supply-2' not in sites
assert not maintenance
assert all('Transport ERR_SiteTransport' not in site for site in sites.values())
assert 'RequireAirfield 1' in sites['everon-extraction-1']
assert 'Position 4800.953 0 11487.412' in sites['everon-extraction-1']
# Own container must be finite and must not turn off native resource eligibility.
kit=(R/'addons/core'/kitpath).read_text()
assert kit.startswith('GenericEntity : "{33B2DFDCD0EBA3DB}Prefabs/Items/Equipment/Kits/RepairKit_01/RepairKit_01_wrench.et"')
@@ -38,10 +41,10 @@ assert kit.count('SCR_ResourceContainer ')==1 and 'm_fResourceValueCurrent 20' i
for forbidden in ('m_aGenerators','m_aDisabledResourceTypes','m_eFactionUsageCheck','m_fMaxHealScaled','SCR_RepairSupportStationComponent','SCR_ResourceConsumer '):
assert forbidden not in kit,forbidden
meta=(R/'addons/core'/Path(kitpath+'.meta')).read_text();guid=re.search(r'Name "\{([A-F0-9]{16})\}',meta)[1]
assert [i[0] for _,i in maintenance if i[1]==kitpath]==[guid]
assert guid=='C09C7C97EFA34909'
vanilla=json.loads((R/'docs/vanilla-resources.json').read_text())
assert vanilla[canpath]=='12D5AD21E383B768'
assert vanilla['Prefabs/Items/Equipment/Kits/RepairKit_01/RepairKit_01_wrench.et']=='33B2DFDCD0EBA3DB'
evidence=json.loads((R/'docs/vehicle-maintenance-source-evidence.json').read_text())
assert evidence['finite_initial_stock']['litres_per_can']==5 and evidence['native_validation']=='pending'
print('PASS: finite maintenance stock/resource identity, bounded scenario stock and preserved two-cache clue redundancy; native interaction not claimed')
print('PASS: removed guarded vehicle cache and maintenance stock, airfield-only extraction config, and two remaining finite radio-clue sources')