// 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<int> 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;
 }
}
