Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
155 lines
8.0 KiB
Plaintext
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;
|
|
}
|
|
}
|