// Prepared control mechanism only. No global hooks, force allocation or HQ selection. enum ERR_AircraftFlightState { IDLE, HOOKS, STARTING, POSITIVE, NEGATIVE, CLIMB, LANDING, SHUTDOWN, COMPLETE, CANCELLED, FAILED, RELINQUISHED, TRANSLATING } class ERR_AircraftFlightRules { static bool Terminal(int state) { return state >= ERR_AircraftFlightState.COMPLETE && state <= ERR_AircraftFlightState.RELINQUISHED; } static bool Deadline(float now, float deadline) { return now >= 0 && deadline > now && deadline - now <= 240000; } static bool Envelope(float height, float drift, float upright) { return height >= -3 && height <= 18 && drift >= 0 && drift < 8 && upright >= 0.85; } static bool Hover(float height, float speed, float vertical, float upright) { return height >= 7 && height <= 9 && speed >= 0 && speed < 1.5 && Math.AbsFloat(vertical) < 0.5 && upright > 0.98; } static bool Landed(bool contact, float height, float speed, float angular, float upright) { return contact && height > -1 && height < 1 && speed >= 0 && speed < 0.5 && angular >= 0 && angular < 0.1 && upright > 0.98; } static float Vertical(float height, bool landing) { float target = 8; if (landing) target = 0; return Math.Clamp((target - height) * 0.6, -0.8, 1.5); } // Ground aborts may stop the engine; airborne cancellation retains the flight lease. static int CancelAction(bool ownershipValid, bool landed, bool engineOn) { if (!ownershipValid) return 0; if (!landed) return 1; if (engineOn) return 2; return 3; } } // Bounded migration of the separately proven native-autohover translation. class ERR_AircraftTranslationRules { static bool Target(vector home, vector target) { float distance = vector.DistanceXZ(home, target); return distance >= 0 && distance <= 5; } static bool Reachable(float distance) { return distance >= 0 && distance <= 4.05; } static vector Demand(vector target, vector position, vector velocity) { vector error = target - position; error[1] = 0; vector desired = error * 0.25; float speed = desired.Length(); if (speed > 0.5) desired = desired * (0.5 / speed); velocity[1] = 0; vector demand = (desired - velocity) * 0.05; float magnitude = demand.Length(); if (magnitude > 0.03) demand = demand * (0.03 / magnitude); return demand; } static bool Held(bool contact, float height, float speed, float vertical, float upright, float horizontalSpeed, float targetError, float displacement, float distance) { if (contact || !ERR_AircraftFlightRules.Hover(height, speed, vertical, upright)) return false; if (!(horizontalSpeed >= 0 && horizontalSpeed < 0.25 && targetError >= 0 && targetError <= 0.75)) return false; return Reachable(distance) && displacement >= Math.Max(0, distance - 0.75); } } class ERR_AircraftFlight { protected static ref map s_Owners = new map(); protected ERR_GameMode m_Mode; protected Vehicle m_Aircraft; protected SCR_AIGroup m_Group; protected IEntity m_Pilot; protected PilotCompartmentSlot m_Seat; protected HelicopterControllerComponent m_Controller; protected VehicleHelicopterSimulation m_Simulation; protected CharacterControllerComponent m_Character; protected CharacterInputContext m_Input; protected AIAgent m_Agent; protected ActionManager m_Actions; protected FuelManagerComponent m_Fuel; protected ref ERR_AircraftFlightEnvelope m_Qualification; protected ERR_AircraftFlightState m_State; protected int m_Token; protected vector m_Home; protected float m_Heading; protected float m_Deadline; protected float m_StageDeadline; protected float m_Stable = -1; protected float m_LastTick = -1; protected float m_LastControl = -1; protected float m_InitialFuel; protected float m_Collective = -0.8; protected int m_ActionUpdates; protected int m_HelicopterUpdates; protected int m_GateUpdates; protected int m_OriginalLOD; protected CollectiveMode m_OriginalMode; protected bool m_OriginalAutohover; protected bool m_SettingsOwned; protected bool m_Readback = true; protected bool m_Cancelled; protected bool m_StartAttempted; protected string m_Reason; protected bool m_TranslationRequested; protected bool m_TranslationStarted; protected bool m_TranslationProven; protected vector m_TranslationTarget; protected vector m_TranslationOrigin; protected float m_TranslationDistance; protected int m_TranslationUpdates; ERR_AircraftFlightState State() { return m_State; } string Reason() { return m_Reason; } int OrderToken() { return m_Token; } bool Finished() { return ERR_AircraftFlightRules.Terminal(m_State); } Vehicle Aircraft() { return m_Aircraft; } bool TranslationStarted() { return m_TranslationStarted; } bool TranslationProven() { return m_TranslationProven; } vector TranslationTarget() { return m_TranslationTarget; } vector TranslationOrigin() { return m_TranslationOrigin; } int TranslationUpdates() { return m_TranslationUpdates; } // Absolute horizontal target fixed before takeoff. Its altitude is the existing // eight-metre hover, not a caller-supplied route or a new qualification home. bool StartTranslation(ERR_GameMode mode, Vehicle aircraft, SCR_AIGroup group, IEntity pilot, PilotCompartmentSlot seat, ERR_AircraftFlightEnvelope qualification, int token, float deadline, vector target) { if (m_StartAttempted || m_TranslationRequested || !qualification || !ERR_AircraftTranslationRules.Target(qualification.Home(), target)) return false; m_TranslationRequested = true; m_TranslationTarget = target; vector home = qualification.Home(); m_TranslationTarget[1] = home[1] + 8; return Start(mode, aircraft, group, pilot, seat, qualification, token, deadline); } protected float Now() { if (!GetGame() || !GetGame().GetWorld()) return -1; return GetGame().GetWorld().GetWorldTime(); } protected bool NoPlayers() { if (!GetGame() || !GetGame().GetPlayerManager()) return false; if (!m_Group || m_Group.GetPlayerCount(true) > 0 || ERR_Actors.Player(m_Pilot)) return false; array players = {}; GetGame().GetPlayerManager().GetPlayers(players); foreach (int id : players) { IEntity actor = GetGame().GetPlayerManager().GetPlayerControlledEntity(id); if (actor && (actor == m_Pilot || CompartmentAccessComponent.GetVehicleIn(actor) == m_Aircraft)) return false; } return true; } protected bool Owns() { if (!GetGame() || !GetGame().GetWorld() || !GetGame().GetPlayerManager()) return false; if (!Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active() || !m_Aircraft || !m_Group || !m_Pilot) return false; if (!m_Controller || m_Controller.GetOwner() != m_Aircraft || !m_Seat || m_Controller.GetPilotCompartmentSlot() != m_Seat || m_Seat.GetOccupant() != m_Pilot) return false; if (CompartmentAccessComponent.GetVehicleIn(m_Pilot) != m_Aircraft || ERR_Actors.Group(m_Pilot) != m_Group || m_Group.GetFaction() != m_Mode.EnemyFaction()) return false; if (SCR_FactionManager.SGetFaction(m_Aircraft) != m_Mode.EnemyFaction() || !NoPlayers()) return false; return m_Agent && m_Agent.GetControlledEntity() == m_Pilot && m_Agent.GetActionManager() == m_Actions && m_Actions && m_Actions != GetGame().GetInputManager(); } protected bool Healthy() { if (!ERR_Actors.Alive(m_Aircraft) || !ERR_Actors.CanFight(m_Pilot) || !m_Simulation || !m_Simulation.IsValid() || !m_Fuel || !m_Fuel.HasFuel()) return false; SCR_VehicleDamageManagerComponent damage = SCR_VehicleDamageManagerComponent.Cast(m_Aircraft.FindComponent(SCR_VehicleDamageManagerComponent)); if (!damage || !damage.GetEngineFunctional() || damage.GetMovementDamage() >= 1 || damage.IsOnFire() || m_Simulation.RotorCount() != 2) return false; for (int rotor = 0; rotor < 2; rotor++) if (m_Simulation.RotorGetState(rotor) == RotorState.DESTROYED) return false; return true; } protected bool Grounded() { if (!GetGame() || !GetGame().GetWorld() || !m_Aircraft || !m_Simulation) return false; vector transform[4]; m_Aircraft.GetWorldTransform(transform); return ERR_AircraftFlightRules.Landed(m_Simulation.HasAnyGroundContact(), transform[3][1] - m_Home[1], m_Simulation.GetVelocity().Length(), m_Simulation.GetAngularVelocity().Length(), transform[1][1]); } // Single use. A future HQ owner supplies its immutable unique order token and // retains this instance, calling Tick and the two scoped callbacks until terminal. bool Start(ERR_GameMode mode, Vehicle aircraft, SCR_AIGroup group, IEntity pilot, PilotCompartmentSlot seat, ERR_AircraftFlightEnvelope qualification, int token, float deadline) { if (m_StartAttempted || m_State != ERR_AircraftFlightState.IDLE || m_Token != 0 || !Replication.IsServer() || !GetGame() || !qualification || !aircraft || !pilot || !group || !seat) return false; m_StartAttempted = true; float now = Now(); if (!ERR_AircraftFlightRules.Deadline(now, deadline) || token <= 0 || s_Owners.Get(aircraft)) return false; m_Mode = mode; m_Aircraft = aircraft; m_Group = group; m_Pilot = pilot; m_Seat = seat; m_Controller = HelicopterControllerComponent.Cast(aircraft.FindComponent(HelicopterControllerComponent)); if (m_Controller) m_Simulation = m_Controller.GetSimulation(); m_Character = CharacterControllerComponent.Cast(pilot.FindComponent(CharacterControllerComponent)); if (m_Character) m_Input = m_Character.GetInputContext(); AIControlComponent control = AIControlComponent.Cast(pilot.FindComponent(AIControlComponent)); if (control) m_Agent = control.GetControlAIAgent(); if (m_Agent) m_Actions = m_Agent.GetActionManager(); m_Fuel = FuelManagerComponent.Cast(aircraft.FindComponent(FuelManagerComponent)); if (!Owns() || !Healthy() || !m_Input || m_Controller.IsEngineOn() || !ERR_CasualtyLoadingRules.AircraftReady(aircraft)) return false; Physics physics = aircraft.GetPhysics(); if (!physics || !physics.IsDynamic() || !physics.IsActive() || !m_Simulation.HasAnyGroundContact()) return false; SignalsManagerComponent signals = m_Controller.GetSignalsManager(); if (!signals || signals.FindSignal("MainRotorCollective") < 0) return false; if (!qualification.Claim(mode, aircraft, pilot, token)) return false; m_Qualification = qualification; m_Home = qualification.Home(); m_Token = token; m_Deadline = deadline; m_Heading = aircraft.GetYawPitchRoll()[0]; m_InitialFuel = m_Fuel.GetTotalFuel(); m_OriginalMode = m_Controller.GetCollectiveMode(); m_OriginalAutohover = m_Controller.GetAutohoverEnabled(); m_OriginalLOD = m_Agent.GetPermanentLOD(); m_SettingsOwned = true; s_Owners.Set(aircraft, this); m_Agent.SetPermanentLOD(0); m_Agent.ActivateAI(); m_Controller.SetAutohoverEnabled(false); if (m_Controller.GetAutohoverEnabled()) { Fail("native autohover disable refused"); return false; } m_State = ERR_AircraftFlightState.HOOKS; m_StageDeadline = Math.Min(deadline, now + 5000); PrintFormat("ERR AIRCRAFT CONTROL: token=%1 bound qualified finite aircraft; no HQ dispatch installed", token); return true; } protected bool Lease() { return m_Token > 0 && !Finished() && m_State != ERR_AircraftFlightState.IDLE && s_Owners.Get(m_Aircraft) == this && m_Qualification && m_Qualification.ClaimedBy(m_Aircraft, m_Pilot, m_Token); } // Every input entry checks the immutable deadlines even if owner Tick stalls. protected bool ControlLease() { if (!Lease()) return false; if (!Owns()) { Relinquish("ownership or world unavailable at control callback"); return false; } float now = Now(); if (now < 0 || now >= m_Deadline || now >= m_StageDeadline) { Fail("control callback reached bounded flight deadline"); return false; } if (!Healthy() || !m_Readback) { Fail("flight capability or input readback lost"); return false; } vector transform[4]; m_Aircraft.GetWorldTransform(transform); if (!ERR_AircraftFlightRules.Envelope(transform[3][1] - m_Home[1], vector.DistanceXZ(transform[3], m_Home), transform[1][1])) { Fail("control callback exceeded qualified flight envelope"); return false; } return true; } // Must be called after the native helicopter's OnPrepareControls. void PrepareHelicopter(HelicopterControllerComponent controller) { if (!Lease() || controller != m_Controller) return; if (!ControlLease()) return; controller.SetCollectiveMode(CollectiveMode.Direct); m_HelicopterUpdates++; } protected bool Pair(string positive, string negative, float demand) { float a = Math.Clamp(demand, 0, 1), b = Math.Clamp(-demand, 0, 1); m_Actions.SetActionValue(positive, a); m_Actions.SetActionValue(negative, b); return Math.AbsFloat(m_Actions.GetActionValue(positive) - a) < 0.001 && Math.AbsFloat(m_Actions.GetActionValue(negative) - b) < 0.001; } // Must be called on the exact AI actor manager before native OnPrepareControls. void PrepareActions(CharacterControllerComponent controller, IEntity owner, ActionManager actions, float dt) { if (!Lease() || controller != m_Character || owner != m_Pilot || actions != m_Actions) return; if (!ControlLease()) return; if (!m_Readback || dt <= 0 || dt > 0.2) return; if (!actions.IsContextActive("HelicopterContext")) actions.ActivateContext("HelicopterContext", 1); if (!actions.IsContextActive("HelicopterContext")) { m_Readback = false; return; } float collective, torque, forward, aside; if (m_State == ERR_AircraftFlightState.POSITIVE) collective = 0.05; else if (m_State == ERR_AircraftFlightState.NEGATIVE) collective = -0.05; else if (m_State == ERR_AircraftFlightState.SHUTDOWN) collective = -1; else if (m_State == ERR_AircraftFlightState.CLIMB || m_State == ERR_AircraftFlightState.LANDING || m_State == ERR_AircraftFlightState.TRANSLATING) { if (!m_Controller.GetAutohoverEnabled()) { Fail("native autohover lost while flying"); return; } float height = m_Aircraft.GetOrigin()[1] - m_Home[1]; float error = ERR_AircraftFlightRules.Vertical(height, m_State == ERR_AircraftFlightState.LANDING) - m_Simulation.GetVelocity()[1]; m_Collective = Math.Clamp(m_Collective + error * dt * 0.12, -0.95, 0.8); collective = Math.Clamp(m_Collective + error * 0.12, -1, 0.9); float headingError = m_Heading - m_Aircraft.GetYawPitchRoll()[0]; while (headingError > 180) headingError -= 360; while (headingError < -180) headingError += 360; torque = Math.Clamp(headingError * 0.01 - m_Simulation.GetAngularVelocityLocal()[1] * 0.3, -0.3, 0.3); if (m_State == ERR_AircraftFlightState.TRANSLATING) { if (!m_TranslationStarted || !ERR_AircraftTranslationRules.Target(m_Home, m_TranslationTarget)) { Fail("immutable translation target unavailable"); return; } vector transform[4]; m_Aircraft.GetWorldTransform(transform); vector demand = ERR_AircraftTranslationRules.Demand(m_TranslationTarget, transform[3], m_Simulation.GetVelocity()); forward = Math.Clamp(vector.Dot(demand, transform[2]), -0.03, 0.03); aside = Math.Clamp(vector.Dot(demand, transform[0]), -0.03, 0.03); m_TranslationUpdates++; } if (m_State == ERR_AircraftFlightState.LANDING && Grounded()) collective = -1; } bool a = Pair("HelicopterCollectiveIncrease", "HelicopterCollectiveDecrease", collective); bool b = Pair("HelicopterCyclicForward", "HelicopterCyclicBack", forward); bool c = Pair("HelicopterCyclicRight", "HelicopterCyclicLeft", aside); bool d = Pair("HelicopterAntiTorqueRight", "HelicopterAntiTorqueLeft", torque); m_Readback = a && b && c && d; m_ActionUpdates++; m_LastControl = Now(); } protected void Release(bool restore) { // No writes after capture/group/seat change, including apparently harmless zeros. if (m_SettingsOwned && Owns()) { Pair("HelicopterCollectiveIncrease", "HelicopterCollectiveDecrease", 0); Pair("HelicopterCyclicForward", "HelicopterCyclicBack", 0); Pair("HelicopterCyclicRight", "HelicopterCyclicLeft", 0); Pair("HelicopterAntiTorqueRight", "HelicopterAntiTorqueLeft", 0); // Restore native modes only after verified ground completion. Airborne // failure leaves native autohover enabled rather than disabling it midair. if (restore && Grounded()) { m_Controller.SetAutohoverEnabled(m_OriginalAutohover); m_Controller.SetCollectiveMode(m_OriginalMode); m_Agent.SetPermanentLOD(m_OriginalLOD); } } if (m_Aircraft && s_Owners && s_Owners.Get(m_Aircraft) == this) s_Owners.Remove(m_Aircraft); m_SettingsOwned = false; } protected void RecordState() { string detail = string.Format("token=%1 state=%2 reason=%3; physical availability remains with the allocating owner", m_Token, m_State, m_Reason); Print("ERR AIRCRAFT CONTROL: " + detail); if (m_Mode && m_Mode == ERR_GameMode.Get()) ERR_HQ.Event("AIRCRAFT CONTROL", detail); } protected void Relinquish(string reason) { if (Finished()) return; m_Reason = reason; m_State = ERR_AircraftFlightState.RELINQUISHED; Release(false); RecordState(); } protected void Fail(string reason) { if (Finished()) return; m_Reason = reason; m_State = ERR_AircraftFlightState.FAILED; Release(true); RecordState(); } protected bool Landing(float now) { m_Controller.SetAutohoverEnabled(true); if (!m_Controller.GetAutohoverEnabled()) { Fail("native autohover enable refused"); return false; } m_State = ERR_AircraftFlightState.LANDING; m_Stable = -1; m_StageDeadline = Math.Min(m_Deadline, now + 65000); return true; } protected void Shutdown(float now) { m_Controller.StopEngine(false); m_State = ERR_AircraftFlightState.SHUTDOWN; m_StageDeadline = Math.Min(m_Deadline, now + 15000); m_Stable = -1; } void Cancel(string reason) { if (!Lease()) return; if (m_Cancelled) return; // repeated invalid reports cannot extend a landing deadline m_Cancelled = true; m_Reason = reason; RecordState(); // After flight begins, even a current contact sample must pass the normal // two-second landing observation; cancellation cannot shortcut touchdown. bool preflightGround = m_State < ERR_AircraftFlightState.CLIMB && Grounded(); int action = ERR_AircraftFlightRules.CancelAction(Owns(), preflightGround, m_Controller && m_Controller.IsEngineOn()); if (action == 0) { Relinquish(reason); return; } if (!Healthy()) { Fail(reason + "; flight capability lost"); return; } if (action == 3) { m_State = ERR_AircraftFlightState.CANCELLED; Release(true); RecordState(); return; } float now = Now(); if (action == 2) { Shutdown(now); return; } if (m_State != ERR_AircraftFlightState.LANDING && m_State != ERR_AircraftFlightState.SHUTDOWN) Landing(now); } protected bool BeginTranslation(float now, vector position) { if (!m_TranslationRequested || m_TranslationStarted || m_State != ERR_AircraftFlightState.CLIMB) { Cancel("translation stage ownership refused"); return false; } float distance = vector.DistanceXZ(position, m_TranslationTarget); if (!ERR_AircraftTranslationRules.Target(m_Home, m_TranslationTarget) || !ERR_AircraftTranslationRules.Reachable(distance)) { Cancel("fixed target beyond qualified four-metre translation after hover drift"); return false; } SignalsManagerComponent signals = m_Controller.GetSignalsManager(); if (!signals || signals.FindSignal("MainRotorCyclicForward") < 0 || signals.FindSignal("MainRotorCyclicAside") < 0 || !m_Controller.GetAutohoverEnabled()) { Cancel("native autohover cyclic signals unavailable"); return false; } m_TranslationOrigin = position; m_TranslationDistance = distance; m_TranslationStarted = true; m_State = ERR_AircraftFlightState.TRANSLATING; m_Stable = -1; m_StageDeadline = Math.Min(m_Deadline, now + 45000); PrintFormat("ERR AIRCRAFT TRANSLATION: token=%1 origin=%2 immutableTarget=%3 home=%4 distance=%5 deadline=%6", m_Token, position, m_TranslationTarget, m_Home, distance, m_StageDeadline); return true; } protected void CheckTranslation(float now, vector position, float height, float speed, float upright) { if (!m_TranslationStarted || !m_Controller.GetAutohoverEnabled() || !ERR_AircraftTranslationRules.Target(m_Home, m_TranslationTarget)) { Cancel("translation ownership or autohover lost"); return; } vector velocity = m_Simulation.GetVelocity(); vector horizontal = velocity; horizontal[1] = 0; bool held = m_TranslationUpdates >= 3 && ERR_AircraftTranslationRules.Held(m_Simulation.HasAnyGroundContact(), height, speed, velocity[1], upright, horizontal.Length(), vector.DistanceXZ(position, m_TranslationTarget), vector.DistanceXZ(position, m_TranslationOrigin), m_TranslationDistance); if (!held) { m_Stable = -1; return; } if (m_Stable < 0) m_Stable = now; if (now - m_Stable < 5000) return; m_TranslationProven = true; PrintFormat("ERR AIRCRAFT TRANSLATION HELD: token=%1 targetError=%2 displacement=%3 updates=%4", m_Token, vector.DistanceXZ(position, m_TranslationTarget), vector.DistanceXZ(position, m_TranslationOrigin), m_TranslationUpdates); Landing(now); } // The owner continues ticking after Cancel until Finished, including landing. void Tick() { if (!Lease()) return; float now = Now(); if (!Owns()) { Relinquish("ownership or mission changed"); return; } if (!Healthy()) { Fail("flight capability lost"); return; } if (!m_Readback) { Fail("native action readback failed"); return; } if (now >= m_Deadline || now >= m_StageDeadline || now < 0) { Fail("bounded flight stage expired"); return; } if (m_LastTick >= 0 && (now < m_LastTick || now - m_LastTick > 1500)) m_Stable = -1; m_LastTick = now; vector transform[4]; m_Aircraft.GetWorldTransform(transform); float height = transform[3][1] - m_Home[1], speed = m_Simulation.GetVelocity().Length(); if (!ERR_AircraftFlightRules.Envelope(height, vector.DistanceXZ(transform[3], m_Home), transform[1][1])) { Fail("qualified flight envelope exceeded"); return; } if (m_State == ERR_AircraftFlightState.HOOKS) { if (m_ActionUpdates < 3 || m_HelicopterUpdates < 3) return; if (m_Controller.GetCollectiveMode() != CollectiveMode.Direct || m_Input.GetVehicleCollective() != 0 || m_Controller.IsEngineOn()) { Fail("native initial control state refused"); return; } m_Controller.StartEngine(); m_State = ERR_AircraftFlightState.STARTING; m_StageDeadline = Math.Min(m_Deadline, now + 65000); return; } if (m_LastControl < 0 || now - m_LastControl > 1000) { Fail("pilot callback stopped"); return; } if (m_State == ERR_AircraftFlightState.STARTING) { float rpm = m_Simulation.RotorGetRPMTarget(0); if (!m_Controller.IsEngineOn() || rpm <= 0 || m_Simulation.RotorGetRPM(0) < rpm * 0.9 || m_Fuel.GetTotalFuel() >= m_InitialFuel) return; m_State = ERR_AircraftFlightState.POSITIVE; m_StageDeadline = Math.Min(m_Deadline, now + 3000); m_GateUpdates = m_ActionUpdates; m_Stable = -1; return; } if (m_State != ERR_AircraftFlightState.SHUTDOWN && !m_Controller.IsEngineOn()) { Fail("running engine lost"); return; } if (m_State == ERR_AircraftFlightState.POSITIVE || m_State == ERR_AircraftFlightState.NEGATIVE) { SignalsManagerComponent signals = m_Controller.GetSignalsManager(); int index = -1; if (signals) index = signals.FindSignal("MainRotorCollective"); if (index < 0 || m_Controller.GetCollectiveMode() != CollectiveMode.Direct || m_Controller.GetAutohoverEnabled()) { Fail("native collective proof unavailable"); return; } float expected = 0.05; if (m_State == ERR_AircraftFlightState.NEGATIVE) expected = -0.05; bool matched = m_ActionUpdates >= m_GateUpdates + 3 && Math.AbsFloat(signals.GetSignalValue(index) - expected) <= 0.01 && Math.AbsFloat(m_Input.GetVehicleCollective() - expected) <= 0.01; if (!matched) { m_Stable = -1; return; } if (m_Stable < 0) m_Stable = now; if (now - m_Stable < 500) return; m_Stable = -1; if (m_State == ERR_AircraftFlightState.POSITIVE) { m_State = ERR_AircraftFlightState.NEGATIVE; m_StageDeadline = Math.Min(m_Deadline, now + 3000); m_GateUpdates = m_ActionUpdates; return; } m_Controller.SetAutohoverEnabled(true); if (!m_Controller.GetAutohoverEnabled()) { Fail("native autohover enable refused"); return; } m_State = ERR_AircraftFlightState.CLIMB; m_StageDeadline = Math.Min(m_Deadline, now + 65000); return; } if (m_State == ERR_AircraftFlightState.TRANSLATING) { CheckTranslation(now, transform[3], height, speed, transform[1][1]); return; } if (m_State == ERR_AircraftFlightState.CLIMB || m_State == ERR_AircraftFlightState.LANDING) { if (!m_Controller.GetAutohoverEnabled()) { Fail("native autohover lost"); return; } bool stable = Grounded(); float duration = 2000; if (m_State == ERR_AircraftFlightState.CLIMB) { stable = !m_Simulation.HasAnyGroundContact() && ERR_AircraftFlightRules.Hover(height, speed, m_Simulation.GetVelocity()[1], transform[1][1]); duration = 5000; } if (!stable) m_Stable = -1; if (stable && m_Stable < 0) m_Stable = now; if (m_Stable < 0 || now - m_Stable < duration) return; if (m_State == ERR_AircraftFlightState.CLIMB) { if (m_TranslationRequested) BeginTranslation(now, transform[3]); else Landing(now); return; } Shutdown(now); return; } if (m_State == ERR_AircraftFlightState.SHUTDOWN && !m_Controller.IsEngineOn()) { if (!Grounded()) { Fail("engine-off physical ground check failed"); return; } if (m_Cancelled) m_State = ERR_AircraftFlightState.CANCELLED; else m_State = ERR_AircraftFlightState.COMPLETE; Release(true); RecordState(); } } void ~ERR_AircraftFlight() { Release(true); } }