Fix low-terrain startup and extraction placement; record Workshop 0.0.121
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
+7
-1
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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¢er,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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user