236 lines
14 KiB
C
236 lines
14 KiB
C
// Unqualified 100m field-route prototype, admitted by the typed service only.
|
|
// No allocation or HQ dispatch; the shared registry/bridge owns native inputs.
|
|
class ERR_AircraftFieldFlight : ERR_AircraftFlight
|
|
{
|
|
protected ref ERR_AircraftFieldEnvelope m_Field;
|
|
protected int m_FieldPhase; // 0 startup, 1 cruise, 2 braking, 3 slow settling
|
|
protected vector m_FieldReference;
|
|
protected vector m_FieldIntegral;
|
|
protected vector m_FieldLandingPoint;
|
|
protected bool m_FieldStopFrozen;
|
|
protected bool m_FieldArrived;
|
|
protected float m_FieldLowSince = -1;
|
|
int FieldPhase() { return m_FieldPhase; }
|
|
bool Arrived() { return m_FieldArrived; }
|
|
vector LandingPoint() { return m_FieldLandingPoint; }
|
|
vector ReferenceVelocity() { return m_FieldReference; }
|
|
vector IntegralDemand() { return m_FieldIntegral; }
|
|
vector Destination() { vector result = "0 0 0"; if (m_Field) result = m_Field.Destination(); return result; }
|
|
// Candidate native fuel quantities, not a demonstrated range/reserve model.
|
|
protected bool FuelAdmission(Vehicle aircraft)
|
|
{
|
|
if (!aircraft) return false;
|
|
FuelManagerComponent fuel = FuelManagerComponent.Cast(aircraft.FindComponent(FuelManagerComponent));
|
|
return fuel && fuel.HasFuel() && fuel.GetTotalFuel() >= 50;
|
|
}
|
|
bool StartField(ERR_GameMode mode, Vehicle aircraft, SCR_AIGroup group, IEntity pilot, PilotCompartmentSlot seat, ERR_AircraftFieldEnvelope field, int token, float deadline)
|
|
{
|
|
if (m_StartAttempted || m_Field || !field || !ERR_AircraftFieldRules.Length(vector.DistanceXZ(field.Home(), field.Destination()))) return false;
|
|
if (!ERR_AircraftFieldRules.LeaseAdmitted(Now(), deadline) || !FuelAdmission(aircraft)) return false;
|
|
m_Field = field; m_TranslationRequested = true;
|
|
vector home = field.Home();
|
|
m_TranslationTarget = field.Destination(); m_TranslationTarget[1] = home[1] + 8;
|
|
m_FieldLandingPoint = field.Home();
|
|
return super.Start(mode, aircraft, group, pilot, seat, field, token, deadline);
|
|
}
|
|
override bool Start(ERR_GameMode mode, Vehicle aircraft, SCR_AIGroup group, IEntity pilot, PilotCompartmentSlot seat, ERR_AircraftFlightEnvelope qualification, int token, float deadline) { return false; }
|
|
override bool StartTranslation(ERR_GameMode mode, Vehicle aircraft, SCR_AIGroup group, IEntity pilot, PilotCompartmentSlot seat, ERR_AircraftFlightEnvelope qualification, int token, float deadline, vector target) { return false; }
|
|
protected bool FieldContains(vector position, float upright)
|
|
{
|
|
if (!m_Field || !m_Field.Contains(position, upright)) return false;
|
|
if (m_FieldPhase == 0) return vector.DistanceXZ(position, m_Home) < 8;
|
|
return true;
|
|
}
|
|
protected override bool Grounded()
|
|
{
|
|
if (!super.Grounded()) return false;
|
|
if (m_State == ERR_AircraftFlightState.LANDING || m_State == ERR_AircraftFlightState.SHUTDOWN)
|
|
return vector.DistanceXZ(m_Aircraft.GetOrigin(), m_FieldLandingPoint) <= 0.75;
|
|
return true;
|
|
}
|
|
// A normal arrival and an airborne cancellation both brake before descent.
|
|
// Once selected, the landing centre and braking deadline cannot be renewed.
|
|
protected bool BeginFieldBrake(float now)
|
|
{
|
|
if (m_FieldStopFrozen) return true;
|
|
if (!Owns() || !m_Field || !Healthy()) { Fail("field braking ownership or capability unavailable"); return false; }
|
|
if (m_Simulation.HasAnyGroundContact()) { Fail("field airborne braking refused ground contact"); return false; }
|
|
vector point = m_Field.Destination();
|
|
if (m_Cancelled)
|
|
{
|
|
point = m_Aircraft.GetOrigin();
|
|
vector velocity = m_Simulation.GetVelocity(); velocity[1] = 0;
|
|
float speed = velocity.Length();
|
|
if (speed >= 0.25) point = point + velocity * (ERR_AircraftFieldRules.BrakingReserve(speed) / speed);
|
|
}
|
|
point[1] = m_Home[1];
|
|
if (!m_Field.Contains(point, 1)) { Fail("field stopping centre outside original qualified corridor"); return false; }
|
|
// Cancel can arrive while airborne during the signed collective checks,
|
|
// before shared startup enables autohover. Match the base landing handshake.
|
|
m_Controller.SetAutohoverEnabled(true);
|
|
if (!m_Controller.GetAutohoverEnabled()) { Fail("native field braking autohover enable refused"); return false; }
|
|
m_FieldLandingPoint = point; m_FieldStopFrozen = true;
|
|
// Remove cruise integral once; retain the bounded vector slew toward rest.
|
|
// Repeated cancellation must not reset this state or the frozen deadline.
|
|
m_FieldIntegral = "0 0 0";
|
|
m_FieldPhase = 2; m_State = ERR_AircraftFlightState.TRANSLATING;
|
|
m_FieldLowSince = -1; m_Stable = -1;
|
|
// Includes braking, two low-speed seconds and five seconds of final hold.
|
|
m_StageDeadline = Math.Min(m_Deadline, now + 45000);
|
|
PrintFormat("ERR AIRCRAFT FIELD BRAKE: token=%1 cancelled=%2 centre=%3 deadline=%4 originalDeadline=%5", m_Token, m_Cancelled, point, m_StageDeadline, m_Deadline);
|
|
return true;
|
|
}
|
|
protected override bool Landing(float now)
|
|
{
|
|
if (!Owns() || !m_Field) { Relinquish("field landing lost original ownership"); return false; }
|
|
if (m_Cancelled)
|
|
{
|
|
if (m_FieldPhase != 3) return BeginFieldBrake(now);
|
|
if (m_Stable < 0 || now - m_Stable < 5000) return true; // retain the current settle/hold budget
|
|
}
|
|
if (!m_FieldStopFrozen || m_FieldPhase != 3 || m_Stable < 0 || now - m_Stable < 5000) { Fail("field descent requires observed braking and destination hold"); return false; }
|
|
if (!super.Landing(now)) return false;
|
|
m_StageDeadline = Math.Min(m_Deadline, now + 25000);
|
|
return true;
|
|
}
|
|
protected override bool BeginTranslation(float now, vector position)
|
|
{
|
|
if (!m_Field || !m_TranslationRequested || m_TranslationStarted || m_State != ERR_AircraftFlightState.CLIMB) { Cancel("field stage ownership refused"); return false; }
|
|
SignalsManagerComponent signals = m_Controller.GetSignalsManager();
|
|
if (!signals || signals.FindSignal("MainRotorCyclicForward") < 0 || signals.FindSignal("MainRotorCyclicAside") < 0 || !m_Controller.GetAutohoverEnabled()) { Cancel("native field cyclic signals unavailable"); return false; }
|
|
m_TranslationOrigin = position; m_TranslationDistance = vector.DistanceXZ(position, m_Field.Destination());
|
|
m_TranslationStarted = true; m_State = ERR_AircraftFlightState.TRANSLATING; m_FieldPhase = 1;
|
|
m_FieldReference = "0 0 0"; m_FieldIntegral = "0 0 0"; m_Stable = -1;
|
|
m_StageDeadline = Math.Min(m_Deadline, now + 160000);
|
|
PrintFormat("ERR AIRCRAFT FIELD BEGIN: token=%1 home=%2 destination=%3 actualOrigin=%4 deadline=%5", m_Token, m_Home, m_Field.Destination(), position, m_Deadline);
|
|
return true;
|
|
}
|
|
protected override bool ControlLease()
|
|
{
|
|
if (!Lease()) return false;
|
|
if (!Owns()) { Relinquish("field callback lost original ownership"); return false; }
|
|
float now = Now();
|
|
if (now < 0 || now >= m_Deadline || now >= m_StageDeadline) { Fail("bounded field stage expired"); return false; }
|
|
if (!Healthy() || !m_Readback) { Fail("field capability or native input readback lost"); return false; }
|
|
if (m_FieldPhase == 2 && m_Simulation.HasAnyGroundContact()) { Fail("field ground contact during airborne braking"); return false; }
|
|
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
|
|
if (!FieldContains(transform[3], transform[1][1]) || !ERR_AircraftFieldRules.SpeedAllowed(m_Simulation.GetVelocity())) { Fail("qualified field position, attitude or speed exceeded"); return false; }
|
|
return true;
|
|
}
|
|
protected vector FieldDemand(vector position, vector velocity, float dt)
|
|
{
|
|
vector target = m_Field.Destination(); if (m_FieldStopFrozen) target = m_FieldLandingPoint;
|
|
if (m_FieldPhase == 3 || m_State == ERR_AircraftFlightState.LANDING) return ERR_AircraftTranslationRules.Demand(target, position, velocity);
|
|
vector direction = target - position; direction[1] = 0;
|
|
float remaining = direction.Length();
|
|
vector desired = "0 0 0";
|
|
// Braking targets rest immediately, independently of distance or bearing.
|
|
// A lagging native airframe can still accelerate while the reference slews;
|
|
// only native measurements can qualify the declared actual-speed reserve.
|
|
if (m_FieldPhase != 2 && remaining > 0.001) desired = direction * (2 / remaining);
|
|
m_FieldReference = ERR_AircraftFieldRules.RampReference(m_FieldReference, desired, dt);
|
|
vector nextIntegral;
|
|
vector demand = ERR_AircraftFieldRules.VelocityDemand(m_FieldReference, velocity, m_FieldIntegral, dt, nextIntegral);
|
|
m_FieldIntegral = nextIntegral;
|
|
return demand;
|
|
}
|
|
override void PrepareActions(CharacterControllerComponent controller, IEntity owner, ActionManager actions, float dt)
|
|
{
|
|
if (m_State != ERR_AircraftFlightState.TRANSLATING && m_State != ERR_AircraftFlightState.LANDING) { super.PrepareActions(controller, owner, actions, dt); return; }
|
|
if (!Lease() || controller != m_Character || owner != m_Pilot || actions != m_Actions || !ControlLease()) return;
|
|
if (!m_Readback || !ERR_AircraftFieldRules.ValidStep(dt) || dt > 0.2) return;
|
|
if (!actions.IsContextActive("HelicopterContext")) actions.ActivateContext("HelicopterContext", 1);
|
|
if (!actions.IsContextActive("HelicopterContext")) { m_Readback = false; return; }
|
|
if (!m_Controller.GetAutohoverEnabled()) { Fail("native field autohover lost"); return; }
|
|
vector position = m_Aircraft.GetOrigin(); vector velocity = m_Simulation.GetVelocity();
|
|
float height = position[1] - m_Home[1];
|
|
float error = ERR_AircraftFlightRules.Vertical(height, m_State == ERR_AircraftFlightState.LANDING) - velocity[1];
|
|
m_Collective = Math.Clamp(m_Collective + error * dt * 0.12, -0.95, 0.8);
|
|
float 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;
|
|
vector angular = m_Simulation.GetAngularVelocityLocal();
|
|
float torque = Math.Clamp(headingError * 0.01 - angular[1] * 0.3, -0.3, 0.3);
|
|
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
|
|
vector demand = FieldDemand(transform[3], m_Simulation.GetVelocity(), dt);
|
|
float limit = 0.08; if (m_FieldPhase == 3 || m_State == ERR_AircraftFlightState.LANDING) limit = 0.03;
|
|
float forward = Math.Clamp(vector.Dot(demand, transform[2]), -limit, limit);
|
|
float aside = Math.Clamp(vector.Dot(demand, transform[0]), -limit, limit);
|
|
if (m_State == ERR_AircraftFlightState.LANDING && Grounded()) { collective = -1; forward = 0; aside = 0; }
|
|
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();
|
|
if (m_State == ERR_AircraftFlightState.TRANSLATING) m_TranslationUpdates++;
|
|
}
|
|
protected override void CheckTranslation(float now, vector position, float height, float speed, float upright)
|
|
{
|
|
if (!m_Field || !m_Controller.GetAutohoverEnabled()) { Fail("field route or autohover unavailable"); return; }
|
|
vector velocity = m_Simulation.GetVelocity(); float horizontal = ERR_AircraftFieldRules.HorizontalSpeed(velocity);
|
|
if (m_FieldPhase == 1)
|
|
{
|
|
if (ERR_AircraftFieldRules.ShouldBrake(vector.DistanceXZ(position, m_Field.Destination()), horizontal)) BeginFieldBrake(now);
|
|
return;
|
|
}
|
|
if (m_FieldPhase == 2)
|
|
{
|
|
if (m_Simulation.HasAnyGroundContact()) { Fail("field ground contact during airborne braking"); return; }
|
|
if (!(horizontal < 0.25)) m_FieldLowSince = -1;
|
|
else if (m_FieldLowSince < 0) m_FieldLowSince = now;
|
|
if (!ERR_AircraftFieldRules.LowSpeedHeld(horizontal, m_FieldLowSince, now) || !(m_FieldReference.Length() <= 0.01)) return;
|
|
m_FieldPhase = 3; m_FieldReference = "0 0 0"; m_FieldIntegral = "0 0 0"; m_Stable = -1;
|
|
// No new deadline: slow positioning/hold belongs to the same braking budget.
|
|
}
|
|
bool held = m_FieldPhase == 3 && !m_Simulation.HasAnyGroundContact() && ERR_AircraftFlightRules.Hover(height, speed, velocity[1], upright) && horizontal < 0.25 && vector.DistanceXZ(position, m_FieldLandingPoint) <= 0.75;
|
|
if (!held) { m_Stable = -1; return; }
|
|
if (m_Stable < 0) m_Stable = now;
|
|
if (now - m_Stable < 5000) return;
|
|
if (!m_Cancelled) { m_FieldArrived = true; m_TranslationProven = true; }
|
|
Landing(now);
|
|
}
|
|
override void Tick()
|
|
{
|
|
if (!ControlLease()) return;
|
|
float now = Now();
|
|
// The owner must continue Tick even with no players. Do not transition an
|
|
// inherited startup callback into a field phase halfway through native input.
|
|
// 86.5s soft threshold reserves the 85s phase budget plus the maximum
|
|
// accepted 1.5s observation gap. Hard stage/original deadlines never move.
|
|
bool reserveAbort = ERR_AircraftFieldRules.AbortDue(now + 1500, m_Deadline);
|
|
bool fuelAbort = !m_Fuel || !(m_Fuel.GetTotalFuel() > 20);
|
|
if (!m_Cancelled && !m_FieldArrived && m_State != ERR_AircraftFlightState.LANDING && m_State != ERR_AircraftFlightState.SHUTDOWN && (reserveAbort || fuelAbort))
|
|
{
|
|
string reason = "field remaining lease reserved for braking and landing";
|
|
if (fuelAbort) reason = "field candidate low-fuel reserve reached";
|
|
Cancel(reason);
|
|
if (Finished()) return;
|
|
}
|
|
// The stock-independent shared startup includes native signed collective
|
|
// consumption, engine/fuel proof and five-second hover without duplication.
|
|
if (m_State != ERR_AircraftFlightState.TRANSLATING && m_State != ERR_AircraftFlightState.LANDING && m_State != ERR_AircraftFlightState.SHUTDOWN) { super.Tick(); return; }
|
|
if (m_LastTick >= 0 && (now < m_LastTick || now - m_LastTick > 1500)) { m_Stable = -1; m_FieldLowSince = -1; }
|
|
m_LastTick = now;
|
|
if (m_LastControl < 0 || now - m_LastControl > 1000) { Fail("field pilot callback stopped"); return; }
|
|
if (m_State != ERR_AircraftFlightState.SHUTDOWN && !m_Controller.IsEngineOn()) { Fail("field running engine lost"); return; }
|
|
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
|
|
if (m_State == ERR_AircraftFlightState.TRANSLATING) { CheckTranslation(now, transform[3], transform[3][1] - m_Home[1], m_Simulation.GetVelocity().Length(), transform[1][1]); return; }
|
|
if (m_State == ERR_AircraftFlightState.LANDING)
|
|
{
|
|
if (!m_Controller.GetAutohoverEnabled()) { Fail("field landing autohover lost"); return; }
|
|
if (!Grounded()) m_Stable = -1;
|
|
else if (m_Stable < 0) m_Stable = now;
|
|
if (m_Stable >= 0 && now - m_Stable >= 2000) Shutdown(now);
|
|
return;
|
|
}
|
|
if (m_State == ERR_AircraftFlightState.SHUTDOWN && !m_Controller.IsEngineOn())
|
|
{
|
|
if (!Grounded()) { Fail("field engine-off physical ground check failed"); return; }
|
|
if (m_Cancelled) m_State = ERR_AircraftFlightState.CANCELLED;
|
|
else m_State = ERR_AircraftFlightState.COMPLETE;
|
|
Release(true); RecordState();
|
|
}
|
|
}
|
|
}
|