Files
EscapistsReintroduced/addons/ai/Scripts/Game/ERR/ERR_AircraftFlight.c
T

464 lines
26 KiB
C

// Explicitly started control mechanism. The exact-owner production bridge adds
// no force allocation, flight creation 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<Vehicle, ERR_AircraftFlight> s_Owners = new map<Vehicle, ERR_AircraftFlight>();
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<int> 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_Mode.GetWorld() != GetGame().GetWorld() || m_Aircraft.GetWorld() != m_Mode.GetWorld() || m_Pilot.GetWorld() != m_Mode.GetWorld()) 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();
}
// Registry lookup never adopts an aircraft or starts a flight. Recheck the
// original lease/ownership before dispatch, including human capture. The
// character lookup is O(1), based on the callback actor's physical vehicle.
static void DispatchActions(CharacterControllerComponent controller, IEntity owner, ActionManager actions, float dt, bool player)
{
if (!Replication.IsServer() || !s_Owners || !controller || !owner || !actions) return;
Vehicle aircraft = Vehicle.Cast(CompartmentAccessComponent.GetVehicleIn(owner));
if (!aircraft) return;
ERR_AircraftFlight flight = s_Owners.Get(aircraft);
if (!flight || !flight.Lease()) return;
if (!flight.Owns()) { flight.Relinquish("native character callback lost original ownership"); return; }
if (player) return;
flight.PrepareActions(controller, owner, actions, dt);
}
static void DispatchHelicopter(HelicopterControllerComponent controller)
{
if (!Replication.IsServer() || !s_Owners || !controller) return;
Vehicle aircraft = Vehicle.Cast(controller.GetOwner());
if (!aircraft) return;
ERR_AircraftFlight flight = s_Owners.Get(aircraft);
if (!flight || !flight.Lease()) return;
if (!flight.Owns()) { flight.Relinquish("native helicopter callback lost original ownership"); return; }
flight.PrepareHelicopter(controller);
}
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 and calls Tick until terminal. The production bridge
// dispatches the two exact-owner native callbacks; callers must not duplicate them.
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); }
}