class ERR_AirPassenger { IEntity Actor; BaseCompartmentSlot Seat; bool Locked = true; bool Jumped; ERR_AssaultParachute Chute; } class ERR_AirAssaultTeam { SCR_AIGroup Group; ref array Members = {}; ref ERR_AirDropOrder Order; bool Released; void Landed() {} // Canopy completion is observed by the owning load for the entire original team. void Update() { if (Released || !Group || !Order || Group.GetPlayerCount(true) > 0) return; bool survivor; foreach (ERR_AirPassenger member : Members) { if (!ERR_Actors.Alive(member.Actor)) continue; if (!member.Jumped || CompartmentAccessComponent.GetVehicleIn(member.Actor) || member.Actor.GetOrigin()[1] - SCR_TerrainHelper.GetTerrainY(member.Actor.GetOrigin()) > 3) return; survivor = survivor || ERR_Actors.CanFight(member.Actor); } if (!survivor) return; Released = true; Group.ActivateAI(); Group.SetPermanentLOD(-1); ERR_HQ.Get().LandAirTeam(Group, Order); } } class ERR_AirDropGeometry { static string LastFailure; static float Along(vector position, vector center, vector direction) { vector offset = position - center; offset[1] = 0; return vector.Dot(offset, direction); } static bool Corridor(vector position, vector center, vector direction, vector velocity) { float along = Along(position, center, direction); vector lateral = position - center - direction * along; lateral[1] = 0; velocity[1] = 0; return along >= -175 && along <= 250 && lateral.Length() <= 45 && velocity.Length() >= 15 && vector.Dot(velocity.Normalized(), direction) >= 0.8; } static bool LandingLine(vector center, vector direction) { LastFailure = "clear"; for (int step = -4; step <= 5; step++) { vector point = ERR_Actors.Ground(center + direction * (step * 50)); if (point[1] < 2 || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)) { LastFailure = "water/shore"; return false; } if (!ERR_HQRoadIntercept.Standable(point)) { LastFailure = "landing ground not standable"; return false; } TraceBox trace = new TraceBox(); trace.Start = point + "0 3 0"; trace.End = point + "0 190 0"; trace.Mins = "-4 -1 -4"; trace.Maxs = "4 1 4"; trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD; if (GetGame().GetWorld().TraceMove(trace, null) < 1) { LastFailure = "obstructed descent column"; return false; } } return true; } } // Eight original passengers, two original teams, one consumed load per aircraft. class ERR_AirAssaultLoad { static const ResourceName PARACHUTE = "{C31F210ABE0781A0}Prefabs/ERR/AssaultParachute.et"; protected ERR_GameMode m_Mode; protected ERR_ReconFlightComponent m_Flight; protected Vehicle m_Aircraft; protected int m_Serial, m_NextPassenger; protected ref array m_Seats = {}; protected ref array m_Passengers = {}; protected ref array m_Teams = {}; protected ref ERR_AirDropOrder m_Order; protected float m_CoverUntil; bool Cover(float now, out vector center) { if (!m_Order || !m_Used || m_Approaching || now >= m_CoverUntil) return false; center = m_Order.Target; return true; } bool NeedsService(float now) { return m_Used && !m_Approaching && now >= m_CoverUntil; } protected float m_LoadDeadline, m_NextBoard, m_NextJump, m_AirspaceUntil; protected bool m_Ready, m_Failed, m_Stopped, m_Used, m_Approaching, m_RunCommitted; protected vector m_Center, m_Direction, m_RunStart, m_RunEnd; static void SetLock(IEntity actor, bool locked) { ERR_HQ hq = ERR_HQ.Get(); if (hq && hq.AirAssault()) hq.AirAssault().Lock(actor, locked); } void ReuseRemaining() { m_Used = false; m_Order = null; m_CoverUntil = 0; foreach (ERR_AirPassenger member : m_Passengers) if (member.Actor && CompartmentAccessComponent.GetVehicleIn(member.Actor) == m_Aircraft) { member.Jumped = false; member.Locked = true; } } int Count() { return m_Passengers.Count(); } ERR_AirPassenger At(int index) { return m_Passengers[index]; } bool Used() { return m_Used; } bool Preparing() { return !m_Ready && !m_Failed && !m_Stopped; } bool Ready() { return m_Ready && !m_Failed && !m_Stopped && !m_Used && m_Flight && !m_Flight.Lost() && (m_Flight.Phase() == ERR_ReconPhase.SEARCH || m_Flight.Phase() == ERR_ReconPhase.SHADOW) && Aboard() > 0; } void ERR_AirAssaultLoad(ERR_GameMode mode, ERR_ReconFlightComponent flight, int serial) { m_Mode = mode; m_Flight = flight; m_Aircraft = flight.Aircraft(); m_Serial = serial; } protected void Seats(IEntity entity) { BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(entity.FindComponent(BaseCompartmentManagerComponent)); if (manager) { array seats = {}; manager.GetCompartments(seats); foreach (BaseCompartmentSlot seat : seats) if (seat && seat.GetType() == ECompartmentType.CARGO && seat.IsCompartmentAccessible() && !seat.IsOccupied() && !m_Seats.Contains(seat)) m_Seats.Insert(seat); } IEntity child = entity.GetChildren(); while (child) { Seats(child); child = child.GetSibling(); } } protected void Failure(string reason) { m_Failed = true; m_Approaching = false; foreach (ERR_AirPassenger member : m_Passengers) member.Locked = false; ERR_HQ.Event("AIR LOAD UNAVAILABLE", reason + "; original partial entities retained, reconnaissance remains available"); } void Load(vector pad) { m_LoadDeadline = GetGame().GetWorld().GetWorldTime() + 30000; Seats(m_Aircraft); ResourceName soldier = m_Mode.FactionSelection().Soldier(); Resource chute = Resource.Load(PARACHUTE); if (m_Seats.Count() < 8 || soldier.IsEmpty() || !chute || !chute.IsValid()) { Failure("eight usable cargo seats, soldier or canopy unavailable"); return; } for (int t = 0; t < 2; t++) { ERR_AirAssaultTeam team = new ERR_AirAssaultTeam(); team.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn("{C31F210ABE078195}Prefabs/ERR/ReconCrew.et", pad)); m_Teams.Insert(team); if (!team.Group) { Failure("passenger group spawn failed"); return; } m_Mode.Own(team.Group); team.Group.SetFaction(m_Mode.EnemyFaction()); team.Group.SetPermanentLOD(0); } for (int i = 0; i < 8; i++) { ERR_AirPassenger member = new ERR_AirPassenger(); member.Seat = m_Seats[i]; AIGroup group = m_Teams[i % 2].Group; member.Actor = member.Seat.SpawnCharacterInCompartment(soldier, group, "{C31F210ABE078195}Prefabs/ERR/ReconCrew.et"); m_Passengers.Insert(member); m_Teams[i % 2].Members.Insert(member); if (!member.Actor || group != m_Teams[i % 2].Group) { Failure("native passenger creation or group identity failed"); return; } } } protected void Boarding(float now) { foreach (ERR_AirAssaultTeam loadingTeam : m_Teams) { if (!loadingTeam.Group) continue; loadingTeam.Group.ActivateAI(); array loadingAgents = {}; loadingTeam.Group.GetAgents(loadingAgents); foreach (AIAgent loadingAgent : loadingAgents) { loadingAgent.SetPermanentLOD(0); loadingAgent.ActivateAI(); } } if (now >= m_LoadDeadline) { Failure(string.Format("native passenger boarding timed out; seated=%1/%2", Aboard(), Count())); return; } bool all = m_Passengers.Count() == 8; foreach (ERR_AirPassenger member : m_Passengers) { if (!ERR_Actors.CanFight(member.Actor)) { all = false; continue; } if (CompartmentAccessComponent.GetVehicleIn(member.Actor) == m_Aircraft && member.Seat.GetOccupant() == member.Actor) continue; all = false; if (now < m_NextBoard || CompartmentAccessComponent.GetVehicleIn(member.Actor) || member.Seat.IsOccupied()) continue; SCR_CompartmentAccessComponent access = SCR_CompartmentAccessComponent.Cast(member.Actor.FindComponent(SCR_CompartmentAccessComponent)); if (access) access.MoveInVehicle(m_Aircraft, ECompartmentType.CARGO, false, member.Seat); } if (now >= m_NextBoard) m_NextBoard = now + 1000; if (!all) return; for (int t = 0; t < m_Teams.Count(); t++) { ERR_AirAssaultTeam team = m_Teams[t]; foreach (ERR_AirPassenger original : team.Members) if (ERR_Actors.Group(original.Actor) != team.Group || !ERR_Actors.SetFaction(original.Actor, m_Mode.EnemyFaction())) { Failure("original passenger faction/group changed"); return; } ERR_ForceRecord record = m_Mode.Forces().Register(string.Format("airborne-%1-team-%2", m_Serial, t), ERR_ForceKind.PATROL, m_Aircraft.GetOrigin(), 4, null, team.Group, now); if (!record) { Failure("finite passenger ledger refused registration"); return; } record.Pinned = true; m_Mode.RegisterGroup(team.Group); ERR_HQ.Get().ReserveGroup(team.Group); team.Group.ActivateAI(); array agents = {}; team.Group.GetAgents(agents); foreach (AIAgent agent : agents) { agent.SetPermanentLOD(0); agent.ActivateAI(); } } m_Ready = true; ERR_HQ.Event("AIR LOAD READY", "eight original passengers seated in two four-person teams"); } int Aboard() { if (!m_Aircraft) return 0; int count; foreach (ERR_AirPassenger member : m_Passengers) if (ERR_Actors.CanFight(member.Actor) && !ERR_Actors.Player(member.Actor) && CompartmentAccessComponent.GetVehicleIn(member.Actor) == m_Aircraft) count++; return count; } bool HoldsSeat(IEntity actor) { if (m_Stopped || m_Failed || !ERR_Actors.CanFight(actor) || ERR_Actors.Player(actor)) return false; foreach (ERR_AirAssaultTeam team : m_Teams) foreach (ERR_AirPassenger member : team.Members) if (member.Actor == actor && member.Locked && ERR_Actors.Group(actor) == team.Group) return (m_Flight && m_Flight.Phase() < ERR_ReconPhase.PARKED && CompartmentAccessComponent.GetVehicleIn(actor) == m_Aircraft) || (member.Chute && CompartmentAccessComponent.GetVehicleIn(actor) == member.Chute); return false; } void Lock(IEntity actor, bool locked) { foreach (ERR_AirPassenger member : m_Passengers) if (member.Actor == actor) member.Locked = locked; } bool Begin(ERR_AirDropOrder order, float now) { if (!Ready() || !order || now >= order.ExpiresAt) return false; vector radial = m_Aircraft.GetOrigin() - order.Target; radial[1] = 0; float heading = Math.Atan2(radial[0], radial[2]); bool found; int surveyed; map rejected = new map(); // Several distances and both runway directions provide alternatives around // woodland while retaining every landing/descent safety check. for (int attempt = 0; attempt < 72; attempt++) { int bearing = attempt % 12; int ring = Math.Floor(attempt / 24.0); float radius = 300 + ring * 200; float angle = heading + bearing * Math.PI / 6; radial = Vector(Math.Sin(angle), 0, Math.Cos(angle)); m_Center = ERR_Actors.Ground(order.Target + radial * radius); m_Direction = Vector(-radial[2], 0, radial[0]); if ((attempt % 24) >= 12) m_Direction = radial; surveyed++; if (ERR_AirDropGeometry.LandingLine(m_Center, m_Direction)) { found = true; break; } int count; rejected.Find(ERR_AirDropGeometry.LastFailure, count); rejected.Set(ERR_AirDropGeometry.LastFailure, count + 1); } string summary; foreach (string reason, int failures : rejected) summary += reason + "=" + failures.ToString() + "; "; ERR_HQ.Event("AIR DROP SURVEY", string.Format("target=%1 checked=%2 accepted=%3 center=%4; %5", order.Target, surveyed, found, m_Center, summary)); if (!found) return false; m_RunStart = m_Center - m_Direction * 350; m_RunEnd = m_Center + m_Direction * 450; m_CoverUntil = now + 300000; m_Order = order; m_Used = true; m_Approaching = true; m_RunCommitted = false; m_NextPassenger = 0; m_NextJump = now; foreach (ERR_AirAssaultTeam team : m_Teams) team.Order = order; m_AirspaceUntil = order.ExpiresAt + 150000; return true; } bool Approach(out vector point) { point = m_RunStart; if (m_RunCommitted) point = m_RunEnd; return m_Approaching && !m_Stopped; } void Abort() { if (m_Approaching && m_Order) m_CoverUntil = GetGame().GetWorld().GetWorldTime() + 300000; m_Approaching = false; m_RunCommitted = false; } bool BlocksMortar(vector impact, float now) { if (!m_Order || vector.DistanceXZ(impact, m_Order.Target) >= 1200) return false; foreach (ERR_AirPassenger member : m_Passengers) if (member.Chute && member.Actor && CompartmentAccessComponent.GetVehicleIn(member.Actor) == member.Chute) return true; return now < m_AirspaceUntil; } void Update(float now) { if (m_Stopped || !Replication.IsServer() || !m_Mode.Active()) return; foreach (ERR_AirAssaultTeam team : m_Teams) team.Update(); if (Preparing()) { Boarding(now); return; } if (!m_Approaching) return; if (!m_Flight || m_Flight.Lost() || m_Flight.Phase() >= ERR_ReconPhase.RETURNING || !ERR_Actors.Alive(m_Aircraft) || now >= m_Order.ExpiresAt) { Abort(); ERR_HQ.Event("AIR DROP ABORTED", "authorization expired or aircraft unavailable; no new passengers created"); return; } foreach (ERR_AirAssaultTeam originalTeam : m_Teams) if (!originalTeam.Group || originalTeam.Group.GetPlayerCount(true) > 0 || originalTeam.Group.GetFaction() != m_Mode.EnemyFaction()) { Abort(); return; } vector position = m_Aircraft.GetOrigin(); if (!m_RunCommitted && vector.DistanceXZ(position, m_RunStart) <= 80) m_RunCommitted = true; if (m_RunCommitted && ERR_AirDropGeometry.Along(position, m_Center, m_Direction) > 300) { Abort(); return; } float height = position[1] - SCR_TerrainHelper.GetTerrainY(position); if (!m_RunCommitted || now < m_NextJump || height < 90 || height > 180 || !m_Aircraft.GetPhysics() || !ERR_AirDropGeometry.Corridor(position, m_Center, m_Direction, m_Aircraft.GetPhysics().GetVelocity())) return; while (m_NextPassenger < m_Passengers.Count()) { int index = m_NextPassenger++; ERR_AirPassenger passenger = m_Passengers[index]; if (passenger.Jumped || !ERR_Actors.CanFight(passenger.Actor) || ERR_Actors.Player(passenger.Actor) || CompartmentAccessComponent.GetVehicleIn(passenger.Actor) != m_Aircraft) continue; vector exit = m_Aircraft.CoordToParent("12 -4 0"); ERR_AssaultParachute chute = ERR_AssaultParachute.Cast(ERR_Actors.Spawn(PARACHUTE, exit)); m_NextJump = now + 2000; if (!chute) { ERR_HQ.Event("AIR DROP FAILED", "canopy creation refused; original passenger retained"); continue; } m_Mode.Own(chute); passenger.Chute = chute; vector landing = m_Center + m_Direction * ERR_AirDropGeometry.Along(position, m_Center, m_Direction); passenger.Jumped = chute.BeginJump(passenger.Actor, m_Teams[index % 2], landing); ERR_HQ.Event("AIR DROP JUMP", string.Format("original passenger=%1 transfer requested=%2", index, passenger.Jumped)); break; } if (m_NextPassenger >= m_Passengers.Count()) Abort(); } void Stop() { m_Stopped = true; Abort(); foreach (ERR_AirPassenger passenger : m_Passengers) passenger.Locked = false; } }