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

255 lines
11 KiB
C

// Explicit finite retention only. A future HQ owner supplies existing assets and
// delivered orders, retains this service, and ticks it even on an empty server.
class ERR_AircraftServiceEntry
{
Vehicle Aircraft;
SCR_AIGroup Group;
IEntity Pilot;
PilotCompartmentSlot Seat;
ref ERR_AircraftFlight Flight;
int Token;
float Deadline;
vector Destination;
bool Pending;
bool CancelRequested;
bool Retired;
string Reason;
}
class ERR_AircraftService
{
static const int MAX_AIRCRAFT = 4;
static const int MAX_ORDERS = 128;
protected static ref map<Vehicle, ERR_AircraftService> s_Retainers = new map<Vehicle, ERR_AircraftService>();
protected ERR_GameMode m_Mode;
protected BaseWorld m_World;
protected ref array<ref ERR_AircraftServiceEntry> m_Entries = {};
protected ref set<int> m_UsedTokens = new set<int>();
protected bool m_Stopping;
void ERR_AircraftService(ERR_GameMode mode)
{
m_Mode = mode;
if (GetGame()) m_World = GetGame().GetWorld();
if (!Context()) m_Stopping = true;
}
protected bool Context()
{
return Replication.IsServer() && GetGame() && m_World && GetGame().GetWorld() == m_World && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.GetWorld() == m_World && m_Mode.Active();
}
protected ERR_AircraftServiceEntry Find(Vehicle aircraft)
{
if (!aircraft) return null;
foreach (ERR_AircraftServiceEntry entry : m_Entries) if (entry.Aircraft == aircraft) return entry;
return null;
}
protected bool Original(ERR_AircraftServiceEntry entry)
{
if (!Context() || !entry || !entry.Aircraft || !entry.Group || !entry.Pilot || !entry.Seat || !GetGame().GetPlayerManager()) return false;
if (entry.Aircraft.GetWorld() != m_World || entry.Pilot.GetWorld() != m_World || entry.Group.GetWorld() != m_World) return false;
if (entry.Group.GetFaction() != m_Mode.EnemyFaction() || SCR_FactionManager.SGetFaction(entry.Aircraft) != m_Mode.EnemyFaction() || ERR_Actors.Group(entry.Pilot) != entry.Group) return false;
if (entry.Group.GetPlayerCount(true) > 0 || ERR_Actors.Player(entry.Pilot)) return false;
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(entry.Aircraft.FindComponent(HelicopterControllerComponent));
if (!controller || controller.GetOwner() != entry.Aircraft || controller.GetPilotCompartmentSlot() != entry.Seat || entry.Seat.GetOccupant() != entry.Pilot || CompartmentAccessComponent.GetVehicleIn(entry.Pilot) != entry.Aircraft) return false;
array<int> players = {}; GetGame().GetPlayerManager().GetPlayers(players);
foreach (int id : players)
{
IEntity actor = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
if (actor && (actor == entry.Pilot || CompartmentAccessComponent.GetVehicleIn(actor) == entry.Aircraft)) return false;
}
return true;
}
protected bool PhysicalReady(ERR_AircraftServiceEntry entry)
{
if (!Original(entry) || !ERR_Actors.Alive(entry.Aircraft) || !ERR_Actors.CanFight(entry.Pilot) || !ERR_CasualtyLoadingRules.AircraftReady(entry.Aircraft)) return false;
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(entry.Aircraft.FindComponent(HelicopterControllerComponent));
VehicleHelicopterSimulation simulation = controller.GetSimulation();
FuelManagerComponent fuel = FuelManagerComponent.Cast(entry.Aircraft.FindComponent(FuelManagerComponent));
SCR_VehicleDamageManagerComponent damage = SCR_VehicleDamageManagerComponent.Cast(entry.Aircraft.FindComponent(SCR_VehicleDamageManagerComponent));
Physics physics = entry.Aircraft.GetPhysics();
if (controller.IsEngineOn() || !simulation || !simulation.IsValid() || !simulation.HasAnyGroundContact() || simulation.RotorCount() != 2) return false;
if (!fuel || !fuel.HasFuel() || !damage || !damage.GetEngineFunctional() || damage.GetMovementDamage() >= 1 || damage.IsOnFire()) return false;
if (!physics || !physics.IsDynamic() || !physics.IsActive()) return false;
for (int rotor = 0; rotor < 2; rotor++) if (simulation.RotorGetState(rotor) == RotorState.DESTROYED) return false;
return true;
}
bool Register(Vehicle aircraft, SCR_AIGroup group, IEntity pilot, PilotCompartmentSlot seat)
{
if (!Context() || m_Stopping || !aircraft || !group || !pilot || !seat || m_Entries.Count() >= MAX_AIRCRAFT || Find(aircraft) || s_Retainers.Get(aircraft)) return false;
foreach (ERR_AircraftServiceEntry known : m_Entries)
if (known.Pilot == pilot || known.Group == group || known.Seat == seat) return false;
ERR_AircraftServiceEntry entry = new ERR_AircraftServiceEntry();
entry.Aircraft = aircraft; entry.Group = group; entry.Pilot = pilot; entry.Seat = seat;
if (!PhysicalReady(entry)) return false;
m_Entries.Insert(entry); s_Retainers.Set(aircraft, this);
return true;
}
bool Ready(Vehicle aircraft)
{
ERR_AircraftServiceEntry entry = Find(aircraft);
if (!Context() || m_Stopping || !entry || entry.Pending || entry.Retired || s_Retainers.Get(aircraft) != this) return false;
return PhysicalReady(entry);
}
// Both explicit kinds share one attempt history and physical reservation.
protected ERR_AircraftServiceEntry BeginAttempt(Vehicle aircraft, int token, float deadline)
{
if (!Context() || m_Stopping || token <= 0 || m_UsedTokens.Contains(token) || m_UsedTokens.Count() >= MAX_ORDERS || !Ready(aircraft)) return null;
float now = m_World.GetWorldTime();
if (!ERR_AircraftFlightRules.Deadline(now, deadline)) return null;
ERR_AircraftServiceEntry entry = Find(aircraft);
// Commit the attempt before calling the native-backed controller. No retry of
// this token, including a partially initialized or conflicting Start refusal.
m_UsedTokens.Insert(token); entry.Token = token; entry.Deadline = deadline;
entry.CancelRequested = false;
return entry;
}
bool StartRoute(Vehicle aircraft, ERR_AircraftRouteEnvelope qualification, int token, float deadline)
{
if (!qualification) return false;
ERR_AircraftServiceEntry entry = BeginAttempt(aircraft, token, deadline);
if (!entry) return false;
entry.Destination = qualification.Destination();
ERR_AircraftRouteFlight flight = new ERR_AircraftRouteFlight();
entry.Flight = flight; entry.Pending = true;
bool started = flight.StartRoute(m_Mode, entry.Aircraft, entry.Group, entry.Pilot, entry.Seat, qualification, token, deadline);
return RecordStart(entry, started);
}
// This explicit prototype entry point accepts only a field certificate and
// constructs its known controller. No arbitrary caller-owned flight is adopted.
bool StartField(Vehicle aircraft, ERR_AircraftFieldEnvelope qualification, int token, float deadline)
{
if (!qualification) return false;
ERR_AircraftServiceEntry entry = BeginAttempt(aircraft, token, deadline);
if (!entry) return false;
entry.Destination = qualification.Destination();
ERR_AircraftFieldFlight flight = new ERR_AircraftFieldFlight();
entry.Flight = flight; entry.Pending = true;
bool started = flight.StartField(m_Mode, entry.Aircraft, entry.Group, entry.Pilot, entry.Seat, qualification, token, deadline);
return RecordStart(entry, started);
}
protected bool RecordStart(ERR_AircraftServiceEntry entry, bool started)
{
if (!started)
{
entry.Retired = true; entry.Reason = "flight start refused; no automatic retry or replacement";
// A started lease must not be dropped even if a future controller reports
// refusal after committing it. Unclaimed IDLE refusals have no active flight.
entry.Pending = entry.Flight.OrderToken() > 0 && !entry.Flight.Finished();
if (entry.Pending) { entry.CancelRequested = true; entry.Flight.Cancel(entry.Reason); }
Finish(entry);
return false;
}
entry.Reason = "flight active";
return true;
}
protected void Finish(ERR_AircraftServiceEntry entry)
{
if (!entry || !entry.Pending || !entry.Flight || !entry.Flight.Finished()) return;
entry.Pending = false;
int state = entry.Flight.State();
entry.Reason = entry.Flight.Reason();
if (state != ERR_AircraftFlightState.COMPLETE && state != ERR_AircraftFlightState.CANCELLED)
{
entry.Retired = true;
if (entry.Reason.IsEmpty()) entry.Reason = "flight ended without verified landing";
}
// COMPLETE/CANCELLED is historical. Ready always rechecks the physical asset.
}
bool Cancel(Vehicle aircraft, int token, string reason)
{
if (!Context()) return false;
ERR_AircraftServiceEntry entry = Find(aircraft);
if (!entry || !entry.Pending || !entry.Flight || entry.Token != token || token <= 0 || entry.CancelRequested) return false;
entry.CancelRequested = true;
entry.Flight.Cancel(reason); Finish(entry);
return true;
}
void Tick()
{
if (!Replication.IsServer()) return;
bool context = Context();
if (!context) m_Stopping = true;
foreach (ERR_AircraftServiceEntry entry : m_Entries)
{
if (entry.Pending && entry.Flight)
{
// Controller owns expiry/capture/terminal rules. It must keep receiving Tick
// during cancellation; there is no no-player pause in this service.
entry.Flight.Tick(); Finish(entry);
}
if (!context || !Original(entry))
{
entry.Retired = true;
if (entry.Reason.IsEmpty()) entry.Reason = "original mission or asset ownership lost";
}
}
ReleaseRetainers();
}
void Stop()
{
if (!Replication.IsServer() || m_Stopping) return;
m_Stopping = true;
foreach (ERR_AircraftServiceEntry entry : m_Entries)
{
if (!entry.Pending || !entry.Flight) continue;
entry.CancelRequested = true;
entry.Flight.Cancel("aircraft service stopping"); Finish(entry);
}
// Closing admissions is not flight completion. Caller retains/ticks until
// Drained; terminal-mode teardown is reported by the controller as relinquish.
ReleaseRetainers();
}
protected void ReleaseRetainers()
{
if (!m_Stopping || !Drained()) return;
foreach (ERR_AircraftServiceEntry entry : m_Entries)
if (entry.Aircraft && s_Retainers.Get(entry.Aircraft) == this) s_Retainers.Remove(entry.Aircraft);
}
bool Drained()
{
foreach (ERR_AircraftServiceEntry entry : m_Entries) if (entry.Pending) return false;
return true;
}
bool Stopping() { return m_Stopping; }
int RegisteredCount() { return m_Entries.Count(); }
int AttemptedOrders() { return m_UsedTokens.Count(); }
bool Pending(Vehicle aircraft) { ERR_AircraftServiceEntry entry = Find(aircraft); return entry && entry.Pending; }
bool Retired(Vehicle aircraft) { ERR_AircraftServiceEntry entry = Find(aircraft); return !entry || entry.Retired; }
int OrderToken(Vehicle aircraft)
{
int token = 0;
ERR_AircraftServiceEntry entry = Find(aircraft);
if (entry) token = entry.Token;
return token;
}
float OrderDeadline(Vehicle aircraft)
{
float deadline = -1;
ERR_AircraftServiceEntry entry = Find(aircraft);
if (entry) deadline = entry.Deadline;
return deadline;
}
int FlightState(Vehicle aircraft)
{
int state = -1;
ERR_AircraftServiceEntry entry = Find(aircraft);
if (entry && entry.Flight) state = entry.Flight.State();
return state;
}
string Reason(Vehicle aircraft)
{
string reason = "unregistered aircraft";
ERR_AircraftServiceEntry entry = Find(aircraft);
if (entry) reason = entry.Reason;
return reason;
}
vector Destination(Vehicle aircraft) { vector destination = "0 0 0"; ERR_AircraftServiceEntry entry = Find(aircraft); if (entry) destination = entry.Destination; return destination; }
void ~ERR_AircraftService()
{
// Destruction is teardown, never proof of landing. Normal shutdown must use
// Stop + retained Tick until Drained. Entity ownership stays with its creator.
Stop();
foreach (ERR_AircraftServiceEntry entry : m_Entries)
if (entry.Aircraft && s_Retainers.Get(entry.Aircraft) == this) s_Retainers.Remove(entry.Aircraft);
}
}