134 lines
8.2 KiB
Python
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')
|