Files
EscapistsReintroduced/tests/test_aircraft_route_demand.py
T

134 lines
8.2 KiB
Python

"""Execute actual route demand and control callback; native boundaries are fakes."""
from policy_harness import ROOT, body, run
BASE = ROOT / 'addons/ai/Scripts/Game/ERR/ERR_AircraftFlight.c'
ROUTE = ROOT / 'addons/ai/Scripts/Game/ERR/ERR_AircraftRouteFlight.c'
RULES = ROOT / 'addons/ai/Scripts/Game/ERR/ERR_AircraftRouteEnvelope.c'
PROBE = ROOT / 'addons/ai/Scripts/Game/ERR/ERR_NativeAircraftRouteProbe.c'
def cpp(text):
for old, new in (
('protected ', ''), ('override ', ''), ('super.', 'Base::'),
('ERR_AircraftFlightState.', 'State::'),
('ERR_AircraftFlightRules.', 'FlightRules::'),
('ERR_AircraftTranslationRules.', 'Shared::'),
('ERR_AircraftRouteRules.', 'RouteRules::'),
('Math.', 'Math::'), ('vector.', 'vector::'),
('CharacterControllerComponent controller', 'int controller'),
('IEntity owner', 'int owner'), ('ActionManager actions', 'Actions* actions'),
('actions.', 'actions->'), ('m_Route.', 'm_Route->'),
):
text = text.replace(old, new)
# Enforce default-initializes scalar locals, unlike C++.
return text.replace('float collective, torque, forward, aside;',
'float collective=0, torque=0, forward=0, aside=0;')
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <string>
#include <initializer_list>
using string=std::string;
namespace State {enum {IDLE,HOOKS,STARTING,POSITIVE,NEGATIVE,CLIMB,LANDING,SHUTDOWN,COMPLETE,CANCELLED,FAILED,RELINQUISHED,TRANSLATING};}
struct Math {
static float Clamp(float v,float lo,float hi){return std::clamp(v,lo,hi);}
static float Min(float a,float b){return std::min(a,b);}
};
struct vector {
float x=0,y=0,z=0;
float& operator[](int i){return i==0?x:(i==1?y:z);}
float operator[](int i)const{return i==0?x:(i==1?y:z);}
vector operator-(vector b)const{return {x-b.x,y-b.y,z-b.z};}
vector operator*(float n)const{return {x*n,y*n,z*n};}
float Length()const{return std::sqrt(x*x+y*y+z*z);}
static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}
};
bool close(float a,float b){return std::abs(a-b)<.000001f;}
struct Shared {
''' + cpp(body(BASE, 'static vector Demand(')) + r'''
};
struct RouteRules {
''' + cpp(body(RULES, 'static vector Demand(')) + r'''
};
struct FlightRules {
''' + cpp(body(BASE, 'static float Vertical(')) + cpp(body(BASE, 'static int CancelAction(')) + r'''
};
struct Actions {bool active=true;bool IsContextActive(const char*){return active;}void ActivateContext(const char*,int){active=true;}};
struct Aircraft {
vector position={0,8,0};
vector GetOrigin(){return position;}
vector GetYawPitchRoll(){return {};}
void GetWorldTransform(vector (&t)[4]){t[0]={1,0,0};t[1]={0,1,0};t[2]={0,0,1};t[3]=position;}
};
struct Controller {bool hover=true;bool GetAutohoverEnabled(){return hover;}explicit operator bool()const{return true;}bool IsEngineOn(){return true;}};
struct Simulation {vector velocity;vector GetVelocity(){return velocity;}vector GetAngularVelocityLocal(){return {};}};
struct Route {vector target={20,0,0};vector Destination(){return target;}bool Contains(vector,float){return true;}};
struct Base {
bool leased=true,owned=true,health=true,grounded=false,control=true,m_Readback=true,m_Cancelled=false;
int m_State=State::TRANSLATING,m_Character=1,m_Pilot=2,m_ActionUpdates=0,m_TranslationUpdates=0,pairs=0,landings=0;
float clock=1000,m_Deadline=240000,m_StageDeadline=160000,m_Stable=-1,m_Collective=0,m_Heading=0,m_LastControl=-1;
float writtenForward=0,writtenAside=0,writtenCollective=0;
vector m_Home;string m_Reason;Actions local;Actions* m_Actions=&local;
Aircraft m_Aircraft;Controller m_Controller;Simulation m_Simulation;
bool Lease(){return leased;}bool Owns(){return owned;}bool Healthy(){return health;}bool Grounded(){return grounded;}bool ControlLease(){return control;}
float Now(){return clock;}void Fail(string){m_State=State::FAILED;}void Relinquish(string){m_State=State::RELINQUISHED;}
void RecordState(){}void Release(bool){}void Shutdown(float){m_State=State::SHUTDOWN;}
bool Landing(float now){landings++;m_State=State::LANDING;m_Stable=-1;m_StageDeadline=Math::Min(m_Deadline,now+65000);return true;}
bool Pair(string increase,string,float value){pairs++;if(increase=="HelicopterCyclicForward")writtenForward=value;if(increase=="HelicopterCyclicRight")writtenAside=value;if(increase=="HelicopterCollectiveIncrease")writtenCollective=value;return true;}
};
struct Flight:Base {
Route route;Route* m_Route=&route;bool m_RouteArrived=false;vector m_LandingPoint;
''' + cpp(body(ROUTE, 'protected override bool Landing(')) + cpp(body(BASE, 'void Cancel(')) + cpp(body(ROUTE, 'override void PrepareActions(')) + r'''
void Apply(){PrepareActions(1,2,m_Actions,.05f);}
};
int main(){
// The unchanged .5m/s requested speed is visible at its zero-error velocity.
assert(RouteRules::Demand({20,900,0},{0,-900,0},{.5f,500,0}).Length()==0);
assert(RouteRules::Demand({200,0,0},{0,0,0},{.5f,0,0}).Length()==0);
// Below saturation the route candidate doubles gain, not desired speed.
auto route=RouteRules::Demand({20,0,0},{0,8,0},{.4f,0,0});
auto shared=Shared::Demand({20,0,0},{0,8,0},{.4f,0,0});
assert(close(route.x,.01f)&&close(shared.x,.005f));
for(float x:{-100.f,-2.f,-.1f,0.f,.1f,2.f,100.f})
for(float z:{-100.f,-1.f,0.f,1.f,100.f})
for(float v:{-100.f,-1.f,0.f,1.f,100.f}){
auto d=RouteRules::Demand({x,999,z},{0,-999,0},{v,999,-v});
assert(d.Length()<=.030001f&&d.y==0);
}
auto away=RouteRules::Demand({20,0,0},{0,8,0},{-1,50,0});assert(close(away.x,.03f)&&away.y==0&&away.z==0);
auto overspeed=RouteRules::Demand({20,0,0},{0,8,0},{1,-50,0});assert(close(overspeed.x,-.03f));
auto brake=RouteRules::Demand({0,0,0},{0,8,0},{1,0,-1});assert(brake.x<0&&brake.z>0&&close(brake.Length(),.03f));
assert(RouteRules::Demand({0,100,0},{0,-100,0},{0,99,0}).Length()==0);
// Execute the real callback: TRANSLATING selects candidate; ordinary landing
// keeps the original .05 gain with precisely the same target and velocity.
Flight translating;translating.m_Simulation.velocity={.4f,0,0};translating.Apply();
assert(close(translating.writtenAside,.01f)&&translating.writtenForward==0&&translating.m_TranslationUpdates==1);
Flight landing;landing.m_State=State::LANDING;landing.m_LandingPoint={20,0,0};landing.m_Simulation.velocity={.4f,0,0};landing.Apply();
assert(close(landing.writtenAside,.005f)&&landing.m_TranslationUpdates==0);
// The actual inherited Cancel and route Landing freeze the original position,
// retain authority/lease, and thereafter use old landing braking—not route gain.
Flight cancelled;cancelled.m_Aircraft.position={10,8,1};cancelled.m_Simulation.velocity={.2f,0,0};cancelled.Cancel("radio lost");
assert(cancelled.m_State==State::LANDING&&cancelled.m_Cancelled&&cancelled.leased&&cancelled.landings==1);
assert(cancelled.m_LandingPoint.x==10&&cancelled.m_LandingPoint.z==1&&cancelled.m_Deadline==240000);
cancelled.Apply();assert(close(cancelled.writtenAside,-.01f));
float deadline=cancelled.m_StageDeadline;cancelled.clock=5000;cancelled.m_Aircraft.position={11,8,1};cancelled.Cancel("duplicate");
assert(cancelled.landings==1&&cancelled.m_LandingPoint.x==10&&cancelled.m_StageDeadline==deadline);
// Grounded landing suppresses both axes; wrong owner or revoked lease writes none.
landing.grounded=true;landing.Apply();assert(landing.writtenAside==0&&landing.writtenForward==0&&landing.writtenCollective==-1);
Flight foreign;foreign.PrepareActions(1,99,foreign.m_Actions,.05f);assert(foreign.pairs==0);
foreign.control=false;foreign.Apply();assert(foreign.pairs==0);
}
''')
# Read-only diagnostic must describe the same state-specific equation, not the
# historical shared demand. It remains a computed sample, not input readback.
diagnostic = body(PROBE, 'protected void Diagnostic(')
assert 'if (state == ERR_AircraftFlightState.TRANSLATING) demand = ERR_AircraftRouteRules.Demand(' in diagnostic
assert 'else if (state == ERR_AircraftFlightState.LANDING) demand = ERR_AircraftTranslationRules.Demand(' in diagnostic
# The shared controller still has no dependency on the experimental route policy.
assert 'ERR_AircraftRouteRules' not in BASE.read_text()
print('PASS: actual route gain/caps/braking, callback selection, old landing/cancellation control and matching read-only telemetry')