Fix low-terrain startup and extraction placement; record Workshop 0.0.121
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-10-25 14:01:49 +01:00
parent 3768ce7ed8
commit 6397fe89b6
18 changed files with 211 additions and 38 deletions
@@ -78,6 +78,15 @@ class ERR_Actors
return position;
}
// Map altitude is not water depth. Query the terrain surface itself, without
// Ground's 10cm spawn lift, so even shallow water remains unsuitable.
static bool DryGround(vector position)
{
position[1] = GetGame().GetWorld().GetSurfaceY(position[0], position[2]);
if (!ERR_ForceRules.Finite(position[1])) return false;
return !ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), position);
}
static void Players(out array<IEntity> players, bool conscious = false)
{
players.Clear();
@@ -36,7 +36,7 @@ class ERR_AirfieldPlacement
}
static bool Clear(vector position)
{
if (!Replication.IsServer() || position[1] < 2 || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), position)) return false;
if (!Replication.IsServer() || !ERR_Actors.DryGround(position)) return false;
ERR_AirfieldPlacement survey = new ERR_AirfieldPlacement();
array<vector> authored = {};
AuthoredAirports(authored);
@@ -49,12 +49,12 @@ class ERR_AirfieldPlacement
// Same aircraft footprint/vertical sweep, without requiring airport scenery.
static bool ClearGround(vector position)
{
if (!Replication.IsServer() || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), position)) return false;
if (!Replication.IsServer() || !ERR_Actors.DryGround(position)) return false;
for (int edge = 0; edge < 4; edge++)
{
float side = edge * Math.PI / 2;
vector sample = position + Vector(Math.Cos(side) * 14, 0, Math.Sin(side) * 14);
if (Math.AbsFloat(SCR_TerrainHelper.GetTerrainY(sample) - position[1]) > 0.8) return false;
if (!ERR_Actors.DryGround(sample) || Math.AbsFloat(SCR_TerrainHelper.GetTerrainY(sample) - position[1]) > 0.8) return false;
}
if (!SCR_WorldTools.TraceCilinderUtil(position + "0 2.5 0", 40, 4, TraceFlags.ENTS)) return false;
TraceBox clearance = new TraceBox();
@@ -32,20 +32,16 @@ class ERR_CampTerrain
{
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 Reject("water");
}
// A 2m grid covers the walls, door approach, furniture and all interior slots.
for (float dx = -halfSize; dx <= halfSize; dx += 2)
for (float dz = -halfSize; dz <= halfSize; dz += 2)
{
vector sample = center + Rotate(Vector(dx, 0, dz), heading);
float height = GetGame().GetWorld().GetSurfaceY(sample[0], sample[2]);
if (!ERR_ForceRules.Finite(height)) return Reject("invalid terrain height");
sample[1] = height;
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), sample)) return Reject("water");
lowest = Math.Min(lowest, height); highest = Math.Max(highest, height);
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)
@@ -70,12 +66,20 @@ class ERR_CampTerrain
{
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 Reject("road network unavailable");
BaseRoad road; float distance;
aiWorld.GetRoadNetworkManager().GetClosestRoad(position, road, distance, skipNavlinks: true);
if (road && distance < 12) return Reject("road proximity");
if (!Clear(position, heading, 10, 0.6, baseHeight)) return false;
bool extraction = site.Kind == ERR_LocationKind.EXTRACTION;
// Camps must stay off roads; a helicopter pad may use clear paved ground.
// Its physical terrain, obstacle and fleet checks still decide acceptance.
if (!extraction)
{
SCR_AIWorld aiWorld = SCR_AIWorld.Cast(GetGame().GetAIWorld());
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 Reject("road proximity");
}
float maximumRange = 0.6;
if (extraction) maximumRange = 0.8;
if (!Clear(position, heading, 10, maximumRange, baseHeight)) return false;
if (ERR_StartSetup.IsCrash(site))
{
vector offset, center; float halfSize, height;
@@ -389,7 +389,7 @@ class ERR_SiteService
if (only && spec != only) continue;
if (startupOnly && !ERR_GeneratedSites.StartupSite(spec)) continue;
vector position = ERR_Actors.Ground(spec.Position);
if (position[1] < 1) return Fail("site below sea level: " + spec.Id);
if (!ERR_Actors.DryGround(position)) return Fail("site ground is submerged or invalid: " + spec.Id);
vector rootPosition = position;
if (spec.PlacementResolved && spec.Kind == ERR_LocationKind.PRISON) rootPosition = spec.Position;
rootPosition[1] = rootPosition[1] + spec.RootHeight;
@@ -29,13 +29,13 @@ class ERR_SpawnPlacement
foreach (int id, vector reserved : reservations)
if (id != ownReservation && vector.DistanceXZ(position, reserved) < 1.2 && Math.AbsFloat(position[1] - reserved[1]) < 2) return false;
vector ground = ERR_Actors.Ground(position);
if (ground[1] < 1 || Math.AbsFloat(position[1] - ground[1]) > 0.3) return false;
if (!ERR_Actors.DryGround(ground) || Math.AbsFloat(position[1] - ground[1]) > 0.3) return false;
// Feet need supported, reasonably level terrain across the character width.
for (int x = -1; x <= 1; x++)
for (int z = -1; z <= 1; z++)
{
vector sample = ERR_Actors.Ground(position + Vector(x * 0.45, 0, z * 0.45));
if (Math.AbsFloat(sample[1] - ground[1]) > 0.3) return false;
if (!ERR_Actors.DryGround(sample) || Math.AbsFloat(sample[1] - ground[1]) > 0.3) return false;
}
m_Exclude = exclude;
m_Candidate = ground; m_Blocked = false;
@@ -91,6 +91,13 @@ class ERR_TerrainSites : ERR_GeneratedSites
{
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]);
// Aircraft spawn at the centre's terrain height, not the camp grid's minimum.
// Using the minimum here falsely steepens the uphill edge of a valid pad.
if (site.Kind == ERR_LocationKind.EXTRACTION)
{
ground = ERR_Actors.Ground(position);
floor = ground[1];
}
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; }
@@ -56,7 +56,7 @@ class ERR_MortarSystem
protected bool CreateBattery(vector authoredPosition, int id)
{
vector position = ERR_Actors.Ground(authoredPosition);
if (position[1] < 1) { PrintFormat("Reintroduced [mortars]: below land at %1", position); return false; }
if (!ERR_Actors.DryGround(position)) { PrintFormat("Reintroduced [mortars]: submerged or invalid ground at %1", position); return false; }
IEntity gun = ERR_Actors.Spawn(ERR_MortarBattery.GUN, position);
SCR_AIGroup group = SCR_AIGroup.Cast(ERR_Actors.Spawn(MortarGroup(), ERR_Actors.Ground(position + "0 0 4")));
if (!gun || !group)