Files
EscapistsReintroduced/tests/test_rescue_descent.py
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

128 lines
6.2 KiB
Python

"""Execute production rescue landing policy and routing; no native physics claim."""
from policy_harness import ROOT, body, run
base = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_ReconFlightComponent.c'
rescue = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_RescueFlightComponent.c'
def native(method):
return (method.replace('protected override ', '').replace('protected ', 'virtual ')
.replace('Math.', 'Math::').replace('super.', 'Base::')
.replace('ERR_RescueLandingRules.', 'ERR_RescueLandingRules::'))
run(r'''
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdio>
struct Math {
static float Min(float a,float b){return std::min(a,b);}
static float Max(float a,float b){return std::max(a,b);}
static float Clamp(float a,float low,float high){return std::clamp(a,low,high);}
};
struct vector {
float x=0,y=0,z=0;
vector()=default;vector(float a,float b,float c):x(a),y(b),z(c){}
float& operator[](int i){return i==0?x:i==1?y:z;}
vector operator*(float f)const{return{x*f,y*f,z*f};}
vector operator-(vector b)const{return{x-b.x,y-b.y,z-b.z};}
float Length()const{return std::sqrt(x*x+y*y+z*z);}
vector Normalized()const{float n=Length();return n>0?(*this)*(1/n):vector{};}
};
float Horizontal(vector a){return std::hypot(a.x,a.z);}
bool Near(float a,float b){return std::abs(a-b)<0.0001f;}
bool Same(vector a,vector b){return Near(a.x,b.x)&&Near(a.y,b.y)&&Near(a.z,b.z);}
struct Base {
''' + native(body(base, 'protected vector LandingVelocity(')) + r'''
};
struct ERR_RescueLandingRules {
''' + native(body(rescue, 'static vector Velocity(')) + r'''
};
struct Rescue : Base {
bool m_Escort=false,m_Departing=false;
''' + native(body(rescue, 'protected override vector LandingVelocity(')) + r'''
};
// Integrate only the velocity policy with the controller's unchanged 2-second
// settling condition. Native collision, suspension and frame scheduling are not
// modeled here and require the separate in-engine landing fixture.
float IdealLandingSeconds(Base& flight,int hz){
vector offset{90,-150,0};float dt=1.0f/hz,settled=0;
for(int frame=0;frame<hz*180;frame++){
vector velocity=flight.LandingVelocity(offset);
if(Horizontal(offset)<3&&std::abs(offset.y)<1&&velocity.Length()<1)settled+=dt;
else settled=0;
if(settled>=2)return frame*dt;
vector next=offset-velocity*dt;
assert(next.y<=0); // Continuous taper must not command an overshoot through the pad.
assert(Horizontal(next)<=Horizontal(offset)+0.0001f);
offset=next;
}
assert(false&&"landing policy never settled");return -1;
}
int main(){
Base base;Rescue rescue;Base& dispatched=rescue;
assert(Same(base.LandingVelocity({0,0,0}),{}));
assert(Same(dispatched.LandingVelocity({0,0,0}),{}));
assert(Same(base.LandingVelocity({0,-150,0}),{0,-3,0}));
assert(Same(dispatched.LandingVelocity({0,-150,0}),{0,-9,0}));
assert(Same(dispatched.LandingVelocity({0,-30,0}),{0,-9,0}));
assert(Same(dispatched.LandingVelocity({0,-20,0}),{0,-6,0}));
assert(Same(dispatched.LandingVelocity({0,-10,0}),{0,-3,0}));
// The extra speed is downward only. Below-pad recovery remains the old
// proportional climb with its original 3 m/s limit, even outside alignment.
for(float below : {0.1f,1.0f,5.0f,10.0f,100.0f}){
assert(Same(base.LandingVelocity({0,below,0}),dispatched.LandingVelocity({0,below,0})));
assert(dispatched.LandingVelocity({100,below,0}).y>0);
assert(dispatched.LandingVelocity({100,below,0}).y<=3);
}
// Exactly the original final approach vector within 16 m horizontally and
// ten metres vertically, including the 15 m alignment boundary.
for(float x : {-16.0f,-15.01f,-15.0f,-7.0f,0.0f,7.0f,15.0f,15.01f,16.0f})
for(float y : {-10.0f,-9.99f,-5.0f,-1.0f,0.0f,1.0f,5.0f,10.0f})
assert(Same(base.LandingVelocity({x,y,0}),dispatched.LandingVelocity({x,y,0})));
assert(dispatched.LandingVelocity({15,-100,0}).y==-9);
assert(dispatched.LandingVelocity({15.001f,-100,0}).y==0);
assert(dispatched.LandingVelocity({12,-100,9}).y==-9); // Radial gate, not an axis-only test.
assert(dispatched.LandingVelocity({12,-100,9.001f}).y==0);
// All approach directions remain aligned with the pad; the altitude must not
// inflate the horizontal speed, and the horizontal cap cannot affect climb.
for(float x : {-100.0f,-10.0f,0.0f,10.0f,100.0f})
for(float z : {-100.0f,-10.0f,0.0f,10.0f,100.0f}){
vector offset{x,-150,z};vector velocity=dispatched.LandingVelocity(offset);
assert(Horizontal(velocity)<=14.0001f);
assert(Horizontal(base.LandingVelocity(offset))<=8.0001f);
assert(Near(velocity.x*z,velocity.z*x));
assert(velocity.x*x+velocity.z*z>=0);
if(Horizontal(offset)>15)assert(velocity.y==0);
if(Horizontal(offset)>28)assert(Near(Horizontal(velocity),14));
assert(Same({velocity.x,0,velocity.z},dispatched.LandingVelocity({x,0,z})));
}
// No downward speed jump at 30 m or 10 m: braking is monotonic and bounded
// by the original 0.3 gain all the way through touchdown.
float previous=0;
for(int centimetres=0;centimetres<=4000;centimetres++){
float height=centimetres/100.0f,speed=-dispatched.LandingVelocity({0,-height,0}).y;
assert(speed>=previous-0.00001f&&speed-previous<0.00301f&&speed<=9);
previous=speed;
}
// Exercise virtual dispatch with each flight role. Escorts and outbound
// transport landings continue to use the default recon/logistics profile.
for(bool escort : {false,true})for(bool departing : {false,true}){
rescue.m_Escort=escort;rescue.m_Departing=departing;
for(vector offset : {vector{80,-150,0},vector{0,-100,0},vector{8,-5,4},vector{20,50,30}}){
vector expected=(escort||departing)?base.LandingVelocity(offset):ERR_RescueLandingRules::Velocity(offset);
assert(Same(dispatched.LandingVelocity(offset),expected));
}
}
rescue.m_Escort=false;rescue.m_Departing=false;
for(int hz : {15,30,60,120}){
float original=IdealLandingSeconds(base,hz),pickup=IdealLandingSeconds(dispatched,hz);
assert(original>60&&original<70&&pickup>30&&pickup<36);
assert(pickup<original*0.6f);
std::printf("Policy-only arrival from 90 m offset / 150 m height at %d Hz: original %.2fs, rescue %.2fs\n",hz,original,pickup);
}
}
''')
print('PASS: faster rescue descent, continuous near-ground braking, alignment gate, unchanged recovery/escort/default profile, and bounded settling at 15/30/60/120 Hz')