Files
EscapistsReintroduced/tests/test_aircraft_flight_policy.py
jakub cab998fb64 Combine Workshop addon and fix prison startup
Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
2026-09-20 16:42:52 +02:00

129 lines
10 KiB
Python

"""Execute production controller rules/cancellation/stage bodies with native boundaries substituted.
No native flight, callback ordering, collision or multiplayer claim.
"""
import re
from policy_harness import ROOT, body, run
SOURCE=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_AircraftFlight.c'
def cpp(s):
return s.replace('protected ','').replace('ERR_AircraftFlightState.','State::').replace('ERR_AircraftFlightRules.','Rules::').replace('Math.','Math::').replace('CollectiveMode.','CollectiveMode::').replace('vector.','vector::').replace('SignalsManagerComponent signals','SignalsManagerComponent* signals').replace('signals.FindSignal','signals->FindSignal').replace('signals.GetSignalValue','signals->GetSignalValue')
rules='\n'.join(cpp(body(SOURCE,s)) for s in ['static bool Terminal(', 'static bool Deadline(', 'static bool Envelope(', 'static bool Hover(', 'static bool Landed(', 'static float Vertical(', 'static int CancelAction('])
methods='\n'.join(cpp(body(SOURCE,s)) for s in ['protected bool ControlLease(', 'protected bool Landing(', 'protected void Shutdown(', 'void Cancel(', 'void Tick('])
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};}
namespace CollectiveMode {enum {Direct=3};}
struct Math {static float AbsFloat(float x){return std::abs(x);} static float Min(float a,float b){return std::min(a,b);} static float Clamp(float x,float a,float b){return std::clamp(x,a,b);}};
struct vector {float x=0,y=0,z=0;float operator[](int i)const{return i==0?x:(i==1?y:z);}float Length()const{return std::sqrt(x*x+y*y+z*z);}static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
struct Rules {
'''+rules+r'''
};
struct SignalsManagerComponent {float value=.05;int FindSignal(const char*){return 0;}float GetSignalValue(int){return value;}};
struct Controller {
explicit operator bool()const{return true;}
bool engine=true,autohover=true,accept=true;int starts=0,stops=0,hoverWrites=0;
SignalsManagerComponent signals;
void SetAutohoverEnabled(bool value){hoverWrites++;if(accept)autohover=value;}
bool GetAutohoverEnabled(){return autohover;}
bool IsEngineOn(){return engine;}int GetCollectiveMode(){return CollectiveMode::Direct;}
void StopEngine(bool force){assert(!force);stops++;engine=false;}
void StartEngine(){starts++;engine=true;}
SignalsManagerComponent* GetSignalsManager(){return &signals;}
};
struct Aircraft {vector position={0,8,0};void GetWorldTransform(vector* out){out[0]={1,0,0};out[1]={0,1,0};out[2]={0,0,1};out[3]=position;}};
struct Simulation {vector velocity;bool contact=false;vector GetVelocity(){return velocity;}bool HasAnyGroundContact(){return contact;}float RotorGetRPMTarget(int){return 100;}float RotorGetRPM(int){return 100;}};
struct Fuel{float GetTotalFuel(){return 99;}};
struct Input {float collective=.05;float GetVehicleCollective(){return collective;}};
struct Probe {
bool owned=true,healthy=true,ground=false,lease=true,m_Cancelled=false,m_Readback=true,m_TranslationRequested=false;
int m_State=State::CLIMB,m_ActionUpdates=10,m_HelicopterUpdates=10,m_GateUpdates=0;
float now=1000,m_Deadline=200000,m_StageDeadline=66000,m_Stable=-1,m_LastTick=-1,m_LastControl=1000,m_InitialFuel=100;
vector m_Home;string m_Reason;
Controller m_Controller;Aircraft m_Aircraft;Simulation m_Simulation;Fuel m_Fuel;Input m_Input;
int released=0,restored=0,logs=0;
bool Lease(){return lease&&!Rules::Terminal(m_State);}bool Owns(){return owned;}bool Healthy(){return healthy;}bool Grounded(){return ground;}float Now(){return now;}
void Release(bool restore){released++;if(restore){assert(owned&&ground);restored++;}}
bool BeginTranslation(float,vector){return true;}
void CheckTranslation(float,vector,float,float,float){}
void RecordState(){logs++;}
void Relinquish(string reason){m_Reason=reason;m_State=State::RELINQUISHED;Release(false);RecordState();}
void Fail(string reason){m_Reason=reason;m_State=State::FAILED;Release(false);RecordState();}
'''+methods+r'''
};
int main(){
for(int flags=0;flags<8;flags++){
bool owned=flags&1,ground=flags&2,engine=flags&4;
assert(Rules::CancelAction(owned,ground,engine)==(!owned?0:(!ground?1:(engine?2:3))));
}
assert(Rules::Deadline(1,240001)&&!Rules::Deadline(1,240002));
assert(!Rules::Terminal(State::TRANSLATING));
assert(!Rules::Deadline(1,1)&&!Rules::Deadline(-1,100)&&!Rules::Deadline(NAN,10));
assert(Rules::Envelope(-3,0,.85)&&Rules::Envelope(18,7.999f,1));
assert(!Rules::Envelope(18.001f,0,1)&&!Rules::Envelope(8,8,1)&&!Rules::Envelope(8,0,.849f));
assert(!Rules::Envelope(NAN,0,1));assert(!Rules::Envelope(0,NAN,1));
assert(Rules::Landed(true,0,0,0,1)&&!Rules::Landed(false,0,0,0,1));
assert(Rules::Hover(8,0,0,1)&&!Rules::Hover(8,-1,0,1));
assert(Rules::Vertical(8,true)<0&&Rules::Vertical(0,false)>0);
// Airborne cancellation retains control and original deadline, including repeats.
Probe airborne;airborne.Cancel("report expired");assert(airborne.m_State==State::LANDING&&airborne.released==0);
assert(airborne.m_StageDeadline==66000&&airborne.m_Deadline==200000);
airborne.now=5000;airborne.Cancel("duplicate");assert(airborne.m_StageDeadline==66000&&airborne.m_Reason=="report expired");
Probe translating;translating.m_State=State::TRANSLATING;translating.m_TranslationRequested=true;translating.Cancel("cancel translation");
assert(translating.m_State==State::LANDING&&translating.released==0&&translating.m_Deadline==200000);
translating.now=5000;translating.Cancel("duplicate translation cancellation");
assert(translating.m_State==State::LANDING&&translating.released==0&&translating.m_Deadline==200000&&translating.m_StageDeadline==66000&&translating.m_Reason=="cancel translation");
// Normal cancellation ends only after two distinct grounded seconds and engine off.
airborne.ground=true;airborne.m_Simulation.contact=true;airborne.m_Aircraft.position={0,0,0};
airborne.m_LastControl=5000;airborne.Tick();assert(airborne.m_State==State::LANDING);
airborne.now=6000;airborne.m_LastControl=6000;airborne.Tick();
airborne.now=7000;airborne.m_LastControl=7000;airborne.Tick();assert(airborne.m_State==State::SHUTDOWN&&airborne.m_Controller.stops==1);
airborne.now=7500;airborne.m_LastControl=7500;airborne.Tick();assert(airborne.m_State==State::CANCELLED&&airborne.restored==1);
// Noncancelled natural run preserves continuous five-second hover then landing.
Probe normal;for(int i=0;i<=5;i++){normal.now=1000+i*1000;normal.m_LastControl=normal.now;normal.Tick();}
assert(normal.m_State==State::LANDING&&!normal.m_Cancelled&&normal.released==0);
// A gap cannot manufacture five continuous seconds of observed hover.
Probe gap;gap.Tick();gap.now=7000;gap.m_LastControl=7000;gap.Tick();assert(gap.m_State==State::CLIMB&&gap.m_Stable==7000);
// Deadline failure remains explicitly FAILED, never successful/available or engine killed.
Probe expired;expired.m_Deadline=1500;expired.Cancel("cancel");assert(expired.m_StageDeadline==1500);
expired.now=1500;expired.Tick();assert(expired.m_State==State::FAILED&&expired.restored==0&&expired.m_Controller.stops==0);
assert(expired.logs>0&&!expired.m_Reason.empty());
// Capture on either Cancel or Tick causes no autohover/engine writes.
for(bool cancel:{false,true}){Probe capture;capture.owned=false;if(cancel)capture.Cancel("capture");else capture.Tick();
assert(capture.m_State==State::RELINQUISHED&&capture.m_Controller.hoverWrites==0&&capture.m_Controller.stops==0&&capture.restored==0);}
Probe dead;dead.healthy=false;dead.Cancel("pilot dead");assert(dead.m_State==State::FAILED&&dead.m_Controller.hoverWrites==0&&dead.m_Controller.stops==0);
Probe grounded;grounded.m_State=State::HOOKS;grounded.ground=true;grounded.m_Controller.engine=false;grounded.Cancel("before start");
assert(grounded.m_State==State::CANCELLED&&grounded.restored==1&&grounded.m_Controller.stops==0);
Probe groundRunning;groundRunning.m_State=State::STARTING;groundRunning.ground=true;groundRunning.Cancel("on ground");assert(groundRunning.m_State==State::SHUTDOWN&&groundRunning.m_Controller.stops==1);
Probe touchdown;touchdown.ground=true;touchdown.Cancel("new contact");assert(touchdown.m_State==State::LANDING&&touchdown.m_Controller.stops==0);
Probe noHover;noHover.m_Controller.accept=false;noHover.m_Controller.autohover=false;noHover.Cancel("abort");assert(noHover.m_State==State::FAILED&&noHover.m_Controller.stops==0);
Probe escape;escape.m_Aircraft.position={8,8,0};escape.Tick();assert(escape.m_State==State::FAILED&&escape.m_Controller.stops==0);
// Input callbacks independently enforce expiry, even with no owner Tick.
Probe staleCallback;staleCallback.now=staleCallback.m_StageDeadline;
assert(!staleCallback.ControlLease()&&staleCallback.m_State==State::FAILED&&staleCallback.m_Controller.stops==0);
Probe callbackCapture;callbackCapture.owned=false;
assert(!callbackCapture.ControlLease()&&callbackCapture.m_State==State::RELINQUISHED&&callbackCapture.m_Controller.hoverWrites==0);
Probe callbackEscape;callbackEscape.m_Aircraft.position={9,8,0};
assert(!callbackEscape.ControlLease()&&callbackEscape.m_State==State::FAILED);
Probe callbackOK;assert(callbackOK.ControlLease());
Probe quiet;quiet.m_LastControl=-1;quiet.Tick();assert(quiet.m_State==State::FAILED);
}
''')
# Integration contracts: no implicit activation or physical shortcuts introduced.
source=SOURCE.read_text()
assert 'modded class' not in source and 'CallLater(' not in source and 'SpawnEntity' not in source
for forbidden in ('SetWorldTransform(', 'SetVelocity(', 'SetHealth(', 'SetFuel(', 'SetActive(', 'SetSignalValue('):assert forbidden not in source
release=body(SOURCE,'protected void Release(')
assert release.index('m_SettingsOwned && Owns()') < release.index('Pair(')
assert release.index('restore && Grounded()') < release.index('SetAutohoverEnabled(m_OriginalAutohover)')
assert 'StopEngine' not in release
prepare=body(SOURCE,'void PrepareActions(')
assert prepare.index('!ControlLease()') < prepare.index('actions.ActivateContext(')
assert 'actions != m_Actions' in prepare
assert source.count('m_Controller.StartEngine()')==1
assert source.count('m_Controller.StopEngine(false)')==1
print('PASS: actual aircraft cancellation/stage rules, ground-only completion, expiry/capture faults, input ownership and no physical shortcuts')