Files
EscapistsReintroduced/docs/evidence/terrain-takeover-feature-2026-10-01/client-experiment-21/ERR_TerrainSites.c.txt
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

155 lines
8.0 KiB
Plaintext

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