Fix convoy coordination, AI combat and spectator recovery

This commit is contained in:
2026-09-27 22:28:07 +02:00
parent 2551d171b3
commit c5629f7f4f
108 changed files with 4337 additions and 282 deletions
+20 -1
View File
@@ -29,9 +29,28 @@ int main(){
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);
// Clearing the target area specifies a minimum outward SPEED, not a
// per-frame impulse. Repeated calls must not accelerate an already-safe
// radial velocity, even while the aircraft remains inside the radius.
vector outward{25,0,28};
for(int i=0;i<600;i++)outward=StandOff({100,120,0},{0,0,0},outward);
assert(std::abs(outward.x-25)<.001&&outward.z==28);
vector correction=StandOff({170,120,0},{0,0,0},{-40,0,28});
assert(std::abs(correction.x-5)<.001&&correction.z==28);
// The same stationary close-contact observation at different frame rates
// must not inject unbounded speed into the persistent smoothed velocity.
for(int hz: {30,60,120}){
vector v{-40,0,28};
for(int frame=0;frame<hz*5;frame++){
vector desired{-10,0,28};v=v+(desired-v)*(0.6f/hz);
v=StandOff({100,120,0},{0,0,0},v);
assert(v.Length()<40);
}
}
}
''')
print('PASS: original 15-minute launch/loss cadence, active/captured timer disarm, saved delay reset, stop, 300m orbit and close-contact standoff')
print('PASS: original 15-minute launch/loss cadence, active/captured timer disarm, saved delay reset, stop, 300m orbit and bounded close-contact standoff at 30/60/120 Hz')
# Explicit entity deletion must not leave an inactive launch deadline behind.
removed=body(path,'void Removed(')
+21 -1
View File
@@ -21,7 +21,7 @@ int main(){
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.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));
@@ -30,6 +30,26 @@ int main(){
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()));
// Artillery blocks authorization, not the history of fresh sightings and
// ground-force shortage. Repeated <45-second clear windows used to starve
// airborne support by restarting its confirmation timer on every salvo.
ERR_AirSupportPolicy blocked;auto contact=good;contact.BeliefId=9;contact.GroundFit=0;contact.ConfirmedLosses=0;
assert(!blocked.Consider(&contact,0));
contact.ArtilleryBusy=true;
assert(!blocked.Consider(&contact,10000));
assert(!blocked.Consider(&contact,46000));
contact.ArtilleryBusy=false;assert(blocked.Consider(&contact,47000));
// Stale intelligence and a changed target still invalidate confirmation.
contact.Age=91;assert(!blocked.Consider(&contact,48000));
contact.Age=5;assert(!blocked.Consider(&contact,49000));
contact.BeliefId=10;assert(!blocked.Consider(&contact,90000));
assert(!blocked.Consider(&contact,134999));assert(blocked.Consider(&contact,135000));
// Busy airspace must not preconfirm a new drop during the 8-minute cooldown.
blocked.Committed(135000);contact.ArtilleryBusy=true;
assert(!blocked.Consider(&contact,614999));
contact.ArtilleryBusy=false;assert(!blocked.Consider(&contact,615000));
assert(!blocked.Consider(&contact,659999));assert(blocked.Consider(&contact,660000));
}
''')
print('PASS: production airborne memory confidence, persistence, ground coverage, confirmed losses, artillery veto, cooldown and invalid-input rules')
+234
View File
@@ -0,0 +1,234 @@
"""Run the production column controller against physical-position/native-order doubles."""
from policy_harness import ROOT, body, run
import re
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyColumn.c'
def cpp(s):
s=re.sub(r'//[^\n]*', '', s).replace('protected ', '').replace('out ', '')
s=s.replace('foreach (', 'for (').replace('null','nullptr')
for typ in ('ERR_ConvoyVehicle','ERR_ConvoyColumnVehicle'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for obj in ('m_Task','vehicle','state','scan','promoted','ahead','m_State[i]','m_Task->Vehicles[previous]'):
s=s.replace(obj+'.',obj+'->')
for obj in ('Entity','Group','Waypoint','Route','Receipt','Combat'):
s=s.replace(obj+'.',obj+'->')
for cls in ('ERR_ConvoyColumnRules','ERR_ConvoyDriving','ERR_SupplyConvoyTactics','ERR_HQ','Math','vector','string'):
s=s.replace(cls+'.',cls+'::')
s=s.replace('m_Task->Vehicles[index].', 'm_Task->Vehicles[index]->').replace('(index + 1).ToString()', 'std::to_string(index + 1)')
s=s.replace('float largestGap; bool supplyHold;', 'float largestGap=0; bool supplyHold=false;')
return s
rules='\n'.join(cpp(body(p, x)) for x in ('static bool WaitForRear(', 'static bool TooClose(', 'static float Speed('))
methods='\n'.join(cpp(body(p,x)) for x in ('protected float DistanceToTarget(', 'protected void Reorder(', 'void Reset(', 'protected void Hold(', 'protected bool Progress(', 'bool Tick('))
run(r'''
#include <cassert>
#include <cmath>
#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 DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
namespace Math{float Max(float a,float b){return a>b?a:b;}int Min(int a,int b){return a<b?a:b;}float Round(float x){return std::round(x);}}
namespace ERR_HQ{int stranded=0,retries=0;void Event(string type,string){if(type=="CONVOY STRANDED")stranded++;if(type=="CONVOY ROUTE RETRY")retries++;}}
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}};
struct ERR_ConvoyColumnRules{'''+rules+r'''};
struct Entity{vector position;vector GetOrigin(){return position;}};
struct Waypoint:Entity{};
struct Group{int added=0;void AddWaypoint(Waypoint*){added++;}};
struct Route{Array<vector>points={{0},{400}};int Count(){return points.Count();}vector At(int i){return points[i];}};
struct Combat{bool Held=false;};
struct ERR_ConvoyVehicle{
int Index=0,RouteIndex=0,clears=0;bool Released=false,Arrived=false,SupplyHeld=false;
struct Entity entity;struct Entity* Entity=&entity;struct Group group;struct Group* Group=&group;
struct Combat combat;struct Combat* Combat=&combat;struct Waypoint* Waypoint=nullptr;
void ClearOrder(){if(Waypoint)clears++;delete Waypoint;Waypoint=nullptr;}
void Release(void*){Released=true;ClearOrder();}
bool Order(struct Route* route,float){if(!Waypoint){Waypoint=new struct Waypoint();Waypoint->position=route->At(RouteIndex);Group->AddWaypoint(Waypoint);}return true;}
~ERR_ConvoyVehicle(){ClearOrder();}
};
namespace ERR_ConvoyDriving{bool Restore(void*,ERR_ConvoyVehicle*){return false;}}
namespace ERR_SupplyConvoyTactics{Waypoint* Move(vector p,float){auto w=new Waypoint();w->position=p;return w;}}
struct Receipt{string SourceId="test";};
struct ERR_SupplyConvoyTask{Array<ERR_ConvoyVehicle*>Vehicles;struct Route route;struct Route* Route=&route;int Legs=0;struct Receipt receipt;struct Receipt* Receipt=&receipt;};
struct ERR_ConvoyColumnVehicle{bool Holding=false;vector Progress;float LastProgress=-1,NextRetry=0,NextTarget=0,Speed=-1;int Ahead=-1;};
struct Column{
void* m_Mode=nullptr;ERR_SupplyConvoyTask* m_Task;Array<ERR_ConvoyColumnVehicle*>m_State;
Array<int>m_Order={0,1,2};int m_Leader=-1,m_RouteIndex=0;bool m_RearWait=false;float m_NextLog=0;
void Speed(ERR_ConvoyVehicle*,ERR_ConvoyColumnVehicle* state,float speed){state->Speed=speed;}
'''+methods+r'''};
int main(){
assert(!ERR_ConvoyColumnRules::WaitForRear(160,false));assert(ERR_ConvoyColumnRules::WaitForRear(161,false));
assert(ERR_ConvoyColumnRules::WaitForRear(91,true));assert(!ERR_ConvoyColumnRules::WaitForRear(90,true));
assert(ERR_ConvoyColumnRules::TooClose(39,false));assert(!ERR_ConvoyColumnRules::TooClose(40,false));
assert(ERR_ConvoyColumnRules::TooClose(64,true));assert(!ERR_ConvoyColumnRules::TooClose(65,true));
ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;b.entity.position={-35};c.entity.position={-70};
ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
assert(column.Tick(0));assert(a.RouteIndex==1&&a.Waypoint&&a.Waypoint->position.x==400);assert(!b.Waypoint&&!c.Waypoint);
a.entity.position={80};column.Tick(1000);assert(b.Waypoint&&b.Waypoint->position.x==80&&sb.Speed==45);assert(!c.Waypoint);
int additions=b.group.added;a.entity.position={110};column.Tick(2000);assert(b.group.added==additions); // stable target, no per-second churn
a.entity.position={200};column.Tick(3000);assert(column.m_RearWait&&!a.Waypoint&&b.Waypoint); // leader waits for laggard
b.entity.position={130};c.entity.position={60};column.Tick(11000);assert(!column.m_RearWait&&a.Waypoint);
assert(b.Waypoint->position.x==200); // native target follows predecessor, never remote base marker
b.SupplyHeld=true;auto supplyOrder=b.Waypoint;column.Tick(12000);assert(!a.Waypoint&&b.Waypoint==supplyOrder&&sb.LastProgress==-1);
b.SupplyHeld=false;b.combat.Held=true;column.Tick(13000);assert(b.Waypoint==supplyOrder&&sb.LastProgress==-1);b.combat.Held=false;
// Real vehicle positions determine arrival; no stale seated-group origin is consulted.
a.entity.position={400};b.entity.position={330};c.entity.position={260};column.Tick(20000);
assert(a.Arrived&&b.Arrived&&c.Arrived&&!a.Waypoint&&!b.Waypoint&&!c.Waypoint);
// Loss promotes the next surviving vehicle at the current route cursor, no replacement/restart at spawn.
column.Reset(20500);assert(column.m_Leader==0&&column.m_RouteIndex==2); // combat reset preserves route progress for a later promotion
a.Release(nullptr);b.Arrived=c.Arrived=false;column.Tick(21000);assert(column.m_Leader==1&&b.RouteIndex==1&&!b.Arrived&&b.Waypoint->position.x==400);
// Commanded stationary time alone retries and eventually releases; holds reset this clock.
ERR_ConvoyVehicle stuck;ERR_ConvoyColumnVehicle ss;
assert(column.Progress(&stuck,&ss,0));assert(column.Progress(&stuck,&ss,45000));assert(ERR_HQ::retries==1&&!stuck.Released);
column.Hold(&stuck,&ss,170000);assert(column.Progress(&stuck,&ss,180000));assert(!stuck.Released);
stuck.entity.position={6};assert(column.Progress(&stuck,&ss,300000));assert(ss.LastProgress==300000);
assert(column.Progress(&stuck,&ss,479999));assert(!column.Progress(&stuck,&ss,480000)&&stuck.Released&&ERR_HQ::stranded==1);
column.Reset(500000,true);assert(sa.LastProgress==-1&&sb.LastProgress==-1&&!column.m_RearWait&&column.m_Leader==-1);
// Post-combat physical order avoids sending the front truck toward a rear vehicle coming toward it.
b.entity.position={100};c.entity.position={300};b.Arrived=c.Arrived=false;column.m_RouteIndex=1;column.Reset(500001,false,true);
assert(column.m_Order[0]==2&&column.m_Order[1]==1&&column.m_Order[2]==0); // released vehicle last
assert(task.Vehicles[1]==&b&&task.Vehicles[2]==&c); // combat roles and canonical identities unchanged
column.Tick(500002);assert(column.m_Leader==2&&!c.Waypoint&&b.Waypoint&&b.Waypoint->position.x==300);
b.entity.position={230};column.Tick(500003);assert(c.Waypoint&&c.Waypoint->position.x==400);
b.Release(nullptr);c.Release(nullptr);assert(!column.Tick(510000));
}
''')
print('PASS: production convoy spacing, physical arrival, stable follower targets, supply ownership, leader promotion and bounded stuck recovery')
q=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c'
orders=body(q,'protected void Orders(').replace('protected ','').replace('m_Task.','m_Task->').replace('vector.','vector::')
run(r'''
#include <cassert>
#include <cmath>
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
struct Items{int count;int Count(){return count;}};
struct Task{Items Vehicles{6},Teams{2};};
struct Controller{Task task;Task* m_Task=&task;float m_NextOrders=0;vector m_LastOrderContact,m_Contact;bool m_OrdersIssued=false,m_TruckWithdrawn=false;int support=0,flank=0;
void Support(int,bool){support++;}void Flank(int,bool){flank++;}
'''+orders+r'''};
int main(){
Controller c;c.Orders(true,0);assert(c.support==6&&c.flank==2);
c.Orders(false,30000);assert(c.support==6&&c.flank==2&&c.m_NextOrders==60000);
c.m_Contact={39};c.Orders(false,60000);assert(c.support==6&&c.flank==2);
c.m_Contact={40};c.Orders(false,90000);assert(c.support==12&&c.flank==4);
c.m_OrdersIssued=false;c.Orders(true,100000);assert(c.support==18&&c.flank==6); // new engagement can issue at same location
}
''')
print('PASS: convoy combat keeps native manoeuvres for unchanged contacts and refreshes when contact moves')
probe=ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSupplyConvoyProbe.c'
departure=body(probe,'protected int Departure(').replace('protected ','').replace('m_CombatTask.','m_CombatTask->').replace('Vehicles[2].','Vehicles[2]->').replace('Entity.','Entity->').replace('vector.','vector::')
run(r'''
#include <cassert>
#include <cmath>
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
void Print(const char*){}
struct Entity{vector position;vector GetOrigin(){return position;}};
struct Vehicle{bool Released=false;struct Entity entity;struct Entity* Entity=&entity;};
struct Bases{int Count(){return 2;}};
struct Task{bool Stopped=false;int Legs=0;Vehicle* Vehicles[3];struct Bases Bases;};
struct Probe{Task* m_CombatTask;int m_Phase=3,m_TestLegs=0;vector m_RouteResumePosition;bool m_AllMoved=true;
'''+departure+r'''};
int main(){
Task task;Vehicle truck;task.Vehicles[2]=&truck;Probe p{&task};truck.entity.position={200};
assert(p.Departure()==0&&p.m_Phase==3); // earlier regroup displacement cannot pass
task.Legs=1;assert(p.Departure()==0&&p.m_Phase==4&&p.m_RouteResumePosition.x==200);
assert(p.Departure()==0);truck.entity.position={219};assert(p.Departure()==0);
truck.entity.position={220};assert(p.Departure()==1&&p.m_Phase==5);
truck.Released=true;assert(p.Departure()==-1);
}
''')
smoke=(ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c').read_text()
convoy=smoke.split('else if (m_ERRSmokeStep == 106)')[1].split('else if (m_ERRSmokeStep == 105)')[0]
assert 'Transition(ERR_Phase.DEFEAT)' in convoy and 'm_ERRSmokeStep = 1061' in convoy
assert 'damage.Kill' not in convoy and 'm_ERRSmokeStep = 9' not in convoy
print('PASS: native convoy proof excludes pre-departure movement and ends via explicit fixture reset, not prohibited player death')
boarding=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyBoardingProgress.c'
retry=body(boarding,'bool Retry(')
refresh=body(q,'protected void RefreshBoarding(').replace('protected ','').replace('foreach (','for (').replace('m_Task.','m_Task->').replace('team.','team->').replace('Vehicles[2].','Vehicles[2]->').replace('BoardingProgress.','BoardingProgress->').replace('Receipt.','Receipt->').replace('ERR_HQ.','ERR_HQ::').replace('Math.','Math::').replace('string.','string::').replace('ERR_SupplyTeam team','ERR_SupplyTeam* team')
run(r'''
#include <cassert>
#include <cmath>
#include <vector>
struct string{template<class...T>static string Format(T...){return {};}};
namespace Math{float Round(float x){return std::round(x);}}
namespace ERR_HQ{void Event(const char*,string){}}
struct ERR_ConvoyBoardingProgress{int Seated=-1,Retries=0;float Distance=-1,LastProgress=0,NextRetry=0;
'''+retry+r'''};
struct ERR_SupplyTeam{bool Released=false,aboard=false;float BoardingRetryAt=0,distance=200;int seated=0,clears=0,adds=0;ERR_ConvoyBoardingProgress* BoardingProgress=nullptr;
bool Aboard(void*,bool){return aboard;}int BoardingCount(void*,float& d){d=distance;return seated;}void Clear(){clears++;}void Add(int){adds++;}};
struct Vehicle{void* Entity=nullptr;};struct Receipt{const char* SourceId="test";};
struct Task{std::vector<ERR_SupplyTeam*>Teams;Vehicle* Vehicles[3];struct Receipt receipt;struct Receipt* Receipt=&receipt;};
struct Tactics{Task* m_Task;int Board(void*,bool){return 1;}
'''+refresh+r'''};
int main(){
ERR_ConvoyBoardingProgress p;assert(!p.Retry(0,200,0));
assert(!p.Retry(0,194,29000));assert(!p.Retry(0,194,58000));assert(p.Retry(0,194,59000));
assert(!p.Retry(1,100,60000));assert(!p.Retry(1,100,89999));assert(p.Retry(1,100,90000));
assert(p.Retry(1,100,120000));assert(!p.Retry(1,100,150000)&&p.Retries==3); // bounded even if no progress
Task task;Vehicle truck;task.Vehicles[2]=&truck;ERR_SupplyTeam team,finished,released;finished.aboard=true;released.Released=true;
task.Teams={&team,&finished,&released};Tactics t{&task};t.RefreshBoarding(0);assert(team.BoardingProgress&&!finished.BoardingProgress&&!released.BoardingProgress);
t.RefreshBoarding(30000);assert(team.clears==1&&team.adds==0&&team.BoardingRetryAt==31000);
t.RefreshBoarding(30999);assert(team.adds==0);t.RefreshBoarding(31000);assert(team.adds==1&&team.BoardingRetryAt==0);
team.seated=6;team.distance=10;t.RefreshBoarding(40000);assert(team.clears==1);t.RefreshBoarding(69999);assert(team.clears==1);
t.RefreshBoarding(70000);assert(team.clears==2);team.aboard=true;t.RefreshBoarding(71000);assert(team.adds==1); // do not disturb completed boarding
delete team.BoardingProgress;
}
''')
print('PASS: boarding retries only after approach/seat progress stops, capped at three, with delayed native reissue and completed-team preservation')
safe='\n'.join(body(q,s) for s in ('protected float InfantryDistance(', 'protected void ProtectInfantry('))
safe=safe.replace('protected ','').replace('foreach (','for (')
safe=safe.replace('AICarMovementComponent movement = AICarMovementComponent.Cast(vehicle.Entity.FindComponent(AICarMovementComponent));', 'AICarMovementComponent* movement = &vehicle->Entity->movement;')
for typ in ('IEntity','ERR_SupplyTeam','ERR_ConvoyVehicle'):safe=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',safe)
for obj in ('m_Task','team','member','vehicle','movement'):safe=safe.replace(obj+'.',obj+'->')
for cls in ('ERR_Actors','CompartmentAccessComponent','Math','vector'):safe=safe.replace(cls+'.',cls+'::')
run(r'''
#include <cassert>
#include <cmath>
#include <vector>
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
namespace Math{float Min(float a,float b){return a<b?a:b;}}
struct AICarMovementComponent{float speed=99;void SetCruiseSpeed(float s){speed=s;}};
struct IEntity{vector position;bool fit=true,aboard=false;AICarMovementComponent movement;vector GetOrigin(){return position;}};
namespace ERR_Actors{bool CanFight(IEntity* m){return m&&m->fit;}}
namespace CompartmentAccessComponent{IEntity* GetVehicleIn(IEntity* m){return m->aboard?m:nullptr;}}
struct ERR_SupplyTeam{bool Released=false;std::vector<IEntity*>Members;};
struct ERR_ConvoyVehicle{bool Released=false;IEntity* Entity;};
struct Task{std::vector<ERR_SupplyTeam*>Teams;std::vector<ERR_ConvoyVehicle*>Vehicles;};
struct Tactics{Task* m_Task;
'''+safe+r'''};
int main(){
IEntity truck{{0}},car{{100}},foot{{30}},passenger{{0}};passenger.aboard=true;ERR_SupplyTeam team{false,{&foot,&passenger}};
ERR_ConvoyVehicle a{false,&truck},b{false,&car};Task task{{&team},{&a,&b}};Tactics t{&task};
assert(t.InfantryDistance(&truck)==30);t.ProtectInfantry();assert(truck.movement.speed==8&&car.movement.speed==35);
foot.aboard=true;t.ProtectInfantry();assert(truck.movement.speed==35&&car.movement.speed==35);
foot.aboard=false;foot.fit=false;assert(t.InfantryDistance(&truck)>100000);foot.fit=true;team.Released=true;assert(t.InfantryDistance(&truck)>100000);
team.Released=false;a.Released=true;truck.movement.speed=99;t.ProtectInfantry();assert(truck.movement.speed==99); // never retake a released vehicle
}
''')
print('PASS: slow only owned vehicles near fit dismounted infantry; ignore passengers, casualties and released teams/vehicles')
restore=body(p,'static bool Restore(').replace('ERR_GameMode mode','void* mode').replace('ERR_ConvoyVehicle vehicle','ERR_ConvoyVehicle* vehicle')
restore=restore.replace('SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(vehicle.Entity.FindComponent(SCR_AIVehicleUsageComponent));','SCR_AIVehicleUsageComponent* usage = vehicle->Entity->usage;')
restore=restore.replace('SCR_AIGroupUtilityComponent utility', 'SCR_AIGroupUtilityComponent* utility')
for obj in ('vehicle','Group','utility','usage'):restore=restore.replace(obj+'.',obj+'->')
restore=restore.replace('ERR_ConvoyMountedCrew.','ERR_ConvoyMountedCrew::')
run(r'''
#include <cassert>
struct SCR_AIVehicleUsageComponent{bool pilot=true;bool CanBePiloted(){return pilot;}};
struct Entity{SCR_AIVehicleUsageComponent* usage=nullptr;};
struct SCR_AIGroupUtilityComponent{void* m_VehicleMgr=this;bool known=false;int added=0;bool IsUsableVehicle(Entity*){return known;}void AddUsableVehicle(SCR_AIVehicleUsageComponent*){known=true;added++;}};
struct Group{SCR_AIGroupUtilityComponent* utility=nullptr;SCR_AIGroupUtilityComponent* GetGroupUtilityComponent(){return utility;}};
struct ERR_ConvoyVehicle{bool Released=false,valid=true;struct Group group;struct Group* Group=&group;struct Entity entity;struct Entity* Entity=&entity;void* Driver=this;bool CrewValid(void*){return valid;}};
namespace ERR_ConvoyMountedCrew{bool healthyOwned=true;bool Retain(void*,Entity*){return healthyOwned;}}
'''+restore+r'''
int main(){
int mode;ERR_ConvoyVehicle car;SCR_AIGroupUtilityComponent utility;SCR_AIVehicleUsageComponent usage;car.group.utility=&utility;car.entity.usage=&usage;
assert(!Restore(nullptr,&car)&&!Restore(&mode,nullptr));car.Released=true;assert(!Restore(&mode,&car));car.Released=false;
car.valid=false;assert(!Restore(&mode,&car));car.valid=true;ERR_ConvoyMountedCrew::healthyOwned=false;assert(!Restore(&mode,&car));ERR_ConvoyMountedCrew::healthyOwned=true;
car.group.utility=nullptr;assert(!Restore(&mode,&car));car.group.utility=&utility;utility.m_VehicleMgr=nullptr;assert(!Restore(&mode,&car));utility.m_VehicleMgr=&utility;
car.entity.usage=nullptr;assert(!Restore(&mode,&car));car.entity.usage=&usage;usage.pilot=false;assert(!Restore(&mode,&car));usage.pilot=true;
assert(Restore(&mode,&car)&&utility.known&&utility.added==1);assert(!Restore(&mode,&car)&&utility.added==1);
}
''')
print('PASS: restore missing native driving registration only for healthy owned seated crews; preserve existing association and refuse invalid/released vehicles')
+1
View File
@@ -78,5 +78,6 @@ int main(){
''')
build=body(p,'static bool Build(')
assert 'candidateIndex < 33' in build
assert build.index('ERR_SupplyConvoyRules.Separated(') < build.index('Prefer(distance,')
assert build.index('Prefer(distance,') < build.index('ERR_ConvoyRules.GroundClear(')
print('PASS: actual road interpolation/projection/orientation, six35m starts,200m forward tail, preserved bends, short/far/degenerate/nonfinite/overdense/hairpin refusal and deterministic candidate ties')
+10 -7
View File
@@ -7,12 +7,13 @@ def native(signature):
return s.replace('perceptionComp.', 'perceptionComp->').replace('m_CombatComponent.', 'm_CombatComponent->')
run(r'''
#include <cassert>
#include <cmath>
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;}
float error=1;int calls=0;EAISkill sampled=EAISkill::CYLON;
float GetRandomFactor(EAISkill s,float mu){calls++;sampled=s;return mu+error;}
void UpdatePerceptionFactor(PerceptionComponent* p,SCR_AIThreatSystem*){calls++;p->factor=.4f;}
};
struct Combat:Native{
@@ -27,15 +28,17 @@ struct Aim:Native{
};
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);
assert(aim.GetRandomFactor(EAISkill::CYLON,0)==1);assert(aim.sampled==EAISkill::CYLON);
aim.m_CombatComponent=&combat;assert(aim.GetRandomFactor(EAISkill::CYLON,0)==1);assert(aim.sampled==EAISkill::CYLON);
combat.bound=true;assert(std::abs(aim.GetRandomFactor(EAISkill::CYLON,0)-.9f)<.00001f);assert(aim.sampled==EAISkill::REGULAR);
assert(std::abs(aim.GetRandomFactor(EAISkill::CYLON,5)-5.9f)<.00001f);
aim.error=-2;assert(std::abs(aim.GetRandomFactor(EAISkill::CYLON,0)+1.8f)<.00001f);aim.error=1;
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);
assert(aim.GetRandomFactor(EAISkill::CYLON,0)==1);assert(aim.sampled==EAISkill::CYLON);
}
''')
print('PASS: actual scoped REGULAR aim error and alert perception; preserves equipment multiplier and restores vanilla behavior when unbound')
print('PASS: actual scoped 10% tighter REGULAR aim error and alert perception; preserves equipment multiplier and restores vanilla behavior when unbound')
+61
View File
@@ -0,0 +1,61 @@
"""Execute actual medicine allowance/insertion coordinator with native inventory adapters."""
import re
from policy_harness import ROOT, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyMedicine.c'
s=p.read_text();s=s[s.index('class ERR_EnemyMedicine'):]
s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('ref ','').replace('class ERR_EnemyMedicine','struct ERR_EnemyMedicine')
s=s.replace('array<IEntity>', 'Array<IEntity*>').replace('foreach (','for (').replace('ResourceName','string')
s=s.replace('InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent))','actor->inventory').replace('BaseInventoryStorageComponent.Cast(item.FindComponent(BaseInventoryStorageComponent))','item->bag').replace('ERR_HQ.Cast(mode.Command())','mode->Command()')
for cls in ('IEntity','ERR_GameMode','SCR_AIGroup','InventoryStorageManagerComponent','BaseInventoryStorageComponent','ERR_HQ'):
s=re.sub(r'\b'+cls+r' (\w+)',cls+r'* \1',s)
s=s.replace('ERR_EnemyEquipmentCallback m_Operation','ERR_EnemyEquipmentCallback* m_Operation')
for obj in ('actor','existing','item','mode','inventory','m_Operation','hq'):
s=s.replace(obj+'.',obj+'->')
s=s.replace('GetPrefabData().','GetPrefabData()->').replace('Hunters().','Hunters()->')
for ns in ('ERR_EnemyEquipmentRules','ERR_EnemyMedicalCatalog','ERR_HQ','Math','EStoragePurpose','string'):
s=s.replace(ns+'.',ns+'::')
s=s.replace('string::Format(', 'Format(').replace('null','nullptr')
s=re.sub(r'\b(bool|int|float|\w+\*) (\w+);',r'\1 \2 {};',s).replace('\n}\n','\n};\n')
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <algorithm>
struct string:std::string {using std::string::string;string(const std::string& s):std::string(s){}bool Contains(string x){return find(x)!=npos;}void ToLower(){std::transform(begin(),end(),begin(),::tolower);}};
template<class...T>string Format(string,T...){return "event";}
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}};
struct Prefab{string path;string GetPrefabName(){return path;}};
struct BaseInventoryStorageComponent{};
struct InventoryStorageManagerComponent;
struct IEntity{Prefab prefab;bool owned=true;InventoryStorageManagerComponent* inventory=nullptr;BaseInventoryStorageComponent* bag=nullptr;Prefab* GetPrefabData(){return &prefab;}int GetOrigin(){return 0;}};
struct SCR_AIGroup{};
struct ERR_EnemyEquipmentCallback{bool Finished=false,Succeeded=false;};
namespace EStoragePurpose{enum{PURPOSE_DEPOSIT};}
struct InventoryStorageManagerComponent{Array<IEntity*> items;int attempts=0,accepted=0;bool fullBag=false,full=false,async=false;ERR_EnemyEquipmentCallback* pending=nullptr;
void GetItems(Array<IEntity*>& out){out=items;}
bool TrySpawnPrefabToStorage(string prefab,BaseInventoryStorageComponent* bag,int,int,ERR_EnemyEquipmentCallback* callback){attempts++;if(full||(bag&&fullBag))return false;accepted++;auto* item=new IEntity;item->prefab.path=prefab;items.push_back(item);pending=callback;if(!async){callback->Finished=true;callback->Succeeded=true;}return true;}
};
struct Hunters{bool Contains(SCR_AIGroup*){return false;}};
struct ERR_HQ{Hunters hunters;Hunters* Hunters(){return &hunters;}static void Event(string,string){}};
struct ERR_GameMode{ERR_HQ hq;ERR_HQ* Command(){return &hq;}};
namespace ERR_EnemyEquipmentRules {bool Eligible(ERR_GameMode*,SCR_AIGroup*,IEntity* a){return a&&a->owned;}}
namespace ERR_EnemyMedicalCatalog {const string EPI="epi",MORPHINE="morphine";bool Medic(string p){return p.Contains("medic");}bool Specialist(string p){return p.Contains("_sf");}}
namespace Math{int RandomInt(int,int){return 0;}}
'''.replace('Hunters hunters;Hunters* Hunters()', 'struct Hunters hunters;struct Hunters* Hunters()')+s+r'''
int main(){ERR_GameMode mode;SCR_AIGroup group;IEntity actor;InventoryStorageManagerComponent inventory;actor.inventory=&inventory;actor.prefab.path="_sf_medic";
IEntity backpack;backpack.prefab.path="backpack";BaseInventoryStorageComponent bag;backpack.bag=&bag;inventory.items.push_back(&backpack);
IEntity authored;authored.prefab.path="morphine";inventory.items.push_back(&authored);
ERR_EnemyMedicine grant;inventory.fullBag=true;for(int i=0;i<20;i++)grant.Check(&mode,&group,&actor);
assert(grant.m_Done&&grant.m_Issued==11&&inventory.accepted==11&&inventory.attempts==22); // six epi plus five missing morphine; refused bag falls back once
int attempts=inventory.attempts;inventory.items.clear();for(int i=0;i<5;i++)grant.Check(&mode,&group,&actor);assert(inventory.attempts==attempts); // no refill after loot
InventoryStorageManagerComponent blocked;actor.inventory=&blocked;blocked.async=true;ERR_EnemyMedicine pending;for(int i=0;i<35;i++)pending.Check(&mode,&group,&actor);assert(pending.m_Done&&blocked.accepted==1); // pending operation quarantined, never duplicated
InventoryStorageManagerComponent unavailable;actor.inventory=&unavailable;unavailable.full=true;ERR_EnemyMedicine noSpace;noSpace.Check(&mode,&group,&actor);assert(noSpace.m_Done&&unavailable.accepted==0);
InventoryStorageManagerComponent specialStock;actor.inventory=&specialStock;actor.prefab.path="soldier_sf";
ERR_EnemyMedicine special;for(int i=0;i<8;i++)special.Check(&mode,&group,&actor);
assert(special.m_Done&&specialStock.accepted==4);int epi=0,morphine=0;
for(auto* item:specialStock.items){epi+=item->prefab.path=="epi";morphine+=item->prefab.path=="morphine";}
assert(epi==2&&morphine==2); // Every special-forces soldier has two finite injectors.
actor.owned=false;ERR_EnemyMedicine captured;captured.Check(&mode,&group,&actor);assert(!captured.m_Started);captured.Stop();actor.owned=true;captured.Check(&mode,&group,&actor);assert(!captured.m_Started);
}
''')
print('PASS: actual finite medicine grants, authored stock deduction, full-backpack refusal/pouch fallback, no loot replenishment, unresolved-operation quarantine and ownership/stop guards')
+4 -2
View File
@@ -14,8 +14,9 @@ code=code.replace('ref array<','Array<').replace('array<','Array<').replace('ref
code=code.replace('static const ResourceName','inline static const ResourceName').replace('void ERR_EnemyRadio(','ERR_EnemyRadio(')
code=re.sub(r'\bERR_GameMode (?=\w)','ERR_GameMode* ',code)
code=code.replace('foreach (','for (').replace('null','nullptr')
for name in ('ERR_EnemyEquipmentRules','ERR_HQ','Replication','SCR_Global','ERR_GameMode','EStoragePurpose'):
for name in ('ERR_EnemyEquipmentRules','ERR_HQ','Replication','SCR_Global','ERR_GameMode','EStoragePurpose','Math'):
code=code.replace(name+'.',name+'::')
code=code.replace('ERR_HQ hq','ERR_HQ* hq').replace('hq.Hunters().','hq->Hunters()->').replace('hq.Hunters()','hq->Hunters()')
for name in ('actor','inventory','item','radio','agent','m_Group','m_Operation','mode'):
code=re.sub(r'\b'+name+r'\.',name+'->',code)
code=re.sub(r'\b(bool|int|IEntity|SCR_AIGroup|ERR_EnemyEquipmentCallback\*) (\w+);',r'\1 \2{};',code)
@@ -61,7 +62,8 @@ struct Group{Array<Agent*>agents;int players=0,faction=1;bool slave=false,expand
struct ERR_GameMode{inline static ERR_GameMode*current=nullptr;bool active=true;static ERR_GameMode*Get(){return current;}bool Active(){return active;}int EnemyFaction(){return 1;}};
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
struct SCR_Global{inline static bool editor=false;static bool IsEditMode(){return editor;}};
struct ERR_HQ{inline static Array<string>events;static void Event(string,string message){events.Insert(message);}};
struct Math{static int Max(int a,int b){return std::max(a,b);}};
struct ERR_HQ{static ERR_HQ* Get(){return nullptr;} ERR_HQ* Hunters(){return this;} int VehicleRadioCapability(IEntity){return 0;} inline static Array<string>events;static void Event(string,string message){events.Insert(message);}};
struct ERR_EnemyEquipmentRules{static bool Owned(ERR_GameMode*m,Group*g,IEntity a){return m&&g&&a&&a->eligible&&a->group==g;}static bool Eligible(ERR_GameMode*m,Group*g,IEntity a){return Owned(m,g,a);}};
struct Inventory{Array<IEntity>items;Array<int>requests;bool basicFits=true,manpackFits=true,accept=true;int callback=0;ScriptedInventoryOperationCallback*last=nullptr;
void GetItems(Array<IEntity>&out){out=items;}
+58
View File
@@ -0,0 +1,58 @@
"""Execute fixes for the 26 September log failures against boundary doubles."""
from policy_harness import ROOT, body, run
r=ROOT/'addons/core/Scripts/Game/ERR/10_AI'
cleanup=body(r/'ERR_HunterCrewRecovery.c','static void Complete(')
cleanup=cleanup.replace('SCR_AIVehicleBehavior behavior','Behavior* behavior').replace('IEntity actor','Actor* actor')
cleanup=cleanup.replace('SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(actor));','Agent* agent = actor->agent;')
cleanup=cleanup.replace('array<ref AIActionBase> actions = {};','Actions actions;').replace('agent.','agent->').replace('m_UtilityComponent.','m_UtilityComponent->').replace('behavior.Complete()','behavior->Complete()')
run('''#include <cassert>
#include <vector>
#include <algorithm>
struct Behavior {int completions=0;void Complete(){completions++;}};
struct Actions:std::vector<Behavior*>{bool Contains(Behavior* b){return std::find(begin(),end(),b)!=end();}};
struct Utility {void* m_AIInfo=this;Actions queued;void GetActions(Actions& out){out=queued;}};
struct Agent {Utility* m_UtilityComponent=nullptr;};struct Actor{Agent* agent=nullptr;};
'''+cleanup+'''
int main(){Behavior b;Actor actor;Complete(&b,nullptr);Complete(&b,&actor);assert(!b.completions);
Agent agent;actor.agent=&agent;Complete(&b,&actor);assert(!b.completions);
Utility utility;agent.m_UtilityComponent=&utility;Complete(&b,&actor);assert(!b.completions);
utility.queued.push_back(&b);utility.m_AIInfo=nullptr;Complete(&b,&actor);assert(!b.completions);
utility.m_AIInfo=&utility;Complete(&b,&actor);assert(b.completions==1);
utility.queued.clear();Complete(&b,&actor);assert(b.completions==1);}
''')
# Execute the actual HQ eligibility branch. Its success permits the existing
# radio request path, never directly a shot; per-shot checks remain separate.
tick=body(r/'ERR_MortarSystem.c','void Tick(')
a=tick.index(' vector predicted;');b=tick.index(' selected = battery;',a)
preflight=tick[a:b].replace('vector predicted;','Vec predicted;').replace('ERR_MortarPrediction.Predict','Predict').replace('battery.Trace','battery->Trace').replace('predicted.ToString()','std::string("reported")')
run('''#include <cassert>
#include <string>
struct Vec{};struct Battery{int holds=0;void Trace(std::string,std::string,std::string){holds++;}};
struct Test{bool prediction=true,safe=true;int predictions=0,safetyChecks=0,orders=0;std::string SafetyReason="friendly";
bool Predict(void*,float,float,Vec&){predictions++;return prediction;}
bool Safe(Battery*,Vec,float){safetyChecks++;return safe;}
void Tick(Battery* battery){void* report=nullptr;float now=100;for(int i=0;i<1;i++){
'''+preflight+'''orders++;}}};
int main(){Test t;Battery battery;t.prediction=false;t.Tick(&battery);assert(!t.orders&&!t.safetyChecks);
t.prediction=true;t.safe=false;t.Tick(&battery);assert(!t.orders&&battery.holds==1);
t.safe=true;t.Tick(&battery);assert(t.orders==1&&t.safetyChecks==2);}
''')
# Prevent the exact mounted Defend regression from returning: mounted escorts
# keep Move-to-cover/native vehicle combat, while infantry retain flank/search.
tactics=r/'ERR_SupplyConvoyTactics.c'
assert 'DefendWaypoint()' not in body(tactics,'protected void Support(')
assert 'orders.Add(Move(support))' in body(tactics,'protected void Support(')
assert 'SearchWaypoint()' in body(tactics,'protected void Flank(')
print('PASS: stale/deleted AI utility cannot receive Complete; mortar HQ preflight withholds unsafe orders; mounted convoy cover never creates infantry Defend')
# The native RHS fixture reproduces this boundary: the convenience API boards
# before applying a group faction. Keep production affiliation ahead of entry.
flight=r/'ERR_ReconFlightComponent.c'
prepare=body(flight,'protected bool PrepareCrew(')
assert 'SpawnCharacterInCompartment(' not in prepare
assert prepare.index('ERR_Actors.SetFaction(initialCrew,') < prepare.index('access.GetInVehicle(')
assert prepare.index('m_Crew.AddAgent(') < prepare.index('access.GetInVehicle(')
assert 'SpawnCharacterInCompartment(' not in body(flight,'void Initialize(')
finish=body(flight,'protected bool FinishCrew(')
assert finish.index('now - m_Started > 45000') < finish.index('m_Assault.Preparing()')
print('PASS: flight crew affiliation and group precede native boarding; preparation remains bounded even while passengers wait')
+4 -3
View File
@@ -69,7 +69,7 @@ for name in ('unit', 'utility'):
for name in ('Entity', 'Group', 'Waypoint', 'm_Perception'):
movement = movement.replace(name+'.', name+'->')
movement = movement.replace('GetPhysics().', 'GetPhysics()->').replace('unit->Repaths.ToString()', 'std::to_string(unit->Repaths)')
movement = movement.replace('string.Format', 'Format').replace('ERR_HQ.Event', 'Event')
movement = movement.replace('string.Format', 'Format').replace('ERR_HQ.Event', 'Event').replace('ERR_HunterCombat.Active', 'ERR_HunterCombat::Active')
movement = re.sub(r'\b(bool|float|Vec) (\w+);', r'\1 \2 {};', movement)
run(r'''
#include <cassert>
@@ -82,6 +82,7 @@ struct Utility {Perception* m_Perception;};
struct Physics {Vec GetVelocity(){return {};}};
struct Vehicle {Vec pos;Physics physics;Vec GetOrigin(){return pos;}Physics* GetPhysics(){return &physics;}};
struct Group {Utility utility;Utility* GetGroupUtilityComponent(){return &utility;}void AddWaypoint(void*){}};
namespace ERR_HunterCombat {bool fresh=false;bool Active(Group*){return fresh;}};
struct Waypoint {Vec pos;bool arrived=false;bool IsWithinCompletionRadius(Group*){return arrived;}Vec GetOrigin(){return pos;}void SetCompletionRadius(float){}};
struct MovingHunter {Vehicle* Entity;struct Group* Group;struct Waypoint* Waypoint;Vec Destination,ProgressPosition;float ProgressAt=1,NextMotionLog=0;int Repaths=0;std::string Source="test";void ClearOrder(){Waypoint=nullptr;}};
Waypoint spawned;Waypoint* Spawn(Vec pos){spawned.pos=pos;return &spawned;}
@@ -96,8 +97,8 @@ int main(){
ObserveMovement(&hunter,120001);ObserveMovement(&hunter,180001);assert(hunter.Repaths==3);
ObserveMovement(&hunter,240001);assert(hunter.Repaths==3);
vehicle.pos={20,0};ObserveMovement(&hunter,241001);assert(hunter.Repaths==0);
perception.m_aTargets.empty=false;ObserveMovement(&hunter,400000);assert(hunter.Repaths==0);
perception.m_aTargets.empty=true;ObserveMovement(&hunter,400001);assert(hunter.Repaths==0);
perception.m_aTargets.empty=false;ERR_HunterCombat::fresh=true;ObserveMovement(&hunter,400000);assert(hunter.Repaths==0);
ERR_HunterCombat::fresh=false;ObserveMovement(&hunter,400001);assert(hunter.Repaths==0);
ObserveMovement(&hunter,460000);assert(hunter.Repaths==1);
}
''')
+32 -6
View File
@@ -2,23 +2,27 @@
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=body(p,'protected bool SkipOptional(')+'\n'+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;')
s=s.replace('float floor;','float floor=0;').replace('protected ', '').replace('ref ERR_SiteDefinition retained', 'Site* retained').replace('retained.', 'retained->').replace('m_Definition.', 'm_Definition->').replace('ERR_LocationKind.', 'Kind::')
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <algorithm>
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;}};
namespace Kind{enum{PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
struct Site{int Kind=Kind::PRISON;bool required=false;string Label="Test 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();}int Find(T v){auto i=std::find(this->begin(),this->end(),v);return i==this->end()?-1:i-this->begin();}void Remove(int i){this->erase(this->begin()+i);}void InsertAt(T v,int i){this->insert(this->begin()+i,v);}};
struct Definition{Array<Site*> Sites;Site* required=nullptr;bool Valid(){return !required||Sites.Find(required)>=0;}};
struct Terrain{bool clear=true;Site* refused=nullptr;int queries=0;bool SiteAt(Site* site,vector,float,float& height){queries++;height=42;return clear&&site!=refused;}};
struct Scheduler{
Definition definition;Definition* m_Definition=&definition;
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;}
@@ -31,6 +35,28 @@ int main(){
{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);}
// Each optional type can fail after exhausting candidates without blocking the next site.
for(int kind : {Kind::OUTPOST,Kind::HOSPITAL,Kind::DEPOT}){
Clock::now=0;Scheduler s;Site a,b;a.Kind=kind;a.Id="unplaceable";b.Id="next";
s.m_Pending={&a,&b};s.definition.Sites={&a,&b};s.m_Terrain.refused=&a;s.m_Attempts=12000;
while(s.Tick()==Result::PENDING){}
assert(s.m_Result==Result::READY&&s.m_Error.empty()&&!a.PlacementResolved&&b.PlacementResolved);
assert(s.definition.Sites.size()==1&&s.definition.Sites[0]==&b);
int calls=s.m_Terrain.queries;assert(s.Tick()==Result::READY&&s.m_Terrain.queries==calls);
}
// A deadline skip gets a fresh candidate/time budget for the following optional site.
{Clock::now=30001;Scheduler s;Site a,b;a.Kind=Kind::OUTPOST;b.Kind=Kind::HOSPITAL;
s.m_Pending={&a,&b};s.definition.Sites={&a,&b};s.m_Terrain.refused=&a;
while(s.Tick()==Result::PENDING){}assert(s.m_Result==Result::READY&&b.Position[0]!=10&&b.PlacementResolved);}
// Required objectives (including mortars) still fail; no unsafe terrain is accepted.
for(int kind : {Kind::PRISON,Kind::RADIO,Kind::EXTRACTION,Kind::MORTAR}){
Clock::now=0;Scheduler s;Site a;a.Kind=kind;s.m_Pending={&a};s.definition.Sites={&a};s.m_Attempts=12000;s.m_Terrain.clear=false;
assert(s.Tick()==Result::FAILED&&s.definition.Sites[0]==&a&&!a.PlacementResolved);
}
// Optional by type can still be indispensable for minimum accessible radio intel.
{Clock::now=0;Scheduler s;Site before,a,after;a.Kind=Kind::HOSPITAL;
s.definition.Sites={&before,&a,&after};s.definition.required=&a;s.m_Pending={&a};s.m_Attempts=12000;s.m_Terrain.clear=false;
assert(s.Tick()==Result::FAILED&&s.definition.Sites.size()==3&&s.definition.Sites[1]==&a&&!a.PlacementResolved);}
}
''')
print('PASS: actual generation scheduler bounds each tick, enforces deadline/candidate ceiling, validates fallback, commits only accepted positions, and terminates without repeated searches')
print('PASS: actual generation scheduler bounds each tick, enforces deadline/candidate ceiling, validates fallback, commits only accepted positions, skips only dispensable sites, preserves required objectives/intel, and resets the next search budget')
+1
View File
@@ -47,6 +47,7 @@ struct RplId {static int Invalid(){return -1;}};
struct Game {int spawns=0;SCR_ManualCamera* SpawnEntityPrefab(Resource*,int,EntitySpawnParams){spawns++;return &camera;}int GetWorld(){return 1;}CameraManager* GetCameraManager(){return &manager;}} game;
Game* GetGame(){return &game;}
struct Controller {
void ERR_UpdateRecoveryCamera(ERR_GameMode*){}
int nativeChanges=0,starts=0;void ERR_StartGuide(){starts++;}
int m_ERRGuidePhase=-1;string m_ERRGuideMessage;
Actor* controlled=&actor;int m_ERRObserverReason=ERR_ObserverReason::NONE;
+5 -3
View File
@@ -8,7 +8,7 @@ H=BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'
A=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c'
def cpp(s):
s=s.replace('protected ','').replace('ERR_HQPursuitRules.', 'ERR_HQPursuitRules::').replace('string.Format','Format')
s=s.replace('ERR_HQLocalAwareness.Priority','ERR_HQLocalAwareness::Priority').replace('protected ','').replace('ERR_HQPursuitRules.', 'ERR_HQPursuitRules::').replace('string.Format','Format')
for t in ('ERR_HQStatusReport','ERR_HQAmmoSnapshot','ERR_HQUnit','ERR_HQRoute'):
s=re.sub(r'\b'+t+r' (?=\w)',t+'* ',s)
for v in ('stock','report','unit','route'):
@@ -45,10 +45,12 @@ struct ERR_HQRoute{int MessageId=0;string Payload;};
struct Network{bool available=true;int routes=0;ERR_HQRoute* Route(void*){routes++;return available?new ERR_HQRoute:nullptr;}};
struct Journal{int seq=0,writes=0;int Message(){return ++seq;}void Write(string,string,int){writes++;}};
struct Task{float ArrivedAt=0;};
struct ERR_HQUnit{struct Task* Task=nullptr;void* Radio=this;ERR_HQRoute* Uplink=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;
struct ERR_HQLocalAwareness{static bool Priority(float,float,float){return false;}};
struct ERR_HQUnit{float NextContact=0;struct Task* Task=nullptr;void* Radio=this;ERR_HQRoute* Uplink=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;
float NextStatus=0;ERR_HQAmmoReporting AmmoReporting;Sampler StatusSampler;bool visible=true,contact=false,sweep=false;
ERR_HQUnit(){AmmoReporting.Initialize(6,0);}};
struct HQ{Network m_Network;Journal m_Journal;int delivered=0;
struct HQ{void ContactReport(ERR_HQUnit*,float){}
Network m_Network;Journal m_Journal;int delivered=0;
bool OriginOccupied(void*,ERR_HQUnit*){return false;}
void Observe(ERR_HQUnit* u,float){if(!u->Uplink&&u->Radio&&u->visible&&m_Network.available){u->contact=true;u->Uplink=new ERR_HQRoute;}}
void ObserveBody(ERR_HQUnit*,float){}
+89
View File
@@ -0,0 +1,89 @@
"""Execute the actual local evidence cache and priority/radio intake methods."""
import re
from policy_harness import ROOT,body,run
base=ROOT/'addons/core/Scripts/Game/ERR/10_AI'
s=(base/'ERR_HQLocalAwareness.c').read_text().split('modded class')[0]
h=(base/'ERR_HQLocalHandoff.c').read_text().split('class ERR_HQObservationHistory')[1]
h='class ERR_HQObservationHistory'+h
q='\n'.join(body(base/'ERR_HQ.c',sig) for sig in ['protected void ContactReport(', 'protected void QueueReports('])
def trans(s):
s=s.replace('protected ','').replace('ref ','').replace('class ','struct ').replace('\n}\n','\n};\n')
s=s.replace('array<ERR_HQReport>', 'Array<ERR_HQReport*>').replace('array<IEntity>','Array<IEntity>').replace('array<float>','Array<float>')
for t in ['ERR_HQReport','ERR_HQObservationHistory','ERR_HQUnit','ERR_HQRoute','SCR_AIGroup']:
s=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',s)
s=s.replace('foreach (','for (').replace('null','nullptr')
for c in ['ERR_ForceRules','ERR_HQMemoryRules','ERR_HQLocalAwareness','Math']:
s=s.replace(c+'.',c+'::')
for c in ['known','report','source','history','unit','route']:
s=re.sub(r'\b'+c+r'\.',c+'->',s)
s=s.replace('m_Sound.', 'm_Sound->').replace('m_Visual[i].','m_Visual[i]->')
s=s.replace('unit->LocalAwareness.','unit->LocalAwareness->').replace('unit->SentObservations.','unit->SentObservations->')
s=s.replace('m_Mode.Settings().Runtime','m_Mode.Settings().Runtime')
s=s.replace('ERR_HQReport* m_Sound;', 'ERR_HQReport* m_Sound=nullptr;').replace('float m_NextSound;', 'float m_NextSound=0;')
return s.replace('unit->Task.', 'unit->Task->')
run(r'''
#include <cassert>
#include <algorithm>
#include <cmath>
#include <string>
#include <vector>
using string=std::string;
struct Actor{};using IEntity=Actor*;
struct SCR_AIGroup:Actor{};
struct vector{float v[3]={0,0,0};float& operator[](int i){return v[i];}};
vector Vector(float x,float y,float z){return {{x,y,z}};}
namespace Math{float Floor(float f){return std::floor(f);}}
namespace ERR_ForceRules{bool Finite(float f){return std::isfinite(f);}}
namespace ERR_HQMemoryRules{bool PositionValid(vector p){return std::isfinite(p[0])&&std::isfinite(p[1])&&std::isfinite(p[2]);}}
template<class T>struct Array:std::vector<T>{void Clear(){this->clear();}int Count(){return this->size();}
void Insert(T x){this->push_back(x);}void Remove(int i){this->erase(this->begin()+i);}int Find(T x){auto p=std::find(this->begin(),this->end(),x);return p==this->end()?-1:p-this->begin();}};
struct ERR_HQReport{IEntity Target=nullptr;SCR_AIGroup* Source=nullptr;vector Position;float ObservedAt=0;
int MessageId=0;string TravelMode="unknown";string Weapon="unknown";
ERR_HQReport* Snapshot(){return new ERR_HQReport(*this);}string Description(float){return TravelMode;}};
'''+trans(h)+trans(s)+r'''
struct ERR_HQRoute{int MessageId=0;string Payload;};
struct Task{float ArrivedAt=0;};
struct ERR_HQUnit{SCR_AIGroup* Group=nullptr;IEntity Radio=nullptr;ERR_HQLocalAwareness own;
ERR_HQLocalAwareness* LocalAwareness=&own;ERR_HQObservationHistory hist;ERR_HQObservationHistory* SentObservations=&hist;
ERR_HQRoute* Uplink=nullptr;ERR_HQReport* PendingReport=nullptr;struct Task* Task=nullptr;
float NextContact=0,NextReport=0,NextStatus=0;bool status=false;};
struct Settings{int Runtime(string){return 10;}};
struct Mode{struct Settings Settings(){return {};}};
struct Journal{int id=0;int Message(){return ++id;}void Write(string,string,int){}};
struct HQ{Mode m_Mode;Journal m_Journal;bool network=true,occupied=false;
ERR_HQRoute* ObservationRoute(ERR_HQUnit*){return network?new ERR_HQRoute:nullptr;}
bool OriginOccupied(IEntity,ERR_HQUnit*){return occupied;}
void StatusReport(ERR_HQUnit* u,float now){if(!u->Uplink&&now>=u->NextStatus){u->Uplink=new ERR_HQRoute;u->status=true;u->NextStatus=now+180000;}}
void Observe(ERR_HQUnit*,float){} void ObserveBody(ERR_HQUnit*,float){} void Sweep(ERR_HQUnit*,float){}
'''+trans(q)+r'''
};
int main(){
SCR_AIGroup group;Actor enemy;ERR_HQLocalAwareness cache;ERR_HQObservationHistory history;
ERR_HQReport r;r.Target=&enemy;r.Source=&group;r.Position=Vector(20,0,40);r.ObservedAt=100;r.Weapon="rifle";
cache.Remember(&r,100);r.Position[0]=900;r.Weapon="launcher";
auto held=cache.Peek(1200,&history);assert(held&&held->Position[0]==20&&held->Weapon=="rifle"&&held->ObservedAt==100);
assert(cache.Peek(30100,&history)&&!cache.Peek(30101,&history)); // original time, never refresh on reads
r.ObservedAt=32000;cache.Remember(&r,32000);assert(!cache.CaptureDue(&enemy,32999)&&cache.CaptureDue(&enemy,33000));
held=cache.Peek(33000,&history);history.Queued(held->Target,held->ObservedAt,33000);cache.Queued(held,33000);
cache.Remember(&r,33000);assert(!cache.Peek(33000,&history)); // same observation cannot replay
cache.Hear(&group,Vector(12,8,-1),34000,34000);auto sound=cache.Peek(34000,&history);
assert(sound&&!sound->Target&&sound->Source==&group&&sound->Position[0]==75&&sound->Position[2]==-75);
assert(sound->TravelMode=="heard gunfire"&&sound->Weapon=="unknown");
cache.Queued(sound,34000);cache.Hear(&group,Vector(800,0,0),35000,35000);
assert(!cache.Peek(63999,&history)&&cache.Peek(64000,&history));
cache.Clear();assert(!cache.Peek(64000,&history));
Actor targets[40];for(int i=0;i<40;i++){r.Target=&targets[i];r.ObservedAt=70000+i;cache.Remember(&r,70000+i);}
assert(cache.m_Visual.Count()==16);cache.Clear();
HQ h;ERR_HQUnit u;u.Group=&group;r.Target=&enemy;r.ObservedAt=1000;
u.own.Remember(&r,1000);h.QueueReports(&u,2000);assert(u.PendingReport&&!u.status&&u.PendingReport->ObservedAt==1000);
auto inFlight=u.Uplink;r.ObservedAt=3000;u.own.Remember(&r,3000);h.QueueReports(&u,4000);assert(u.Uplink==inFlight);
u.Uplink=nullptr;u.PendingReport=nullptr;r.ObservedAt=16000;u.own.Remember(&r,16000);
h.QueueReports(&u,16000);assert(u.status&&!u.PendingReport); // bounded status delay under continuous contacts
u.Uplink=nullptr;u.status=false;h.QueueReports(&u,17000);assert(u.PendingReport&&u.PendingReport->ObservedAt==16000);
u.Uplink=nullptr;u.PendingReport=nullptr;r.ObservedAt=23000;u.own.Remember(&r,23000);
h.network=false;h.ContactReport(&u,23000);assert(!u.Uplink&&u.own.Peek(24000,&u.hist));
h.network=true;h.ContactReport(&u,24000);assert(!u.Uplink); // failed-route retry bounded
h.ContactReport(&u,28000);assert(u.PendingReport&&u.PendingReport->ObservedAt==23000);
}
''')
print('PASS: captured sighting retention, immutable details, expiry, replay rejection, bounded sound uncertainty/cooldown, radio loss, priority and overdue-status fairness')
+1 -1
View File
@@ -83,7 +83,7 @@ int main(){
// Per-source watermark rejects duplicate/replayed/future/stale sightings including failed-route attempts.
{ERR_HQObservationHistory history;IEntity target;assert(history.CanQueue(&target,100,100));history.Queued(&target,100,100);
assert(!history.CanQueue(&target,100,101)&&!history.CanQueue(&target,99,101));assert(history.CanQueue(&target,101,101));
assert(!history.CanQueue(&target,102,101)&&!history.CanQueue(&target,101,602));
assert(!history.CanQueue(&target,102,101)&&history.CanQueue(&target,101,602)&&history.CanQueue(&target,101,30101)&&!history.CanQueue(&target,101,30102));
assert(!history.CanQueue(&target,std::numeric_limits<float>::quiet_NaN(),602));}
{ERR_HQObservationHistory history;IEntity targets[65];for(int i=0;i<64;i++){history.Queued(&targets[i],100,100);}assert(!history.CanQueue(&targets[64],100,100));
assert(history.CanQueue(&targets[64],120101,120101));assert(!history.CanQueue(&targets[0],100,120101));}
+5 -3
View File
@@ -4,7 +4,7 @@ from pathlib import Path
from policy_harness import ROOT,body,run
p=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
s='\n'.join(body(p,x) for x in ('protected bool OriginOccupied(', 'protected ERR_HQRoute ObservationRoute(', 'protected void Observe(', 'protected void QueueReports('))
s=s.replace('protected ','').replace('array<IEntity>','Arr<IEntity>').replace('array<ref ERR_HQVisualContact>','Arr<ERR_HQVisualContact*>').replace('foreach (','for (')
s=s.replace('ERR_HQLocalAwareness.Priority','ERR_HQLocalAwareness::Priority').replace('protected ','').replace('array<IEntity>','Arr<IEntity>').replace('array<ref ERR_HQVisualContact>','Arr<ERR_HQVisualContact*>').replace('foreach (','for (')
for name in ('ERR_HQUnit','ERR_HQRoute','ERR_HQReport','ERR_HQVisualContact'):
s=re.sub(r'\b'+name+r' (\w+)',name+r'* \1',s)
for name in ('unit','other','route','own','seen','report'):
@@ -33,12 +33,14 @@ struct ERR_HQReport{int MessageId=0,Position=0;IEntity Target=nullptr;Group*Sour
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;};
struct ERR_HQLocalAwareness{static bool Priority(float,float,float){return false;}};
struct ERR_HQUnit{float NextContact=0,NextStatus=0;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;};
struct Network{ERR_HQRoute*own=nullptr,*local=nullptr;IEntity ownCarrier=nullptr;int attempts=0;Arr<IEntity>lastBusy;
ERR_HQRoute*Route(IEntity caller){return caller==ownCarrier?own:nullptr;}
ERR_HQRoute*LocalRoute(Group*source,int,Arr<IEntity>&busy){attempts++;lastBusy=busy;if(!local||busy.Contains(local->Nodes[0]->Carrier))return nullptr;local->LocalSource=source;return local;}};
struct Journal{int next=0,reports=0;int Message(){return ++next;}void Write(string kind,string,int){if(kind=="REPORT")reports++;}};
struct HQ{Mode mode;Mode*m_Mode=&mode;Network m_Network;Journal m_Journal;Arr<ERR_HQUnit*>m_Units;int statuses=0,bodies=0,sweeps=0;
struct HQ{void ContactReport(ERR_HQUnit*,float){}
Mode mode;Mode*m_Mode=&mode;Network m_Network;Journal m_Journal;Arr<ERR_HQUnit*>m_Units;int statuses=0,bodies=0,sweeps=0;
void StatusReport(ERR_HQUnit*,float){statuses++;}void ObserveBody(ERR_HQUnit*,float){bodies++;}void Sweep(ERR_HQUnit*,float){sweeps++;}
''' + s.replace('static void Describe(auto*,auto&,float)','template<class T,class A>static void Describe(T*,A&,float)') + r'''
};
+32
View File
@@ -132,6 +132,26 @@ int main(){
assert(memory.Get(&a)->SampleCount==8&&memory.Get(&a)->BeliefRevision==12&&memory.Get(&a)->HasVelocity);
auto old=Report(13,&a,23000);assert(!memory.Ingest(&old,25000)&&memory.Get(&a)->BeliefRevision==12);
auto jump=Report(14,&a,26000,{10000,0,0});assert(memory.Ingest(&jump,26000)&&!memory.Get(&a)->HasVelocity);
// A near-simultaneous second observer must not erase credible motion
// history. Use the nearest sample at least two seconds old, never a live
// entity position or an older convenient sample across a mode change.
memory.Clear();
auto spaced=Report(1,&a,1000,{10,0,20});assert(memory.Ingest(&spaced,1000));
auto closeA=Report(2,&a,4000,{10,0,20});assert(memory.Ingest(&closeA,4000));
auto closeB=Report(3,&a,4500,{10,0,20});assert(memory.Ingest(&closeB,4500));
assert(memory.Get(&a)->HasVelocity&&near(memory.Get(&a)->Speed,0));
auto moving=Report(4,&a,5000,{18,0,20});assert(memory.Ingest(&moving,5000));
assert(memory.Get(&a)->HasVelocity&&near(memory.Get(&a)->Speed,2));
auto boarded=Report(5,&a,5500,{18,0,20},"vehicle");assert(memory.Ingest(&boarded,5500));
assert(!memory.Get(&a)->HasVelocity);
auto dismounted=Report(6,&a,6000,{18,0,20});assert(memory.Ingest(&dismounted,6000));
assert(!memory.Get(&a)->HasVelocity); // do not bridge the intervening vehicle observation
auto farJump=Report(7,&a,8000,{10000,0,0});assert(memory.Ingest(&farJump,8000));
assert(!memory.Get(&a)->HasVelocity); // do not skip an implausible eligible pair
memory.Clear();
auto rapidA=Report(1,&a,1000);assert(memory.Ingest(&rapidA,1000));
auto rapidB=Report(2,&a,1500);assert(memory.Ingest(&rapidB,1500));
assert(!memory.Get(&a)->HasVelocity); // insufficient time baseline still refuses motion
// No equal/weak burst can evict a stronger current sighting; later stronger reports can.
memory.Clear();Identity people[70];for(int i=0;i<70;i++)people[i].id=i+10;
for(int i=0;i<64;i++){auto r=Report(i+1,&people[i],0);assert(memory.Ingest(&r,0));}
@@ -139,6 +159,18 @@ int main(){
over.ObservedAt=10000;assert(memory.Ingest(&over,10000)&&memory.Count()==64);
auto weak=Report(66,&people[65],10000);weak.Distress=true;assert(!memory.Ingest(&weak,10000));
assert(!memory.At(-1)&&!memory.At(64)&&!memory.Get(nullptr));
// Heard fire is source-keyed, imprecise, short-lived, never a precision target.
memory.Clear();auto sound=Report(1,nullptr,1000,{75,0,225},"heard gunfire");sound.Source=&friendly;
assert(memory.Ingest(&sound,1000));auto noise=memory.At(0);
assert(noise->Kind==ERR_HQMemoryKind::GUNFIRE&&!noise->Target&&!noise->HasVelocity&&!noise->Actionable());
assert(near(noise->Radius,150)&&near(noise->Confidence,.4));
int soundId=noise->BeliefId;assert(memory.NegativeSearch(soundId,1,1000));
assert(near(memory.GetById(soundId)->Confidence,.26));
sound.MessageId=2;sound.ObservedAt=3000;sound.Position={375,0,225};assert(memory.Ingest(&sound,3000));
assert(memory.Count()==1&&!memory.At(0)->HasVelocity&&memory.At(0)->Target==nullptr);
auto visual=Report(3,&a,3000,{400,0,210});assert(memory.Ingest(&visual,3000));
assert(memory.Count()==2&&memory.Get(&a)->Actionable()&&near(memory.Get(&a)->Confidence,1));
memory.Tick(63000);assert(near(memory.GetById(soundId)->Confidence,.2));
// The global replay table is bounded even when valid observation IDs arrive rapidly.
memory.Clear();for(int i=1;i<=4096;i++){auto r=Report(i,&a,float(i));assert(memory.Ingest(&r,float(i)));}
auto full=Report(4097,&a,4097);assert(!memory.Ingest(&full,4097)&&memory.m_Seen.Count()==4096);
+5 -3
View File
@@ -10,7 +10,7 @@ legacy='Observe(unit, now);\n ObserveBody(unit, now);\n StatusReport(unit, n
assert legacy in original or 'QueueReports(unit, now);' in original
def cpp(s):
s=s.replace('protected ','').replace('array<ref ERR_HQVisualContact>','Array<ERR_HQVisualContact*>')
s=s.replace('ERR_HQLocalAwareness.Priority','ERR_HQLocalAwareness::Priority').replace('protected ','').replace('array<ref ERR_HQVisualContact>','Array<ERR_HQVisualContact*>')
for t in ('ERR_HQAmmoSnapshot','ERR_HQUnit','ERR_HQVisualContact','ERR_HQRoute','ERR_HQReport','ERR_HQStatusReport','ERR_HQTask','ERR_HQSearchReport'):
s=re.sub(r'\b'+t+r' (?=\w)',t+'* ',s)
s=s.replace('ERR_HQRoute* route;', 'ERR_HQRoute* route=nullptr;')
@@ -62,7 +62,8 @@ struct Network{bool available=true;int routes=0;ERR_HQRoute* Route(void*){routes
struct Bodies{bool visible=false;bool Observe(Group*,float now,ERR_HQReport*& r){if(!visible)return false;r=new ERR_HQReport;r->ObservedAt=now;return true;}};
struct Journal{int seq=0;int Message(){return ++seq;}void Write(string,string,int){}};
struct History{bool CanQueue(void*,float,float){return true;}void Queued(void*,float,float){}};
struct ERR_HQUnit{History SentObservations;int Id=1;struct Group ownGroup;struct Utility ownUtility;Selection selection;Sampler sampler;
struct ERR_HQLocalAwareness{static bool Priority(float,float,float){return false;}};
struct ERR_HQUnit{float NextContact=0;History SentObservations;int Id=1;struct Group ownGroup;struct Utility ownUtility;Selection selection;Sampler sampler;
struct Group* Group=&ownGroup;struct Utility* Utility=&ownUtility;void* Radio=this;
Selection* ReportSelection=&selection;Sampler* StatusSampler=&sampler;
ERR_HQRoute* Uplink=nullptr;ERR_HQReport* PendingReport=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;ERR_HQSearchReport* PendingSweep=nullptr;
@@ -71,7 +72,8 @@ struct ERR_HQUnit{History SentObservations;int Id=1;struct Group ownGroup;struct
ERR_HQTask* Task=nullptr;void* Assignment=nullptr;void* Waypoint=nullptr;
bool Eligible(Mode*){return eligible;}
};
struct HQ{Mode mode;Mode* m_Mode=&mode;Network m_Network;Bodies m_Bodies;Journal m_Journal;
struct HQ{void ContactReport(ERR_HQUnit*,float){}
Mode mode;Mode* m_Mode=&mode;Network m_Network;Bodies m_Bodies;Journal m_Journal;
bool OriginOccupied(void*,ERR_HQUnit*){return false;}
ERR_HQRoute* ObservationRoute(ERR_HQUnit* u){return u->Radio?m_Network.Route(u->Radio):nullptr;}
bool Fighting(ERR_HQUnit* u){return u->fighting;}
+3 -3
View File
@@ -107,7 +107,7 @@ struct BodyAdapter{int confirmations=0;void ConfirmDelivered(void*){confirmation
struct CoverageAdapter{int forgotten=0;void Forget(vector){forgotten++;}};
struct JournalAdapter{void Write(string,string,int){}};
template<class... Args> string Format(string,Args...){return "delivered";}
struct HQDelivery{
struct HQDelivery{float m_NextAssignment=5000;
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'''
@@ -251,7 +251,7 @@ int main(){
assert(wire->CasualtySlot==0&&wire->Position.x==900&&wire->CasualtySourceId=="deleted-during-uplink");
assert(intake.m_Watch.Count()==0); // preparing/queuing local evidence changes nothing
wire->Target=nullptr; // simulate native weak-reference invalidation after captured LOS, before radio completes
intake.Accept(wire,2000);
intake.Accept(wire,2000);assert(intake.m_NextAssignment==5000);
auto delivered=intake.m_Watch.Find("deleted-during-uplink");
assert(delivered&&delivered->Closed&&delivered->Count==0&&intake.m_Watch.ConfirmedLosses()==1);
assert(delivered->MessageId==0&&delivered->ObservedAt==-1); // no invented check-in or corpse position
@@ -259,7 +259,7 @@ int main(){
intake.Accept(wire,2001);assert(intake.m_Watch.ConfirmedLosses()==1); // delivered duplicates cannot add losses
wire->CasualtySourceId="stale";intake.Accept(wire,121001);assert(!intake.m_Watch.Find("stale"));
wire->ObservedAt=2000;wire->TravelMode="walking";wire->Target=&local;
intake.Accept(wire,2000);assert(!intake.m_Watch.Find("stale")&&intake.m_Contacts.Count()==1);
intake.Accept(wire,2000);assert(intake.m_NextAssignment==2000);assert(!intake.m_Watch.Find("stale")&&intake.m_Contacts.Count()==1);
wire->TravelMode="body";wire->Distress=true;intake.Accept(wire,2001);
assert(!intake.m_Watch.Find("stale")&&intake.m_Contacts.Count()==1); // distress cannot claim bodily identity
Watch bodyOnly;body.CasualtySourceId="no-survivor";body.CasualtySlot=0;body.ObservedAt=1000;
+27
View File
@@ -0,0 +1,27 @@
from policy_harness import ROOT,body,run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterCombat.c'
s=body(p,'static bool Recent(').replace('EAITargetInfoCategory.','EAITargetInfoCategory::')
run('''#include <cassert>
namespace EAITargetInfoCategory {enum {UNKNOWN,DESTROYED,DISARMED,LOST,DETECTED,IDENTIFIED};}
'''+s+'''
int main(){for(int c=0;c<6;c++){bool threat=c==4||c==5;assert(Recent(c,0)==threat);assert(Recent(c,15)==threat);assert(!Recent(c,-1));assert(!Recent(c,15.001f));}}
''')
print('PASS: only recent native detected/identified threats suspend hunter travel; stale/disarmed/destroyed contacts cannot strand patrols')
# Execute the actual state transition and owned-order cleanup with native perception doubled.
s=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c','protected void UpdateCombat(')
s=s.replace('protected ','').replace('ERR_HunterVehicle unit','Unit* unit').replace('unit.','unit->').replace('ERR_HunterCombat.Active','Active').replace('ERR_HQ.Event','Event').replace('null','nullptr')
run('''#include <cassert>
#include <string>
struct Group{bool contact=false;};
bool Active(Group* g){return g&&g->contact;}
int messages=0;void Event(std::string,std::string){messages++;}
struct Unit{bool Registered=true,CombatHeld=false;Group* Group=nullptr;float CombatQuietUntil=0,NextOrder=100;void* Downlink=nullptr;std::string Source="test";int clears=0;void ClearOrder(){clears++;}};
'''.replace('Group* Group','struct Group* Group')+s+'''
int main(){Group group;Unit u;u.Group=&group;u.Downlink=&group;UpdateCombat(&u,1000);assert(!u.CombatHeld&&u.clears==0&&u.Downlink);
group.contact=true;UpdateCombat(&u,2000);assert(u.CombatHeld&&u.clears==1&&!u.Downlink&&u.CombatQuietUntil==17000);
UpdateCombat(&u,5000);assert(u.clears==1&&u.CombatQuietUntil==20000&&messages==1);
group.contact=false;UpdateCombat(&u,19999);assert(u.CombatHeld);UpdateCombat(&u,20000);assert(!u.CombatHeld&&u.NextOrder==0&&messages==2);
Unit pending;pending.Group=&group;pending.Registered=false;group.contact=true;UpdateCombat(&pending,100);assert(!pending.CombatHeld&&pending.clears==0);
}
''')
print('PASS: actual hunter combat transition cancels only its travel once, holds through renewed contact and resumes after quiet; unregistered startup preserved')
+10 -1
View File
@@ -35,7 +35,7 @@ struct Group{int GetFaction(){return 1;}};
struct Entity{Vec GetOrigin(){return {};}};
struct Waypoint{bool complete=false;bool IsWithinCompletionRadius(Group*){return complete;}};
struct Route{};
struct ERR_HunterVehicle{float NextOrder=99999,LastContact=-1,NextContactOrder=0;Route* Downlink=nullptr;int SourceMessage=0;Vec Destination;string Source="hunter";struct Group* Group=nullptr;struct Entity* Entity=nullptr;struct Waypoint* Waypoint=nullptr;};
struct ERR_HunterVehicle{float NextOrder=99999,LastContact=-1,NextContactOrder=0;Route* Downlink=nullptr;int SourceMessage=0;Vec Destination,ContactPosition,DeliveredContactPosition;bool DeliveredContact=false;string Source="hunter";struct Group* Group=nullptr;struct Entity* Entity=nullptr;struct Waypoint* Waypoint=nullptr;};
struct ERR_HQReport{int Target=1;};
struct ERR_HQBelief{bool actionable=true;float ObservedAt=10,Confidence=1;int MessageId=7;Vec PredictedPosition={1000,0,2000};bool Actionable(){return actionable;}};
struct Memory{ERR_HQBelief belief;ERR_HQBelief* Get(int){return &belief;}};
@@ -72,6 +72,15 @@ int main(){
unit.Downlink=nullptr;service.QueueDestination(&unit,&hq,7000);assert(hq.requests==2); // no repeated order for same report
ERR_HunterVehicle disconnected;hq.connected=false;service.QueueDestination(&disconnected,&hq,10000);
assert(!disconnected.Downlink&&disconnected.LastContact==-1);
// Repeated/new reports in an already delivered area must not churn routing.
Waypoint route;unit.Waypoint=&route;unit.DeliveredContact=true;unit.DeliveredContactPosition={1000,0,2000};
unit.Downlink=nullptr;unit.NextOrder=0;unit.NextContactOrder=0;
int kept=hq.requests;int projections=ERR_SupplyConvoySystem::calls;
hq.memory.belief.ObservedAt=12;service.QueueDestination(&unit,&hq,12000);
assert(hq.requests==kept&&ERR_SupplyConvoySystem::calls==projections&&unit.LastContact==12);
hq.memory.belief.PredictedPosition.x=1100;hq.memory.belief.ObservedAt=13;
service.QueueDestination(&unit,&hq,18000);assert(hq.requests==kept+1); // Material displacement redirects.
hq.memory.belief.ObservedAt=11;
int requests=hq.requests;service.QueueDestination(&disconnected,&hq,11000);assert(hq.requests==requests);
hq.connected=true;service.QueueDestination(&disconnected,&hq,15000);assert(disconnected.Downlink&&disconnected.LastContact==11);
}
+80
View File
@@ -0,0 +1,80 @@
"""Execute the pre-spawn profile gate; verify registry-backed faction resources."""
from policy_harness import ROOT, body, run
import re
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'
method=body(p,'protected bool SpawnHunter(')
# The real preflight block runs before construction/insertion/native spawning.
preflight=method[method.index('{')+1:method.index(' ERR_HunterVehicle unit =')]
preflight=preflight.replace('ResourceName','std::string').replace('ERR_EnemyMedicalCatalog.','ERR_EnemyMedicalCatalog::').replace('ERR_EnemyLoadoutCatalog.','ERR_EnemyLoadoutCatalog::').replace('ERR_HQ.Event','ERR_HQ::Event').replace('string.Format','Format').replace('m_Mode.EnemyFaction().GetFactionKey()','"TEST_FACTION"')
run('''#include <cassert>
#include <string>
namespace ERR_EnemyMedicalCatalog {std::string soldier="soldier",medic="medic";std::string Crew(void*,bool m){return m?medic:soldier;}}
namespace ERR_EnemyLoadoutCatalog {bool Available(std::string p){return !p.empty()&&p!="invalid";}}
int logged=0;namespace ERR_HQ {void Event(std::string,std::string){logged++;}}
template<class...T>std::string Format(std::string text,T...){return text;}
struct Gate {void* m_Mode=nullptr;float m_NextSpawn=0;bool Check(float now){
'''+preflight+'''return true;}};
int main(){Gate gate;assert(gate.Check(1000)&&logged==0);
ERR_EnemyMedicalCatalog::medic="";assert(!gate.Check(1000)&&gate.m_NextSpawn==31000&&logged==1);
ERR_EnemyMedicalCatalog::medic="medic";ERR_EnemyMedicalCatalog::soldier="invalid";assert(!gate.Check(5000)&&gate.m_NextSpawn==35000);
ERR_EnemyMedicalCatalog::soldier="soldier";assert(gate.Check(35000));
}
''')
registry=(ROOT/'addons/core/Configs/ERR/Content/Registry.conf').read_text()
for key in ('RHS_ION','RHS_USAF','RHS_AFRF'):
option=next(x for x in registry.split('ERR_ContentOption {') if f'NativeFactionKey "{key}"' in x)
assert re.search(r'ResponseSoldier "\{[A-F0-9]{16}\}[^"\n]+\.et"',option)
assert re.search(r'ResponseMedic "\{[A-F0-9]{16}\}[^"\n]*Medic[^"\n]*\.et"',option)
print('PASS: missing/invalid faction crew defers before any native spawn or allowance insertion; all registered RHS factions have explicit soldier and medic profiles')
# Execute actual finite vehicle-radio initialization: pending/refused/lost radios
# never cause repeat allocations, and admission requires physical capability.
prepare=body(p,'protected bool PrepareVehicleRadio(').replace('protected ','').replace('ERR_HunterVehicle unit','Unit* unit')
prepare=prepare.replace('InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(unit.Entity.FindComponent(InventoryStorageManagerComponent));','Storage* storage = vehicleStorage;')
prepare=prepare.replace('ref ','').replace('unit.','unit->').replace('storage.','storage->').replace('RadioOperation.','RadioOperation->').replace('ERR_HQ.Event','Event').replace('ERR_EnemyRadio.MANPACK','"manpack"').replace('EStoragePurpose.PURPOSE_DEPOSIT','1').replace('null','nullptr')
run('''#include <cassert>
#include <string>
struct ERR_EnemyEquipmentCallback{bool Finished=false,Succeeded=false;};
struct Unit{bool RadioRequested=false;ERR_EnemyEquipmentCallback* RadioOperation=nullptr;void* Driver=nullptr;std::string Source="uaz";};
struct Storage{int calls=0;bool accepts=true;bool TrySpawnPrefabToStorage(std::string,void*,int,int,ERR_EnemyEquipmentCallback*){calls++;return accepts;}};
void Event(std::string,std::string){}
struct Service{Storage* vehicleStorage=nullptr;int physicalCapability=0;int VehicleRadioCapability(void*){return physicalCapability;}
'''+prepare+'''
};
int main(){Service service;Storage storage;service.vehicleStorage=&storage;Unit unit;
assert(!service.PrepareVehicleRadio(&unit)&&storage.calls==1);for(int i=0;i<10;i++)assert(!service.PrepareVehicleRadio(&unit));assert(storage.calls==1);
unit.RadioOperation->Finished=true;unit.RadioOperation->Succeeded=true;assert(!service.PrepareVehicleRadio(&unit));
service.physicalCapability=2;assert(service.PrepareVehicleRadio(&unit)&&storage.calls==1);
service.physicalCapability=0;assert(!service.PrepareVehicleRadio(&unit)&&storage.calls==1); // Loot does not replenish the radio.
Unit refused;storage.accepts=false;assert(!service.PrepareVehicleRadio(&refused));for(int i=0;i<10;i++)assert(!service.PrepareVehicleRadio(&refused));assert(storage.calls==2);
}
''')
print('PASS: vehicle radio issued once; pending, refusal and looting cannot duplicate or fabricate a physical radio link')
capability=body(p,'int VehicleRadioCapability(')
capability=capability.replace('IEntity actor','Entity* actor').replace('foreach (ERR_HunterVehicle unit : m_Hunters)','for (Unit* unit : m_Hunters)').replace('InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(unit.Entity.FindComponent(InventoryStorageManagerComponent));','Storage* storage = unit->Entity->storage;')
capability=capability.replace('array<IEntity> items = {};','std::vector<Entity*> items;').replace('foreach (IEntity item : items)','for (Entity* item : items)').replace('item.FindComponent(BaseRadioComponent)','item->baseRadio').replace('SCR_RadioComponent radio = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));','Radio* radio = item->radio;')
for obj in ('m_Mode','unit','storage','radio'):capability=capability.replace(obj+'.',obj+'->')
capability=capability.replace('ERR_Actors.Alive','Alive').replace('CompartmentAccessComponent.GetVehicleIn','VehicleIn').replace('ERR_ConvoyRules.Captured','Captured')
run('''#include <cassert>
#include <vector>
#include <algorithm>
struct Radio{bool pack=true;bool ERR_IsHQManpack(){return pack;}};
struct Storage;
struct Entity{bool alive=true,baseRadio=true,captured=false;Entity* vehicle=nullptr;Storage* storage=nullptr;Radio* radio=nullptr;};
struct Storage{std::vector<Entity*>items;void GetItems(std::vector<Entity*>&out){out=items;}};
struct Members:std::vector<Entity*>{bool Contains(Entity* p){return std::find(begin(),end(),p)!=end();}};
struct Mode{bool active=true;bool Active(){return active;}};
struct Unit{struct Entity* Entity=nullptr;void* Group=nullptr;bool Released=false,valid=true;struct Members Members;bool CrewValid(Mode*){return valid;}};
bool Alive(Entity* e){return e&&e->alive;}Entity* VehicleIn(Entity* e){return e->vehicle;}bool Captured(Entity* e,void*){return e->captured;}
struct Service{Mode* m_Mode=nullptr;bool m_Stopped=false;std::vector<Unit*>m_Hunters;
'''+capability+'''
};
int main(){Service s;Mode mode;s.m_Mode=&mode;Entity actor,car,item;Radio radio;Storage storage;Unit unit;
unit.Entity=&car;unit.Members.push_back(&actor);actor.vehicle=&car;car.storage=&storage;item.radio=&radio;storage.items={&item};s.m_Hunters={&unit};
assert(s.VehicleRadioCapability(&actor)==2);storage.items.clear();assert(s.VehicleRadioCapability(&actor)==0);storage.items={&item};
actor.vehicle=nullptr;assert(s.VehicleRadioCapability(&actor)==0);actor.vehicle=&car;car.captured=true;assert(s.VehicleRadioCapability(&actor)==0);car.captured=false;
car.alive=false;assert(s.VehicleRadioCapability(&actor)==0);car.alive=true;unit.Released=true;assert(s.VehicleRadioCapability(&actor)==0);unit.Released=false;
mode.active=false;assert(s.VehicleRadioCapability(&actor)==0);mode.active=true;s.m_Stopped=true;assert(s.VehicleRadioCapability(&actor)==0);s.m_Stopped=false;
radio.pack=false;assert(s.VehicleRadioCapability(&actor)==0);radio.pack=true;Entity stranger;stranger.vehicle=&car;assert(s.VehicleRadioCapability(&stranger)==0);
}
''')
print('PASS: actual vehicle-radio link requires the original seated crew, physical manpack and live uncaptured mission vehicle; looting, dismount, release and teardown remove it')
+107
View File
@@ -0,0 +1,107 @@
"""Execute the production infantry combat state machine against waypoint doubles."""
import re
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryCombat.c'
s=p.read_text()
s=s.replace('class ERR_InfantryCombat', 'struct ERR_InfantryCombat')
s=s.replace('protected ', '').replace('ref ', '').replace('null','nullptr')
s=s.replace('EAIGroupCombatMode.FIRE_AT_WILL','1').replace('.IsEmpty()', '.empty()')
for typ in ('AIWaypoint','ERR_HQTask','ERR_HQUnit'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
s=s.replace('bool Held;', 'bool Held=false;').replace('float QuietUntil;', 'float QuietUntil=0;')
s=s.replace('m_PausedWaypoint;', 'm_PausedWaypoint=nullptr;').replace('m_PausedTask;', 'm_PausedTask=nullptr;')
for name in ('unit','task','Task','Group','Utility'):
s=s.replace(name+'.',name+'->')
s=s.rstrip()+';'
run(r'''
#include <cassert>
#include <vector>
#include <string>
#include <algorithm>
struct AIWaypoint{};
struct ERR_HQTask {bool ReleaseOnly=false,RoadIntercept=false;std::string SecuritySite;float ExpiresAt=100000;};
struct Group {std::vector<AIWaypoint*> queue;int adds=0,removes=0;
void RemoveWaypoint(AIWaypoint* w){removes++;queue.erase(std::remove(queue.begin(),queue.end(),w),queue.end());}
void AddWaypointAt(AIWaypoint* w,int slot){assert(slot==0);adds++;queue.insert(queue.begin(),w);}};
struct Utility{int mode=0,changes=0;void SetCombatMode(int m){mode=m;changes++;}};
struct ERR_HQUnit{AIWaypoint* Waypoint=nullptr;ERR_HQTask* Task=nullptr;void* Assignment=nullptr;struct Group* Group;struct Utility* Utility;};
'''+s+r'''
struct Fixture {Group group;Utility utility;AIWaypoint search,home;ERR_HQTask task;ERR_HQUnit unit;ERR_InfantryCombat combat;
Fixture(){unit.Group=&group;unit.Utility=&utility;unit.Task=&task;unit.Assignment=&task;unit.Waypoint=&search;group.queue={&search,&home};}
};
int main(){
{Fixture f;assert(f.combat.Update(&f.unit,true,false,1000)==0);assert(f.group.removes==0);
assert(f.combat.Update(&f.unit,true,true,2000)==1);assert(f.combat.Held&&f.combat.Paused());
assert(f.group.queue.size()==1&&f.group.queue[0]==&f.home&&f.utility.mode==1);
assert(f.unit.Task==&f.task&&f.unit.Waypoint==&f.search&&f.unit.Assignment);
assert(f.combat.Update(&f.unit,true,true,4000)==0&&f.group.removes==1&&f.utility.changes==1);
assert(f.combat.Update(&f.unit,true,false,18999)==0&&f.combat.Held);
assert(f.combat.Update(&f.unit,true,false,19000)==2&&!f.combat.Held&&!f.combat.Paused());
assert(f.group.adds==1&&f.group.queue.size()==2&&f.group.queue[0]==&f.search);
assert(f.combat.Update(&f.unit,true,false,20000)==0&&f.group.adds==1);}
// Never resurrect expired, released, replaced, or newly defensive tasks.
for(int invalid=0;invalid<6;invalid++){Fixture f;ERR_HQTask replacement;AIWaypoint other;
f.combat.Update(&f.unit,true,true,0);
if(invalid==0)f.task.ExpiresAt=15000;
if(invalid==1)f.unit.Task=nullptr;
if(invalid==2)f.unit.Waypoint=&other;
if(invalid==3)f.unit.Task=&replacement;
if(invalid==4)f.unit.Assignment=nullptr;
if(invalid==5)f.task.RoadIntercept=true;
assert(f.combat.Update(&f.unit,true,false,15000)==3&&f.group.adds==0);}
// Existing defensive posts and recalls keep their queue and fire policy.
for(int defence=0;defence<3;defence++){Fixture f;
if(defence==0){f.task.SecuritySite="radio";}if(defence==1){f.task.RoadIntercept=true;}if(defence==2){f.task.ReleaseOnly=true;}
assert(f.combat.Update(&f.unit,true,true,0)==1&&!f.combat.Paused());
assert(f.group.removes==0&&f.utility.changes==0);
assert(f.combat.Update(&f.unit,true,false,15000)==3&&f.group.adds==0);}
// Reservations/player/vehicle control cannot insert or remove waypoints.
{Fixture f;assert(f.combat.Update(&f.unit,false,true,0)==0&&f.group.removes==0);
f.combat.Update(&f.unit,true,true,0);
assert(f.combat.Update(&f.unit,false,false,20000)==0&&f.group.adds==0);
assert(f.combat.Update(&f.unit,true,false,20000)==2&&f.group.adds==1);}
}
''')
print('PASS: actual infantry search suspension, quiet grace, finite resume, expiry/replacement and defensive/ownership boundaries')
# Guard the production integration: local pause occurs before radio delivery, and
# only task downlinks are cancelled; status/vehicle messages remain independent.
hq=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'
tick=body(hq,'override void Tick(')
assert tick.index('UpdateInfantryCombat(unit, now)') < tick.index('unit.Downlink.Advance')
assert 'unit.InfantryCombat.Held || ERR_HunterCombat.Active(unit.Group)' in body(hq,'protected bool Fighting(')
assert 'if (unit.PendingTask) LostOrder(unit, now)' in body(hq,'protected void UpdateInfantryCombat(')
print('PASS: infantry combat gate precedes radio delivery and preserves non-task transports')
update=body(hq,'protected void UpdateInfantryCombat(').replace('protected ', '')
update=update.replace('ERR_HQUnit unit','ERR_HQUnit* unit').replace('unit.', 'unit->')
update=update.replace('InfantryCombat.', 'InfantryCombat->').replace('Group.', 'Group->')
update=update.replace('array<AIAgent> reacting', 'std::vector<AIAgent*> reacting')
update=update.replace('foreach (AIAgent actor : reacting)', 'for (AIAgent* actor : reacting)').replace('actor.', 'actor->')
update=update.replace('ERR_HunterCombat.Active', 'ERR_HunterCombat::Active').replace('ERR_Actors.CanFight', 'ERR_Actors::CanFight').replace('string.Format', 'Format')
run(r'''
#include <cassert>
#include <string>
#include <vector>
using string=std::string;
template<class... T>string Format(T...){return "entry";}
struct AIAgent{bool fit=true;int activations=0;AIAgent* GetControlledEntity(){return this;}void ActivateAI(){activations++;}};
struct Group{int activations=0;std::vector<AIAgent*> members;void ActivateAI(){activations++;}void GetAgents(std::vector<AIAgent*>& a){a=members;}};
struct ERR_Actors{static bool CanFight(AIAgent* a){return a->fit;}};
struct ERR_HunterCombat{static bool Active(Group*){return true;}};
struct ERR_HQUnit;
struct State{int change=1;bool paused=true;int Update(ERR_HQUnit*,bool eligible,bool,float){return eligible?change:0;}bool Paused(){return paused;}};
struct ERR_HQUnit{int Id=7;string ForceSourceId="town";struct Group* Group;State* InfantryCombat;void* PendingTask=nullptr;bool eligible=true;bool Eligible(int){return eligible;}};
struct Journal{int logs=0;void Write(string,string){logs++;}};
struct HQ{int m_Mode=1,cancelled=0;Journal m_Journal;void LostOrder(ERR_HQUnit* u,float){cancelled++;u->PendingTask=nullptr;}
'''+update+r'''
};
int main(){HQ h;Group g;State state;ERR_HQUnit u;u.Group=&g;u.InfantryCombat=&state;
AIAgent fit,casualty;casualty.fit=false;g.members={&fit,&casualty,nullptr};u.PendingTask=&fit;
h.UpdateInfantryCombat(&u,1000);assert(g.activations==1&&fit.activations==1&&casualty.activations==0&&h.cancelled==1&&h.m_Journal.logs==1);
state.change=0;h.UpdateInfantryCombat(&u,2000);assert(g.activations==1&&h.cancelled==1);
state.change=1;u.PendingTask=nullptr;h.UpdateInfantryCombat(&u,3000);assert(h.cancelled==1&&fit.activations==2);
u.eligible=false;h.UpdateInfantryCombat(&u,4000);assert(g.activations==2&&h.m_Journal.logs==2);
u.eligible=true;state.change=2;h.UpdateInfantryCombat(&u,5000);assert(g.activations==2&&h.m_Journal.logs==3);
}
''')
print('PASS: actual HQ contact transition wakes only fit squad members, cancels only task orders and logs changes once')
+1 -1
View File
@@ -39,7 +39,7 @@ struct Mode {
struct Fixture {Mode m;Controller first,second;Fixture(){server=true;world.now=0;game.players.connected={{1,&first}};m.roster.lives[1]=Life();}void tick(float t){world.now=t;m.ServerTick();}void exhaust(){m.InitialAdmissionExhausted(1,&first);}};
int main(){
{Fixture f;f.m.roster.survivor=true;f.m.service.defeat=true;f.tick(31000);
assert(!f.m.Active() && f.m.m_EndReason=="All prisoners are unconscious and no treatment or recovery is progressing.");}
assert(!f.m.Active() && f.m.m_EndReason=="Everybody died. All prisoners are unconscious and nobody can recover or receive help.");}
// Sole exhausted first admission reaches existing grace, without a life/body.
{Fixture f;f.exhaust();f.tick(0);f.tick(29999);assert(f.m.Active());f.tick(30000);
assert(f.m.m_Phase==5&&!f.m.m_StartedPlayers&&!f.m.roster.lives[1].UsedLife);
+5 -4
View File
@@ -63,12 +63,12 @@ run(r'''
#include <set>
#include <string>
#include <vector>
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));}static float DistanceXZ(vector a,vector b){a.y=0;b.y=0;return Distance(a,b);}};
struct vector {float x=0,y=0,z=0;std::string ToString(){return "point";}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));}static float DistanceXZ(vector a,vector b){a.y=0;b.y=0;return Distance(a,b);}};
using IEntity=void*;
template<class T>struct Items {std::vector<T> values;void Insert(T t){values.push_back(t);}};
struct Replication {static inline bool server=true;static bool IsServer(){return server;}};
struct ERR_HQReport {IEntity Target=this;int MessageId=1;float ObservedAt=0;bool Distress=false;vector Position{};std::string TravelMode="foot";std::string Description(float){return "contact";}ERR_HQReport* Snapshot(){return new ERR_HQReport(*this);}};
struct ERR_MortarPrediction {static constexpr float MAX_REPORT_AGE_MS=60000;
struct ERR_MortarPrediction {static constexpr float MAX_REPORT_AGE_MS=60000;static bool Predict(ERR_HQReport*,float,float,vector&){return true;}
'''+fresh+r'''};
struct ERR_MortarRefreshRules {
'''+newer+r'''};
@@ -90,6 +90,7 @@ struct ERR_MortarBattery {
bool Operational(){return operational;}
bool CanRefresh(ERR_HQReport*,float);bool BeginRefresh(ERR_HQReport*,ERR_HQRoute*,float);void RejectRefresh(std::string);
void Cancel(std::string reason="cancel requested");void ObserveProjectile(IEntity,IEntity,bool);void FlushTraces(){}void DetachActivity(){}
void Trace(std::string,std::string,std::string){}
void Update(float){}bool SupplyPending(){return false;}int CountShells(){return 27;}
bool CanRequest(ERR_HQReport*,float){return false;}bool BeginRequest(ERR_HQReport*,float){assert(false);return false;}
};
@@ -100,7 +101,7 @@ struct ERR_MortarAimSolution {
IEntity Gun=nullptr;ERR_HQReport* Report=nullptr;int OrderId=1,Shot=0,Charge=0;vector GunOrigin{},Impact{500,0,0},AimTarget{500,500,0};float FlightTime=30;
'''+matches+r'''};
struct ERR_MortarSystem {
HQ hq;HQ* m_HQ=&hq;int failures=0;
HQ hq;HQ* m_HQ=&hq;int failures=0;std::string SafetyReason;bool Safe(ERR_MortarBattery*,vector,float){return true;}
Items<ERR_MortarBattery*> m_Batteries;bool m_Stopped=false;float m_NextDecision=1e9f,m_NextRefreshDecision=0;
bool Withhold(ERR_MortarBattery* b,const char* why){b->Rejected=true;b->LastFailure=why;failures++;return false;}
void AdvanceRefresh(ERR_MortarBattery*,float);
@@ -196,7 +197,7 @@ int main(){
interrupted.b.ObserveProjectile(&interrupted.next,nullptr,false); // actual physical-event bookkeeping, not native launch proof
assert(!interrupted.b.Released&&interrupted.b.OrderShots==1&&interrupted.b.TotalShots==1&&interrupted.b.Report==&interrupted.old);
interrupted.system.Tick(25003);assert(!interrupted.b.CommandOpen&&!interrupted.b.Activity&&!interrupted.b.Report&&!interrupted.b.RefreshRejected);
assert(interrupted.b.originalActivity.completions==1&&interrupted.b.transfer.invalidated==1&&interrupted.b.NextOrder==35003);
assert(interrupted.b.originalActivity.completions==1&&interrupted.b.transfer.invalidated==1&&interrupted.b.NextOrder==85003);
assert(interrupted.b.OrderShots==1&&interrupted.b.TotalShots==1&&interrupted.b.m_ReportLedger.claimed.count(2));
F noRoute;noRoute.system.hq.contacts.Insert(&noRoute.next);noRoute.b.Released=true;
noRoute.system.Tick(25000);assert(noRoute.system.hq.downlinks==1&&noRoute.b.RefreshRejected&&noRoute.b.Released);
+58
View File
@@ -128,3 +128,61 @@ supply_state=body(battery,'ERR_MortarSupplyState SupplyState(')
assert supply_state.index('m_Transfer.Poll(now)') < supply_state.index('if (!SupplyAuthorized(now))')
assert shoot.count('super.EOnTaskSimulate(')==1 and 'battery.SupplyGunner()' not in shoot
print('PASS: actual startup/generation/ownership/timeout/callback policies and native-counter wait; integration preserves one-shot allocation and pending-crew cancellation')
# Execute the production post-shot node, including the superclass completion
# callback. A fast repeater must not start a ninth aim while HQ is still waiting
# to observe native completion; pending ammunition and duplicate ticks stay safe.
from types import SimpleNamespace
section=(r/'ERR_MortarFiring.c').read_text().split('modded class SCR_AIStaticArtilleryBehavior_ShootingLogic',1)[1]
cycle=body(SimpleNamespace(read_text=lambda:section),'override ENodeResult EOnTaskSimulate(')
cycle=cycle.replace('override ', '').replace('AIAgent owner','void* owner')
cycle=cycle.replace('ERR_MortarBattery battery;', 'ERR_MortarBattery* battery=nullptr;').replace('SCR_AIStaticArtilleryBehavior behavior;', 'SCR_AIStaticArtilleryBehavior* behavior=nullptr;')
cycle=cycle.replace('SCR_AIStaticArtilleryBehavior.Cast(m_Utility.GetExecutedAction())','m_Utility->GetExecutedAction()')
cycle=cycle.replace('ERR_MortarSystem system = ERR_MortarSystem.Get();','ERR_MortarSystem* system = &systemInstance;')
for name in ['battery','behavior','system']:cycle=cycle.replace(name+'.',name+'->')
for name in ['ERR_MortarSupplyRules','ERR_MortarSalvo','ERR_MortarSupplyState','ENodeResult']:cycle=cycle.replace(name+'.',name+'::')
cycle=cycle.replace('GetGame().GetWorld().GetWorldTime()', '1000.f').replace('super.EOnTaskSimulate(owner, dt)', 'Base::EOnTaskSimulate(owner, dt)')
gate=body(readiness,'static int CycleGate(')
run('''#include <cassert>
enum class ENodeResult {FAIL,RUNNING,SUCCESS};
enum ERR_MortarSupplyState {FAILED=-1,PENDING,READY};
struct ERR_MortarSalvo {static constexpr int ROUNDS=8;};
struct ERR_MortarSupplyRules {'''+gate+'''};
struct ERR_MortarBattery {
bool CommandOpen=true;int OrderShots=0,checks=0;ERR_MortarSupplyState supply=ERR_MortarSupplyState::READY;
ERR_MortarSupplyState SupplyState(float){checks++;return supply;}
};
struct SCR_AIStaticArtilleryBehavior {
ERR_MortarBattery* battery=nullptr;int count=0,completions=0;
int GetShotCount(){return count;}ERR_MortarBattery* ERR_GetMortarBattery(){return battery;}
};
struct Utility {SCR_AIStaticArtilleryBehavior* behavior;auto GetExecutedAction(){return behavior;}};
struct ERR_MortarSystem {int refusals=0;void Withhold(ERR_MortarBattery*,int){refusals++;}int SupplyFailure(ERR_MortarBattery*,float){return 0;}} systemInstance;
struct Base {
Utility* m_Utility=nullptr;int calls=0;bool aiming=false;static constexpr int PORT_AIMING_NEEDED=1;
ENodeResult EOnTaskSimulate(void*,float){calls++;auto b=m_Utility->behavior;if(++b->count==8)b->completions++;return ENodeResult::SUCCESS;}
void SetVariableOut(int,bool value){aiming=value;}
};
struct Node:Base {'''+cycle+'''};
int main(){
ERR_MortarBattery battery;SCR_AIStaticArtilleryBehavior behavior;behavior.battery=&battery;Utility utility{&behavior};Node n;n.m_Utility=&utility;
assert(n.EOnTaskSimulate(nullptr,0)==ENodeResult::RUNNING&&n.calls==0);
battery.OrderShots=1;battery.supply=ERR_MortarSupplyState::PENDING;
assert(n.EOnTaskSimulate(nullptr,0)==ENodeResult::RUNNING&&n.calls==0);
battery.supply=ERR_MortarSupplyState::READY;
for(int shot=1;shot<=8;shot++){
battery.OrderShots=shot;n.aiming=false;
if(shot==8)battery.supply=ERR_MortarSupplyState::FAILED; // final shot needs no ninth shell
auto result=n.EOnTaskSimulate(nullptr,0);
assert(n.calls==shot&&behavior.count==shot);
assert(result==(shot<8?ENodeResult::SUCCESS:ENodeResult::RUNNING));
assert(n.aiming==(shot<8));
}
assert(behavior.completions==1&&systemInstance.refusals==0);
for(int tick=0;tick<100;tick++)assert(n.EOnTaskSimulate(nullptr,0)==ENodeResult::RUNNING);
assert(n.calls==8&&behavior.completions==1); // no duplicate count or ninth aim
battery.CommandOpen=false;assert(n.EOnTaskSimulate(nullptr,0)==ENodeResult::FAIL);
behavior.battery=nullptr;assert(n.EOnTaskSimulate(nullptr,0)==ENodeResult::SUCCESS); // unrelated native artillery unchanged
}
''')
print('PASS: rapid mortar cycle counts eight physical rounds once and waits for native completion without a ninth aim or shell')
+3
View File
@@ -13,6 +13,8 @@ s=s.replace('ERR_HQ hq','ERR_HQ* hq').replace('hq.','hq->')
run(r'''
#include <cassert>
#include <string>
using string = std::string;
struct CLI {string outcome="hq-memory"; void GetCLIParam(string,string& value){value=outcome;}} System;
struct ERR_HQ{bool ready=true;bool ERR_ProbeSquadsReady(){return ready;}};
struct Probe{bool enabled=true,m_Mode=true,m_Failed=false,m_AutoChecked=false;int m_Step=0,automatic=0,checks=0;float m_Deadline=0;bool Enabled(){return enabled;}void Check(bool,std::string){checks++;m_Failed=true;}int AutomaticConvoy(ERR_HQ*){if(automatic==1)m_AutoChecked=true;return automatic;}
METHOD
@@ -22,6 +24,7 @@ int main(){ERR_HQ hq;
Probe missing;assert(missing.Tick(&hq,0)==0);assert(missing.Tick(&hq,150001)==-1&&missing.checks==1);
Probe refused;refused.Tick(&hq,0);refused.automatic=-1;assert(refused.Tick(&hq,40000)==-1&&refused.m_Deadline==150000&&!refused.m_AutoChecked);
Probe unavailable;hq.ready=false;assert(unavailable.Tick(&hq,0)==0);assert(unavailable.Tick(&hq,150001)==-1);hq.ready=true;
System.outcome="hq-awareness"; hq.ready=false; Probe awareness; assert(awareness.Tick(&hq,1000)==1 && !awareness.m_AutoChecked && awareness.m_Deadline==151000); assert(awareness.Tick(&hq,151001)==-1);
Probe invalid;invalid.enabled=false;assert(invalid.Tick(&hq,0)==-1&&invalid.m_Deadline==0);
}
'''.replace('METHOD',s))
+2 -1
View File
@@ -61,7 +61,8 @@ struct F {
int main(){
assert(Rules::QueueUpdate(true,false,0,0,1000,20000));assert(!Rules::QueueUpdate(true,false,0,0,1000,19999));
assert(!Rules::QueueUpdate(false,false,0,0,1000,20000));assert(!Rules::QueueUpdate(true,true,0,0,1000,20000));
assert(!Rules::QueueUpdate(true,false,4,0,1000,20000));assert(!Rules::QueueUpdate(true,false,0,0,1000,91000));
assert(Rules::QueueUpdate(true,false,4,0,1000,20000));assert(Rules::QueueUpdate(true,false,5,0,1000,20000));
assert(!Rules::QueueUpdate(true,false,6,0,1000,20000));assert(!Rules::QueueUpdate(true,false,0,0,1000,91000));
assert(!Rules::QueueUpdate(true,false,0,0,22000,20000));
F f;f.initial();assert(f.p.ReportKnown(&f.first));assert(!f.p.ReportKnown(&f.next));assert(!f.p.AllAccepted());
assert(!f.p.AddObservation(1,1));assert(!f.p.AddObservation(2,0));assert(!f.p.AddObservation(0,100));
+75 -13
View File
@@ -1,17 +1,79 @@
"""Execute player policy methods; native damage/regen boundaries are modeled."""
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerMedical.c'
rule=body(p,'static float AllowedDamage(').replace('Math.Clamp','std::clamp').replace('Math.Min','std::min').replace('Math.Max','std::max').replace('0.01','0.01f').replace(', 1)',', 1.0f)').replace('std::max(0,','std::max(0.0f,')
run('''#include <algorithm>
#include <cassert>
'''+rule+'''
int main(){
assert(AllowedDamage(-40,10,100,0)==-40); // medical treatment must pass unchanged
assert(AllowedDamage(0,10,100,0)==0);
assert(AllowedDamage(5,100,100,0)==5); // ordinary damage still wounds
assert(AllowedDamage(100000,100,100,0)==99); // lethal blow stops above destruction
assert(AllowedDamage(100000,30,100,0.2f)>8.99f&&AllowedDamage(100000,30,100,0.2f)<9.01f);
assert(AllowedDamage(50,1,100,0)==0); // repeated damage cannot finish casualty
assert(AllowedDamage(50,0,100,0)==0); // never turn positive damage into free healing
source=p.read_text()
methods='\n'.join(body(p,s) for s in ['void ERR_EnableCasualtyProtection(', 'override protected bool IsIndefinitelyUnconscious(', 'override float GetResilienceRegenScale('])
methods=methods.replace('override ','').replace('protected ','').replace('Replication.IsServer()','server').replace('super.','Native::')
run('''#include <cassert>
bool server=true;
struct Native {bool unconscious=false,changed=false;float regen=1;int writes=0;bool bleeding=false;
void SetPermitUnconsciousness(bool value,bool c){unconscious=value;changed=c;writes++;}
void SetRegenScale(float value,bool){regen=value;writes++;}
bool IsIndefinitelyUnconscious(bool){return true;}bool IsBleeding(){return bleeding;}float GetResilienceRegenScale(){return .8f;}
};
struct Policy:Native{bool m_ERRRevivablePlayer=false;
'''+methods+'''
};
int main(){Policy p;server=false;p.ERR_EnableCasualtyProtection();assert(!p.m_ERRRevivablePlayer&&p.writes==0);
server=true;p.ERR_EnableCasualtyProtection();assert(p.unconscious&&p.changed&&p.regen==.25f);
assert(!p.IsIndefinitelyUnconscious());p.bleeding=true;assert(p.GetResilienceRegenScale()==0);
p.bleeding=false;assert(p.GetResilienceRegenScale()==.8f);Policy ai;assert(ai.IsIndefinitelyUnconscious());ai.bleeding=true;assert(ai.GetResilienceRegenScale()==.8f);
}
''')
print('PASS: lethal and repeated damage clamp above configured destruction thresholds; ordinary wounds and healing retain their values')
# Execute the real damage cap across wound/blood scales; healing is untouched.
cap=body(p, 'static float AllowedDamage(').replace('Math.Clamp','std::clamp').replace('Math.Min','std::min').replace('Math.Max','std::max').replace('0.01','0.01f').replace(', 1)',', 1.0f)').replace('(0,','(0.0f,')
run('''#include <algorithm>
#include <cassert>
#include <cmath>
'''+cap+'''
int main(){
for(float maximum:{100.0f,5000.0f}) for(float threshold:{0.0f,.05f,.2f}) {
float floor=maximum*(threshold+.01f);
assert(AllowedDamage(-10,maximum,maximum,threshold)==-10);
assert(AllowedDamage(100000, floor,maximum,threshold)==0);
assert(AllowedDamage(100000, floor/2,maximum,threshold)==0);
float health=maximum;
for(int i=0;i<1000;i++) health-=AllowedDamage(13,health,maximum,threshold);
assert(std::abs(health-floor)<.001f);
assert(AllowedDamage(1,maximum,maximum,threshold)==1);
}
}
''')
# Passive resilience has its own native rate; treatment DPS bypasses this getter.
rate=body(p,'override float CalculatePassiveRegenDPS(').replace('override ','').replace('super.','Native::').replace('SCR_CharacterDamageManagerComponent manager = SCR_CharacterDamageManagerComponent.Cast(GetHitZoneContainer());','Manager* manager = target;').replace('manager.','manager->')
run('''#include <cassert>
struct Manager { bool player; bool ERR_IsRevivablePlayer(){return player;} };
struct Native {float CalculatePassiveRegenDPS(bool){return 4;}};
struct Policy:Native {Manager* target=nullptr;
'''+rate+'''
};
int main(){Policy p;assert(p.CalculatePassiveRegenDPS()==4);Manager ai{false};p.target=&ai;assert(p.CalculatePassiveRegenDPS()==4);ai.player=true;assert(p.CalculatePassiveRegenDPS()==1);assert(p.CalculatePassiveRegenDPS(false)==4);}
''')
assert 'SCR_CharacterResilienceHitZone.Cast(this)) return damage' in source
assert 'damage <= 0' in source and '!manager.ERR_IsRevivablePlayer()' in source
assert 'override void Kill(' in source
assert 'FullHeal(' not in source and 'SetHealth(' not in source
print('PASS: authority-scoped slow recovery, bleeding pauses resilience, medicine unaffected, player-only lethal damage cap without healing')
# Run the production recovery prediction over real effect states modeled at the
# native boundary, including a pending regeneration delay and exhausted effects.
future=body(p,'bool ERR_CanRecoverNaturally(')
future=future.replace('array<ref SCR_PersistentDamageEffect> effects','auto effects').replace('GetAllPersistentEffectsOfType(SCR_PassiveHitZoneRegenDamageEffect, true)','GetAllPersistentEffectsOfType()')
future=future.replace('foreach (SCR_PersistentDamageEffect effect : effects)','for (Effect* effect : effects)').replace('DotDamageEffect dot = DotDamageEffect.Cast(effect);','Effect* dot = effect;').replace('HitZone zone','Zone* zone').replace('dot.','dot->').replace('zone.','zone->').replace('EDamageType.REGENERATION','1')
run('''#include <cassert>
#include <vector>
struct Zone {float value=0;float GetHealthScaled(){return value;}};
struct Effect {Zone* zone;float dps;int type=1;int GetDamageType(){return type;}float GetDPS(){return dps;}Zone* GetAffectedHitZone(){return zone;}};
struct Policy {bool m_ERRRevivablePlayer=true,bleeding=false;float scale=1;Zone blood,resilience;std::vector<Effect*> effects;
bool IsBleeding(){return bleeding;}auto GetAllPersistentEffectsOfType(){return effects;}
Zone* GetBloodHitZone(){return &blood;}Zone* GetResilienceHitZone(){return &resilience;}float GetResilienceRegenScale(){return scale;}
'''+future+'''
};
int main(){Policy p;assert(!p.ERR_CanRecoverNaturally());Effect e{&p.blood,-.1f};p.effects={&e};
assert(p.ERR_CanRecoverNaturally());p.bleeding=true;assert(!p.ERR_CanRecoverNaturally());p.bleeding=false;
p.blood.value=1;assert(!p.ERR_CanRecoverNaturally());p.blood.value=.2;
e.dps=0;assert(!p.ERR_CanRecoverNaturally());e.dps=-1;e.type=2;assert(!p.ERR_CanRecoverNaturally());e.type=1;
e.zone=&p.resilience;p.scale=0;assert(!p.ERR_CanRecoverNaturally());p.scale=.1;assert(p.ERR_CanRecoverNaturally());
p.m_ERRRevivablePlayer=false;assert(!p.ERR_CanRecoverNaturally());
}
''')
print('PASS: viable passive recovery survives native delay; bleeding, disabled/full effects and low-blood resilience do not block defeat')
+8
View File
@@ -59,6 +59,9 @@ class ProjectSchemaTests(unittest.TestCase):
def test_menu_extension_cannot_redirect_override_or_add_unreviewed_settings(self):
mutations = (
CURRENT.replace('B48C7FA12692DE03', 'FFFFFFFFFFFFFFFF'),
CURRENT.replace('A7B19F416E541000', 'FFFFFFFFFFFFFFFF'),
CURRENT.replace('50A48858902D5F64', 'FFFFFFFFFFFFFFFF'),
CURRENT.replace('Configs/ERR/ObserverInput.conf', 'Configs/Foreign/Input.conf'),
CURRENT.replace('50C890C996B0A6FC', 'FFFFFFFFFFFFFFFF'),
CURRENT.replace('Configs/ERR/AdminMenus.conf', 'Configs/Foreign/Menu.conf'),
CURRENT.replace('MenuConfigs {', 'MenuConfigs +{'),
@@ -104,6 +107,11 @@ class ProjectSchemaTests(unittest.TestCase):
with self.assertRaises(ValueError): merged.inventory(self.candidate)
def test_source_menu_reference_must_resolve_to_its_owned_resource(self):
for name in ('addons/core/Configs/ERR/ObserverInput.conf',):
payload = dict(self.payload)
payload.pop(name)
payload.pop(name+'.meta')
with self.assertRaisesRegex(ValueError, 'unresolved or nonvanilla'): bundle.validate(payload, ROOT)
payload = dict(self.payload)
payload.pop(MENU_FILE)
payload.pop(MENU_FILE+'.meta')
+80
View File
@@ -0,0 +1,80 @@
"""Execute production input dispatch and recovery with native camera/editor doubles."""
from policy_harness import ROOT,body,run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c'
s=body(p,'protected void ERR_UpdateRecoveryCamera(').replace('protected ','').replace('ERR_GameMode mode','ERR_GameMode* mode').replace('null','nullptr')
for name in ('ERR_Actors','SCR_EditorManagerEntity','LogLevel'):
s=s.replace(name+'.',name+'::')
for name in ('SCR_EditorManagerEntity','CameraManager','CameraBase'):
s=s.replace(name+' ',name+'* ')
for name in ('mode','editor','manager'):
s=s.replace(name+'.',name+'->')
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->')
run(r'''#include <cassert>
struct Actor {bool alive=true,conscious=true;};using IEntity=Actor*;
struct ERR_Actors {static bool Alive(IEntity a){return a&&a->alive;}static bool CanFight(IEntity a){return Alive(a)&&a->conscious;}};
struct ERR_GameMode {bool active=true;bool Active(){return active;}};
struct SCR_EditorManagerEntity {bool opened=false,canClose=true;int closes=0;static SCR_EditorManagerEntity* current;static SCR_EditorManagerEntity* GetInstance(){return current;}bool IsOpened(){return opened;}void Close(bool){closes++;if(canClose)opened=false;}} editor;
SCR_EditorManagerEntity* SCR_EditorManagerEntity::current=&editor;
struct CameraBase{} player,other;
struct CameraManager {CameraBase* current=&other;int sets=0;void SetCamera(CameraBase* c){current=c;sets++;}CameraBase* CurrentCamera(){return current;}} manager;
struct World {float now=0;float GetWorldTime(){return now;}} world;
struct Game {World* GetWorld(){return &world;}CameraManager* GetCameraManager(){return &manager;}} game;
Game* GetGame(){return &game;}namespace LogLevel {enum{WARNING};}int warnings=0;void Print(const char*,int){warnings++;}
struct Controller {
IEntity controlled=nullptr,m_ERRRecoveryBody=nullptr;float m_ERRRecoveryDeadline=-1;CameraBase* camera=&player;
IEntity GetControlledEntity(){return controlled;}CameraBase* GetPlayerCamera(){return camera;}
'''+s+r'''
};
int main(){
ERR_GameMode mode;Actor body,replacement;Controller c;c.controlled=&body;
// Healthy admins retain their camera: restoration belongs only to recovery.
c.ERR_UpdateRecoveryCamera(&mode);assert(manager.sets==0&&editor.closes==0);
body.conscious=false;c.ERR_UpdateRecoveryCamera(&mode);assert(c.m_ERRRecoveryBody==&body&&manager.sets==0);
// Recovery works with NO observer-camera instance and a different active camera.
editor.opened=true;body.conscious=true;c.ERR_UpdateRecoveryCamera(&mode);
assert(editor.closes==1&&!editor.opened&&manager.current==&player&&!c.m_ERRRecoveryBody);
manager.current=&other;c.ERR_UpdateRecoveryCamera(&mode);assert(manager.current==&other); // no permanent camera lock
// An asynchronously closing editor retains the pending recovery; next tick completes.
body.conscious=false;c.ERR_UpdateRecoveryCamera(&mode);body.conscious=true;editor.opened=true;editor.canClose=false;
c.ERR_UpdateRecoveryCamera(&mode);assert(c.m_ERRRecoveryBody==&body);
editor.canClose=true;c.ERR_UpdateRecoveryCamera(&mode);assert(!c.m_ERRRecoveryBody&&manager.current==&player);
// Wait for late camera creation, then return once.
body.conscious=false;c.ERR_UpdateRecoveryCamera(&mode);body.conscious=true;c.camera=nullptr;
c.ERR_UpdateRecoveryCamera(&mode);assert(c.m_ERRRecoveryBody==&body);
c.camera=&player;c.ERR_UpdateRecoveryCamera(&mode);assert(!c.m_ERRRecoveryBody);
// Refused editor close is bounded, warning instead of permanently stealing input.
body.conscious=false;c.ERR_UpdateRecoveryCamera(&mode);body.conscious=true;editor.opened=true;editor.canClose=false;
c.ERR_UpdateRecoveryCamera(&mode);world.now=10001;c.ERR_UpdateRecoveryCamera(&mode);assert(!c.m_ERRRecoveryBody&&warnings==1);
editor.opened=false;editor.canClose=true;
// Changed possession / stopped round must not return a stale casualty's camera.
body.conscious=false;c.ERR_UpdateRecoveryCamera(&mode);c.controlled=&replacement;int before=manager.sets;
c.ERR_UpdateRecoveryCamera(&mode);assert(!c.m_ERRRecoveryBody&&manager.sets==before);
c.controlled=&body;c.ERR_UpdateRecoveryCamera(&mode);mode.active=false;body.conscious=true;
c.ERR_UpdateRecoveryCamera(&mode);assert(!c.m_ERRRecoveryBody&&manager.sets==before);
}
''')
input_body=body(p,'void Input(').replace('SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());','SCR_PlayerController* controller = &pc;').replace('controller.','controller->').replace('ERR_ObserverRules.','ERR_ObserverRules::')
run(r'''#include <cassert>
struct ERR_ObserverRules {
'''+body(p,'static int Cycle(')+body(p,'static bool Toggle(')+r'''
};
struct SCR_PlayerController {int calls=0,direction=0;void ERR_CycleObserver(int d){calls++;direction=d;}} pc;
struct Follow {bool m_LeftDown=false,m_RightDown=false,m_EnterDown=false,m_PlayerView=false;
'''+input_body+r'''
};
int main(){
Follow f;f.Input(true,true,false,false);assert(pc.calls==1&&pc.direction==-1);
f.Input(true,true,false,false);assert(pc.calls==1);
f.Input(true,false,false,false);f.Input(true,false,true,false);assert(pc.calls==2&&pc.direction==1);
f.Input(true,true,true,false);assert(pc.calls==2);
f.Input(false,false,false,true);f.Input(true,false,false,true);assert(!f.m_PlayerView); // menu-held Enter
f.Input(true,false,false,false);f.Input(true,false,false,true);assert(f.m_PlayerView);
f.Input(true,false,false,true);assert(f.m_PlayerView);
f.Input(true,false,false,false);f.Input(true,false,false,true);assert(!f.m_PlayerView);
}
''')
config=(ROOT/'addons/core/Configs/ERR/ObserverInput.conf').read_text()
for key in ('KC_LEFT','KC_RIGHT','KC_RETURN','KC_NUMPADENTER'): assert f'keyboard:{key}' in config
assert 'chimeraInputCommon.conf' in config and 'ActionContext/ManualCameraContext.conf' in config
assert 'Debug.KeyState' not in p.read_text()
print('PASS: actual input dispatch, one press per edge, menu-held suppression; actual revival camera handoff with editor close/retry, missing camera, replacement body and stopped round')
+23 -13
View File
@@ -6,15 +6,16 @@ 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'):
for typ in ('ERR_SupplyConvoyTask','ERR_ConvoyVehicle','ERR_SupplyTeam','ERR_ForceRoute','ERR_ConvoyBoardingProgress','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('bool surviving, pending;','bool surviving=false, pending=false;').replace('bool pending;', 'bool 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('->Column.', '->Column->').replace('->Combat.', '->Combat->')
s=s.replace('->Receipt.', '->Receipt->').replace('Vehicles[2].', 'Vehicles[2]->')
s=s.replace('.Entity.', '.Entity->').replace('->Entity.', '->Entity->')
return s
@@ -36,9 +37,11 @@ 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;
struct Combat{bool Held=false;};
struct Column{bool live=true;int resets=0;bool Tick(float){return live;}void Reset(float,bool){resets++;}};
struct ERR_ConvoyVehicle{bool Released=false,Arrived=false,SupplyHeld=false;struct Combat combat;struct Combat* Combat=&combat;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;
struct ERR_SupplyConvoyTask{struct Column column;struct Column* Column=&column;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;
@@ -47,18 +50,22 @@ 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
s.Circuit(&t,999);assert(!t.Waiting&&!a.Arrived);s.Circuit(&t,1000);assert(!a.Arrived&&!b.Arrived&&!t.Waiting); // timeout is not arrival
a.Arrived=b.Arrived=true;a.SupplyHeld=true;s.Circuit(&t,2000);assert(!t.Waiting);
a.SupplyHeld=false;b.combat.Held=true;s.Circuit(&t,3000);assert(!t.Waiting);
b.combat.Held=false;s.Circuit(&t,4000);assert(t.Waiting&&t.Departure==49000);
s.Circuit(&t,48999);assert(t.Legs==0);s.Circuit(&t,49000);assert(t.Legs==1&&!t.Waiting&&!a.Arrived&&!b.Arrived&&t.LastBase==0&&t.column.resets==1);
a.entity.position={250};b.entity.position={249};s.Circuit(&t,50000);assert(!a.Arrived&&!b.Arrived&&!t.Waiting); // no 250m shortcut
a.Arrived=true;b.Arrived=true;s.Circuit(&t,51000);assert(t.Waiting);s.Circuit(&t,96000);assert(t.LastBase==1&&t.Legs==2);
a.Released=true;b.Released=true;t.column.live=false;s.Circuit(&t,97000);assert(t.stopped); // no replacements
}
''')
run(common+r'''
struct ERR_ConvoyBoardingProgress{};
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(){}
struct ERR_SupplyTeam{ERR_ConvoyBoardingProgress* BoardingProgress=nullptr;float BoardingRetryAt=0;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;};
@@ -67,7 +74,7 @@ struct Tactics{ERR_SupplyConvoyTask* m_Task;Mode mode;Mode* m_Mode=&mode;Array<E
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++;}
float infantryDistance=100;float InfantryDistance(void*){return infantryDistance;}void ProtectInfantry(){}void RefreshBoarding(float){}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};
@@ -75,8 +82,8 @@ int main(){
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);
assert(t.Tick(61000)&&t.m_State==2&&t.withdraws==1&&left.orders==1&&right.orders==1&&t.m_BoardDeadline==361000);
assert(t.Tick(360999));left.aboard=true;assert(!t.Tick(361000)&&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
@@ -84,6 +91,9 @@ int main(){
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
left.aboard=right.aboard=false;Tactics safety{&task};safety.Report({100},0,0);safety.infantryDistance=24;
safety.Tick(1000);assert(!safety.m_TruckWithdrawn&&safety.withdraws==0); // do not drive through troops at the doors
safety.infantryDistance=25;safety.Tick(2000);assert(safety.m_TruckWithdrawn&&safety.withdraws==1);
}
''')
+3 -1
View File
@@ -6,8 +6,10 @@ firearm=body(core/'ERR_UnconsciousWeapon.c','static bool Firearm(').replace('EWe
run('''#include <cassert>
#include <initializer_list>
namespace EWeaponType {enum{WT_NONE,WT_RIFLE,WT_GRENADELAUNCHER,WT_SNIPERRIFLE,WT_ROCKETLAUNCHER,WT_MACHINEGUN,WT_HANDGUN,WT_FRAGGRENADE,WT_SMOKEGRENADE,WT_AUTOCANNON,WT_COUNT};}
'''+cycle+firearm+'''
'''+cycle+body(core/'ERR_Observer.c','static bool Toggle(')+firearm+'''
int main(){
for(bool enabled:{false,true})for(bool pressed:{false,true})for(bool previous:{false,true})
assert(Toggle(enabled,pressed,previous)==(enabled&&pressed&&!previous));
assert(Cycle(true,true,false,false,false)==-1);
assert(Cycle(true,false,true,false,false)==1);
assert(Cycle(true,true,false,true,false)==0); // held arrow does not repeat