Improve small-terrain placement and show startup failure causes; record Workshop 0.0.120
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
class ERR_CampTerrain
|
||||
{
|
||||
protected ref array<vector> m_Airfields = {};
|
||||
protected string m_Rejection;
|
||||
string Rejection() { return m_Rejection; }
|
||||
protected bool Reject(string reason) { m_Rejection = reason; return false; }
|
||||
protected bool Airport(IEntity entity)
|
||||
{
|
||||
if (!entity || !entity.GetPrefabData()) return false;
|
||||
@@ -27,12 +30,13 @@ class ERR_CampTerrain
|
||||
}
|
||||
bool Clear(vector center, float heading, float halfSize, float maximumRange, out float lowest, float clearanceHeight = 4)
|
||||
{
|
||||
m_Rejection = "";
|
||||
lowest = float.MAX; float highest = -float.MAX;
|
||||
for (int x = -1; x <= 1; x++)
|
||||
for (int z = -1; z <= 1; z++)
|
||||
{
|
||||
vector corner = center + Rotate(Vector(x * halfSize, 0, z * halfSize), heading);
|
||||
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), corner)) return false;
|
||||
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), corner)) return Reject("water");
|
||||
}
|
||||
// A 2m grid covers the walls, door approach, furniture and all interior slots.
|
||||
for (float dx = -halfSize; dx <= halfSize; dx += 2)
|
||||
@@ -41,7 +45,8 @@ class ERR_CampTerrain
|
||||
vector sample = center + Rotate(Vector(dx, 0, dz), heading);
|
||||
float height = GetGame().GetWorld().GetSurfaceY(sample[0], sample[2]);
|
||||
lowest = Math.Min(lowest, height); highest = Math.Max(highest, height);
|
||||
if (lowest < 1 || highest - lowest > maximumRange) return false;
|
||||
if (lowest < 1) return Reject("ground below minimum elevation");
|
||||
if (highest - lowest > maximumRange) return Reject("uneven ground");
|
||||
}
|
||||
for (float cx = -halfSize + 1; cx < halfSize; cx += 2)
|
||||
for (float cz = -halfSize + 1; cz < halfSize; cz += 2)
|
||||
@@ -53,33 +58,34 @@ class ERR_CampTerrain
|
||||
// Bounding the rotated cell conservatively also protects the exit corridor.
|
||||
trace.Mins = "-1.42 0 -1.42"; trace.Maxs = "1.42 0.1 1.42";
|
||||
trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||
if (GetGame().GetWorld().TraceMove(trace, null) < 1) return false;
|
||||
if (GetGame().GetWorld().TraceMove(trace, null) < 1) return Reject("obstructed footprint");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool Site(ERR_SiteDefinition site, vector position, float heading, out float baseHeight)
|
||||
bool Site(ERR_SiteDefinition site, vector position, float heading, out float baseHeight, float airportBuffer = 1500)
|
||||
{
|
||||
m_Rejection = "";
|
||||
bool prison = site.Kind == ERR_LocationKind.PRISON;
|
||||
if (prison)
|
||||
{
|
||||
foreach (vector airport : m_Airfields) if (vector.DistanceXZ(position, airport) < 1500) return false;
|
||||
foreach (vector airport : m_Airfields) if (vector.DistanceXZ(position, airport) < airportBuffer) return Reject("airport separation");
|
||||
}
|
||||
SCR_AIWorld aiWorld = SCR_AIWorld.Cast(GetGame().GetAIWorld());
|
||||
if (!aiWorld || !aiWorld.GetRoadNetworkManager()) return false;
|
||||
if (!aiWorld || !aiWorld.GetRoadNetworkManager()) return Reject("road network unavailable");
|
||||
BaseRoad road; float distance;
|
||||
aiWorld.GetRoadNetworkManager().GetClosestRoad(position, road, distance, skipNavlinks: true);
|
||||
if (road && distance < 12) return false;
|
||||
if (road && distance < 12) return Reject("road proximity");
|
||||
if (!Clear(position, heading, 10, 0.6, baseHeight)) return false;
|
||||
if (ERR_StartSetup.IsCrash(site))
|
||||
{
|
||||
vector offset, center; float halfSize, height;
|
||||
if (!ERR_StartSetup.Geometry(offset, center, halfSize, height)) return false;
|
||||
if (!ERR_StartSetup.Geometry(offset, center, halfSize, height)) return Reject("crash footprint unavailable");
|
||||
float wreckFloor;
|
||||
if (!Clear(position + Rotate(center, heading), heading, halfSize, 0.6, wreckFloor, height)) return false;
|
||||
if (!Clear(position + Rotate(center, heading), heading, halfSize, 0.6, wreckFloor, height)) return Reject("wreck area: " + m_Rejection);
|
||||
}
|
||||
float guardHeight;
|
||||
if (site.Guarded && !Clear(position + Rotate(site.GuardOffset, heading), heading, 6, 0.6, guardHeight)) return false;
|
||||
if (site.ResponseCount > 0 && !Clear(position + Rotate(site.ResponseOffset, heading), heading, 6, 0.6, guardHeight)) return false;
|
||||
if (site.Guarded && !Clear(position + Rotate(site.GuardOffset, heading), heading, 6, 0.6, guardHeight)) return Reject("guard area: " + m_Rejection);
|
||||
if (site.ResponseCount > 0 && !Clear(position + Rotate(site.ResponseOffset, heading), heading, 6, 0.6, guardHeight)) return Reject("patrol area: " + m_Rejection);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -90,6 +96,38 @@ class ERR_GeneratedSites
|
||||
protected ref array<ERR_SiteDefinition> m_Pending = {};
|
||||
protected vector m_Low, m_High;
|
||||
protected int m_Index, m_Attempts, m_Began;
|
||||
protected int m_NextDiagnostic;
|
||||
protected ref map<string, int> m_Rejections = new map<string, int>();
|
||||
protected void RecordRejection(string reason)
|
||||
{
|
||||
if (reason.IsEmpty()) reason = "unspecified terrain clearance";
|
||||
int count;
|
||||
m_Rejections.Find(reason, count);
|
||||
m_Rejections.Set(reason, count + 1);
|
||||
}
|
||||
protected string RejectionSummary()
|
||||
{
|
||||
string summary;
|
||||
foreach (string reason, int count : m_Rejections)
|
||||
{
|
||||
if (!summary.IsEmpty()) summary += "; ";
|
||||
summary += reason + "=" + count.ToString();
|
||||
}
|
||||
if (summary.IsEmpty()) return "none recorded";
|
||||
return summary;
|
||||
}
|
||||
protected void ReportSearch(ERR_SiteDefinition site, string stage)
|
||||
{
|
||||
PrintFormat("Reintroduced [placement search]: stage=%1 site=%2 (%3) attempts=%4 elapsedMs=%5 failed/deferred checks: %6", stage, site.Id, site.Label, m_Attempts, System.GetTickCount() - m_Began, RejectionSummary());
|
||||
}
|
||||
protected void ReportProgress(ERR_SiteDefinition site)
|
||||
{
|
||||
int now = System.GetTickCount();
|
||||
if (now - m_Began < 30000 || now < m_NextDiagnostic) return;
|
||||
ReportSearch(site, "still searching");
|
||||
m_NextDiagnostic = now + 30000;
|
||||
}
|
||||
protected void ClearDiagnostics() { m_Rejections.Clear(); m_NextDiagnostic = 0; }
|
||||
protected bool m_Started;
|
||||
protected ERR_PrepareResult m_Result = ERR_PrepareResult.PENDING;
|
||||
protected string m_Error;
|
||||
@@ -180,6 +218,7 @@ class ERR_GeneratedSites
|
||||
{
|
||||
if (m_StartupOnly && StartupReady()) return ERR_PrepareResult.PENDING;
|
||||
ERR_SiteDefinition site = m_Pending[m_Index];
|
||||
ReportProgress(site);
|
||||
bool fallback = m_Attempts >= 12000 || System.GetTickCount() - m_Began >= 30000;
|
||||
vector position = site.Position; float heading = site.Heading;
|
||||
if (!fallback)
|
||||
@@ -188,22 +227,30 @@ class ERR_GeneratedSites
|
||||
heading = Math.RandomFloat(0, 360); m_Attempts++;
|
||||
}
|
||||
float floor;
|
||||
if (Separated(site, position, fallback) && m_Terrain.Site(site, position, heading, floor) && RescueFits(site, position, heading))
|
||||
bool fits;
|
||||
if (!Separated(site, position, fallback)) RecordRejection("site spacing / escape boundary");
|
||||
else if (!m_Terrain.Site(site, position, heading, floor)) RecordRejection(m_Terrain.Rejection());
|
||||
else if (!RescueFits(site, position, heading)) RecordRejection("secondary aircraft pad");
|
||||
else fits = true;
|
||||
if (fits)
|
||||
{
|
||||
position[1] = floor + 0.02;
|
||||
site.Position = position; site.Heading = heading; site.PlacementResolved = true;
|
||||
PrintFormat("Co10 Escape Reintroduced [generated site]: id=%1 position=%2 heading=%3 attempts=%4 fallback=%5", site.Id, position, heading, m_Attempts, fallback);
|
||||
ReportSearch(site, "placed"); ClearDiagnostics();
|
||||
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount();
|
||||
}
|
||||
else if (fallback)
|
||||
{
|
||||
ReportSearch(site, "exhausted; authored fallback rejected");
|
||||
if (SkipOptional(site))
|
||||
{
|
||||
ClearDiagnostics();
|
||||
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Error = "No safe terrain or valid authored fallback for required site " + site.Id;
|
||||
m_Error = "No safe terrain or valid authored fallback for required site " + site.Id + ". Checks: " + RejectionSummary();
|
||||
m_Result = ERR_PrepareResult.FAILED; return m_Result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
modded class ERR_CampTerrain
|
||||
{
|
||||
static bool s_ERROptionalSiteRefused;
|
||||
override bool Site(ERR_SiteDefinition site, vector position, float heading, out float baseHeight)
|
||||
override bool Site(ERR_SiteDefinition site, vector position, float heading, out float baseHeight, float airportBuffer = 1500)
|
||||
{
|
||||
string outcome;
|
||||
if ((site.Id == "everon-camp-5" || site.Id == "everon-outpost-1") && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "optional-site-skip")
|
||||
{
|
||||
s_ERROptionalSiteRefused = true; return false;
|
||||
}
|
||||
return super.Site(site, position, heading, baseHeight);
|
||||
return super.Site(site, position, heading, baseHeight, airportBuffer);
|
||||
}
|
||||
}
|
||||
modded class ERR_GameMode
|
||||
|
||||
@@ -35,7 +35,7 @@ class ERR_ObjectiveText
|
||||
ERR_Phase phase = mode.Phase();
|
||||
if (phase == ERR_Phase.PREPARING)
|
||||
{
|
||||
if (!mode.PreparationError().IsEmpty()) return "The mission could not start. An administrator can use /esc status for the cause. Spawning is disabled until the configuration is corrected and the scenario restarted.";
|
||||
if (!mode.PreparationError().IsEmpty()) return "Reason: " + mode.PreparationError() + "\n\nAn administrator must correct the problem and restart the scenario before players can spawn.";
|
||||
return "Preparing mission sites and equipment. You will spawn when the mission is ready.";
|
||||
}
|
||||
if (phase == ERR_Phase.PRISON)
|
||||
|
||||
@@ -10,10 +10,20 @@ class ERR_TerrainSites : ERR_GeneratedSites
|
||||
protected ref ERR_ReconAirports m_Airports = new ERR_ReconAirports();
|
||||
protected bool m_AirportsReady;
|
||||
protected int m_GeometryRejected, m_NavigationRejected, m_PadRejected;
|
||||
protected string m_NavigationReason;
|
||||
// Keep the same work slice; difficult terrain gets more slices, not larger stalls.
|
||||
static int SearchAttempts() { return 48000; }
|
||||
static int SearchMilliseconds() { return 120000; }
|
||||
static float PrisonAirportBuffer(vector low, vector high)
|
||||
{
|
||||
// The old 1.5km tactical separation could exclude an entire small island.
|
||||
// Physical ground, scenery and navigation checks still apply independently.
|
||||
return Math.Clamp(Math.Min(high[0] - low[0], high[2] - low[2]) * 0.15, 200, 1500);
|
||||
}
|
||||
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;
|
||||
return width >= 1000 && depth >= 1000 && width <= 100000 && depth <= 100000;
|
||||
}
|
||||
static float Spacing(vector low, vector high)
|
||||
{
|
||||
@@ -23,7 +33,7 @@ class ERR_TerrainSites : ERR_GeneratedSites
|
||||
{
|
||||
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; }
|
||||
if (!Bounds(m_Low, m_High)) { m_Error = "Terrain bounds must span 1-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;
|
||||
@@ -62,19 +72,28 @@ class ERR_TerrainSites : ERR_GeneratedSites
|
||||
}
|
||||
protected bool Navigable(vector position)
|
||||
{
|
||||
m_NavigationReason = "";
|
||||
NavmeshWorldComponent mesh = GetGame().GetAIWorld().GetNavmeshWorldComponent("Soldiers");
|
||||
if (!mesh) return false;
|
||||
if (!mesh.IsTileLoaded(position)) { m_WaitingNav = mesh.LoadTileIn(position); return false; }
|
||||
if (!mesh) { m_NavigationReason = "Soldiers navmesh unavailable"; return false; }
|
||||
if (!mesh.IsTileLoaded(position))
|
||||
{
|
||||
m_WaitingNav = mesh.LoadTileIn(position);
|
||||
m_NavigationReason = "navmesh tile unavailable";
|
||||
if (m_WaitingNav) m_NavigationReason = "waiting for navmesh tile";
|
||||
return false;
|
||||
}
|
||||
vector reachable;
|
||||
return mesh.GetReachablePoint(position, 10, reachable);
|
||||
if (mesh.GetReachablePoint(position, 10, reachable)) return true;
|
||||
m_NavigationReason = "no reachable AI ground";
|
||||
return false;
|
||||
}
|
||||
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; }
|
||||
if (!m_Terrain.Site(site, position, heading, floor, PrisonAirportBuffer(m_Low, m_High))) { m_GeometryRejected++; RecordRejection(m_Terrain.Rejection()); 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 (!Navigable(ground)) { m_NavigationRejected++; RecordRejection(m_NavigationReason); return false; }
|
||||
if (site.Guarded && !Navigable(ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(site.GuardOffset, heading)))) { m_NavigationRejected++; RecordRejection("guard area: " + m_NavigationReason); return false; }
|
||||
if (site.ResponseCount > 0 && !Navigable(ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(site.ResponseOffset, heading)))) { m_NavigationRejected++; RecordRejection("patrol area: " + m_NavigationReason); return false; }
|
||||
if (site.Kind != ERR_LocationKind.EXTRACTION) return true;
|
||||
float padFloor;
|
||||
if (m_Terrain.Clear(ground, heading, 14, 0.8, padFloor))
|
||||
@@ -82,18 +101,34 @@ class ERR_TerrainSites : ERR_GeneratedSites
|
||||
bool primaryClear;
|
||||
if (site.RequireAirfield) primaryClear = ERR_AirfieldPlacement.Clear(ground);
|
||||
else primaryClear = ERR_AirfieldPlacement.ClearGround(ground);
|
||||
if (primaryClear && ERR_RescueLanding.Fits(ground)) return true;
|
||||
if (!primaryClear) RecordRejection("primary aircraft clearance");
|
||||
else if (!ERR_RescueLanding.Fits(ground)) RecordRejection("secondary aircraft pad");
|
||||
else return true;
|
||||
}
|
||||
else RecordRejection("landing zone: " + m_Terrain.Rejection());
|
||||
m_PadRejected++; return false;
|
||||
}
|
||||
protected vector SamplePosition(ERR_SiteDefinition site)
|
||||
{
|
||||
if (site.Kind != ERR_LocationKind.EXTRACTION || m_Airports.m_Positions.IsEmpty())
|
||||
if (site.Kind != ERR_LocationKind.EXTRACTION || !site.RequireAirfield || m_Airports.m_Positions.IsEmpty())
|
||||
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 void ExpandExtractionSearch(ERR_SiteDefinition site)
|
||||
{
|
||||
if (site.Kind != ERR_LocationKind.EXTRACTION || !site.RequireAirfield) return;
|
||||
if (m_Attempts < 12000 && System.GetTickCount() - m_Began < 30000) return;
|
||||
// An airport is a preferred starting area, not proof that a usable pad exists.
|
||||
// Use the remaining budget to find a clear helicopter landing zone elsewhere.
|
||||
// Do not reset the total deadline or carry an airport candidate into this pass.
|
||||
site.RequireAirfield = false;
|
||||
site.Label = "Extraction landing zone";
|
||||
m_HaveCandidate = false;
|
||||
ReportSearch(site, "airport search exhausted; expanding search");
|
||||
PrintFormat("Reintroduced [terrain]: %1 airport search exhausted; surveying off-airport landing zones with full aircraft clearance", site.Id);
|
||||
}
|
||||
protected bool PrepareAirports()
|
||||
{
|
||||
if (m_AirportsReady) return true;
|
||||
@@ -135,16 +170,20 @@ class ERR_TerrainSites : ERR_GeneratedSites
|
||||
{
|
||||
if (m_StartupOnly && StartupReady()) return ERR_PrepareResult.PENDING;
|
||||
ERR_SiteDefinition site = m_Pending[m_Index];
|
||||
if (m_Attempts >= 12000 || System.GetTickCount() - m_Began >= 30000)
|
||||
ReportProgress(site);
|
||||
ExpandExtractionSearch(site);
|
||||
if (m_Attempts >= SearchAttempts() || System.GetTickCount() - m_Began >= SearchMilliseconds())
|
||||
{
|
||||
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);
|
||||
ReportSearch(site, "exhausted");
|
||||
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_Error = "Terrain has no safe navigable placement for " + site.Label + " (" + site.Id + "). Checks: " + RejectionSummary();
|
||||
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;
|
||||
ClearDiagnostics();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -157,11 +196,14 @@ class ERR_TerrainSites : ERR_GeneratedSites
|
||||
vector position = m_Candidate;
|
||||
float heading = m_CandidateHeading, floor;
|
||||
m_WaitingNav = false;
|
||||
if (Separated(site, position, false) && Survey(site, position, heading, floor))
|
||||
bool separated = Separated(site, position, false);
|
||||
if (!separated) RecordRejection("site spacing / escape boundary");
|
||||
if (separated && 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);
|
||||
ReportSearch(site, "placed"); ClearDiagnostics();
|
||||
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount();
|
||||
m_GeometryRejected = 0; m_NavigationRejected = 0; m_PadRejected = 0;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ FrameWidgetClass {
|
||||
OffsetTop 0
|
||||
SizeX 900
|
||||
OffsetRight -900
|
||||
SizeY 240
|
||||
OffsetBottom -240
|
||||
SizeY 520
|
||||
OffsetBottom -520
|
||||
Alignment 0.5 0.5
|
||||
}
|
||||
"Z Order" 10
|
||||
|
||||
Reference in New Issue
Block a user