// Experimental single immutable100m field only. No route tiling or fallback. // Inherits the original exact-instance, single-use, two-second claim contract. class ERR_AircraftFieldEnvelope : ERR_AircraftFlightEnvelope { static const int MAX_TERRAIN_NODES = 4096; protected vector m_Destination; vector Destination() { return m_Destination; } bool Contains(vector position, float upright) { return ERR_AircraftFieldRules.Contains(m_Home, m_Destination, position, upright); } static bool FinitePoint(vector point) { for (int axis = 0; axis < 3; axis++) if (!(Math.AbsFloat(point[axis]) <= 1000000)) return false; return true; } static int TerrainColumns(float distance) { if (!ERR_AircraftFieldRules.Length(distance)) return 0; return Math.Ceil((distance + 80) / 2); } static bool TerrainBudget(int columns, int rows) { if (columns < 1 || rows < 1 || columns >= MAX_TERRAIN_NODES || rows >= MAX_TERRAIN_NODES) return false; return (columns + 1) * (rows + 1) <= MAX_TERRAIN_NODES; } #ifdef WORKBENCH // At most the first refusal of each exact survey call is logged. No query, // predicate, retry or resource allocation is added to the qualification. protected static bool SurveyDiagnostics() { string enabled, outcome; return System.GetCLIParam("errSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "aircraft-field"; } protected static void SurveyTraceFailure(string stage, float fraction, TraceBox trace) { if (!SurveyDiagnostics()) return; IEntity hit = trace.TraceEnt; string kind = "none", resource = ""; if (hit) { kind = hit.Type().ToString(); if (hit.GetPrefabData()) resource = hit.GetPrefabData().GetPrefabName(); } if (hit) { vector lower, upper; hit.GetWorldBounds(lower, upper); PrintFormat("ERR AIRCRAFT FIELD SURVEY HIT BOUNDS: lower=%1 upper=%2", lower, upper); } PrintFormat("ERR AIRCRAFT FIELD SURVEY REFUSED: stage=%1 fraction=%2 entity=%3 class=%4 prefab=%5 start=%6 end=%7", stage, fraction, hit, kind, resource, trace.Start, trace.End); } #endif static bool Survey(vector start, vector end, array excluded = null) { if (!GetGame() || !GetGame().GetWorld() || !FinitePoint(start) || !FinitePoint(end)) { #ifdef WORKBENCH if (SurveyDiagnostics()) PrintFormat("ERR AIRCRAFT FIELD SURVEY REFUSED: stage=context-or-finite-points start=%1 end=%2", start, end); #endif return false; } float distance = vector.DistanceXZ(start, end); int columns = TerrainColumns(distance), rows = 40; if (!TerrainBudget(columns, rows) || !(start[1] >= 2) || !(Math.AbsFloat(end[1] - start[1]) <= 0.3)) { #ifdef WORKBENCH if (SurveyDiagnostics()) PrintFormat("ERR AIRCRAFT FIELD SURVEY REFUSED: stage=budget-or-endpoints start=%1 end=%2 distance=%3 columns=%4 rows=%5 startHeight=%6 endHeight=%7 delta=%8", start, end, distance, columns, rows, start[1], end[1], Math.AbsFloat(end[1] - start[1])); #endif return false; } vector along = end - start; along[1] = 0; along = along * (1 / distance); vector across = Vector(-along[2], 0, along[0]); float span = distance + 80; // Route-aligned grid includes40m beyond both ends and each side, enclosing // every allowed centre (8m cap) plus the measured complete footprint/margin. // <=2m along either grid axis, <=4096 total; all nodes must pass before traces. for (int x = 0; x <= columns; x++) for (int z = 0; z <= rows; z++) { vector sample = start + along * (-40 + span * x / columns) + across * (-40 + 80 * z / rows); float terrain = SCR_TerrainHelper.GetTerrainY(sample); if (!(Math.AbsFloat(terrain - start[1]) <= 0.3)) { #ifdef WORKBENCH if (SurveyDiagnostics()) PrintFormat("ERR AIRCRAFT FIELD SURVEY REFUSED: stage=terrain x=%1 z=%2 columns=%3 rows=%4 sample=%5 terrainHeight=%6 startHeight=%7 delta=%8", x, z, columns, rows, sample, terrain, start[1], Math.AbsFloat(terrain - start[1])); #endif return false; } } vector lower, upper; for (int axis = 0; axis < 3; axis++) { lower[axis] = Math.Min(start[axis], end[axis]); upper[axis] = Math.Max(start[axis], end[axis]); } vector centre = (lower + upper) * 0.5; centre[1] = start[1]; vector half = (upper - lower) * 0.5; half[0] = half[0] + 40; half[2] = half[2] + 40; // AABB encloses the entire finite segment expanded40m, including endpoint // caps. A failed whole-volume trace never becomes a partly qualified leg. TraceBox corridor = new TraceBox(); corridor.Start = centre + "0 3 0"; corridor.End = centre + "0 60 0"; corridor.Mins = Vector(-half[0], -1, -half[2]); corridor.Maxs = Vector(half[0], 1, half[2]); corridor.Flags = TraceFlags.ENTS | TraceFlags.WORLD; corridor.ExcludeArray = excluded; float corridorFraction = GetGame().GetWorld().TraceMove(corridor, null); if (!(corridorFraction >= 1)) { #ifdef WORKBENCH SurveyTraceFailure("corridor", corridorFraction, corridor); #endif return false; } TraceBox low = new TraceBox(); low.Start = centre + "0 0.5 0"; low.End = centre + "0 3 0"; low.Mins = Vector(-half[0], -0.2, -half[2]); low.Maxs = Vector(half[0], 0.2, half[2]); low.Flags = TraceFlags.ENTS; low.ExcludeArray = excluded; float lowFraction = GetGame().GetWorld().TraceMove(low, null); #ifdef WORKBENCH if (!(lowFraction >= 1)) SurveyTraceFailure("low-objects", lowFraction, low); #endif return lowFraction >= 1; } static ERR_AircraftFieldEnvelope QualifyField(ERR_GameMode mode, Vehicle aircraft, IEntity pilot, vector destination) { if (!GetGame() || !GetGame().GetWorld() || !FinitePoint(destination)) return null; BaseWorld world = GetGame().GetWorld(); float began = world.GetWorldTime(); // Reuse complete native model/rotor measurement, exact vanilla airframe, // original occupied pilot seat and grounded qualification. Never synthesize it. ERR_AircraftFlightEnvelope point = ERR_AircraftFlightEnvelope.Qualify(mode, aircraft, pilot); if (!point) return null; vector start = ERR_Actors.Ground(point.Home()); vector end = ERR_Actors.Ground(destination); if (!FinitePoint(start) || !FinitePoint(end) || !ERR_AircraftFieldRules.Length(vector.DistanceXZ(start, end)) || !(end[1] >= 2) || !(Math.AbsFloat(end[1] - start[1]) <= 0.3)) return null; array excluded = {}; if (!Hierarchy(aircraft, excluded) || !Hierarchy(pilot, excluded)) return null; if (!ClearColumn(aircraft, pilot, end) || !Survey(start, end, excluded)) return null; if (!GetGame() || GetGame().GetWorld() != world || !mode || mode != ERR_GameMode.Get() || !mode.Active()) return null; float now = world.GetWorldTime(); if (!(now >= began && now - began <= 2000) || !aircraft || !pilot || aircraft.GetWorld() != world || pilot.GetWorld() != world || vector.Distance(aircraft.GetOrigin(), point.Home()) > 0.1) return null; ERR_AircraftFieldEnvelope record = new ERR_AircraftFieldEnvelope(); record.m_Mode = mode; record.m_Aircraft = aircraft; record.m_Pilot = pilot; record.m_Home = point.Home(); record.m_Radius = point.Radius(); record.m_Destination = end; record.m_Destination[1] = record.m_Home[1]; // Survey work consumes the original admission lifetime; it cannot renew it. record.m_QualifiedAt = began; return record; } }