Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
366 lines
15 KiB
C
366 lines
15 KiB
C
class ERR_AirPassenger
|
|
{
|
|
IEntity Actor;
|
|
BaseCompartmentSlot Seat;
|
|
bool Locked = true;
|
|
bool Jumped;
|
|
ERR_AssaultParachute Chute;
|
|
}
|
|
class ERR_AirAssaultTeam
|
|
{
|
|
SCR_AIGroup Group;
|
|
ref array<ref ERR_AirPassenger> 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<BaseCompartmentSlot> m_Seats = {};
|
|
protected ref array<ref ERR_AirPassenger> m_Passengers = {};
|
|
protected ref array<ref ERR_AirAssaultTeam> 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<BaseCompartmentSlot> 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<AIAgent> 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<AIAgent> 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<string, int> rejected = new map<string, int>();
|
|
// 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;
|
|
}
|
|
}
|