Finish airborne extraction after 30 seconds and hold rescue aircraft; record Workshop 0.0.134
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-10-31 15:01:51 +01:00
parent d21adb90d5
commit 2c2158702d
14 changed files with 981 additions and 22 deletions
@@ -8,6 +8,16 @@ class ERR_RescueRules
if (since < 0 || now < since) return now;
return since;
}
static float AirborneSince(bool airborne, float since, float now)
{
if (!airborne || now < 0) return -1;
if (since < 0 || now < since) return now;
return since;
}
static bool FlightComplete(float since, float now)
{
return since >= 0 && now >= since && now - since >= 30000;
}
static bool CanDepart(float since, float now)
{
return since >= 0 && now >= since && now - since >= 8000;
@@ -234,6 +244,7 @@ class ERR_ExtractionService
protected bool m_Stopped;
protected vector m_Center;
protected float m_Radius;
protected float m_AirborneSince = -1;
protected ERR_ExtractionState m_State;
protected ref set<UUID> m_Escapees = new set<UUID>();
protected int m_ParticipantCount;
@@ -328,8 +339,12 @@ class ERR_ExtractionService
if (m_State != ERR_ExtractionState.WAITING || !m_Assigned) return;
if (!FleetAlive()) { Resolve(ERR_ExtractionState.LOST); return; }
if (m_Stage != 5 || phase != ERR_Phase.EXTRACTION) return;
bool airborne = !m_Transports.IsEmpty();
foreach (ERR_RescueFlightComponent departing : m_Transports)
if (vector.DistanceXZ(departing.Aircraft().GetOrigin(), m_Pickup) < 1000 || vector.DistanceXZ(departing.Aircraft().GetOrigin(), m_Center) <= m_Radius) return;
if (!departing || !departing.AirborneAfterPickup()) airborne = false;
float now = GetGame().GetWorld().GetWorldTime();
m_AirborneSince = ERR_RescueRules.AirborneSince(airborne, m_AirborneSince, now);
if (!ERR_RescueRules.FlightComplete(m_AirborneSince, now)) return;
array<ref ERR_PlayerLife> lives = {};
m_Roster.Participants(lives);
foreach (ERR_PlayerLife life : lives)
@@ -359,6 +374,13 @@ class ERR_ExtractionService
}
void Stop()
{
// Capture the final aircraft state before terminal cleanup releases autopilots
// and removes the AI groups. Keep the same occupied airframes until reload.
if (m_State == ERR_ExtractionState.ESCAPED)
{
foreach (ERR_RescueFlightComponent safeTransport : m_Transports) if (safeTransport) safeTransport.HoldForResults();
foreach (ERR_RescueFlightComponent safeEscort : m_Escorts) if (safeEscort && !safeEscort.Lost()) safeEscort.HoldForResults();
}
m_Stopped = true; m_RadioPending = ""; m_PreviousSignals.Clear();
foreach (ERR_RescueFlightComponent transport : m_Transports) if (transport) transport.Release(false);
foreach (ERR_RescueFlightComponent escort : m_Escorts) if (escort) escort.Release(false);
@@ -56,7 +56,7 @@ modded class ERR_GameMode
float now = GetWorld().GetWorldTime();
if (Phase() == ERR_Phase.VICTORY)
{
ERR_RescueCheck(Extraction().EscapedCount() == 1, "native flight carried the original prisoner across the escape boundary");
ERR_RescueCheck(Extraction().EscapedCount() == 1, "native flight carried the original prisoner through thirty seconds of airborne evacuation");
Print("ERR RESCUE COMPLETE"); GetGame().GetCallqueue().Remove(ERR_RescueProbeTick); return;
}
if (!PreparationError().IsEmpty() || Phase() == ERR_Phase.DEFEAT) { ERR_RescueCheck(false, "mission failed: " + PreparationError()); return; }
@@ -18,6 +18,47 @@ class ERR_RescueFlightComponentClass : ERR_ReconFlightComponentClass {}
class ERR_RescueFlightComponent : ERR_ReconFlightComponent
{
protected bool m_Escort, m_Departing, m_LandingConfirmed;
[RplProp(onRplName: "ApplyResultsHold")] protected bool m_ResultsHeld;
// Contact plus real clearance excludes boarding, engine spin-up, and the
// controller's ten-second grounded takeoff hold. Every transport must lift.
bool AirborneAfterPickup()
{
if (!m_Departing || m_Lost || !m_Aircraft) return false;
VehicleHelicopterSimulation sim = VehicleHelicopterSimulation.Cast(m_Aircraft.FindComponent(VehicleHelicopterSimulation));
if (!sim || sim.HasAnyGroundContact()) return false;
float ground = SCR_TerrainHelper.GetTerrainY(m_Aircraft.GetOrigin(), GetGame().GetWorld(), noUnderwater: true);
return m_Aircraft.GetOrigin()[1] - ground >= 3;
}
void HoldForResults()
{
if (!Replication.IsServer() || m_Lost || !ERR_Actors.Alive(GetOwner()) || m_ResultsHeld) return;
m_ResultsHeld = true;
ApplyResultsHold();
Replication.BumpMe();
}
protected void ApplyResultsHold()
{
if (!m_ResultsHeld || !GetOwner()) return;
Physics physics = GetOwner().GetPhysics();
if (physics)
{
physics.SetVelocity(vector.Zero);
physics.SetAngularVelocity(vector.Zero);
physics.ChangeSimulationState(SimulationState.COLLISION);
}
if (Replication.IsServer())
{
DamageManagerComponent damage = DamageManagerComponent.Cast(GetOwner().FindComponent(DamageManagerComponent));
if (damage) damage.EnableDamageHandling(false);
}
}
override void Release(bool captured)
{
// Terminal presentation keeps the engines/rotors; physics is already held.
// Ordinary lost/captured/aborted flights retain their original cleanup.
super.Release(captured || m_ResultsHeld);
}
protected float m_BoardingReadySince = -1;
protected vector m_Pickup;
protected float m_RescueDeadline, m_NextThreat;
@@ -168,6 +209,7 @@ class ERR_RescueFlightComponent : ERR_ReconFlightComponent
}
override void EOnSimulate(IEntity owner, float timeSlice)
{
if (m_ResultsHeld) { ApplyResultsHold(); return; }
// Initial airborne crew allocation has a fixed 45-second deadline.
if (Replication.IsServer() && m_Initialized && m_Phase == ERR_ReconPhase.CREW)
{