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

237 lines
12 KiB
C

// Successor-owned measured envelope, independent of aircraft yaw/pitch/roll.
// Adapted from the separately qualified native vertical fixture; no native hook.
// All distances are around the native physics origin, not the rotor centre.
class ERR_AircraftFlightFootprintRules
{
static float CylinderSphere(float originDistance, float radius, float thickness)
{
// Triangle inequality deliberately overbounds the one-sided rotor cylinder.
return originDistance + radius + thickness;
}
static bool Fits(float radius, float initialDrift, float initialHeight)
{
if (!(radius > 0 && radius < 32 && initialDrift >= 0 && initialDrift < 32)) return false;
if (!(initialHeight >= -3 && initialHeight <= 5)) return false;
return radius + initialDrift + 8 + 1 <= 40 && radius + initialHeight + 18 + 1 <= 60;
}
}
class ERR_AircraftFlightFootprint
{
static const int MAX_ENTITIES = 256;
static const int MAX_DEPTH = 16;
static const int MAX_HITZONES = 512;
protected static bool Refuse(string reason)
{
Print("ERR AIRCRAFT FOOTPRINT: refused " + reason);
return false;
}
protected static bool PointRadius(vector point, vector origin, inout float radius)
{
float distance = vector.Distance(point, origin);
if (!(distance >= 0 && distance < 1000)) return false;
radius = Math.Max(radius, distance);
return true;
}
static bool Measure(Vehicle aircraft, VehicleHelicopterSimulation simulation, vector pad, out vector origin, out float radius)
{
radius = 0;
if (!aircraft || !simulation || !simulation.IsValid() || !aircraft.GetPrefabData()) return Refuse("missing initialized native airframe");
if (aircraft.GetPrefabData().GetPrefabName() != "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et") return Refuse("different airframe resource");
// Model bounds and damage pivots are world-space. The simulation getter
// exposes internal physics space (identity at the airport in 100422), so
// use this same physical aircraft's actual entity world transform.
vector worldTransform[4]; aircraft.GetWorldTransform(worldTransform);
origin = worldTransform[3];
for (int axis = 0; axis < 3; axis++)
if (!(Math.AbsFloat(worldTransform[axis].Length() - 1) < 0.001)) return Refuse("non-unit native entity world basis");
array<IEntity> entities = {aircraft};
array<int> depths = {0};
array<HitZone> seenZones = {};
int meshes, mainSweeps, tailSweeps;
float modelRadius, damageRadius, simulationRadius;
for (int index = 0; index < entities.Count(); index++)
{
IEntity entity = entities[index];
if (!entity) return Refuse("hierarchy changed during measurement");
if (entity.GetVObject())
{
vector mins, maxs; entity.GetWorldBounds(mins, maxs);
for (int dimension = 0; dimension < 3; dimension++)
if (!(mins[dimension] <= maxs[dimension])) return Refuse("invalid model bounds");
// A sphere around every corner encloses the complete world AABB. Its radius
// remains conservative under any rigid orientation of the whole aircraft.
for (int x = 0; x < 2; x++)
for (int y = 0; y < 2; y++)
for (int z = 0; z < 2; z++)
{
vector corner = mins;
if (x == 1) corner[0] = maxs[0];
if (y == 1) corner[1] = maxs[1];
if (z == 1) corner[2] = maxs[2];
if (!PointRadius(corner, origin, modelRadius)) return Refuse("non-finite or excessive model bounds");
}
meshes++;
}
HitZoneContainerComponent container = HitZoneContainerComponent.Cast(entity.FindComponent(HitZoneContainerComponent));
if (container)
{
int count = container.CountAllHitZones();
if (count < 0 || count + seenZones.Count() > MAX_HITZONES) return Refuse("hitzone traversal bound");
array<HitZone> zones = {};
if (container.GetAllHitZones(zones) != count || zones.Count() != count) return Refuse("hitzones changed during measurement");
foreach (HitZone zone : zones)
{
if (!zone || seenZones.Contains(zone)) continue;
seenZones.Insert(zone);
SCR_RotorHitZone rotorZone = SCR_RotorHitZone.Cast(zone);
if (!rotorZone || rotorZone.GetRotorRadius() <= 0) continue; // transmission zones have no sweep
int rotorIndex = rotorZone.GetRotorIndex();
float expectedRadius, thickness;
if (rotorIndex == 0) { expectedRadius = 12; thickness = 1.5; mainSweeps++; }
else if (rotorIndex == 1) { expectedRadius = 2.3; thickness = 0.75; tailSweeps++; }
else return Refuse("unknown rotor sweep index");
// Stock thickness is protected with no getter. Scope to the exact prefab
// and verify its public sweep radius against the audited stock constants.
if (!(Math.AbsFloat(rotorZone.GetRotorRadius() - expectedRadius) < 0.001)) return Refuse("stock damage sweep radius changed");
PointInfo point = rotorZone.GetPointInfo();
if (!point) return Refuse("missing actual rotor sweep pivot");
vector rotorWorld[4]; point.GetWorldTransform(rotorWorld);
// Frobenius norm bounds the transform's maximum stretch even with scale
// or shear; do not assume the PointInfo basis is orthonormal.
float scaleSquared = 0;
for (int basis = 0; basis < 3; basis++) scaleSquared += vector.Dot(rotorWorld[basis], rotorWorld[basis]);
float scale = Math.Sqrt(scaleSquared);
if (!(scale > 0 && scale < 4)) return Refuse("invalid damage sweep basis");
float distance = vector.Distance(origin, rotorWorld[3]);
float sweepBound = ERR_AircraftFlightFootprintRules.CylinderSphere(distance, expectedRadius * scale, thickness * scale);
if (!(sweepBound > 0 && sweepBound < 1000)) return Refuse("invalid damage sweep extent");
damageRadius = Math.Max(damageRadius, sweepBound);
PrintFormat("ERR AIRCRAFT FOOTPRINT ROTOR: index=%1 pivot=%2 damageRadius=%3 thickness=%4 scale=%5 sphereBound=%6", rotorIndex, rotorWorld[3], expectedRadius, thickness, scale, sweepBound);
}
}
IEntity child = entity.GetChildren();
while (child)
{
if (entities.Count() >= MAX_ENTITIES || depths[index] >= MAX_DEPTH || entities.Contains(child)) return Refuse("entity/depth/cycle traversal bound");
entities.Insert(child); depths.Insert(depths[index] + 1);
child = child.GetSibling();
}
}
if (meshes == 0 || mainSweeps != 1 || tailSweeps != 1) return Refuse("complete model and both actual damage sweeps required");
if (simulation.RotorCount() != 2) return Refuse("stock simulation rotor count changed");
for (int rotor = 0; rotor < 2; rotor++)
{
vector localRotor[4]; simulation.RotorGetTransform(rotor, localRotor);
float physicalRadius = simulation.RotorGetRadius(rotor);
float physicalBound = localRotor[3].Length() + physicalRadius;
if (!(physicalRadius > 0 && physicalBound > 0 && physicalBound < 1000)) return Refuse("invalid native simulation rotor extent");
simulationRadius = Math.Max(simulationRadius, physicalBound);
PrintFormat("ERR AIRCRAFT FOOTPRINT PHYSICS ROTOR: index=%1 localPivot=%2 radius=%3 sphereBound=%4", rotor, localRotor[3], physicalRadius, physicalBound);
}
radius = Math.Max(modelRadius, Math.Max(damageRadius, simulationRadius));
float initialDrift = vector.DistanceXZ(origin, pad);
float initialHeight = origin[1] - pad[1];
PrintFormat("ERR AIRCRAFT FOOTPRINT: entities=%1 meshes=%2 zones=%3 modelSphere=%4 damageSphere=%5 simulationSphere=%6 completeSphere=%7 initialDrift=%8 initialHeight=%9", entities.Count(), meshes, seenZones.Count(), modelRadius, damageRadius, simulationRadius, radius, initialDrift, initialHeight);
if (!ERR_AircraftFlightFootprintRules.Fits(radius, initialDrift, initialHeight)) return Refuse("measured footprint plus initial placement,8m drift and1m margin exceeds40m corridor or60m ceiling");
return true;
}
}
// Issued only by physical qualification; fields have no public mutators.
// One claim binds this short-lived record to one original aircraft/pilot/order.
class ERR_AircraftFlightEnvelope
{
protected ERR_GameMode m_Mode;
protected Vehicle m_Aircraft;
protected IEntity m_Pilot;
protected vector m_Home;
protected float m_Radius;
protected float m_QualifiedAt;
protected int m_Claim;
vector Home() { return m_Home; }
float Radius() { return m_Radius; }
bool ClaimedBy(Vehicle aircraft, IEntity pilot, int token)
{
return m_Claim > 0 && m_Claim == token && m_Aircraft == aircraft && m_Pilot == pilot;
}
bool Claim(ERR_GameMode mode, Vehicle aircraft, IEntity pilot, int token)
{
if (!GetGame() || !GetGame().GetWorld()) return false;
float now = GetGame().GetWorld().GetWorldTime();
if (!Replication.IsServer() || !mode || mode != m_Mode || mode != ERR_GameMode.Get() || !mode.Active() || m_Claim != 0 || token <= 0) return false;
if (aircraft != m_Aircraft || pilot != m_Pilot || !aircraft || !pilot || now < m_QualifiedAt || now - m_QualifiedAt > 2000) return false;
if (vector.Distance(aircraft.GetOrigin(), m_Home) > 0.1) return false;
m_Claim = token;
return true;
}
protected static bool Hierarchy(IEntity root, array<IEntity> excluded)
{
if (!root) return false;
array<IEntity> pending = {root};
array<int> depths = {0};
for (int i = 0; i < pending.Count(); i++)
{
IEntity entity = pending[i];
if (!entity || excluded.Count() >= 512) return false;
if (!excluded.Contains(entity)) excluded.Insert(entity);
IEntity child = entity.GetChildren();
while (child)
{
if (pending.Count() >= 256 || depths[i] >= 16 || pending.Contains(child)) return false;
pending.Insert(child); depths.Insert(depths[i] + 1); child = child.GetSibling();
}
}
return true;
}
protected static bool ClearColumn(Vehicle aircraft, IEntity pilot, vector pad)
{
array<IEntity> excluded = {};
if (!Hierarchy(aircraft, excluded) || !Hierarchy(pilot, excluded)) return false;
for (int edge = 0; edge < 4; edge++)
{
float side = edge * Math.PI / 2;
vector point = pad + Vector(Math.Cos(side) * 14, 0, Math.Sin(side) * 14);
if (!(Math.AbsFloat(SCR_TerrainHelper.GetTerrainY(point) - pad[1]) <= 0.8)) return false;
}
// Same four finite diagonal cylinder traces as the native fixture, with
// exclusions restricted to this already-existing aircraft and seated pilot.
array<vector> diagonals = {"40 2 0", "-40 2 0", "0 2 40", "0 2 -40"};
foreach (vector diagonal : diagonals)
{
TraceParam trace = new TraceParam();
trace.Start = pad + "0 2.5 0" + diagonal;
trace.End = pad + "0 2.5 0" - diagonal;
trace.Flags = TraceFlags.ENTS; trace.ExcludeArray = excluded;
if (!(aircraft.GetWorld().TraceMove(trace, null) >= 1)) return false;
}
TraceBox clearance = new TraceBox();
clearance.Start = pad + "0 3 0"; clearance.End = pad + "0 60 0";
clearance.Mins = "-40 -1 -40"; clearance.Maxs = "40 1 40";
clearance.Flags = TraceFlags.ENTS | TraceFlags.WORLD; clearance.ExcludeArray = excluded;
return aircraft.GetWorld().TraceMove(clearance, null) >= 1;
}
static ERR_AircraftFlightEnvelope Qualify(ERR_GameMode mode, Vehicle aircraft, IEntity pilot)
{
if (!GetGame() || !GetGame().GetWorld() || !GetGame().GetPlayerManager()) return null;
if (!Replication.IsServer() || !mode || mode != ERR_GameMode.Get() || !mode.Active() || !aircraft || !pilot || ERR_Actors.Player(pilot)) return null;
if (CompartmentAccessComponent.GetVehicleIn(pilot) != aircraft || !ERR_Actors.Alive(aircraft) || !ERR_Actors.CanFight(pilot)) return null;
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(aircraft.FindComponent(HelicopterControllerComponent));
if (!controller || controller.IsEngineOn() || !controller.GetSimulation() || !controller.GetSimulation().IsValid()) return null;
PilotCompartmentSlot seat = controller.GetPilotCompartmentSlot();
if (!seat || seat.GetOccupant() != pilot) return null;
if (!ERR_CasualtyLoadingRules.AircraftReady(aircraft)) return null;
vector pad = ERR_Actors.Ground(aircraft.GetOrigin());
if (!(pad[1] >= 2) || !ClearColumn(aircraft, pilot, pad)) return null;
vector origin; float radius;
if (!ERR_AircraftFlightFootprint.Measure(aircraft, controller.GetSimulation(), pad, origin, radius)) return null;
ERR_AircraftFlightEnvelope record = new ERR_AircraftFlightEnvelope();
record.m_Mode = mode; record.m_Aircraft = aircraft; record.m_Pilot = pilot;
record.m_Home = origin; record.m_Radius = radius;
record.m_QualifiedAt = GetGame().GetWorld().GetWorldTime();
return record;
}
}