// Opt-in terrain adapter: all positions come from this world's native geometry. // The original Everon authored/fallback contract remains in ERR_GeneratedSites. class ERR_TerrainSites : ERR_GeneratedSites { protected float m_Spacing; protected bool m_HaveCandidate, m_WaitingNav; protected vector m_Candidate; protected float m_CandidateHeading; protected int m_CandidateBegan; protected ref ERR_ReconAirports m_Airports = new ERR_ReconAirports(); protected bool m_AirportsReady; protected int m_GeometryRejected, m_NavigationRejected, m_PadRejected; static bool Bounds(vector low, vector high) { float width = high[0] - low[0], depth = high[2] - low[2]; return width >= 2000 && depth >= 2000 && width <= 100000 && depth <= 100000; } static float Spacing(vector low, vector high) { return Math.Clamp(Math.Min(high[0] - low[0], high[2] - low[2]) * 0.06, 150, 600); } override bool Begin(ERR_ScenarioDefinition definition) { if (m_Started || !Replication.IsServer() || !definition || !definition.Valid() || !GetGame().GetWorldEntity()) return false; GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High); if (!Bounds(m_Low, m_High)) { m_Error = "Terrain bounds must span 2-100 km on each axis."; return false; } AIWorld world = GetGame().GetAIWorld(); if (!world || !world.GetNavmeshWorldComponent("Soldiers")) { m_Error = "Terrain requires its own Soldiers navigation mesh."; return false; } m_Started = true; m_Definition = definition; definition.TerrainGenerated = true; m_Spacing = Spacing(m_Low, m_High); // Resolve the terrain's airport first, then keep the prison in escape range. array order = {ERR_LocationKind.EXTRACTION, ERR_LocationKind.PRISON, ERR_LocationKind.RADIO, ERR_LocationKind.DEPOT, ERR_LocationKind.HOSPITAL, ERR_LocationKind.MORTAR, ERR_LocationKind.OUTPOST}; foreach (int kind : order) foreach (ERR_SiteDefinition site : definition.Sites) { if (site.Kind != kind) continue; site.PlacementResolved = false; // The queue, rather than RandomClear, owns placement including extraction. site.RandomClear = false; if (kind == ERR_LocationKind.EXTRACTION) { site.RequireAirfield = true; site.AircraftOffset = vector.Zero; } if (kind == ERR_LocationKind.DEPOT) site.Label = "Supply cache"; m_Pending.Insert(site); } m_Terrain.Collect(m_Low, m_High); m_Began = System.GetTickCount(); PrintFormat("Reintroduced [terrain]: bounds=%1..%2 spacing=%3 sites=%4; native terrain/navmesh placement, no authored fallback", m_Low, m_High, m_Spacing, m_Pending.Count()); return true; } protected override bool Separated(ERR_SiteDefinition candidate, vector position, bool fallback) { foreach (ERR_SiteDefinition other : m_Definition.Sites) { if (other == candidate || !other.PlacementResolved) continue; float minimum = m_Spacing; if (candidate.Kind == ERR_LocationKind.RADIO && other.Kind == ERR_LocationKind.PRISON) minimum = m_Spacing * 2; if (vector.DistanceXZ(position, other.Position) < minimum) return false; // Every required destination must be inside the prison's escape boundary. if (other.Kind == ERR_LocationKind.PRISON && vector.DistanceXZ(position, other.Position) + 200 >= ERR_GameMode.Get().EscapeRadius()) return false; if (candidate.Kind == ERR_LocationKind.PRISON && other.Kind == ERR_LocationKind.EXTRACTION && vector.DistanceXZ(position, other.Position) + 200 >= ERR_GameMode.Get().EscapeRadius()) return false; } return true; } protected bool Navigable(vector position) { NavmeshWorldComponent mesh = GetGame().GetAIWorld().GetNavmeshWorldComponent("Soldiers"); if (!mesh) return false; if (!mesh.IsTileLoaded(position)) { m_WaitingNav = mesh.LoadTileIn(position); return false; } vector reachable; return mesh.GetReachablePoint(position, 10, reachable); } protected bool Survey(ERR_SiteDefinition site, vector position, float heading, out float floor) { if (!m_Terrain.Site(site, position, heading, floor)) { m_GeometryRejected++; return false; } vector ground = Vector(position[0], floor, position[2]); if (!Navigable(ground)) { m_NavigationRejected++; return false; } if (site.Guarded && !Navigable(ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(site.GuardOffset, heading)))) { m_NavigationRejected++; return false; } if (site.ResponseCount > 0 && !Navigable(ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(site.ResponseOffset, heading)))) { m_NavigationRejected++; return false; } if (site.Kind != ERR_LocationKind.EXTRACTION) return true; float padFloor; if (m_Terrain.Clear(ground, heading, 14, 0.8, padFloor) && ERR_AirfieldPlacement.Clear(ground)) return true; m_PadRejected++; return false; } protected vector SamplePosition(ERR_SiteDefinition site) { if (site.Kind != ERR_LocationKind.EXTRACTION) return Vector(Math.RandomFloat(m_Low[0] + 60, m_High[0] - 60), 0, Math.RandomFloat(m_Low[2] + 60, m_High[2] - 60)); vector centre = m_Airports.m_Positions[Math.RandomInt(0, m_Airports.m_Positions.Count())]; float angle = Math.RandomFloat(0, Math.PI2), radius = Math.RandomFloat(0, 450); return centre + Vector(Math.Cos(angle) * radius, 0, Math.Sin(angle) * radius); } protected bool PrepareAirports() { if (m_AirportsReady) return true; m_Airports.Update(); if (!m_Airports.Complete()) return false; if (m_Airports.m_Positions.IsEmpty()) { m_Error = "No airfield found on this terrain. For terrain takeover, configure m_sERR_Airfields with airport X Z coordinates. Authored scenarios can use ERR_Airfield_0 through _15 markers."; m_Result = ERR_PrepareResult.FAILED; return false; } m_AirportsReady = true; m_Began = System.GetTickCount(); return true; } override ERR_PrepareResult Tick() { if (m_Result != ERR_PrepareResult.PENDING) return m_Result; if (!m_Started || !Replication.IsServer()) return ERR_PrepareResult.FAILED; if (!PrepareAirports()) return m_Result; int tick = System.GetTickCount(); while (m_Index < m_Pending.Count()) { ERR_SiteDefinition site = m_Pending[m_Index]; if (m_Attempts >= 12000 || System.GetTickCount() - m_Began >= 30000) { PrintFormat("Reintroduced [terrain survey exhausted]: id=%1 attempts=%2 rejected geometry=%3 navigation=%4 aircraft=%5", site.Id, m_Attempts, m_GeometryRejected, m_NavigationRejected, m_PadRejected); if (!SkipOptional(site)) { m_Error = "Terrain has no safe navigable placement for " + site.Label + ". Check this map's Soldiers navmesh, terrain clearance and size; no foreign-map fallback was used."; m_Result = ERR_PrepareResult.FAILED; return m_Result; } m_Index++; m_Attempts = 0; m_Began = System.GetTickCount(); m_HaveCandidate = false; m_GeometryRejected = 0; m_NavigationRejected = 0; m_PadRejected = 0; } else { if (!m_HaveCandidate) { m_Candidate = SamplePosition(site); m_CandidateHeading = Math.RandomFloat(0, 360); m_CandidateBegan = System.GetTickCount(); m_HaveCandidate = true; m_Attempts++; } vector position = m_Candidate; float heading = m_CandidateHeading, floor; m_WaitingNav = false; if (Separated(site, position, false) && Survey(site, position, heading, floor)) { position[1] = floor + 0.02; site.Position = position; site.Heading = heading; site.PlacementResolved = true; PrintFormat("Reintroduced [terrain site]: id=%1 position=%2 heading=%3 attempts=%4", site.Id, position, heading, m_Attempts); m_Index++; m_Attempts = 0; m_Began = System.GetTickCount(); m_GeometryRejected = 0; m_NavigationRejected = 0; m_PadRejected = 0; } // Give this tile time to stream before testing a different random point. // A missing/stalled tile must never consume the whole preparation budget. else if (m_WaitingNav && System.GetTickCount() - m_CandidateBegan < 5000) return ERR_PrepareResult.PENDING; m_HaveCandidate = false; } if (System.GetTickCount() - tick >= 8) return ERR_PrepareResult.PENDING; } if (!m_Definition.Valid()) { m_Error = "Generated terrain sites do not preserve required objectives and independent intel sources."; m_Result = ERR_PrepareResult.FAILED; return m_Result; } m_Result = ERR_PrepareResult.READY; return m_Result; } }