Add bounded aircraft control and verify current packaged runtime closure
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
// 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<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_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); }
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
// Successor-owned measured envelope, independent of aircraft yaw/pitch/roll.
|
||||
// Adapted from the separately qualified native vertical fixture; no native hook.
|
||||
// All distances are around the native physics origin, not the rotor centre.
|
||||
class ERR_AircraftFlightFootprintRules
|
||||
{
|
||||
static float CylinderSphere(float originDistance, float radius, float thickness)
|
||||
{
|
||||
// Triangle inequality deliberately overbounds the one-sided rotor cylinder.
|
||||
return originDistance + radius + thickness;
|
||||
}
|
||||
static bool Fits(float radius, float initialDrift, float initialHeight)
|
||||
{
|
||||
if (!(radius > 0 && radius < 32 && initialDrift >= 0 && initialDrift < 32)) return false;
|
||||
if (!(initialHeight >= -3 && initialHeight <= 5)) return false;
|
||||
return radius + initialDrift + 8 + 1 <= 40 && radius + initialHeight + 18 + 1 <= 60;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_AircraftFlightFootprint
|
||||
{
|
||||
static const int MAX_ENTITIES = 256;
|
||||
static const int MAX_DEPTH = 16;
|
||||
static const int MAX_HITZONES = 512;
|
||||
protected static bool Refuse(string reason)
|
||||
{
|
||||
Print("ERR AIRCRAFT FOOTPRINT: refused " + reason);
|
||||
return false;
|
||||
}
|
||||
protected static bool PointRadius(vector point, vector origin, inout float radius)
|
||||
{
|
||||
float distance = vector.Distance(point, origin);
|
||||
if (!(distance >= 0 && distance < 1000)) return false;
|
||||
radius = Math.Max(radius, distance);
|
||||
return true;
|
||||
}
|
||||
static bool Measure(Vehicle aircraft, VehicleHelicopterSimulation simulation, vector pad, out vector origin, out float radius)
|
||||
{
|
||||
radius = 0;
|
||||
if (!aircraft || !simulation || !simulation.IsValid() || !aircraft.GetPrefabData()) return Refuse("missing initialized native airframe");
|
||||
if (aircraft.GetPrefabData().GetPrefabName() != "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et") return Refuse("different airframe resource");
|
||||
// Model bounds and damage pivots are world-space. The simulation getter
|
||||
// exposes internal physics space (identity at the airport in 100422), so
|
||||
// use this same physical aircraft's actual entity world transform.
|
||||
vector worldTransform[4]; aircraft.GetWorldTransform(worldTransform);
|
||||
origin = worldTransform[3];
|
||||
for (int axis = 0; axis < 3; axis++)
|
||||
if (!(Math.AbsFloat(worldTransform[axis].Length() - 1) < 0.001)) return Refuse("non-unit native entity world basis");
|
||||
|
||||
array<IEntity> entities = {aircraft};
|
||||
array<int> depths = {0};
|
||||
array<HitZone> seenZones = {};
|
||||
int meshes, mainSweeps, tailSweeps;
|
||||
float modelRadius, damageRadius, simulationRadius;
|
||||
for (int index = 0; index < entities.Count(); index++)
|
||||
{
|
||||
IEntity entity = entities[index];
|
||||
if (!entity) return Refuse("hierarchy changed during measurement");
|
||||
if (entity.GetVObject())
|
||||
{
|
||||
vector mins, maxs; entity.GetWorldBounds(mins, maxs);
|
||||
for (int dimension = 0; dimension < 3; dimension++)
|
||||
if (!(mins[dimension] <= maxs[dimension])) return Refuse("invalid model bounds");
|
||||
// A sphere around every corner encloses the complete world AABB. Its radius
|
||||
// remains conservative under any rigid orientation of the whole aircraft.
|
||||
for (int x = 0; x < 2; x++)
|
||||
for (int y = 0; y < 2; y++)
|
||||
for (int z = 0; z < 2; z++)
|
||||
{
|
||||
vector corner = mins;
|
||||
if (x == 1) corner[0] = maxs[0];
|
||||
if (y == 1) corner[1] = maxs[1];
|
||||
if (z == 1) corner[2] = maxs[2];
|
||||
if (!PointRadius(corner, origin, modelRadius)) return Refuse("non-finite or excessive model bounds");
|
||||
}
|
||||
meshes++;
|
||||
}
|
||||
HitZoneContainerComponent container = HitZoneContainerComponent.Cast(entity.FindComponent(HitZoneContainerComponent));
|
||||
if (container)
|
||||
{
|
||||
int count = container.CountAllHitZones();
|
||||
if (count < 0 || count + seenZones.Count() > MAX_HITZONES) return Refuse("hitzone traversal bound");
|
||||
array<HitZone> zones = {};
|
||||
if (container.GetAllHitZones(zones) != count || zones.Count() != count) return Refuse("hitzones changed during measurement");
|
||||
foreach (HitZone zone : zones)
|
||||
{
|
||||
if (!zone || seenZones.Contains(zone)) continue;
|
||||
seenZones.Insert(zone);
|
||||
SCR_RotorHitZone rotorZone = SCR_RotorHitZone.Cast(zone);
|
||||
if (!rotorZone || rotorZone.GetRotorRadius() <= 0) continue; // transmission zones have no sweep
|
||||
int rotorIndex = rotorZone.GetRotorIndex();
|
||||
float expectedRadius, thickness;
|
||||
if (rotorIndex == 0) { expectedRadius = 12; thickness = 1.5; mainSweeps++; }
|
||||
else if (rotorIndex == 1) { expectedRadius = 2.3; thickness = 0.75; tailSweeps++; }
|
||||
else return Refuse("unknown rotor sweep index");
|
||||
// Stock thickness is protected with no getter. Scope to the exact prefab
|
||||
// and verify its public sweep radius against the audited stock constants.
|
||||
if (!(Math.AbsFloat(rotorZone.GetRotorRadius() - expectedRadius) < 0.001)) return Refuse("stock damage sweep radius changed");
|
||||
PointInfo point = rotorZone.GetPointInfo();
|
||||
if (!point) return Refuse("missing actual rotor sweep pivot");
|
||||
vector rotorWorld[4]; point.GetWorldTransform(rotorWorld);
|
||||
// Frobenius norm bounds the transform's maximum stretch even with scale
|
||||
// or shear; do not assume the PointInfo basis is orthonormal.
|
||||
float scaleSquared = 0;
|
||||
for (int basis = 0; basis < 3; basis++) scaleSquared += vector.Dot(rotorWorld[basis], rotorWorld[basis]);
|
||||
float scale = Math.Sqrt(scaleSquared);
|
||||
if (!(scale > 0 && scale < 4)) return Refuse("invalid damage sweep basis");
|
||||
float distance = vector.Distance(origin, rotorWorld[3]);
|
||||
float sweepBound = ERR_AircraftFlightFootprintRules.CylinderSphere(distance, expectedRadius * scale, thickness * scale);
|
||||
if (!(sweepBound > 0 && sweepBound < 1000)) return Refuse("invalid damage sweep extent");
|
||||
damageRadius = Math.Max(damageRadius, sweepBound);
|
||||
PrintFormat("ERR AIRCRAFT FOOTPRINT ROTOR: index=%1 pivot=%2 damageRadius=%3 thickness=%4 scale=%5 sphereBound=%6", rotorIndex, rotorWorld[3], expectedRadius, thickness, scale, sweepBound);
|
||||
}
|
||||
}
|
||||
IEntity child = entity.GetChildren();
|
||||
while (child)
|
||||
{
|
||||
if (entities.Count() >= MAX_ENTITIES || depths[index] >= MAX_DEPTH || entities.Contains(child)) return Refuse("entity/depth/cycle traversal bound");
|
||||
entities.Insert(child); depths.Insert(depths[index] + 1);
|
||||
child = child.GetSibling();
|
||||
}
|
||||
}
|
||||
if (meshes == 0 || mainSweeps != 1 || tailSweeps != 1) return Refuse("complete model and both actual damage sweeps required");
|
||||
if (simulation.RotorCount() != 2) return Refuse("stock simulation rotor count changed");
|
||||
for (int rotor = 0; rotor < 2; rotor++)
|
||||
{
|
||||
vector localRotor[4]; simulation.RotorGetTransform(rotor, localRotor);
|
||||
float physicalRadius = simulation.RotorGetRadius(rotor);
|
||||
float physicalBound = localRotor[3].Length() + physicalRadius;
|
||||
if (!(physicalRadius > 0 && physicalBound > 0 && physicalBound < 1000)) return Refuse("invalid native simulation rotor extent");
|
||||
simulationRadius = Math.Max(simulationRadius, physicalBound);
|
||||
PrintFormat("ERR AIRCRAFT FOOTPRINT PHYSICS ROTOR: index=%1 localPivot=%2 radius=%3 sphereBound=%4", rotor, localRotor[3], physicalRadius, physicalBound);
|
||||
}
|
||||
radius = Math.Max(modelRadius, Math.Max(damageRadius, simulationRadius));
|
||||
float initialDrift = vector.DistanceXZ(origin, pad);
|
||||
float initialHeight = origin[1] - pad[1];
|
||||
PrintFormat("ERR AIRCRAFT FOOTPRINT: entities=%1 meshes=%2 zones=%3 modelSphere=%4 damageSphere=%5 simulationSphere=%6 completeSphere=%7 initialDrift=%8 initialHeight=%9", entities.Count(), meshes, seenZones.Count(), modelRadius, damageRadius, simulationRadius, radius, initialDrift, initialHeight);
|
||||
if (!ERR_AircraftFlightFootprintRules.Fits(radius, initialDrift, initialHeight)) return Refuse("measured footprint plus initial placement,8m drift and1m margin exceeds40m corridor or60m ceiling");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Issued only by physical qualification; fields have no public mutators.
|
||||
// One claim binds this short-lived record to one original aircraft/pilot/order.
|
||||
class ERR_AircraftFlightEnvelope
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected Vehicle m_Aircraft;
|
||||
protected IEntity m_Pilot;
|
||||
protected vector m_Home;
|
||||
protected float m_Radius;
|
||||
protected float m_QualifiedAt;
|
||||
protected int m_Claim;
|
||||
vector Home() { return m_Home; }
|
||||
float Radius() { return m_Radius; }
|
||||
bool ClaimedBy(Vehicle aircraft, IEntity pilot, int token)
|
||||
{
|
||||
return m_Claim > 0 && m_Claim == token && m_Aircraft == aircraft && m_Pilot == pilot;
|
||||
}
|
||||
bool Claim(ERR_GameMode mode, Vehicle aircraft, IEntity pilot, int token)
|
||||
{
|
||||
if (!GetGame() || !GetGame().GetWorld()) return false;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (!Replication.IsServer() || !mode || mode != m_Mode || mode != ERR_GameMode.Get() || !mode.Active() || m_Claim != 0 || token <= 0) return false;
|
||||
if (aircraft != m_Aircraft || pilot != m_Pilot || !aircraft || !pilot || now < m_QualifiedAt || now - m_QualifiedAt > 2000) return false;
|
||||
if (vector.Distance(aircraft.GetOrigin(), m_Home) > 0.1) return false;
|
||||
m_Claim = token;
|
||||
return true;
|
||||
}
|
||||
protected static bool Hierarchy(IEntity root, array<IEntity> excluded)
|
||||
{
|
||||
if (!root) return false;
|
||||
array<IEntity> pending = {root};
|
||||
array<int> depths = {0};
|
||||
for (int i = 0; i < pending.Count(); i++)
|
||||
{
|
||||
IEntity entity = pending[i];
|
||||
if (!entity || excluded.Count() >= 512) return false;
|
||||
if (!excluded.Contains(entity)) excluded.Insert(entity);
|
||||
IEntity child = entity.GetChildren();
|
||||
while (child)
|
||||
{
|
||||
if (pending.Count() >= 256 || depths[i] >= 16 || pending.Contains(child)) return false;
|
||||
pending.Insert(child); depths.Insert(depths[i] + 1); child = child.GetSibling();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected static bool ClearColumn(Vehicle aircraft, IEntity pilot, vector pad)
|
||||
{
|
||||
array<IEntity> excluded = {};
|
||||
if (!Hierarchy(aircraft, excluded) || !Hierarchy(pilot, excluded)) return false;
|
||||
for (int edge = 0; edge < 4; edge++)
|
||||
{
|
||||
float side = edge * Math.PI / 2;
|
||||
vector point = pad + Vector(Math.Cos(side) * 14, 0, Math.Sin(side) * 14);
|
||||
if (!(Math.AbsFloat(SCR_TerrainHelper.GetTerrainY(point) - pad[1]) <= 0.8)) return false;
|
||||
}
|
||||
// Same four finite diagonal cylinder traces as the native fixture, with
|
||||
// exclusions restricted to this already-existing aircraft and seated pilot.
|
||||
array<vector> diagonals = {"40 2 0", "-40 2 0", "0 2 40", "0 2 -40"};
|
||||
foreach (vector diagonal : diagonals)
|
||||
{
|
||||
TraceParam trace = new TraceParam();
|
||||
trace.Start = pad + "0 2.5 0" + diagonal;
|
||||
trace.End = pad + "0 2.5 0" - diagonal;
|
||||
trace.Flags = TraceFlags.ENTS; trace.ExcludeArray = excluded;
|
||||
if (!(aircraft.GetWorld().TraceMove(trace, null) >= 1)) return false;
|
||||
}
|
||||
TraceBox clearance = new TraceBox();
|
||||
clearance.Start = pad + "0 3 0"; clearance.End = pad + "0 60 0";
|
||||
clearance.Mins = "-40 -1 -40"; clearance.Maxs = "40 1 40";
|
||||
clearance.Flags = TraceFlags.ENTS | TraceFlags.WORLD; clearance.ExcludeArray = excluded;
|
||||
return aircraft.GetWorld().TraceMove(clearance, null) >= 1;
|
||||
}
|
||||
static ERR_AircraftFlightEnvelope Qualify(ERR_GameMode mode, Vehicle aircraft, IEntity pilot)
|
||||
{
|
||||
if (!GetGame() || !GetGame().GetWorld() || !GetGame().GetPlayerManager()) return null;
|
||||
if (!Replication.IsServer() || !mode || mode != ERR_GameMode.Get() || !mode.Active() || !aircraft || !pilot || ERR_Actors.Player(pilot)) return null;
|
||||
if (CompartmentAccessComponent.GetVehicleIn(pilot) != aircraft || !ERR_Actors.Alive(aircraft) || !ERR_Actors.CanFight(pilot)) return null;
|
||||
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(aircraft.FindComponent(HelicopterControllerComponent));
|
||||
if (!controller || controller.IsEngineOn() || !controller.GetSimulation() || !controller.GetSimulation().IsValid()) return null;
|
||||
PilotCompartmentSlot seat = controller.GetPilotCompartmentSlot();
|
||||
if (!seat || seat.GetOccupant() != pilot) return null;
|
||||
if (!ERR_CasualtyLoadingRules.AircraftReady(aircraft)) return null;
|
||||
vector pad = ERR_Actors.Ground(aircraft.GetOrigin());
|
||||
if (!(pad[1] >= 2) || !ClearColumn(aircraft, pilot, pad)) return null;
|
||||
vector origin; float radius;
|
||||
if (!ERR_AircraftFlightFootprint.Measure(aircraft, controller.GetSimulation(), pad, origin, radius)) return null;
|
||||
ERR_AircraftFlightEnvelope record = new ERR_AircraftFlightEnvelope();
|
||||
record.m_Mode = mode; record.m_Aircraft = aircraft; record.m_Pilot = pilot;
|
||||
record.m_Home = origin; record.m_Radius = radius;
|
||||
record.m_QualifiedAt = GetGame().GetWorld().GetWorldTime();
|
||||
return record;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
#ifdef WORKBENCH
|
||||
// Separate Chat fixture for the ACTUAL dormant AI controller. Core's original
|
||||
// powered-flight probe is disabled for this outcome and retains its own evidence.
|
||||
modded class SCR_CharacterControllerComponent
|
||||
{
|
||||
override void OnPrepareControls(IEntity owner, ActionManager am, float dt, bool player)
|
||||
{
|
||||
ERR_NativeAircraftControlProbe probe = ERR_NativeAircraftControlProbe.Current();
|
||||
if (probe && !player) probe.PrepareActions(this, owner, am, dt);
|
||||
super.OnPrepareControls(owner, am, dt, player);
|
||||
}
|
||||
}
|
||||
modded class SCR_HelicopterControllerComponent
|
||||
{
|
||||
protected override event void OnPrepareControls()
|
||||
{
|
||||
super.OnPrepareControls();
|
||||
ERR_NativeAircraftControlProbe probe = ERR_NativeAircraftControlProbe.Current();
|
||||
if (probe) probe.PrepareHelicopter(this);
|
||||
}
|
||||
}
|
||||
// Read-only native fixture observation; no production method or state override.
|
||||
modded class ERR_AircraftFlight
|
||||
{
|
||||
float ERR_NativeControlDeadline() { return m_Deadline; }
|
||||
}
|
||||
class ERR_NativeAircraftControlRules
|
||||
{
|
||||
static bool Terminal(bool cancellation, int state, int cancellationCount)
|
||||
{
|
||||
if (cancellation) return state == ERR_AircraftFlightState.CANCELLED && cancellationCount == 1;
|
||||
return state == ERR_AircraftFlightState.COMPLETE && cancellationCount == 0;
|
||||
}
|
||||
static bool CanCancel(int state, bool contact, float height)
|
||||
{
|
||||
return state == ERR_AircraftFlightState.CLIMB && !contact && height >= 5 && height <= 18;
|
||||
}
|
||||
static bool CancelProof(bool airborneHooks, bool landingHeld, bool sameDeadline, bool sameToken)
|
||||
{
|
||||
return airborneHooks && landingHeld && sameDeadline && sameToken;
|
||||
}
|
||||
static bool TranslateProof(bool proven, bool held, float requestedDistance, float displacement, float targetError)
|
||||
{
|
||||
return proven && held && requestedDistance >= 1.75 && requestedDistance <= 4.05 && displacement >= 1 && targetError >= 0 && targetError <= 0.75;
|
||||
}
|
||||
static bool PhysicalFinish(bool originalOccupancy, bool landed, bool engineOff, float fuelUsed, int wakes, int starts, int controllerStarts, int pilotHooks, int helicopterHooks)
|
||||
{
|
||||
if (!originalOccupancy || !landed || !engineOff) return false;
|
||||
return fuelUsed > 0.05 && wakes == 1 && starts == 1 && controllerStarts == 1 && pilotHooks > 100 && helicopterHooks > 100;
|
||||
}
|
||||
static bool CanDispose(bool context, bool noPlayers, bool sameFaction, bool groupUnchanged, bool pilotUnchanged)
|
||||
{
|
||||
return context && noPlayers && sameFaction && groupUnchanged && pilotUnchanged;
|
||||
}
|
||||
}
|
||||
class ERR_NativeAircraftControlProbe
|
||||
{
|
||||
protected static ERR_NativeAircraftControlProbe s_Current;
|
||||
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 FuelManagerComponent m_Fuel;
|
||||
protected ref ERR_AircraftFlightEnvelope m_Envelope;
|
||||
protected ref ERR_AircraftFlight m_Flight;
|
||||
protected float m_Deadline;
|
||||
protected float m_StageDeadline;
|
||||
protected float m_Stable = -1;
|
||||
protected float m_LastTick = -1;
|
||||
protected float m_NextLog;
|
||||
protected float m_InitialFuel;
|
||||
protected float m_ControlDeadline;
|
||||
protected float m_CancelAt = -1;
|
||||
protected float m_LandingStable = -1;
|
||||
protected int m_CancelCount;
|
||||
protected int m_CancelPilotHooks;
|
||||
protected int m_CancelHelicopterHooks;
|
||||
protected bool m_CancelFixture;
|
||||
protected bool m_TranslationFixture;
|
||||
protected bool m_TranslationObserved;
|
||||
protected bool m_TranslationHeld;
|
||||
protected vector m_TranslationOrigin;
|
||||
protected vector m_TranslationTarget;
|
||||
protected float m_TranslationStable = -1;
|
||||
protected bool m_ContinuedAirborneControl;
|
||||
protected bool m_LandingHeld;
|
||||
protected vector m_Pad;
|
||||
protected int m_Step;
|
||||
protected int m_Wakes;
|
||||
protected int m_ControllerStarts;
|
||||
protected int m_EngineStarts;
|
||||
protected int m_PilotHooks;
|
||||
protected int m_HelicopterHooks;
|
||||
protected bool m_LastEngine;
|
||||
protected bool m_AllocationRequested;
|
||||
protected bool m_PilotRequested;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_Complete;
|
||||
static const int ORDER_TOKEN = 771001;
|
||||
static const ResourceName AIRCRAFT = "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et";
|
||||
static const ResourceName GROUP = "{000CD338713F2B5A}Prefabs/AI/Groups/Group_Base.et";
|
||||
static const ResourceName PILOT = "{86E565387A634103}Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et";
|
||||
static bool Enabled()
|
||||
{
|
||||
string enabled, outcome;
|
||||
return Replication.IsServer() && System.GetCLIParam("errSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && (outcome == "aircraft-control" || outcome == "aircraft-cancel" || outcome == "aircraft-translate");
|
||||
}
|
||||
static ERR_NativeAircraftControlProbe Current() { return s_Current; }
|
||||
void ERR_NativeAircraftControlProbe(ERR_GameMode mode)
|
||||
{
|
||||
m_Mode = mode;
|
||||
if (!GetGame() || !GetGame().GetWorld() || !Enabled() || s_Current) { m_Stopped = true; return; }
|
||||
string outcome; System.GetCLIParam("errSmokeOutcome", outcome); m_CancelFixture = outcome == "aircraft-cancel"; m_TranslationFixture = outcome == "aircraft-translate";
|
||||
m_Deadline = GetGame().GetWorld().GetWorldTime() + 300000;
|
||||
s_Current = this;
|
||||
}
|
||||
protected bool Check(bool passed, string detail)
|
||||
{
|
||||
string outcome = "aircraft-control"; if (m_CancelFixture) outcome = "aircraft-cancel"; else if (m_TranslationFixture) outcome = "aircraft-translate";
|
||||
if (passed) { Print("ERR SMOKE PASS: " + outcome + " " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: " + outcome + " " + detail, LogLevel.ERROR); Stop(); return false;
|
||||
}
|
||||
protected bool NoPlayers()
|
||||
{
|
||||
if (!GetGame() || !GetGame().GetPlayerManager()) return false;
|
||||
if (m_Group && m_Group.GetPlayerCount(true) > 0) return false;
|
||||
array<int> players = {}; GetGame().GetPlayerManager().GetPlayers(players);
|
||||
foreach (int id : players)
|
||||
{
|
||||
IEntity actor = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
|
||||
if (actor && (actor == m_Pilot || actor == m_Aircraft || (m_Aircraft && CompartmentAccessComponent.GetVehicleIn(actor) == m_Aircraft))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected bool OriginalOccupancy()
|
||||
{
|
||||
if (!m_Pilot || !m_Group || !m_Aircraft || !m_Controller || !m_Seat || !NoPlayers()) return false;
|
||||
if (m_Group.GetAgentsCount() != 1 || ERR_Actors.Group(m_Pilot) != m_Group || !ERR_Actors.CanFight(m_Pilot)) return false;
|
||||
return m_Seat.GetOccupant() == m_Pilot && m_Controller.GetPilotCompartmentSlot() == m_Seat && CompartmentAccessComponent.GetVehicleIn(m_Pilot) == m_Aircraft;
|
||||
}
|
||||
void PrepareActions(CharacterControllerComponent controller, IEntity owner, ActionManager am, float dt)
|
||||
{
|
||||
if (m_Stopped || !m_Flight || owner != m_Pilot) return;
|
||||
m_Flight.PrepareActions(controller, owner, am, dt); m_PilotHooks++;
|
||||
}
|
||||
void PrepareHelicopter(HelicopterControllerComponent controller)
|
||||
{
|
||||
if (m_Stopped || !m_Flight || controller != m_Controller) return;
|
||||
m_Flight.PrepareHelicopter(controller); m_HelicopterHooks++;
|
||||
}
|
||||
protected bool FindPad(out vector result)
|
||||
{
|
||||
vector anchor = m_Mode.ExtractionVehicle().GetOrigin();
|
||||
float bestSlope = float.MAX; vector bestCandidate, refinementAnchor;
|
||||
int localCandidates = ERR_PoweredFlightRules.PAD_CANDIDATES + ERR_PoweredFlightRules.PAD_REFINEMENTS;
|
||||
for (int index = 0; index < localCandidates + ERR_PoweredFlightRules.AIRPORT_CANDIDATES; index++)
|
||||
{
|
||||
if (index == ERR_PoweredFlightRules.PAD_CANDIDATES)
|
||||
{
|
||||
refinementAnchor = anchor; if (bestSlope != float.MAX) refinementAnchor = bestCandidate;
|
||||
}
|
||||
vector candidate;
|
||||
if (index < ERR_PoweredFlightRules.PAD_CANDIDATES) candidate = ERR_Actors.Ground(anchor + ERR_PoweredFlightRules.PadOffset(index));
|
||||
else if (index < localCandidates) candidate = ERR_Actors.Ground(refinementAnchor + ERR_PoweredFlightRules.RefineOffset(index - ERR_PoweredFlightRules.PAD_CANDIDATES));
|
||||
else candidate = ERR_Actors.Ground("4900.953 0 11787.412" + ERR_PoweredFlightRules.AirportOffset(index - localCandidates));
|
||||
if (candidate[1] < 2) continue;
|
||||
float delta;
|
||||
for (int edge = 0; edge < 4; edge++)
|
||||
{
|
||||
float side = edge * Math.PI / 2;
|
||||
vector point = candidate + Vector(Math.Cos(side) * 14, 0, Math.Sin(side) * 14);
|
||||
delta = Math.Max(delta, Math.AbsFloat(SCR_TerrainHelper.GetTerrainY(point) - candidate[1]));
|
||||
}
|
||||
if (delta < bestSlope) { bestSlope = delta; bestCandidate = candidate; }
|
||||
if (delta > 0.8) continue;
|
||||
if (!SCR_WorldTools.TraceCilinderUtil(candidate + "0 2.5 0", 40, 4, TraceFlags.ENTS)) continue;
|
||||
TraceBox clearance = new TraceBox(); clearance.Start = candidate + "0 3 0"; clearance.End = candidate + "0 60 0";
|
||||
clearance.Mins = "-40 -1 -40"; clearance.Maxs = "40 1 40"; clearance.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||
if (!(GetGame().GetWorld().TraceMove(clearance, null) >= 1)) continue;
|
||||
PrintFormat("ERR AIRCRAFT FIXTURE PAD: candidate=%1 tested=%2 terrainDelta=%3; no fallback", candidate, index + 1, delta);
|
||||
result = candidate; return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected bool ReadyComponents()
|
||||
{
|
||||
m_Controller = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
|
||||
if (!m_Controller) return false;
|
||||
m_Simulation = m_Controller.GetSimulation();
|
||||
m_Fuel = FuelManagerComponent.Cast(m_Aircraft.FindComponent(FuelManagerComponent));
|
||||
if (!m_Simulation || !m_Simulation.IsValid() || !m_Fuel || !m_Fuel.HasFuel()) return false;
|
||||
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(m_Aircraft.FindComponent(BaseCompartmentManagerComponent));
|
||||
if (!manager) return false;
|
||||
array<BaseCompartmentSlot> slots = {}; manager.GetCompartments(slots);
|
||||
if (slots.Count() > 64) return false;
|
||||
foreach (BaseCompartmentSlot slot : slots)
|
||||
{
|
||||
PilotCompartmentSlot pilotSlot = PilotCompartmentSlot.Cast(slot);
|
||||
if (!pilotSlot || pilotSlot.GetOwner() != m_Aircraft || pilotSlot.GetVehicle() != m_Aircraft || pilotSlot.GetCompartmentName() != "PilotCompartment") continue;
|
||||
if (pilotSlot.GetType() != ECompartmentType.PILOT) continue;
|
||||
if (pilotSlot.GetOccupant() || pilotSlot.IsReserved() || pilotSlot.IsGetInLocked() || !pilotSlot.IsCompartmentAccessible()) continue;
|
||||
m_Seat = pilotSlot; return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected void Diagnostic(float now)
|
||||
{
|
||||
if (!m_Aircraft || !m_Controller || !m_Simulation || now < m_NextLog) return;
|
||||
m_NextLog = now + 2000;
|
||||
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
|
||||
vector home = m_Pad; if (m_Envelope) home = m_Envelope.Home();
|
||||
int state = -1; string reason; if (m_Flight) { state = m_Flight.State(); reason = m_Flight.Reason(); }
|
||||
PrintFormat("ERR AIRCRAFT FIXTURE: step=%1 state=%2 height=%3 drift=%4 speed=%5 contact=%6 engine=%7 fuelUsed=%8 reason=%9", m_Step, state, transform[3][1] - home[1], vector.DistanceXZ(transform[3], home), m_Simulation.GetVelocity().Length(), m_Simulation.HasAnyGroundContact(), m_Controller.IsEngineOn(), m_InitialFuel - m_Fuel.GetTotalFuel(), reason);
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Complete) return 1;
|
||||
if (m_Stopped) return -1;
|
||||
if (!Enabled() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active() || now >= m_Deadline) { Check(false, "active exact mission and bounded300-second fixture"); return -1; }
|
||||
if (!NoPlayers()) { Check(false, "fixture remains unclaimed by any human"); return -1; }
|
||||
if (m_LastTick >= 0 && (now < m_LastTick || now - m_LastTick > 1500)) { m_Stable = -1; m_LandingStable = -1; m_TranslationStable = -1; }
|
||||
m_LastTick = now;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
if (!m_Mode.ExtractionVehicle()) return 0;
|
||||
if (!Check(!m_AllocationRequested && FindPad(m_Pad), "independent qualified candidate found before one finite allocation")) return -1;
|
||||
m_AllocationRequested = true;
|
||||
m_Aircraft = Vehicle.Cast(ERR_Actors.Spawn(AIRCRAFT, m_Pad));
|
||||
m_Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(GROUP, m_Pad + "20 0 0"));
|
||||
if (!Check(m_Aircraft && m_Group && m_Group.InitFactionKey("USSR") && m_Group.GetFaction() == m_Mode.EnemyFaction(), "one finite native Mi8 and memberless enemy group created")) return -1;
|
||||
m_Group.SetPermanentLOD(0); m_Group.ActivateAI();
|
||||
m_Step = 1; m_StageDeadline = now + 30000; return 0;
|
||||
}
|
||||
if (!m_Aircraft || !m_Group || !ERR_Actors.Alive(m_Aircraft)) { Check(false, "original airframe and group survive without replacement"); return -1; }
|
||||
if (m_Step == 1)
|
||||
{
|
||||
if (now >= m_StageDeadline) { Check(false, "real components and original pilot initialize within30seconds"); return -1; }
|
||||
if (!ReadyComponents()) return 0;
|
||||
if (!Check(!m_PilotRequested && m_Group.GetAgentsCount() == 0, "one original native pilot request only")) return -1;
|
||||
m_PilotRequested = true; AIGroup originalGroup = m_Group;
|
||||
m_Pilot = m_Seat.SpawnCharacterInCompartment(PILOT, originalGroup, GROUP);
|
||||
AIControlComponent control; if (m_Pilot) control = AIControlComponent.Cast(m_Pilot.FindComponent(AIControlComponent));
|
||||
AIAgent agent; if (control) agent = control.GetControlAIAgent();
|
||||
if (!Check(m_Pilot && originalGroup == m_Group && agent, "pilot seated by native mechanism in original group")) return -1;
|
||||
agent.SetPermanentLOD(0); agent.ActivateAI(); m_Step = 2; return 0;
|
||||
}
|
||||
Diagnostic(now);
|
||||
if (m_Step == 2)
|
||||
{
|
||||
if (now >= m_StageDeadline) { Check(false, "actual initial seating and native ground settling within30seconds"); return -1; }
|
||||
if (!OriginalOccupancy()) { m_Stable = -1; return 0; }
|
||||
Physics physics = m_Aircraft.GetPhysics();
|
||||
if (!physics || !physics.IsDynamic()) return 0;
|
||||
if (m_Wakes == 0) { m_Wakes = 1; physics.SetActive(ActiveState.ACTIVE); m_Stable = -1; return 0; }
|
||||
if (!m_Simulation.HasAnyGroundContact() || !ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft)) { m_Stable = -1; return 0; }
|
||||
if (m_Stable < 0) m_Stable = now;
|
||||
if (now - m_Stable < 1000) return 0;
|
||||
if (!Check(!m_Controller.IsEngineOn(), "post-spawn qualification precedes native engine start")) return -1;
|
||||
m_Envelope = ERR_AircraftFlightEnvelope.Qualify(m_Mode, m_Aircraft, m_Pilot);
|
||||
if (!Check(m_Envelope != null, "actual post-spawn column and complete rotor/model footprint issue exact qualification")) return -1;
|
||||
m_InitialFuel = m_Fuel.GetTotalFuel();
|
||||
m_Flight = new ERR_AircraftFlight(); m_ControllerStarts++;
|
||||
m_ControlDeadline = Math.Min(m_Deadline, now + 240000);
|
||||
bool started;
|
||||
if (m_TranslationFixture) started = m_Flight.StartTranslation(m_Mode, m_Aircraft, m_Group, m_Pilot, m_Seat, m_Envelope, ORDER_TOKEN, m_ControlDeadline, m_Envelope.Home());
|
||||
else started = m_Flight.Start(m_Mode, m_Aircraft, m_Group, m_Pilot, m_Seat, m_Envelope, ORDER_TOKEN, m_ControlDeadline);
|
||||
if (!Check(started, "actual controller accepts one qualified original pilot/aircraft order")) return -1;
|
||||
m_Step = 3; return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
if (!OriginalOccupancy()) { Check(false, "original pilot remains conscious and in its exact seat/group"); return -1; }
|
||||
bool engine = m_Controller.IsEngineOn(); if (engine && !m_LastEngine) m_EngineStarts++; m_LastEngine = engine;
|
||||
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
|
||||
vector home = m_Envelope.Home(); float height = transform[3][1] - home[1];
|
||||
bool contact = m_Simulation.HasAnyGroundContact();
|
||||
bool landed = ERR_AircraftFlightRules.Landed(contact, height, m_Simulation.GetVelocity().Length(), m_Simulation.GetAngularVelocity().Length(), transform[1][1]);
|
||||
if (m_CancelFixture && m_CancelCount == 0 && ERR_NativeAircraftControlRules.CanCancel(m_Flight.State(), contact, height))
|
||||
{
|
||||
m_CancelCount = 1; m_CancelAt = now;
|
||||
m_CancelPilotHooks = m_PilotHooks; m_CancelHelicopterHooks = m_HelicopterHooks;
|
||||
m_Flight.Cancel("native fixture deliberate airborne cancellation");
|
||||
PrintFormat("ERR AIRCRAFT CANCEL: height=%1 contact=%2 state=%3 token=%4 deadline=%5 pilotHooks=%6 helicopterHooks=%7", height, contact, m_Flight.State(), m_Flight.OrderToken(), m_Flight.ERR_NativeControlDeadline(), m_PilotHooks, m_HelicopterHooks);
|
||||
if (!Check(m_Flight.State() == ERR_AircraftFlightState.LANDING && !m_Flight.Finished(), "one actual airborne Cancel enters retained LANDING control")) return -1;
|
||||
}
|
||||
if (m_CancelCount == 1)
|
||||
{
|
||||
if (m_Flight.ERR_NativeControlDeadline() != m_ControlDeadline || m_Flight.OrderToken() != ORDER_TOKEN) { Check(false, "cancel retains original overall deadline and exact order token"); return -1; }
|
||||
int state = m_Flight.State();
|
||||
if (state != ERR_AircraftFlightState.LANDING && state != ERR_AircraftFlightState.SHUTDOWN && state != ERR_AircraftFlightState.CANCELLED) { Check(false, "cancel retains landing or grounded shutdown states only"); return -1; }
|
||||
if (state == ERR_AircraftFlightState.LANDING && !contact && height > 1 && m_PilotHooks > m_CancelPilotHooks + 10 && m_HelicopterHooks > m_CancelHelicopterHooks + 10) m_ContinuedAirborneControl = true;
|
||||
if (!landed) m_LandingStable = -1;
|
||||
else if (m_LandingStable < 0) m_LandingStable = now;
|
||||
}
|
||||
if (m_TranslationFixture && m_Flight.State() == ERR_AircraftFlightState.TRANSLATING)
|
||||
{
|
||||
if (!m_TranslationObserved)
|
||||
{
|
||||
m_TranslationObserved = true; m_TranslationOrigin = m_Flight.TranslationOrigin(); m_TranslationTarget = m_Flight.TranslationTarget();
|
||||
if (!Check(vector.DistanceXZ(m_TranslationTarget, home) < 0.001, "pre-takeoff translation target remains the original qualified home")) return -1;
|
||||
}
|
||||
if (m_Flight.TranslationTarget() != m_TranslationTarget || m_Flight.TranslationOrigin() != m_TranslationOrigin || m_Flight.ERR_NativeControlDeadline() != m_ControlDeadline) { Check(false, "translation never retargets or renews its original deadline"); return -1; }
|
||||
vector horizontal = m_Simulation.GetVelocity(); horizontal[1] = 0;
|
||||
float requested = vector.DistanceXZ(m_TranslationOrigin, m_TranslationTarget);
|
||||
bool held = ERR_AircraftTranslationRules.Held(contact, height, m_Simulation.GetVelocity().Length(), m_Simulation.GetVelocity()[1], transform[1][1], horizontal.Length(), vector.DistanceXZ(transform[3], m_TranslationTarget), vector.DistanceXZ(transform[3], m_TranslationOrigin), requested);
|
||||
if (!held) m_TranslationStable = -1;
|
||||
else if (m_TranslationStable < 0) m_TranslationStable = now;
|
||||
if (m_TranslationStable >= 0 && now - m_TranslationStable >= 5000) m_TranslationHeld = true;
|
||||
}
|
||||
m_Flight.Tick();
|
||||
if (m_CancelCount == 1 && m_Flight.State() == ERR_AircraftFlightState.SHUTDOWN)
|
||||
{
|
||||
if (!Check(landed && m_LandingStable >= 0 && now - m_LandingStable >= 2000, "cancel reaches shutdown only after two observed stable grounded seconds")) return -1;
|
||||
m_LandingHeld = true;
|
||||
}
|
||||
if (!m_Flight.Finished()) return 0;
|
||||
PrintFormat("ERR AIRCRAFT FIXTURE RESULT: state=%1 token=%2 engineStarts=%3 controllerStarts=%4 wakes=%5 pilotHooks=%6 helicopterHooks=%7 reason=%8", m_Flight.State(), m_Flight.OrderToken(), m_EngineStarts, m_ControllerStarts, m_Wakes, m_PilotHooks, m_HelicopterHooks, m_Flight.Reason());
|
||||
bool complete = ERR_NativeAircraftControlRules.Terminal(m_CancelFixture, m_Flight.State(), m_CancelCount);
|
||||
complete = complete && ERR_NativeAircraftControlRules.PhysicalFinish(OriginalOccupancy(), landed, !m_Controller.IsEngineOn(), m_InitialFuel - m_Fuel.GetTotalFuel(), m_Wakes, m_EngineStarts, m_ControllerStarts, m_PilotHooks, m_HelicopterHooks);
|
||||
if (m_CancelFixture)
|
||||
{
|
||||
bool proof = ERR_NativeAircraftControlRules.CancelProof(m_ContinuedAirborneControl, m_LandingHeld, m_Flight.ERR_NativeControlDeadline() == m_ControlDeadline, m_Flight.OrderToken() == ORDER_TOKEN);
|
||||
PrintFormat("ERR AIRCRAFT CANCEL RESULT: count=%1 elapsed=%2 airborneHooks=%3 landingHeld=%4 deadline=%5 originalDeadline=%6", m_CancelCount, now - m_CancelAt, m_ContinuedAirborneControl, m_LandingHeld, m_Flight.ERR_NativeControlDeadline(), m_ControlDeadline);
|
||||
complete = complete && proof;
|
||||
}
|
||||
if (m_TranslationFixture)
|
||||
{
|
||||
float requestedDistance = vector.DistanceXZ(m_TranslationOrigin, m_TranslationTarget);
|
||||
float displacement = vector.DistanceXZ(transform[3], m_TranslationOrigin);
|
||||
float targetError = vector.DistanceXZ(transform[3], m_TranslationTarget);
|
||||
bool translated = ERR_NativeAircraftControlRules.TranslateProof(m_Flight.TranslationProven(), m_TranslationObserved && m_TranslationHeld, requestedDistance, displacement, targetError);
|
||||
PrintFormat("ERR AIRCRAFT TRANSLATION RESULT: proven=%1 held=%2 requested=%3 displacement=%4 targetError=%5 updates=%6", m_Flight.TranslationProven(), m_TranslationHeld, requestedDistance, displacement, targetError, m_Flight.TranslationUpdates());
|
||||
complete = complete && translated;
|
||||
}
|
||||
if (!Check(complete && m_Flight.OrderToken() == ORDER_TOKEN, "actual control order completes on original physical grounded aircraft with engine off, finite fuel use and exact pilot")) return -1;
|
||||
m_Complete = true; Stop(); return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
if (s_Current == this) s_Current = null;
|
||||
if (m_Flight && !m_Flight.Finished() && m_CancelCount == 0) m_Flight.Cancel("native fixture stopped");
|
||||
// End-of-test disposal is not successful flight or a production cancellation.
|
||||
// These assets are fixture-owned only, never registered with mission cleanup.
|
||||
// Release the controller before deleting disposable unclaimed physical assets.
|
||||
m_Flight = null;
|
||||
bool context = GetGame() && GetGame().GetWorld() && GetGame().GetPlayerManager() && m_Mode && m_Mode == ERR_GameMode.Get();
|
||||
bool faction = context && (!m_Group || (!m_PilotRequested && m_Group.GetAgentsCount() == 0 && !m_Group.GetFaction()) || m_Group.GetFaction() == m_Mode.EnemyFaction()) && (!m_Aircraft || SCR_FactionManager.SGetFaction(m_Aircraft) == m_Mode.EnemyFaction());
|
||||
bool groupUnchanged = context && (!m_Group || m_Group.GetAgentsCount() == 0 || (m_Group.GetAgentsCount() == 1 && m_Pilot && ERR_Actors.Group(m_Pilot) == m_Group));
|
||||
bool pilotUnchanged = context && (!m_Pilot || (m_Group && ERR_Actors.Group(m_Pilot) == m_Group && !ERR_Actors.Player(m_Pilot)));
|
||||
if (ERR_NativeAircraftControlRules.CanDispose(context, NoPlayers(), faction, groupUnchanged, pilotUnchanged))
|
||||
{
|
||||
ERR_Actors.DeleteSpawned(m_Group); ERR_Actors.DeleteSpawned(m_Pilot); ERR_Actors.DeleteSpawned(m_Aircraft);
|
||||
}
|
||||
else Print("ERR AIRCRAFT FIXTURE CLEANUP: ownership changed; captured entities preserved");
|
||||
m_Envelope = null;
|
||||
}
|
||||
void ~ERR_NativeAircraftControlProbe() { Stop(); }
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user