237 lines
12 KiB
C
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;
|
|
}
|
|
}
|