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.
420 lines
29 KiB
Python
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/00_Core/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()
|