Files
EscapistsReintroduced/tests/test_powered_flight_policy.py
T

420 lines
29 KiB
Python

"""Run actual extracted fixture rules; no native flight or Enforce claims."""
from pathlib import Path
import unittest
from policy_harness import body, run
SOURCE=Path(__file__).resolve().parents[1]/'addons/core/Scripts/Game/ERR/ERR_NativePoweredFlightProbe.c'
class PoweredFlightPolicy(unittest.TestCase):
def test_actual_authority_all_inputs(self):
method=body(SOURCE,'static bool Authority(')
run('#include <cassert>\n'+method+'''
int main(){for(int mask=0;mask<256;mask++) {
bool server=mask&1, enabled=mask&2, stopped=mask&4, pilot=mask&8, seat=mask&16, player=mask&32, alive=mask&64, fuel=mask&128;
assert(Authority(server,enabled,stopped,pilot,seat,player,alive,fuel)==(server&&enabled&&!stopped&&pilot&&seat&&!player&&alive&&fuel));
}}
''')
def test_actual_hover_landing_and_bounded_input_rules(self):
methods='\n'.join(body(SOURCE,key).replace('Math.','Math::') for key in ['static float ClampInput(', 'static float DesiredVertical(', 'static bool Hover(', 'static bool Landed('])
run('''
#include <cassert>
#include <cmath>
#include <algorithm>
struct Math { static float Clamp(float x,float low,float high){return std::clamp(x,low,high);} static float AbsFloat(float x){return std::abs(x);} };
'''+methods+'''
int main(){
assert(ClampInput(3)==1 && ClampInput(-3)==-1);
assert(ClampInput(0.8f,0.15f)==0.15f);
assert(DesiredVertical(0,false)==1.5f && DesiredVertical(8,false)==0);
assert(DesiredVertical(8,true)==-0.8f && DesiredVertical(0,true)==0);
assert(Hover(8,0.2f,0.1f,1,1));
assert(!Hover(0,0,0,1,0) && !Hover(11,0,0,1,0));
assert(!Hover(8,2,0,1,0) && !Hover(8,0,0.6f,1,0));
assert(!Hover(8,0,0,0.9f,0) && !Hover(8,0,0,1,9));
assert(Landed(true,0.1f,0.1f,0.01f,1));
assert(!Landed(false,0,0,0,1)); // low stationary hover is never landing
assert(!Landed(true,2,0,0,1) && !Landed(true,0,1,0,1));
assert(!Landed(true,0,0,0.2f,1) && !Landed(true,0,0,0,0.8f));
}
''')
def test_native_input_only_and_local_hooks(self):
source=SOURCE.read_text()
for forbidden in ('SetVelocity(', 'SetAngularVelocity(', 'SetWorldTransform(', 'SetOrigin(', 'SetAngles(', '.Teleport(', 'ForceStartEngine(', '.EngineStart(', 'RotorSetForce', 'RotorSetTorque', 'SetHealth(', 'SetFuel(', 'SetGlobalFuelConsumptionScale'):
self.assertNotIn(forbidden,source)
self.assertTrue(source.startswith('#ifdef WORKBENCH'))
self.assertIn('outcome == "powered-flight"',source)
self.assertIn('controller != m_Character || owner != m_Pilot',source)
self.assertIn('controller != m_Controller',source)
self.assertIn('super.OnPrepareControls(owner, am, dt, player)',source)
self.assertEqual(source.count('.StartEngine()'),1)
self.assertEqual(source.count('physics.SetActive('),1)
self.assertIn('m_Wakes == 0',source)
for control in ('Collective','CyclicForward','CyclicAside','AntiTorque'):
self.assertIn('m_Input.SetVehicle'+control+'(',source)
self.assertIn('m_Input.GetVehicle'+control+'()',source)
def test_native_initialization_precedes_single_pilot_allocation(self):
source=SOURCE.read_text()
tick=body(SOURCE,'int Tick(')
spawn=tick.split('if (m_Stage == 0)',1)[1].split('if (m_Stage == 8)',1)[0]
initialize=tick.split('if (m_Stage == 8)',1)[1].split('if (m_Stage == 1)',1)[0]
seating=tick.split('if (m_Stage == 1)',1)[1].split('\n Diagnostic(now)',1)[0]
self.assertNotIn('SpawnCharacterInCompartment',spawn)
self.assertIn('m_Stage = 8; m_StageDeadline = now + 30000',spawn)
self.assertEqual(source.count('.SpawnCharacterInCompartment('),1)
call=initialize.index('.SpawnCharacterInCompartment(')
for guard in ('now >= m_StageDeadline', 'if (!componentsReady) return 0;', 'm_PilotSpawnRequested = true;', 'm_Group.GetAgentsCount() == 0'):
self.assertLess(initialize.index(guard),call)
self.assertNotIn('m_StageDeadline =',initialize)
self.assertIn('nativeGroup == m_Group',initialize)
self.assertIn('m_Controller.GetPilotCompartmentSlot() != m_Seat',seating)
self.assertIn('m_Controller.GetPilotCompartmentSlot() == m_Seat',body(SOURCE,'protected bool PoweredAuthority('))
def test_empty_primary_native_seat_and_readiness_are_observed(self):
method=body(SOURCE,'protected bool PollNativeFlightComponents(')
for guard in ('manager.GetCompartments(slots)', 'slots.Count() <= 64', 'PilotCompartmentSlot.Cast(slot)', 'pilotSlot.GetOwner() != m_Aircraft', 'pilotSlot.GetVehicle() != m_Aircraft', 'GetCompartmentName() != "PilotCompartment"', 'ECompartmentType.PILOT', '!pilotSlot.IsCompartmentAccessible()', 'pilotSlot.GetOccupant() || pilotSlot.IsReserved() || pilotSlot.IsGetInLocked()', 'm_Simulation.IsValid()', 'm_Fuel.HasFuel()'):
self.assertIn(guard,method)
self.assertIn('now >= m_NextInitializationLog || now >= m_StageDeadline',method)
self.assertIn('m_NextInitializationLog = now + 2000',method)
self.assertIn('return m_Controller && validSimulation && m_Seat && fueled;',method)
self.assertNotIn('.Spawn(',method)
self.assertNotIn('.SpawnCharacterInCompartment(',method)
def test_seating_diagnostics_observe_all_guards_without_mutation(self):
method=body(SOURCE,'protected void SeatingDiagnostic(')
for predicate in ('OwnPilot()', 'm_Seat.GetOccupant() == m_Pilot', 'm_Controller.GetPilotCompartmentSlot() == m_Seat', 'ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft)', 'ERR_Actors.Group(m_Pilot)', 'ERR_Actors.CanFight(m_Pilot)', 'physics.IsActive()', 'physics.IsDynamic()', 'physics.GetVelocity()', 'physics.GetAngularVelocity()', 'm_Simulation.HasAnyGroundContact()', 'm_Simulation.IsValid()', 'access.IsGettingIn()', 'access.IsGettingOut()', 'm_Simulation.GetWorldTransform(nativeTransform)', 'm_Aircraft.GetWorldTransform(entityTransform)'):
self.assertIn(predicate,method)
self.assertIn('now < m_NextSeatingLog && now < m_StageDeadline',method)
self.assertIn('m_NextSeatingLog = now + 2000',method)
self.assertIn('ERR_CasualtyGroundSupport.Parked(m_Aircraft, now >= m_StageDeadline)',method)
for mutation in ('.Set', '.Spawn', '.Delete', '.GetInVehicle(', 'm_StageDeadline =', 'm_Stable =', 'Check('):
self.assertNotIn(mutation,method)
seating=body(SOURCE,'int Tick(').split('if (m_Stage == 1)',1)[1].split('\n Diagnostic(now);',1)[0]
self.assertIn('SeatingDiagnostic(now);',seating)
self.assertLess(seating.index('SeatingDiagnostic(now);'),seating.index('now >= m_StageDeadline'))
def test_memberless_group_and_exact_pilot_lod_are_explicit(self):
source=SOURCE.read_text()
self.assertIn('static const ResourceName GROUP = "{000CD338713F2B5A}Prefabs/AI/Groups/Group_Base.et";',source)
self.assertNotIn('Prefabs/Groups/OPFOR/Group_USSR_Base.et',source)
self.assertIn('m_Group.InitFactionKey("USSR") && m_Group.GetFactionName() == "USSR"',source)
initialize=body(SOURCE,'int Tick(').split('if (m_Stage == 8)',1)[1].split('if (m_Stage == 1)',1)[0]
self.assertIn('pilotControl.GetControlAIAgent()',initialize)
self.assertIn('m_PilotAgent && OwnPilot()',initialize)
self.assertIn('m_OriginalPilotLOD = m_PilotAgent.GetPermanentLOD()',initialize)
self.assertIn('m_PilotAgent.SetPermanentLOD(0); m_PilotAgent.ActivateAI();',initialize)
self.assertEqual(source.count('m_PilotAgent.SetPermanentLOD(0)'),1)
self.assertIn('m_PilotLODSaved && m_PilotAgent && OwnPilot() && !PlayerPresent()',body(SOURCE,'void Stop()'))
self.assertIn('m_PilotAgent.SetPermanentLOD(m_OriginalPilotLOD)',body(SOURCE,'void Stop()'))
def test_native_collective_encoding_preserves_signed_controller(self):
methods='\n'.join(body(SOURCE,key).replace('Math.','Math::') for key in ['static float ClampInput(', 'static float NativeCollective('])
run('#include <cassert>\n#include <algorithm>\nstruct Math { static float Clamp(float x,float low,float high){return std::clamp(x,low,high);} };\n'+methods+'''
int main(){
assert(NativeCollective(-1)==0); assert(NativeCollective(0)==0.5f); assert(NativeCollective(1)==1);
assert(NativeCollective(-5)==0 && NativeCollective(5)==1);
float previous=-1;
for(int i=-100;i<=100;i++) {float command=NativeCollective(i/100.f); assert(command>=0 && command<=1 && command>=previous); previous=command;}
}
''')
demand=body(SOURCE,'protected bool ComputePilotDemand(')
self.assertIn('ERR_PoweredFlightRules.CollectiveCommand(collective, m_ActionInputs, m_Stage)',demand)
pilot=body(SOURCE,'void ApplyPilot(')
self.assertLess(pilot.index('ComputePilotDemand('),pilot.index('m_Input.SetVehicleCollective(collective)'))
self.assertIn('m_Input.GetVehicleCollective() == 0',SOURCE.read_text())
def test_actual_signed_collective_encoding_and_negative_gate(self):
methods='\n'.join(body(SOURCE,key).replace('Math.','Math::') for key in ['static float ClampInput(', 'static float NativeCollective(', 'static float CollectiveCommand(', 'static bool NegativeCollectiveObserved('])
run('#include <cassert>\n#include <algorithm>\n#include <cmath>\nstruct Math {static float Clamp(float x,float a,float b){return std::clamp(x,a,b);} static float AbsFloat(float x){return std::abs(x);}};\n'+methods+"""
int main(){
for(int stage=0;stage<11;stage++)for(float demand: {-2.f,-1.f,-.05f,0.f,.5f,2.f}){
assert(CollectiveCommand(demand,false,stage)==NativeCollective(demand));
if(stage>=4&&stage<=6)assert(CollectiveCommand(demand,true,stage)==ClampInput(demand));
else if(stage==9)assert(CollectiveCommand(demand,true,stage)==.05f);
else if(stage==10)assert(CollectiveCommand(demand,true,stage)==-.05f);
else if(stage==7)assert(CollectiveCommand(demand,true,stage)==-1);
else assert(CollectiveCommand(demand,true,stage)==0);
}
assert(NegativeCollectiveObserved(-.05f,-.05f));
for(float bad: {0.f,.05f,-1.f,NAN})assert(!NegativeCollectiveObserved(-.05f,bad));
assert(!NegativeCollectiveObserved(0,0));assert(!NegativeCollectiveObserved(.05f,.05f));
assert(!NegativeCollectiveObserved(NAN,-.05f));
}
""")
source=SOURCE.read_text();tick=body(SOURCE,'int Tick(')
gate=tick.split('if (m_Stage == 10)',1)[1].split('if (m_Stage == 4 ||',1)[0]
for required in ('now >= m_StageDeadline', 'm_Controller.GetCollectiveMode() == CollectiveMode.Direct', '!m_Controller.GetAutohoverEnabled()', 'm_ControlUpdates >= m_NegativeGateUpdates + 3', 'now - m_NegativeSignalSince < 500'):
self.assertIn(required,gate)
self.assertLess(gate.index('negativeIndex >= 0'),gate.index('GetSignalValue(negativeIndex)'))
self.assertLess(gate.index('now - m_NegativeSignalSince < 500'),gate.index('m_SignedCollectiveProven = true'))
self.assertLess(gate.index('m_SignedCollectiveProven = true'),gate.index('Check(BeginAutohover()'))
self.assertEqual(source.count('m_SignedCollectiveProven = true'),1)
self.assertIn('m_ActionInputs && m_Stage >= 4 && m_Stage <= 6 && !m_SignedCollectiveProven',body(SOURCE,'protected bool ComputePilotDemand('))
self.assertIn('m_NegativeSignalSince = -1;',tick.split('now - m_PreviousTick > 1500',1)[1].split('\n',1)[0])
def test_heading_uses_explicit_yaw_not_xyz_pitch(self):
source=SOURCE.read_text()
self.assertNotIn('m_Aircraft.GetAngles()[0]',source)
self.assertIn('m_Heading = m_Aircraft.GetYawPitchRoll()[0]',source)
self.assertIn('float headingError = m_Heading - m_Aircraft.GetYawPitchRoll()[0]',source)
def test_control_and_flight_geometry_use_actual_world_pose(self):
for signature in ('protected bool ComputePilotDemand(', 'protected void Diagnostic(', 'int Tick('):
method=body(SOURCE,signature)
self.assertIn('m_Aircraft.GetWorldTransform(transform)',method)
self.assertNotIn('m_Simulation.GetWorldTransform(transform)',method)
# Internal simulation pose remains diagnostic only, never geometry feedback.
self.assertEqual(SOURCE.read_text().count('m_Simulation.GetWorldTransform('),1)
def test_final_apply_is_the_only_integrator_and_positive_signal_is_required(self):
source=SOURCE.read_text()
prepare=body(SOURCE,'override void OnPrepareControls(')
native_apply=body(SOURCE,'protected override void OnApplyControls(')
observe=body(SOURCE,'void ObservePilotPreparation(')
self.assertNotIn('ApplyPilot',prepare)
self.assertNotIn('.SetVehicle',observe)
self.assertNotIn('m_Collective =',observe)
self.assertLess(native_apply.index('super.OnApplyControls(owner, timeSlice)'),native_apply.index('probe.ApplyPilot(this, owner, timeSlice)'))
self.assertEqual(source.count('m_Collective + error * dt'),1)
self.assertEqual(source.count('probe.ApplyPilot('),1)
gate=body(SOURCE,'int Tick(').split('if (m_Stage == 9)',1)[1].split('if (m_Stage == 4 ||',1)[0]
self.assertIn('now >= m_StageDeadline',gate)
self.assertIn('signals.FindSignal("MainRotorCollective")',gate)
self.assertLess(gate.index('signalIndex >= 0'),gate.index('signals.GetSignalValue(signalIndex)'))
self.assertIn('now - m_PositiveSignalSince < 500',gate)
self.assertIn('m_ControlUpdates >= m_PositiveGateUpdates + 3',gate)
method=body(SOURCE,'static bool CollectiveObserved(').replace('Math.','Math::')
run('#include <cassert>\n#include <cmath>\nstruct Math { static float AbsFloat(float x){return std::abs(x);} };\n'+method+'''
int main(){assert(CollectiveObserved(.05f,.05f));assert(!CollectiveObserved(.05f,0));assert(!CollectiveObserved(0,0));assert(!CollectiveObserved(.05f,1));assert(!CollectiveObserved(.5f,.5f));assert(!CollectiveObserved(.05f,NAN));}
''')
def test_actor_action_mode_is_exclusive_owned_and_precedes_native_super(self):
source=SOURCE.read_text()
native=body(SOURCE,'override void OnPrepareControls(')
self.assertLess(native.index('probe.PreparePilotActions('),native.index('super.OnPrepareControls('))
apply=body(SOURCE,'void ApplyPilot(')
self.assertLess(apply.index('if (m_ActionInputs) return;'),apply.index('ComputePilotDemand('))
prepare=body(SOURCE,'void PreparePilotActions(')
self.assertIn('!m_ActionInputs || !m_Bound',prepare)
self.assertIn('controller != m_Character || owner != m_Pilot || !PoweredAuthority()',prepare)
self.assertLess(prepare.index('!OwnActionManager(am)'),prepare.index('am.ActivateContext('))
self.assertNotIn('.SetVehicle',prepare)
ownership=body(SOURCE,'protected bool OwnActionManager(')
self.assertIn('am == m_PilotAgent.GetActionManager()',ownership)
self.assertIn('am != GetGame().GetInputManager()',ownership)
self.assertEqual(source.count('GetInputManager()'),1)
self.assertIn('System.GetCLIParam("errFlightActionInputs", actionInputs)',source)
for axis in ('HelicopterCollectiveIncrease', 'HelicopterCollectiveDecrease', 'HelicopterCyclicForward', 'HelicopterCyclicBack', 'HelicopterCyclicRight', 'HelicopterCyclicLeft', 'HelicopterAntiTorqueRight', 'HelicopterAntiTorqueLeft'):
self.assertIn('"'+axis+'"',prepare)
self.assertIn('"'+axis+'"',body(SOURCE,'void Stop()'))
stop=body(SOURCE,'void Stop()')
self.assertIn('ours && m_ActionInputs && OwnActionManager(m_OwnedActions)',stop)
self.assertIn('ours && !m_ActionInputs && m_Input',stop)
def test_actual_directional_action_writer_zeros_opposite_and_bounds_values(self):
method=body(SOURCE,'bool WriteActionPair(').replace('ActionManager am','ActionManager& am').replace('Math.','Math::')
run('#include <cassert>\n#include <cmath>\n#include <algorithm>\n#include <map>\n#include <string>\nusing string=std::string;\nstruct Math {static float Clamp(float x,float a,float b){return std::clamp(x,a,b);} static float AbsFloat(float x){return std::abs(x);}};\nstruct ActionManager {std::map<string,float> values; void SetActionValue(string key,float value){values[key]=value;} float GetActionValue(string key){return values[key];}};\n'+method+'''
int main(){ActionManager owned; owned.values["back"]=1;assert(WriteActionPair(owned,"forward","back",.3f));assert(owned.values["forward"]==.3f&&owned.values["back"]==0);assert(WriteActionPair(owned,"forward","back",-.4f));assert(owned.values["forward"]==0&&owned.values["back"]==.4f);assert(WriteActionPair(owned,"forward","back",0));assert(owned.values["forward"]==0&&owned.values["back"]==0);assert(WriteActionPair(owned,"forward","back",4));assert(owned.values["forward"]==1&&owned.values["back"]==0);}
''')
def test_stop_and_engine_shutdown_are_bounded(self):
source=SOURCE.read_text();stop=body(SOURCE,'void Stop()')
self.assertIn('OwnPilot() && !PlayerPresent()',stop)
self.assertIn('s_Current = null',stop)
self.assertNotIn('StopEngine',stop) # no engine kill on midair fail/capture
self.assertNotIn('DeleteSpawned',stop)
self.assertIn('((m_Stage >= 4 && m_Stage <= 6) || m_Stage == 9 || m_Stage == 10) && !m_Controller.IsEngineOn()',source)
self.assertIn('m_Fuel.GetTotalFuel() < m_InitialFuel - 0.05',source)
self.assertIn('m_Controller.StopEngine(false)',source)
self.assertIn('ERR_PoweredFlightRules.Landed(m_Simulation.HasAnyGroundContact()',source)
def test_actual_autohover_configuration_readback_and_owned_restore(self):
methods='\n'.join(body(SOURCE,key) for key in ['bool SetAutohoverMode(', 'protected bool BeginAutohover(', 'protected bool AutohoverReady(', 'protected void RestoreAutohover('])
methods=methods.replace('protected ', '').replace('m_Controller.', 'm_Controller->').replace(' = null;', ' = nullptr;').replace('LogLevel.', 'LogLevel::')
run('''
#include <cassert>
#include <initializer_list>
template<class... T> void PrintFormat(T...) {}
struct LogLevel {static const int ERROR=1;};
int nativeErrors=0;
void Print(const char*,int){nativeErrors++;}
struct Controller { void* owner=nullptr; bool enabled=false, accept=true; int writes=0;
void* GetOwner(){return owner;} bool GetAutohoverEnabled(){return enabled;}
void SetAutohoverEnabled(bool value){writes++; if(accept)enabled=value;}
};
struct Probe {
int m_Stage=1;
bool m_Started=false,m_Stopped=false,m_Complete=false,m_Bound=false;
bool m_AutohoverMode=false,m_AutohoverSaved=false,m_OriginalAutohover=false,authority=true;
void* m_Aircraft=nullptr; Controller* m_Controller=nullptr; Controller* m_AutohoverController=nullptr;
bool PoweredAuthority(){return authority;}
'''+methods+'''
void Bind(Controller& c){m_Controller=&c;m_Aircraft=c.owner;m_Bound=true;m_Started=true;}
};
int main(){int airframe, foreign;
Controller c;c.owner=&airframe;
Probe manual;manual.Bind(c);assert(manual.BeginAutohover()&&manual.AutohoverReady());
manual.RestoreAutohover(true);assert(c.writes==0); // default is observational
Probe selected;assert(selected.SetAutohoverMode(true));selected.Bind(c);
assert(!selected.SetAutohoverMode(false));assert(selected.BeginAutohover());
assert(c.enabled&&c.writes==1&&selected.AutohoverReady());
Probe waiting;waiting.SetAutohoverMode(true);waiting.Bind(c);
for(int stage: {2,3,9,10}){waiting.m_Stage=stage;assert(waiting.AutohoverReady());}
for(int stage: {1,4,5,6,7}){waiting.m_Stage=stage;assert(!waiting.AutohoverReady());}
assert(!selected.BeginAutohover()&&c.writes==1); // no retry or timestamp-like renewal
c.enabled=false;assert(!selected.AutohoverReady());c.enabled=true;
selected.RestoreAutohover(true);assert(!c.enabled&&c.writes==2);
selected.RestoreAutohover(true);assert(c.writes==2); // idempotent cleanup
Probe alreadyEnabled;c.enabled=true;assert(alreadyEnabled.SetAutohoverMode(true));alreadyEnabled.Bind(c);
assert(alreadyEnabled.BeginAutohover());alreadyEnabled.RestoreAutohover(true);assert(c.enabled);
Probe captured;assert(captured.SetAutohoverMode(true));captured.Bind(c);assert(captured.BeginAutohover());
int before=c.writes;captured.RestoreAutohover(false);assert(c.writes==before&&c.enabled);
Probe refused;c.enabled=false;c.accept=false;assert(refused.SetAutohoverMode(true));refused.Bind(c);
assert(!refused.BeginAutohover()&&!refused.AutohoverReady());assert(!refused.BeginAutohover());
c.accept=true;refused.RestoreAutohover(true);assert(!c.enabled);
Probe mismatch;assert(mismatch.SetAutohoverMode(true));mismatch.Bind(c);mismatch.m_Aircraft=&foreign;
before=c.writes;assert(!mismatch.BeginAutohover()&&c.writes==before);
Probe inactive;assert(inactive.SetAutohoverMode(true));inactive.Bind(c);inactive.authority=false;
assert(!inactive.BeginAutohover()&&c.writes==before);
Probe changed;assert(changed.SetAutohoverMode(true));changed.Bind(c);assert(changed.BeginAutohover());
Controller other;other.owner=&airframe;changed.m_Controller=&other;
assert(!changed.AutohoverReady());before=c.writes;changed.RestoreAutohover(true);
assert(c.writes==before&&other.writes==0);
Probe restoreFailed;c.enabled=false;assert(restoreFailed.SetAutohoverMode(true));restoreFailed.Bind(c);
assert(restoreFailed.BeginAutohover());c.accept=false;restoreFailed.RestoreAutohover(true);
assert(nativeErrors==1&&c.enabled); // rejected restore is retained as native failure
for(int i=0;i<5;i++){Probe frozen;if(i==0){frozen.m_Started=true;}if(i==1){frozen.m_Stopped=true;}
if(i==2){frozen.m_Complete=true;}if(i==3){frozen.m_Bound=true;}if(i==4){frozen.m_Aircraft=&airframe;}
assert(!frozen.SetAutohoverMode(true));}
}
''')
def test_autohover_neutralizes_only_custom_cyclic_and_keeps_flight_gates(self):
neutral=body(SOURCE,'if (m_AutohoverMode)')
run('#include <cassert>\nvoid controls(bool m_AutohoverMode,float& forward,float& aside){'+neutral+'''}
int main(){float f=.1f,r=-.15f;controls(false,f,r);assert(f==.1f&&r==-.15f);
controls(true,f,r);assert(f==0&&r==0);}
''')
demand=body(SOURCE,'protected bool ComputePilotDemand(')
self.assertIn('!AutohoverReady()',demand)
self.assertIn('- angular[0] * 1.2',demand)
self.assertIn('+ angular[2] * 1.2',demand)
self.assertIn('headingError * 0.01 - angular[1] * 0.3',demand)
tick=body(SOURCE,'int Tick(')
self.assertIn('!m_ReadbackOK || !AutohoverReady()',tick)
self.assertLess(tick.index('m_Bound = true'),tick.index('Check(BeginAutohover()'))
self.assertLess(tick.index('m_Controller.StartEngine()'),tick.index('Check(BeginAutohover()'))
self.assertLess(tick.index('positive collective persists in existing native rotor signal'),tick.index('Check(BeginAutohover()'))
self.assertIn('now - m_Stable >= 5000',tick)
stop=body(SOURCE,'void Stop()')
self.assertIn('m_Seat.GetOccupant() == m_Pilot',stop)
self.assertIn('CompartmentAccessComponent.GetVehicleIn(m_Pilot) == m_Aircraft',stop)
self.assertIn('RestoreAutohover(ownSeat)',stop)
def test_actual_translation_geometry_requests_and_state_gates(self):
signatures=['static bool Hover(', 'static bool TranslationInputs(', 'static bool TranslationActive(', 'static bool TranslationTarget(', 'static vector TranslationDemand(', 'static bool TranslationHeld(']
methods='\n'.join(body(SOURCE,k) for k in signatures)
methods=methods.replace('Math.', 'Math::').replace('vector.', 'vector::').replace('out vector target','vector& target')
callbacks='\n'.join(body(SOURCE,k) for k in ['protected bool BeginTranslation(', 'protected bool CheckTranslation('])
callbacks=callbacks.replace('protected ','').replace('ERR_PoweredFlightRules.','Rules::').replace('vector.','vector::')
callbacks=callbacks.replace('m_Controller.GetSignalsManager()', 'm_Controller.GetSignalsManager()').replace('SignalsManagerComponent signals','SignalsManagerComponent* signals').replace('signals.FindSignal','signals->FindSignal')
run(r'''#include <cassert>
#include <cmath>
#include <algorithm>
#include <initializer_list>
#include <string>
struct Math {static float AbsFloat(float x){return std::abs(x);}};
struct vector {
float x=0,y=0,z=0;
float& operator[](int i){return i==0?x:(i==1?y:z);}
vector operator+(vector v)const{return{x+v.x,y+v.y,z+v.z};}
vector operator-(vector v)const{return{x-v.x,y-v.y,z-v.z};}
vector operator*(float k)const{return{x*k,y*k,z*k};}
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);}
static const vector Zero;
};
const vector vector::Zero={0,0,0};
template<class... T> void PrintFormat(T...){}
struct Rules {
'''+methods+r'''
};
struct SignalsManagerComponent {
bool present=true;
int FindSignal(const char*){return present?0:-1;}
};
struct Controller {SignalsManagerComponent signals; SignalsManagerComponent* GetSignalsManager(){return &signals;}};
struct Simulation {vector velocity; bool contact=false; vector GetVelocity(){return velocity;} bool HasAnyGroundContact(){return contact;}};
struct Probe {
bool m_TranslationMode=true,m_TranslationFrozen=false,m_TranslationProven=false,m_ActionInputs=true,m_AutohoverMode=true,hoverReady=true,stopped=false;
vector m_Home={100,25,200},m_TranslationOrigin,m_TranslationTarget;
int m_Stage=4,m_TranslationUpdates=0;
float m_StageDeadline=0,m_Stable=-1,m_NextLog=0,m_Deadline=240000;
Controller m_Controller; Simulation m_Simulation;
bool AutohoverReady(){return hoverReady;}
bool Check(bool ok,const char*){if(!ok)stopped=true;return ok;}
'''+callbacks+r'''
};
int main(){
const vector home={100,25,200},fallback={1,5,0};
for(float radius:{0.f,.49f,.5f,3.25f,7.99f})for(int angle=0;angle<360;angle+=5){
float radians=angle*3.14159265f/180;
vector start={home.x+radius*std::cos(radians),home.y+8,home.z+radius*std::sin(radians)},target;
assert(Rules::TranslationTarget(home,start,fallback,target));
assert(std::abs(vector::DistanceXZ(start,target)-4)<.0001f);
assert(target.y==start.y&&vector::DistanceXZ(home,target)<=5);
// Whole straight target segment lies inside the unchanged origin envelope.
for(int i=0;i<=20;i++)assert(vector::DistanceXZ(home,start+(target-start)*(i/20.f))<8);
}
vector target;
assert(!Rules::TranslationTarget(home,home,vector::Zero,target));
assert(!Rules::TranslationTarget(home,home+vector{8,0,0},fallback,target));
assert(!Rules::TranslationTarget(home,home+vector{NAN,0,0},fallback,target));
for(int mask=0;mask<16;mask++)for(int stage=0;stage<12;stage++){
bool selected=mask&1,frozen=mask&2,actions=mask&4,hover=mask&8;
assert(Rules::TranslationInputs(selected,actions,hover)==(!selected||(actions&&hover)));
assert(Rules::TranslationActive(selected,frozen,actions,hover,stage)==(selected&&frozen&&actions&&hover&&stage==5));
}
for(float x:{-100.f,-4.f,0.f,4.f,100.f})for(float z:{-10.f,0.f,10.f})for(float speed:{-30.f,-.5f,0.f,.5f,30.f}){
vector demand=Rules::TranslationDemand(home+vector{x,900,z},home,vector{speed,99,-speed});
assert(demand.Length()<=.030001f&&demand.y==0);
}
assert(Rules::TranslationDemand(home,home,vector::Zero).Length()==0);
assert(Rules::TranslationDemand(home+vector{4,0,0},home,vector::Zero).x>0);
assert(Rules::TranslationDemand(home+vector{4,0,0},home,vector{1,0,0}).x<0); // brake overspeed
assert(Rules::TranslationDemand(home,home,vector{0,0,-1}).z>0);
assert(Rules::TranslationHeld(false,8,.2f,0,1,4,.2f,.75f,3.25f));
assert(!Rules::TranslationHeld(true,8,.2f,0,1,4,.2f,0,4));
assert(!Rules::TranslationHeld(false,8,.2f,0,1,4,.25f,0,4));
assert(!Rules::TranslationHeld(false,8,.2f,0,1,4,.2f,.751f,4));
assert(!Rules::TranslationHeld(false,8,.2f,0,1,4,.2f,0,3.249f));
assert(!Rules::TranslationHeld(false,8,.2f,0,1,8,.2f,0,4));
// Execute real stage methods: frozen origin/target, distinct ticks, reset and bounded timeout.
Probe p;vector start=home+vector{3.25f,8,0};
assert(p.BeginTranslation(1000,start,fallback));
assert(p.m_Stage==5&&p.m_StageDeadline==46000&&p.m_Deadline==240000&&p.m_TranslationFrozen);
vector frozen=p.m_TranslationTarget;
assert(!p.BeginTranslation(2000,home,fallback)); // never retarget/rebase a running trip
assert(vector::DistanceXZ(frozen,p.m_TranslationTarget)==0&&p.m_StageDeadline==46000);
p.stopped=false;p.m_TranslationUpdates=3;
assert(p.CheckTranslation(2000,start,8,0,1,3.25));assert(p.m_Stable<0);
assert(p.CheckTranslation(3000,frozen,8,0,1,.75));assert(p.m_Stable==3000);
assert(p.CheckTranslation(7999,frozen,8,0,1,.75));assert(!p.m_TranslationProven);
p.m_Simulation.velocity={.3f,0,0};
assert(p.CheckTranslation(8000,frozen,8,.3f,1,.75));assert(p.m_Stable<0);
p.m_Simulation.velocity={0,0,0};
assert(p.CheckTranslation(9000,frozen,8,0,1,.75));
assert(p.CheckTranslation(13999,frozen,8,0,1,.75));assert(p.m_Stage==5);
assert(p.CheckTranslation(14000,frozen,8,0,1,.75));
assert(p.m_TranslationProven&&p.m_Stage==6&&p.m_StageDeadline==79000&&p.m_Deadline==240000);
Probe expired;assert(expired.BeginTranslation(1000,start,fallback));expired.m_TranslationUpdates=3;
assert(!expired.CheckTranslation(46000,expired.m_TranslationTarget,8,0,1,.75));assert(!expired.m_TranslationProven);
for(int invalid=0;invalid<7;invalid++){
Probe refused;
if(invalid==0)refused.m_TranslationMode=false;
if(invalid==1)refused.m_Stage=5;
if(invalid==2)refused.m_ActionInputs=false;
if(invalid==3)refused.m_AutohoverMode=false;
if(invalid==4)refused.hoverReady=false;
if(invalid==5)refused.m_Controller.signals.present=false;
if(invalid==6)refused.m_TranslationFrozen=true;
assert(!refused.BeginTranslation(1000,start,fallback)&&refused.stopped);
}
}
''')
tick=body(SOURCE,'int Tick(')
self.assertLess(tick.index('TranslationInputs('),tick.index('if (m_Stage == 0)'))
self.assertIn('height > 18 || height < -3 || drift >= 8 || transform[1][1] < 0.85',tick)
self.assertIn('now >= m_Deadline',tick)
self.assertIn('m_TranslationMode && !m_TranslationProven',tick)
demand=body(SOURCE,'protected bool ComputePilotDemand(')
self.assertLess(demand.index('if (m_AutohoverMode) { forward = 0; aside = 0; }'),demand.index('TranslationActive('))
self.assertEqual(SOURCE.read_text().count('m_TranslationTarget = target;'),1)
self.assertEqual(SOURCE.read_text().count('m_TranslationOrigin = position;'),1)
if __name__=='__main__': unittest.main()