106 lines
8.3 KiB
Python
106 lines
8.3 KiB
Python
"""Execute production gates behind the RKS2 delayed-start and hunter failures."""
|
|
import re
|
|
from policy_harness import ROOT, body, run
|
|
r = ROOT/'addons/core/Scripts/Game/ERR/10_AI'
|
|
# Use the actual stateful deadline block, including first-poll initialization.
|
|
poll = body(r/'ERR_MortarReadiness.c', 'ERR_MortarSupplyState Poll(')
|
|
clock = poll[poll.index(' if (m_StartedAt < 0)'):poll.index(' array<AIAgent> agents')]
|
|
admission = body(r/'ERR_HQ.c', 'protected bool ConvoyAdmissionReady(').replace('protected ', '')
|
|
admission = admission.replace('array<ref ERR_MortarBattery> batteries = {};', 'Array<ERR_MortarBattery*> batteries;')
|
|
admission = admission.replace('foreach (', 'for (').replace('ERR_HQUnit unit', 'ERR_HQUnit* unit').replace('ERR_MortarBattery support', 'ERR_MortarBattery* support').replace('ERR_MortarBattery battery', 'ERR_MortarBattery* battery')
|
|
for name in ('m_Mode', 'm_Mortars', 'unit', 'support', 'battery'):
|
|
admission = admission.replace(name+'.', name+'->')
|
|
admission = admission.replace('Forces().', 'Forces()->')
|
|
crew = body(r/'ERR_HunterService.c', 'override bool CrewValid(').replace('override ', '').replace('ERR_GameMode mode','Mode* mode')
|
|
crew = crew.replace('ERR_Actors.', 'Actors::').replace('mode.', 'mode->').replace('Group.', 'Group->').replace('Seats[0].','Seats[0]->')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
struct Text { bool empty=true; bool IsEmpty(){return empty;} };
|
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;bool IsEmpty(){return this->empty();}int Count(){return this->size();}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}};
|
|
struct GroupType {int faction=1;int GetFaction(){return faction;}};
|
|
struct Actor {bool alive=true,fit=true,player=false;GroupType* group=nullptr;};
|
|
struct Seat {Actor* occupant=nullptr;Actor* GetOccupant(){return occupant;}};
|
|
namespace Actors {bool Alive(Actor* x){return x&&x->alive;}bool CanFight(Actor* x){return x&&x->alive&&x->fit;}bool Player(Actor*x){return x&&x->player;}GroupType* Group(Actor*x){return x?x->group:nullptr;}}
|
|
struct Clock {float m_StartedAt=-1; bool Poll(float now){
|
|
'''+clock+r'''
|
|
return expired;
|
|
}};
|
|
struct Ledger {std::vector<GroupType*> registered;bool FindGroup(GroupType*g){return std::find(registered.begin(),registered.end(),g)!=registered.end();}};
|
|
struct Mode {Ledger ledger;Ledger* Forces(){return &ledger;}int EnemyFaction(){return 1;}};
|
|
struct ERR_HQUnit {GroupType* Group=nullptr;};
|
|
struct ERR_MortarBattery {GroupType* group=nullptr;bool stopped=false,StartupActivated=false;Text SpawnFailure;bool Stopped(){return stopped;}};
|
|
struct Mortars {Array<ERR_MortarBattery*> all;ERR_MortarBattery* FindGroup(GroupType*g){for(auto*b:all)if(b->group==g)return b;return nullptr;}void Batteries(Array<ERR_MortarBattery*>&out){out=all;}};
|
|
struct HQ {Array<ERR_HQUnit*> m_Units;Mode* m_Mode;Mortars* m_Mortars;
|
|
'''+admission+r'''
|
|
};
|
|
struct Hunter {GroupType* Group;Actor* Entity;Actor* Driver;Array<Actor*> Members;Array<Seat*> Seats;
|
|
'''+crew+r'''
|
|
};
|
|
int main(){
|
|
Clock c;assert(!c.Poll(150000));assert(!c.Poll(239999));assert(c.Poll(240000)); // 150 seconds in prison does not spend the initialization budget
|
|
Clock early;assert(!early.Poll(1000));assert(!early.Poll(90999));assert(early.Poll(91000));assert(early.Poll(999)); // finite deadline and clock reversal remain enforced
|
|
Mode mode;GroupType ordinary,mortar;ERR_HQUnit u{&ordinary},m{&mortar};ERR_MortarBattery battery{&mortar};Mortars support;support.all={&battery};HQ hq{{&u,&m},&mode,&support};
|
|
mode.ledger.registered={&ordinary};assert(!hq.ConvoyAdmissionReady()); // unfinished mortar waits
|
|
battery.SpawnFailure.empty=false;assert(hq.ConvoyAdmissionReady()); // failed optional mortar cannot block convoy admission
|
|
battery.SpawnFailure.empty=true;battery.stopped=true;assert(hq.ConvoyAdmissionReady());
|
|
battery.stopped=false;battery.StartupActivated=true;assert(!hq.ConvoyAdmissionReady()); // success still requires ledger registration
|
|
mode.ledger.registered.push_back(&mortar);assert(hq.ConvoyAdmissionReady());
|
|
mode.ledger.registered={&mortar};battery.SpawnFailure.empty=false;assert(!hq.ConvoyAdmissionReady()); // unrelated pending group is not bypassed
|
|
Actor vehicle,driver,gunner;driver.group=&ordinary;gunner.group=&ordinary;Seat seat{&driver};Hunter hunter{&ordinary,&vehicle,&driver,{&driver,&gunner},{&seat}};
|
|
assert(hunter.CrewValid(&mode));gunner.alive=false;assert(hunter.CrewValid(&mode)); // loss of gunner does not discard a working driver
|
|
driver.fit=false;assert(!hunter.CrewValid(&mode));driver.fit=true;assert(hunter.CrewValid(&mode)); // temporary incapacitation can resume
|
|
seat.occupant=nullptr;assert(!hunter.CrewValid(&mode));seat.occupant=&driver;assert(hunter.CrewValid(&mode));
|
|
driver.player=true;assert(!hunter.CrewValid(&mode));driver.player=false;vehicle.alive=false;assert(!hunter.CrewValid(&mode));
|
|
}
|
|
''')
|
|
print('PASS: real-session mortar deadline, failed-support convoy admission and hunter survivor/recovery gates')
|
|
|
|
# Execute the real retry method with a newer order still pending: the physical
|
|
# waypoint, not the unsent destination, must be recreated. Exercise retry limits
|
|
# and combat suppression too.
|
|
movement = body(r/'ERR_HunterService.c', 'protected void ObserveMovement(')
|
|
movement = movement.replace('protected ', '').replace('ERR_HunterVehicle unit', 'MovingHunter* unit')
|
|
movement = movement.replace('SCR_AIGroupUtilityComponent utility', 'Utility* utility').replace('vector ', 'Vec ').replace('vector.DistanceXZ', 'Vec::DistanceXZ')
|
|
movement = movement.replace('AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), retryDestination))', 'Spawn(retryDestination)')
|
|
for name in ('unit', 'utility'):
|
|
movement = movement.replace(name+'.', name+'->')
|
|
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').replace('ERR_HunterCombat.Active', 'ERR_HunterCombat::Active')
|
|
movement = re.sub(r'\b(bool|float|Vec) (\w+);', r'\1 \2 {};', movement)
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <string>
|
|
struct Vec {float x=0,z=0;float Length(){return 0;}static float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}};
|
|
struct Targets {bool empty=true;bool IsEmpty(){return empty;}};
|
|
struct Perception {Targets m_aTargets;};
|
|
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;}
|
|
template<class... Args> std::string Format(Args...){return "log";}
|
|
void Event(std::string,std::string){}
|
|
'''+movement+r'''
|
|
int main(){
|
|
Perception perception;Group group{{&perception}};Vehicle vehicle;Waypoint delivered{{100,200}};
|
|
MovingHunter hunter{&vehicle,&group,&delivered,{900,900}};
|
|
ObserveMovement(&hunter,60000);assert(hunter.Waypoint==&delivered&&hunter.Repaths==0);
|
|
ObserveMovement(&hunter,60001);assert(hunter.Repaths==1&&hunter.Waypoint->pos.x==100&&hunter.Waypoint->pos.z==200);assert(hunter.Destination.x==900);
|
|
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;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);
|
|
}
|
|
''')
|
|
print('PASS: native-route retry preserves delivered target, limits attempts and suspends during combat')
|