diff --git a/CHANGELOG.md b/CHANGELOG.md index 0409912..d8ea779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_Actors.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_Actors.c index 251753c..6343205 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_Actors.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_Actors.c @@ -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 players, bool conscious = false) { players.Clear(); diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_AirfieldPlacement.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_AirfieldPlacement.c index af69779..0a61007 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_AirfieldPlacement.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_AirfieldPlacement.c @@ -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 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(); diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_GeneratedSites.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_GeneratedSites.c index e60413b..afacf13 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_GeneratedSites.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_GeneratedSites.c @@ -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; diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c index d96bf29..2761c06 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c @@ -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; diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnPlacement.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnPlacement.c index 6d44c59..3e92552 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnPlacement.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnPlacement.c @@ -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; diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainSites.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainSites.c index b7c4fc2..6722053 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainSites.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainSites.c @@ -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; } diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_MortarSystem.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_MortarSystem.c index 2438489..be554ae 100644 --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_MortarSystem.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_MortarSystem.c @@ -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) diff --git a/docs/evidence/extraction-survey-2026-10-25/README.md b/docs/evidence/extraction-survey-2026-10-25/README.md new file mode 100644 index 0000000..c59c18e --- /dev/null +++ b/docs/evidence/extraction-survey-2026-10-25/README.md @@ -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. diff --git a/docs/evidence/extraction-survey-2026-10-25/cleanup-final-followup.json b/docs/evidence/extraction-survey-2026-10-25/cleanup-final-followup.json new file mode 100644 index 0000000..57c340c --- /dev/null +++ b/docs/evidence/extraction-survey-2026-10-25/cleanup-final-followup.json @@ -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" +} diff --git a/docs/evidence/extraction-survey-2026-10-25/cleanup-followup.json b/docs/evidence/extraction-survey-2026-10-25/cleanup-followup.json new file mode 100644 index 0000000..b9a63d6 --- /dev/null +++ b/docs/evidence/extraction-survey-2026-10-25/cleanup-followup.json @@ -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" +} diff --git a/docs/evidence/extraction-survey-2026-10-25/compile-and-platform.log b/docs/evidence/extraction-survey-2026-10-25/compile-and-platform.log new file mode 100644 index 0000000..1656265 --- /dev/null +++ b/docs/evidence/extraction-survey-2026-10-25/compile-and-platform.log @@ -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. diff --git a/docs/evidence/extraction-survey-2026-10-25/source-sha256.json b/docs/evidence/extraction-survey-2026-10-25/source-sha256.json new file mode 100644 index 0000000..2d434bf --- /dev/null +++ b/docs/evidence/extraction-survey-2026-10-25/source-sha256.json @@ -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" +} diff --git a/docs/terrain-takeover.md b/docs/terrain-takeover.md index 09e840d..0311c5c 100644 --- a/docs/terrain-takeover.md +++ b/docs/terrain-takeover.md @@ -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 diff --git a/tests/test_crash_start.py b/tests/test_crash_start.py index 7fa4728..4dcbe41 100644 --- a/tests/test_crash_start.py +++ b/tests/test_crash_start.py @@ -114,12 +114,22 @@ run(r''' #include #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::vectorm_Airfields;std::vectorcalls;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::vectorm_Airfields;std::vectorcalls;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') diff --git a/tests/test_placement_diagnostics.py b/tests/test_placement_diagnostics.py index 0eb1df7..6666768 100644 --- a/tests/test_placement_diagnostics.py +++ b/tests/test_placement_diagnostics.py @@ -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 #include +#include +#include #include #include 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]::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') diff --git a/tests/test_rough_intel_feedback.py b/tests/test_rough_intel_feedback.py index c28f410..b92bf7c 100644 --- a/tests/test_rough_intel_feedback.py +++ b/tests/test_rough_intel_feedback.py @@ -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")); diff --git a/tests/test_terrain_sites.py b/tests/test_terrain_sites.py index 4357f64..78edbc3 100644 --- a/tests/test_terrain_sites.py +++ b/tests/test_terrain_sites.py @@ -156,12 +156,12 @@ namespace Math { float Min(float a,float b){return ahi?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;}} templatestruct Array:std::vector{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')