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

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();
}
}
}