Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
128 lines
6.2 KiB
Python
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')
|