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
+7 -1
View File
@@ -1,6 +1,12 @@
# Changelog
Latest published Workshop version: **0.0.120**
Latest published Workshop version: **0.0.121**
## 2026-10-25 — Small-terrain startup
- Fixed low-lying dry terrain being rejected during site placement, player spawning and mortar setup; water checks now use the actual ground surface.
- Helicopter landing sites can use clear ground near roads and use the landing-pad flatness limit instead of the stricter camp limit.
- Fixed aircraft clearance checks using the wrong ground height during extraction placement.
## 2026-10-24 — Placement on smaller terrains
@@ -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)
@@ -0,0 +1,72 @@
# Small-terrain survey corrections — 25 October 2026
The reported failure contains 48,000 failed checks, including 5,796 road
proximity refusals and seven primary-aircraft-clearance refusals. No navigation
failure is listed. It identifies exhaustion of the extraction survey, not proof
that the terrain has no usable helicopter landing area.
A second screenshot failed on the prison with 34,381 checks rejected by the
hard-coded minimum elevation. The water query used the candidate's placeholder
height rather than the sampled terrain height. Native API documentation says
`TryGetWaterSurface` tests whether the supplied point is submerged; the simplified
query takes the same world/point inputs.
The site grid now checks finite terrain height and water at each actual sampled
surface point, accepting dry low or negative terrain. The shared `DryGround`
helper samples without the 10 cm spawn lift, so shallow water stays excluded.
Site creation, character spawn footprints, primary/secondary aircraft pads and
mortar setup use this check instead of fixed world-height cutoffs.
The common site survey applied the camp's 12 m road exclusion and 0.6 m flatness
limit before the extraction-specific 0.8 m survey. Extraction now permits clear
roadside/paved ground and uses the landing survey's flatness limit. Camps and
guard footprints keep their existing constraints. The aircraft checks now use
centre terrain height, matching `ERR_SiteService` pickup registration, instead
of the camp grid's lowest sampled height.
Passed focused production-body tests:
```
python3 tests/test_crash_start.py
python3 tests/test_generated_sites.py
python3 tests/test_terrain_sites.py
python3 tests/test_rescue_landing.py
python3 tests/test_terrain_airfields.py
python3 tests/test_placement_diagnostics.py
```
These cover camp/extraction road separation, a 0.7 m height-range candidate
accepted only for extraction, 0.9 m refusal, blocked pad/guard refusal, centre
height passed to aircraft checks, preserved camp floor, navigation and fleet
refusals, finite scheduling and diagnostics. Added tests execute the production
dry-ground helper and clearance grid for dry heights 0.5, 0 and -25 m, submerged
terrain at positive and negative elevations, 5 cm shallow water, a water patch
between the former nine sample points, non-finite terrain height, slope and
physical obstruction. Startup consumers are checked for the shared gate.
The full repository check run passed through the terrain/placement coverage,
then stopped on a stale objective-copy assertion expecting the old generic
startup message. That assertion now checks the actual reason and absence of a
chat-command instruction. Rerunning that check and the remaining 88 checks
completed successfully. No production source changed during this continuation.
Both isolated native attempts compiled the Game module with no script errors, then
failed Steam platform initialization before loading the world. The retained
log excerpt is compilation evidence only; **no native fixture assertions or
map-specific startup success are claimed**. Full attempted-run inputs and logs
are at `/tmp/erri-small-terrain-placement-run-Oct25A1` and
`/tmp/erri-small-terrain-placement-run-Oct25B1` (final source bytes).
The first runner timed out with one orphaned Wine device helper. Follow-up cleanup
verified its exact private prefix, process identity and launch time, stopped
only that helper, and confirmed the prefix idle; see `cleanup-followup.json`.
The final compile attempt required the same verified cleanup; its idle check
also passed (`cleanup-final-followup.json`).
Reproduce the native attempt with an available Steam session and an unused suffix:
```
python3 docs/evidence/small-terrain-placement-2026-10-24/isolated-runner.py Oct25Next
```
The screenshots came from another player; the reported terrain is unidentified. These corrections remove
verified mismatches; they do not guarantee every terrain fits the required fleet.
@@ -0,0 +1,6 @@
{
"initial_runner_result": "cleanup-failed",
"followup_idle_verified": true,
"signalled_process": 1703783,
"proof": "exact owned prefix, winedevice identity, same UID and launch-time window; pidfd-scoped termination"
}
@@ -0,0 +1,6 @@
{
"initial_runner_result": "cleanup-failed",
"followup_idle_verified": true,
"signalled_process": 1629980,
"proof": "exact owned prefix, winedevice identity, same UID and launch-time window; pidfd-scoped SIGKILL after SIGTERM did not stop the helper"
}
@@ -0,0 +1,4 @@
13:41:03.022 SCRIPT : Module: Game; loaded 6786x files; 14050x classes; used 43576K of static memory; defines: "ENF_WB,WORKBENCH,PLATFORM_WINDOWS,ENABLE_DIAG,ENF_ENABLE_VHC_OVERRIDE,ENF_ENABLE_RPLCOMPONENT_IN_GAMECODE,ENABLE_BASE_DESTRUCTION,BUDGET_OPTIMIZATION_CHECKS,SHOW_DISCLAIMER,DEBUG,RESPAWN_TIMER_COMPONENT_DEBUG,GAME_MODE_DEBUG,DEBUG_CONFIGURABLE_DIALOGS,DISABLE_WEAPON_SWITCHING,SCRIPTED_AIM_MODIFIER_DEBUG,WCS_DEFINES_ARMAMENTS,WCS_DEFINES_SPACECORE"; CRC32: 0ac67b79
13:41:03.084 PROFILING : Compiling Game scripts took: 2574.635400 ms
13:41:03.147 PLATFORM (E): Steamworks: SteamAPI_Init failed. Is Steam running?
13:41:03.147 PLATFORM (E): Could not initialize platform services.
@@ -0,0 +1,9 @@
{
"addons/core/Scripts/Game/ERR/00_Core/ERR_Actors.c": "7258b838f8365e4fa575c4f2ccc8532e22a33946405ebb490120534d5ca65607",
"addons/core/Scripts/Game/ERR/00_Core/ERR_AirfieldPlacement.c": "0601d8da570e1f63e43ea7df451d0e997872e4e163669fd8ad37bedd59de7b9b",
"addons/core/Scripts/Game/ERR/00_Core/ERR_GeneratedSites.c": "45bfeec63720fa1019f54d0e2ac21f3bbc40616d0693d94a94e0980dcae5744b",
"addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c": "db95d3caac9bd658a633ac7a69ebe6f1d0c093673c79cc6ad9b122360d773a16",
"addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnPlacement.c": "fc5b61f1877aeaee75fbfd6aed3cd0b4c5277ae8543bc38a0c09836353d54b27",
"addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainSites.c": "109f8ae55f67a79343cae05f103f6ee74679c63db6081c2b73a423bc70b0c3ae",
"addons/core/Scripts/Game/ERR/10_AI/ERR_MortarSystem.c": "01bae0bab685e9f0c86c3a8939d18ce6f20dc0b8c1a389d179d91e93008c6957"
}
+16
View File
@@ -131,6 +131,22 @@ without resetting that total budget. The fallback surveys the terrain for a
helicopter landing zone and retains the normal primary/secondary aircraft
clearance and navigation checks.
Extraction pads do not use the camp-only 12 m road exclusion, and their initial
ground survey uses the same 0.8 m height variation limit as the landing-zone
survey. Camp footprints retain their 0.6 m limit and road exclusion. Aircraft
clearance is tested at the centre's actual terrain height, matching pickup
registration, rather than the lowest point sampled across the camp grid.
Water, physical obstacles, guard/patrol ground and navigation, the 28 m landing
footprint, overhead clearance and additional transport-pad checks still apply.
Dry ground is determined from the terrain's actual water surface, not a fixed
world-altitude minimum. Site clearance queries water at every sampled terrain
height instead of testing placeholder candidate heights. Dry terrain below
one metre (including negative map elevations) can qualify; submerged terrain,
invalid heights, excessive slopes and physical obstructions remain rejected.
Site creation, character spawn slots, airfield pads and mortar setup use the
same terrain-height water check, without the 10 cm character spawn lift.
On terrain-generated maps, prison-to-airport separation is 15% of the shorter
world axis, clamped to 200–1,500 m (300 m on a 2 km map). This changes tactical
spacing, not the physical footprint or slope allowance. Original authored Everon
+15 -5
View File
@@ -114,12 +114,22 @@ run(r'''
#include <vector>
#define null nullptr
struct vector{float x=0,y=0,z=0;};vector operator+(vector a,vector b){return{a.x+b.x,a.y+b.y,a.z+b.z};}float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
namespace Kind{constexpr int PRISON=0;}
namespace Kind{constexpr int PRISON=0,EXTRACTION=2;}
struct SiteData{int Kind=0,ResponseCount=0;bool Guarded=false,crash=false;vector GuardOffset,ResponseOffset;};
namespace StartSetup{bool measured=true;bool IsCrash(SiteData*s){return s->crash;}bool Geometry(vector&offset,vector&center,float&half,float&height){offset={16,1,0};center={17,0,0};half=6;height=6;return measured;}}
struct Road{};struct RoadManager{void GetClosestRoad(vector,Road*&,float&,bool){}};struct AIWorld{RoadManager roads;RoadManager*GetRoadNetworkManager(){return &roads;}};
struct Call{vector p;float half,height;};
struct Survey{std::string m_Rejection;bool Reject(std::string reason){m_Rejection=reason;return false;}AIWorld world;std::vector<vector>m_Airfields;std::vector<Call>calls;int refuse=0;vector Rotate(vector p,float h){assert(h==90);return{p.z,p.y,-p.x};}bool Clear(vector p,float,float half,float,float&floor,float height=4){calls.push_back({p,half,height});floor=42;return int(calls.size())!=refuse;}SITE};
int main(){SiteData a;float floor=0;Survey airport;airport.m_Airfields.push_back({600,0,0});assert(!airport.Site(&a,{0,0,0},90,floor)&&airport.calls.empty());assert(airport.Site(&a,{0,0,0},90,floor,300)&&airport.calls.size()==1);Survey normal;assert(normal.Site(&a,{100,0,200},90,floor)&&normal.calls.size()==1&&normal.calls[0].half==10&&normal.calls[0].height==4);a.crash=true;Survey crash;assert(crash.Site(&a,{100,0,200},90,floor)&&crash.calls.size()==2);assert(crash.calls[1].p.x==100&&crash.calls[1].p.z==183&&crash.calls[1].half==6&&crash.calls[1].height==6);for(int refusal:{1,2}){Survey blocked;blocked.refuse=refusal;assert(!blocked.Site(&a,{100,0,200},90,floor));}StartSetup::measured=false;Survey unknown;assert(!unknown.Site(&a,{100,0,200},90,floor)&&unknown.calls.size()==1);}
struct Road{};struct RoadManager{Road road;bool near=false;int queries=0;void GetClosestRoad(vector,Road*&r,float&distance,bool){queries++;r=near?&road:nullptr;distance=0;}};struct AIWorld{RoadManager roads;RoadManager*GetRoadNetworkManager(){return &roads;}};
struct Call{vector p;float half,height,range;};
struct Survey{std::string m_Rejection;bool Reject(std::string reason){m_Rejection=reason;return false;}AIWorld world;std::vector<vector>m_Airfields;std::vector<Call>calls;int refuse=0;float variation=0;vector Rotate(vector p,float h){assert(h==90);return{p.z,p.y,-p.x};}bool Clear(vector p,float,float half,float range,float&floor,float height=4){calls.push_back({p,half,height,range});floor=42;return int(calls.size())!=refuse&&variation<=range;}SITE};
int main(){
SiteData camp,pad;pad.Kind=Kind::EXTRACTION;float h;
Survey roadside;roadside.world.roads.near=true;
assert(!roadside.Site(&camp,{0,0,0},90,h)&&roadside.m_Rejection=="road proximity");
assert(roadside.Site(&pad,{0,0,0},90,h)&&roadside.world.roads.queries==1);
assert(roadside.calls.size()==1&&std::abs(roadside.calls[0].range-.8f)<.001);
Survey gentle;gentle.variation=.7f;assert(!gentle.Site(&camp,{0,0,0},90,h)&&gentle.Site(&pad,{0,0,0},90,h));gentle.variation=.9f;assert(!gentle.Site(&pad,{0,0,0},90,h));
Survey blockedPad;blockedPad.refuse=1;assert(!blockedPad.Site(&pad,{0,0,0},90,h));
pad.Guarded=true;Survey blockedGuards;blockedGuards.refuse=2;assert(!blockedGuards.Site(&pad,{0,0,0},90,h));
Survey guards;assert(guards.Site(&pad,{0,0,0},90,h)&&guards.calls.size()==2&&std::abs(guards.calls[1].range-.6f)<.001);
SiteData a;float floor=0;Survey airport;airport.m_Airfields.push_back({600,0,0});assert(!airport.Site(&a,{0,0,0},90,floor)&&airport.calls.empty());assert(airport.Site(&a,{0,0,0},90,floor,300)&&airport.calls.size()==1);Survey normal;assert(normal.Site(&a,{100,0,200},90,floor)&&normal.calls.size()==1&&normal.calls[0].half==10&&normal.calls[0].height==4);a.crash=true;Survey crash;assert(crash.Site(&a,{100,0,200},90,floor)&&crash.calls.size()==2);assert(crash.calls[1].p.x==100&&crash.calls[1].p.z==183&&crash.calls[1].half==6&&crash.calls[1].height==6);for(int refusal:{1,2}){Survey blocked;blocked.refuse=refusal;assert(!blocked.Site(&a,{100,0,200},90,floor));}StartSetup::measured=false;Survey unknown;assert(!unknown.Site(&a,{100,0,200},90,floor)&&unknown.calls.size()==1);}
'''.replace('SITE',site))
print('PASS: actual GameMode readiness admission and common full-footprint survey require crash authorization and both surveyed areas')
+29 -6
View File
@@ -31,28 +31,42 @@ int main(){
d.m_Began=Clock::now;d.ReportProgress(&site);assert(reports.size()==3);d.RecordRejection("obstructed footprint");assert(d.RejectionSummary()=="obstructed footprint=1");
}
''')
# Execute the actual water, elevation, flatness and physical-trace short circuits.
# Execute actual terrain-height water queries, low dry land and physical checks.
clear=body(p,'protected bool Reject(')+'\n'+body(p,'bool Clear(')
clear=clear.replace('protected ', '').replace('out float lowest','float& lowest').replace('float.MAX','FLT_MAX').replace('Math.','Math::').replace('TraceBox trace','TraceBox* trace').replace('trace.','trace->').replace('TraceFlags.','TraceFlags::').replace('null','nullptr').replace('ChimeraWorldUtils.','ChimeraWorldUtils::')
clear=clear.replace('protected ', '').replace('out float lowest','float& lowest').replace('float.MAX','FLT_MAX').replace('Math.','Math::').replace('TraceBox trace','TraceBox* trace').replace('trace.','trace->').replace('TraceFlags.','TraceFlags::').replace('null','nullptr').replace('ChimeraWorldUtils.','ChimeraWorldUtils::').replace('ERR_ForceRules.','ERR_ForceRules::')
dry=body(p.parent/'ERR_Actors.c','static bool DryGround(').replace('ERR_ForceRules.','ERR_ForceRules::').replace('ChimeraWorldUtils.','ChimeraWorldUtils::')
for a,b in [('"-1.42 0 -1.42"','Vector(-1.42,0,-1.42)'),('"1.42 0.1 1.42"','Vector(1.42,.1,1.42)')]:clear=clear.replace(a,b)
run(r'''
#include <cassert>
#include <cfloat>
#include <cmath>
#include <limits>
#include <string>
#include <algorithm>
using string=std::string;
struct vector{float p[3];float operator[](int i){return p[i];}};vector Vector(float x,float y,float z){return {{x,y,z}};}vector operator+(vector a,vector b){return Vector(a[0]+b[0],a[1]+b[1],a[2]+b[2]);}
struct vector{float p[3];float& operator[](int i){return p[i];}};vector Vector(float x,float y,float z){return {{x,y,z}};}vector operator+(vector a,vector b){return Vector(a[0]+b[0],a[1]+b[1],a[2]+b[2]);}
namespace Math{float Min(float a,float b){return std::min(a,b);}float Max(float a,float b){return std::max(a,b);}}
namespace TraceFlags{int ENTS=1,WORLD=2;}struct TraceBox{vector Start,End,Mins,Maxs;int Flags;};
struct World{bool water=false,blocked=false,slope=false;float height=42;int traces=0;float GetSurfaceY(float x,float){return height+(slope?x:0);}float TraceMove(TraceBox*,void*){traces++;return blocked?0:1;}};
struct World{bool water=false,puddle=false,blocked=false,slope=false;float height=42,waterLevel=43;int traces=0;float GetSurfaceY(float x,float){return height+(slope?x:0);}float TraceMove(TraceBox*,void*){traces++;return blocked?0:1;}};
struct Game{World world;World&GetWorld(){return world;}}game;Game&GetGame(){return game;}
namespace ChimeraWorldUtils{bool TryGetWaterSurfaceSimple(World&w,vector){return w.water;}}
namespace ERR_ForceRules{bool Finite(float n){return std::isfinite(n);}}
namespace ChimeraWorldUtils{bool TryGetWaterSurfaceSimple(World&w,vector p){return (w.water||(w.puddle&&p[0]==2&&p[2]==2))&&p[1]<w.waterLevel;}}
namespace ERR_Actors{'''+dry+r'''}
struct Terrain{string m_Rejection;vector Rotate(vector p,float){return p;}
'''+clear+r'''
};
int main(){Terrain t;float floor;auto check=[&](){return t.Clear(Vector(0,0,0),0,10,.6,floor);};
game.world.water=true;assert(!check()&&t.m_Rejection=="water"&&game.world.traces==0);
game.world.water=false;game.world.height=.5;assert(!check()&&t.m_Rejection=="ground below minimum elevation");
// Placeholder Y=0 must not make high dry land look submerged.
game.world.waterLevel=0;assert(check()&&t.m_Rejection.empty());
game.world.water=false;
for(float h:{.5f,0.f,-25.f}){game.world.height=h;assert(check()&&floor==h&&ERR_Actors::DryGround(Vector(0,999,0)));}
// A terrain-relative ocean/lake still rejects both high and low submerged ground.
for(float h:{42.f,-25.f,.5f}){game.world.height=h;game.world.waterLevel=h+.05f;game.world.water=true;assert(!check()&&t.m_Rejection=="water"&&!ERR_Actors::DryGround(Vector(0,999,0)));}
game.world.water=false;game.world.height=42;game.world.waterLevel=43;game.world.puddle=true;
assert(!check()&&t.m_Rejection=="water");game.world.puddle=false; // Water between the old nine sample points.
game.world.height=std::numeric_limits<float>::quiet_NaN();assert(!check()&&t.m_Rejection=="invalid terrain height"&&!ERR_Actors::DryGround(Vector(0,0,0)));
game.world.traces=0;
game.world.height=42;game.world.slope=true;assert(!check()&&t.m_Rejection=="uneven ground"&&game.world.traces==0);
game.world.slope=false;game.world.blocked=true;assert(!check()&&t.m_Rejection=="obstructed footprint"&&game.world.traces==1);
game.world.blocked=false;assert(check()&&t.m_Rejection.empty()&&floor==42);
@@ -65,3 +79,12 @@ for filename in ('ERR_GeneratedSites.c','ERR_TerrainSites.c'):
assert 'ReportSearch(site, "placed"); ClearDiagnostics();' in source
assert 'Checks: ' in source and 'RejectionSummary();' in source
print('PASS: actual terrain refusal reasons, counted summaries, site/attempt/time context, 30-second rate limit and per-site reset')
# Every downstream startup gate uses actual dry terrain rather than map altitude.
for filename in ('ERR_SiteService.c','ERR_SpawnPlacement.c','ERR_AirfieldPlacement.c'):
source=(p.parent/filename).read_text()
assert 'ERR_Actors.DryGround(' in source
assert '[1] < 1' not in source and '[1] < 2' not in source
source=(p.parent.parent/'10_AI/ERR_MortarSystem.c').read_text()
assert 'ERR_Actors.DryGround(position)' in source and 'position[1] < 1' not in source
print('PASS: actual low/negative dry terrain, height-correct shallow/lake/ocean/interior water refusal and downstream startup dry-ground gates')
+2 -1
View File
@@ -79,7 +79,8 @@ int main(){
assert(Actual::Coordinate(6501.4f,2)==6501);
assert(Actual::PhaseDescription(nullptr,true).empty());
mode.phase=ERR_Phase::PREPARING;assert(has(Actual::PhaseDescription(&mode,true),"Preparing mission"));
mode.error="bad";assert(has(Actual::PhaseDescription(&mode,true),"could not start"));
mode.error="bad";auto failure=Actual::PhaseDescription(&mode,true);
assert(has(failure,"Reason: bad")&&has(failure,"restart the scenario")&&!has(failure,"/esc"));
mode.phase=ERR_Phase::PRISON;assert(has(Actual::PhaseDescription(&mode,true),"Pick the gate lock"));
mode.phase=ERR_Phase::RADIO_DEFENCE;assert(has(Actual::PhaseDescription(&mode,true),"interrupted"));
mode.occupied=true;assert(has(Actual::PhaseDescription(&mode,true),"in progress"));
+5 -5
View File
@@ -156,12 +156,12 @@ namespace Math {
float Min(float a,float b){return a<b?a:b;}float Clamp(float v,float lo,float hi){return v<lo?lo:(v>hi?hi:v);}
float Cos(float a){return std::cos(a);}float Sin(float a){return std::sin(a);}
}
namespace ERR_Actors {vector Ground(vector p){return p;}}
namespace ERR_Actors {vector Ground(vector p){p[1]=42.5;return p;}}
namespace ERR_CampTerrain {vector Rotate(vector p,float){return p;}}
namespace ERR_AirfieldPlacement {
bool airfield=false,ground=true;int airportChecks=0,groundChecks=0;
bool Clear(vector){airportChecks++;return airfield;}
bool ClearGround(vector){groundChecks++;return ground;}
bool Clear(vector p){assert(p[1]==42.5);airportChecks++;return airfield;}
bool ClearGround(vector p){assert(p[1]==42.5);groundChecks++;return ground;}
}
namespace ERR_RescueLanding {bool fits=true;int checks=0;bool Fits(vector){checks++;return fits;}}
template<class T>struct Array:std::vector<T>{bool IsEmpty(){return this->empty();}int Count(){return this->size();}};
@@ -196,7 +196,7 @@ int main(){
site.Kind=Kind::PRISON;p=s.SamplePosition(&site);assert(p[0]==5000&&p[2]==-1000&&Math::indexed==1);
site.Kind=Kind::EXTRACTION;site.RequireAirfield=false;
p=s.SamplePosition(&site);assert(p[0]==5000&&p[2]==-1000&&Math::indexed==1);
assert(s.Survey(&site,p,0,floor)&&floor==42);assert(ERR_AirfieldPlacement::groundChecks==1&&ERR_AirfieldPlacement::airportChecks==0&&s.m_Terrain.airportBuffer==1200);
assert(s.Survey(&site,p,0,floor)&&floor==42.5);assert(ERR_AirfieldPlacement::groundChecks==1&&ERR_AirfieldPlacement::airportChecks==0&&s.m_Terrain.airportBuffer==1200);
ERR_AirfieldPlacement::ground=false;assert(!s.Survey(&site,p,0,floor)&&s.m_PadRejected==1);
site.RequireAirfield=true;ERR_AirfieldPlacement::ground=true;
assert(!s.Survey(&site,p,0,floor));assert(ERR_AirfieldPlacement::airportChecks==1&&ERR_AirfieldPlacement::groundChecks==2);
@@ -210,7 +210,7 @@ int main(){
int rejected=s.m_PadRejected;assert(!s.Survey(&site,p,0,floor)&&s.m_PadRejected==rejected+1);
ERR_RescueLanding::fits=true;assert(s.Survey(&site,p,0,floor));
site.Kind=Kind::PRISON;ERR_RescueLanding::fits=false;int fleetChecks=ERR_RescueLanding::checks;
assert(s.Survey(&site,p,0,floor)&&ERR_RescueLanding::checks==fleetChecks);
assert(s.Survey(&site,p,0,floor)&&floor==42&&ERR_RescueLanding::checks==fleetChecks);
}
''')
print('PASS: real sampling handles absent airports and extraction clearance preserves geometry/navigation/flatness while selecting the correct aircraft survey')