Files
EscapistsReintroduced/docs/evidence/automatic-terrain-takeover-2026-10-02/changes.patch
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

549 lines
33 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
--- before/README.md
+++ after/README.md
@@ -42,14 +42,14 @@
Earlier five-addon build reports below are historical evidence, not validation of
the merged project. Nothing has been published to Workshop.
-An experimental [server-owner terrain setup](docs/terrain-takeover.md) takes over
-an existing island scenario without terrain-author changes. Local server/client
-manager and restart checks pass; authenticated spawning and published server-config
-validation are still required before retiring connector deployments.
-Terrain authors can also include the [universal Escape connector](docs/terrain-adapters.md)
-in a scenario inside their terrain addon. It supplies the shared game mode and
-managers; the terrain supplies navigation and any airport hints. A ready
-[Nogova integration](docs/nogova-publishing.md) is provided.
+[Automatic terrain takeover](docs/terrain-takeover.md) starts Escape when an admin
+loads Escape alongside a terrain and selects that terrain's existing scenario.
+No connector, custom startup flag or airport coordinates are required. The map
+must provide usable navigation and safe placement space. Published authenticated
+multiplayer validation remains outstanding; see the linked test scope.
+Terrain authors can optionally use the [shared connector prefab](docs/terrain-adapters.md)
+for an authored Escape scenario; [Nogova setup](docs/nogova-publishing.md) describes
+both routes.
See the [player route playtest guide](docs/player-route.md), [player lifecycle](docs/player-lifecycle.md), [manual gameplay gaps](docs/base-gameplay-gaps.md), [release qualification](docs/release-qualification.md), [performance comparison tooling](docs/performance-comparison.md), [configuration and commands](docs/configuration.md), [site authoring](docs/site-authoring.md), [architecture](docs/architecture.md) and [migration priorities](docs/base-replacement.md).
--- before/addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c
+++ after/addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c
@@ -376,6 +376,7 @@
{
vector aircraftPosition = ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(spec.AircraftOffset, spec.Heading));
if (spec.RequireAirfield && !ERR_AirfieldPlacement.Clear(aircraftPosition)) return Fail("extraction airfield pad is missing or obstructed; no off-airfield fallback");
+ if (definition.TerrainGenerated && !spec.RequireAirfield && !ERR_AirfieldPlacement.ClearGround(aircraftPosition)) return Fail("generated extraction landing zone is obstructed");
Vehicle helicopter = Vehicle.Cast(Spawn(m_Mode.Factions().ExtractionPrefab, aircraftPosition));
if (!helicopter) return Fail("extraction vehicle");
if (!m_Mode.SetExtractionVehicle(helicopter)) return Fail("extraction vehicle registration");
--- before/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainSites.c
+++ after/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainSites.c
@@ -1,4 +1,4 @@
-// Opt-in terrain adapter: all positions come from this world's native geometry.
+// Automatic terrain placement: all positions come from this world's native geometry.
// The original Everon authored/fallback contract remains in ERR_GeneratedSites.
class ERR_TerrainSites : ERR_GeneratedSites
{
@@ -77,12 +77,16 @@
if (site.ResponseCount > 0 && !Navigable(ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(site.ResponseOffset, heading)))) { m_NavigationRejected++; return false; }
if (site.Kind != ERR_LocationKind.EXTRACTION) return true;
float padFloor;
- if (m_Terrain.Clear(ground, heading, 14, 0.8, padFloor) && ERR_AirfieldPlacement.Clear(ground)) return true;
+ if (m_Terrain.Clear(ground, heading, 14, 0.8, padFloor))
+ {
+ if (site.RequireAirfield && ERR_AirfieldPlacement.Clear(ground)) return true;
+ if (!site.RequireAirfield && ERR_AirfieldPlacement.ClearGround(ground)) return true;
+ }
m_PadRejected++; return false;
}
protected vector SamplePosition(ERR_SiteDefinition site)
{
- if (site.Kind != ERR_LocationKind.EXTRACTION)
+ if (site.Kind != ERR_LocationKind.EXTRACTION || m_Airports.m_Positions.IsEmpty())
return Vector(Math.RandomFloat(m_Low[0] + 60, m_High[0] - 60), 0, Math.RandomFloat(m_Low[2] + 60, m_High[2] - 60));
vector centre = m_Airports.m_Positions[Math.RandomInt(0, m_Airports.m_Positions.Count())];
float angle = Math.RandomFloat(0, Math.PI2), radius = Math.RandomFloat(0, 450);
@@ -95,9 +99,16 @@
if (!m_Airports.Complete()) return false;
if (m_Airports.m_Positions.IsEmpty())
{
- m_Error = "No airfield found on this terrain. For terrain takeover, configure m_sERR_Airfields with airport X Z coordinates. Authored scenarios can use ERR_Airfield_0 through _15 markers.";
- m_Result = ERR_PrepareResult.FAILED;
- return false;
+ // A painted runway or an island without an airport must not require a
+ // connector or administrator coordinates. Survey a helicopter landing zone
+ // with the same ground/navmesh/aircraft checks and finite placement budget.
+ foreach (ERR_SiteDefinition site : m_Pending)
+ if (site.Kind == ERR_LocationKind.EXTRACTION)
+ {
+ site.RequireAirfield = false;
+ site.Label = "Extraction landing zone";
+ }
+ Print("Reintroduced [terrain]: no airport infrastructure found; surveying a clear helicopter extraction landing zone");
}
m_AirportsReady = true; m_Began = System.GetTickCount();
return true;
--- before/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeover.c
+++ after/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeover.c
@@ -1,5 +1,5 @@
-// Experimental opt-in for an existing island scenario. Native multiplayer
-// replication must be qualified before recommending this route on live servers.
+// Loading Escape starts it on the selected island scenario automatically.
+// Existing authored Escape modes and editor worlds are never replaced.
enum ERR_TerrainTakeoverStage
{
INACTIVE,
@@ -180,6 +180,9 @@
m_ERRTerrainOriginal = SCR_BaseGameMode.Cast(GetGameMode());
if (ERR_PreflightTerrainTakeover())
{
+ // Escape owns a fresh round; do not save the replaced scenario's state.
+ SaveGameManager saves = GetSaveGameManager();
+ if (saves) saves.SetSavingAllowed(false);
m_ERRTerrainStage = ERR_TerrainTakeoverStage.SETTLING;
PrintFormat("Reintroduced [terrain takeover]: enabled; configured airports=%1", m_ERRTerrainAirfields.Count());
}
--- before/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverSettings.c
+++ after/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverSettings.c
@@ -2,7 +2,7 @@
// added to Escape, and no world resource is changed by these settings.
modded class SCR_MissionHeader
{
- [Attribute("0", desc: "Experimental: replace this scenario's game mode with Escape. Use a plain terrain exploration scenario.")]
+ [Attribute("1", desc: "Automatically start Escape on the selected terrain scenario. Set false to keep its original game mode.")]
bool m_bERR_EnableEscape;
[Attribute("", desc: "Optional airport centres: X Z pairs separated by semicolons, for example 1525 5075. Up to 16 centres.")]
string m_sERR_Airfields;
@@ -49,19 +49,19 @@
static bool Enabled()
{
if (!GetGame() || SCR_Global.IsEditMode()) return false;
+ // Loading Escape is the opt-in. Explicit CLI values override the header,
+ // including 0 so a host can temporarily keep the original scenario.
+ string enabled;
+ if (System.GetCLIParam("errTerrainTakeover", enabled)) return enabled == "1";
SCR_MissionHeader header = SCR_MissionHeader.Cast(GetGame().GetMissionHeader());
- if (header && header.m_bERR_EnableEscape) return true;
- // Also support owner-controlled CLI launches (including local-mod servers
- // and Workbench play, which may not apply mission-header JSON overrides).
- string enabled;
- return System.GetCLIParam("errTerrainTakeover", enabled) && enabled == "1";
+ return !header || header.m_bERR_EnableEscape;
}
static string Airfields()
{
+ string centres;
+ if (System.GetCLIParam("errAirfields", centres)) return centres;
SCR_MissionHeader header = SCR_MissionHeader.Cast(GetGame().GetMissionHeader());
- if (header && header.m_bERR_EnableEscape) return header.m_sERR_Airfields;
- string centres;
- System.GetCLIParam("errAirfields", centres);
- return centres;
+ if (header) return header.m_sERR_Airfields;
+ return "";
}
}
--- before/docs/nogova-publishing.md
+++ after/docs/nogova-publishing.md
@@ -1,14 +1,25 @@
# Publish Escape on Nogova
-The instructions below describe the existing authored integration inside **Nogova
-Reforger**, referencing the shared prefab in **Co10 Escape Reintroduced**.
-The requested replacement is [server-owner terrain takeover](terrain-takeover.md),
-which needs no Nogova edits or Escape dependency in the terrain. That replacement
-has passed local server/client manager and connected-restart checks. It remains
-experimental pending authenticated player spawning and published server-config
-validation. For that route, publish the updated **Co10 Escape Reintroduced** core;
-no new Nogova connector or Escape dependency in Nogova is needed. Do not remove a working
-separate connector until the deployment has moved off its mission header.
+## Server-owner setup (no connector)
+
+Publish the updated **Co10 Escape Reintroduced** core. Server owners add Escape
+and **Nogova Reforger** to their mods and select Nogova's exploration scenario:
+
+```text
+{469880DFE1B48F95}Missions/Nogova_TerrainTest.conf
+```
+
+Escape automatically takes over. There are no required custom header fields,
+AMP settings, airport coordinates or changes to Nogova. When no physical airport
+is detected it surveys a clear helicopter landing zone. See
+[automatic terrain takeover](terrain-takeover.md) for requirements and test scope.
+Do not remove a connector while the server still selects its `ERR_nogova.conf`
+header; switch to the existing Nogova scenario first.
+
+## Optional authored integration
+
+The remaining instructions describe the older authored integration inside Nogova.
+Use these only when deliberately providing a separate Escape scenario.
1. Publish the updated **Co10 Escape Reintroduced** core containing
`Prefabs/ERR/TerrainConnector.et` and the terrain-independent runtime. The
@@ -38,7 +49,9 @@
"scenarioId": "{681921DC9921F915}Missions/ERR_nogova.conf"
```
-Do not use `Worlds/Nogova/Nogova.ent` or the exploration scenario for Escape.
+For this authored route, use `ERR_nogova.conf`. The automatic route above uses
+Nogova’s exploration scenario. Neither route accepts `Worlds/Nogova/Nogova.ent`
+as a mission header.
The scenario subscene references the shared Escape connector and supplies a mainland
airport-centre marker at `[1525, 0, 5075]`. The aircraft is placed only after actual
terrain, navigation, clearance and physical-settling checks.
--- before/docs/terrain-adapters.md
+++ after/docs/terrain-adapters.md
@@ -1,9 +1,9 @@
# Universal terrain connector
-For the requested server-owner-only workflow, see the new
-[experimental terrain takeover](terrain-takeover.md). It uses an island's
-existing scenario and requires no terrain-author changes. Keep the connector
-workflow below for current deployments until multiplayer takeover is qualified.
+Server owners normally use [automatic terrain takeover](terrain-takeover.md):
+load Escape and the terrain, select the terrain's existing scenario, and start.
+No connector or terrain-author changes are required. The authored connector below
+remains an optional way to provide a dedicated Escape scenario.
The reusable connector ships in **Co10 Escape Reintroduced**:
@@ -49,9 +49,10 @@
edit the terrain project or its dependency list. The ready Nogova export is in
`terrain-integrations/nogova/`; see [Nogova publishing](nogova-publishing.md).
-Escape now has an opt-in terrain-independent placement path. It uses the loaded
+Escape has a terrain-independent placement path. It uses the loaded
world's bounds and navigation to place the prison, radio, caches and finite enemy
-sites. Extraction is surveyed on an airfield. The existing Everon scenario keeps
+sites. Extraction is surveyed on an airfield, or on a clear helicopter landing zone
+when no airport is detected. The existing Everon scenario keeps
its authored extraction and fallback rules.
Reforger still needs a scenario subscene and mission header on each terrain.
--- before/docs/terrain-takeover.md
+++ after/docs/terrain-takeover.md
@@ -1,77 +1,81 @@
-# Server-owner terrain setup — experimental
+# Automatic Escape on added terrains
-The opt-in takeover implementation is now in Escape's core source. It loads the
-scenario selected by the server owner, stops its original game mode, waits for
-the old managers to unregister, and starts Escape on that same world. Escape
-does not depend on Nogova or any other island. The island's source, navigation,
-dependencies and published scenarios are not modified.
+Add **Co10 Escape Reintroduced** and the desired terrain to the server's mods,
+select an existing scenario supplied by that terrain, and start the server.
+Escape automatically replaces the scenario's game mode and creates its prison,
+objectives, enemy sites and extraction on the loaded terrain. No AMP-specific
+settings, enable flag, custom mission-header fields, connector addon or
+terrain-author changes are required.
-**Do not replace a working connector deployment yet.** Local dedicated-server
-joining now passes the client manager checks. Authenticated player spawning and
-published Workshop/config delivery still need validation. This is development
-functionality, disabled by default; it does not qualify arbitrary scenario types.
-
-## Server-owner startup options
-
-The owner can enable takeover through these server startup arguments:
+For Nogova, select its existing exploration scenario:
```text
--errTerrainTakeover 1 -errAirfields "1525 5075"
+{469880DFE1B48F95}Missions/Nogova_TerrainTest.conf
```
-Use the airport coordinates appropriate to the selected island. Omit
-`-errAirfields` for automatic airport discovery. Only the exact enable value `1`
-activates the CLI option. This route has started Escape on Nogova in the real
-Linux dedicated-server engine with a separate Windows client, using a private
-local-mod launch. The engine's local-mod launch uses `-server`
-and `-addons`; it rejects combining `-addons` with `-config`. See Bohemia's
-[startup parameter reference](https://community.bistudio.com/wiki/Arma_Reforger:Startup_Parameters).
+In a standard server configuration this is the value of `game.scenarioId`.
+Select a mission header (`Missions/*.conf`), not the terrain's world resource.
+Installing multiple terrains does not choose one: the selected scenario still
+controls which world the engine loads. Escape has no dependency on any custom
+terrain. Publish the updated Escape core once; server owners then update it
+alongside their chosen terrain. Workshop 0.0.45 predates this automatic default.
-## Intended mission-header configuration
+Before publishing the updated core, reopen/rescan the project in Workbench so
+`Prefabs/ERR/TerrainTakeover.et` and its metadata enter the resource index. An old
+cache can include the script but still fail to register the replicated prefab.
+This is a publishing check, not a server-admin setup requirement.
-Keep Escape and the desired terrain in the server's mod list. Select a plain
-exploration scenario already supplied by the terrain. Merge the following fields
-into the existing `game` object; keep the other server settings and mods:
+## Placement and requirements
-```json
-{
- "scenarioId": "{469880DFE1B48F95}Missions/Nogova_TerrainTest.conf",
- "gameProperties": {
- "missionHeader": {
- "m_bERR_EnableEscape": true,
- "m_sERR_Airfields": "1525 5075",
- "m_eSaveTypes": 0
- }
- }
-}
-```
+Escape prefers detected airfields. If there are no airport markers, standard
+runways or control towers, it surveys a clear helicopter landing zone instead.
+Nogova's painted runway therefore needs no administrator coordinates. Every
+candidate still passes terrain, navigation, slope and aircraft-clearance checks;
+a missing airport does not allow an unsafe spawn. Searches have finite time and
+attempt budgets and never fall back to coordinates from another map.
-The example selects Nogova's original exploration scenario, **not**
-`Missions/ERR_nogova.conf`. Server configuration uses Reforger's
-[mission-header overrides](https://community.bistudio.com/wiki/Arma_Reforger:Server_Config).
-The custom fields above are supplied by this version of Escape and will not work
-in an older published build. This document is not a claim that the configuration
-has been validated on RKS 2.
+The selected scenario must supply `SCR_BaseGameMode`, a usable `Soldiers` navmesh,
+world bounds spanning 2–100 km on both axes, and enough clear navigable ground.
+Navigation remains the terrain author's responsibility. A plain exploration
+scenario is the recommended starting point. Arbitrary scripted scenarios can
+retain their own tasks and entities; saved campaigns and every third-party game
+mode are not qualified. Escape disables further saving when taking over; it does
+not delete existing saves. Existing authored Escape scenarios and editor worlds
+are left on their normal startup path.
-`m_sERR_Airfields` contains X/Z airport-centre pairs separated by semicolons,
-for example `1525 5075; 6000 9000`. Omit it or leave it empty for automatic runway
-and control-tower detection. Nogova needs the hint because its runway is painted
-into the terrain. Height and aircraft clearance are surveyed in the game; a hint
-does not bypass placement checks. There can be at most 16 configured centres,
-and they must be on dry land within the loaded terrain. Existing authored airport
-markers are retained; configured centres use unused numbered marker slots.
+## Optional overrides
-Use a new session with persistence disabled (`m_eSaveTypes: 0`). Taking over a
-saved Conflict campaign or another scripted mission is not supported. The map
-must supply its own `Soldiers` navigation and enough suitable terrain for Escape.
-Other scenario types can keep their own tasks, scripts and entities even after
-the game mode is replaced; compatibility must be tested individually.
+To keep the original scenario while Escape remains loaded, pass
+`-errTerrainTakeover 0`, or set the mission-header field
+`m_bERR_EnableEscape` to `false`, then restart. The CLI override takes precedence;
+only its exact value `1` enables takeover when explicitly supplied. With no
+overrides, takeover is enabled.
-To use the original terrain scenario again, remove the enable override (or set it
-to `false`) and restart the scenario. Existing authored Escape scenarios are never
-replaced, even when the setting is enabled.
+Optional airport-centre hints use `-errAirfields "1525 5075"` or the header field
+`m_sERR_Airfields`. Use X/Z pairs separated by semicolons, up to 16 centres.
+Hints must be on dry land inside the terrain and do not bypass clearance checks.
+An explicitly supplied CLI hint takes precedence over the header. Empty hints
+use automatic discovery and the landing-zone fallback.
-## Implementation and validation
+## Validation of the automatic default
+
+Production-method regressions exercise default activation, editor/authority
+protection, opt-out precedence, airport discovery and bounded placement. Native
+Nogova server/client evidence is recorded in
+[evidence/automatic-terrain-takeover-2026-10-02](evidence/automatic-terrain-takeover-2026-10-02/).
+The no-flags Nogova test completed two takeovers and two prison phases across a
+connected restart, with all 16 sites and a supported, fueled helicopter in each
+round. Client manager checks passed with no client script errors. The evidence
+report retains server authentication refusals and installed-resource diagnostics;
+it is not a clean-release verdict. A freshly built package also reached the
+prison phase on Nogova without flags, loose Escape source, test probes or script
+errors. Its 351 payload files matched the authored source. Packaging's strict
+resource/shutdown failures remain recorded separately.
+Authenticated prisoner spawning, late joining and reconnecting still require a
+published-build multiplayer check; offline backend-disabled tests cannot prove
+account admission. Publication and live-server changes remain with the owner.
+
+## Handover behavior and earlier validation
- Invalid airport syntax, terrain bounds, navigation or missing bootstrap assets
are rejected before the original mode is stopped.
@@ -114,10 +118,13 @@
Repository checks passed. See the
[feature test report](evidence/terrain-takeover-feature-2026-10-01/report.json).
+Earlier opt-in builds required enable flags and airport hints. The following
+results describe those builds, before the automatic default and landing-zone fallback.
+
The command-line listen-server route did not apply either custom mission-header
overrides or the standard `m_eSaveTypes` override. Its diagnostic read the original
defaults and left Nogova's mode running. Consequently the example server JSON
-above still needs testing through the actual dedicated-server route. A read-only
+used in those tests still needs testing through the actual dedicated-server route. A read-only
copy of the RKS 2 engine executables was subsequently authorized and completed.
The dedicated JSON route could not complete addon/backend initialization with
external networking disabled. The explicit CLI route did load Nogova, generate
@@ -148,15 +155,3 @@
[the existing integration instructions](terrain-adapters.md). Do not delete them
from a deployment that still selects their mission header.
-## Publishing this candidate
-
-Publish the updated **Co10 Escape Reintroduced** core, including both
-`TerrainTakeover.et` and its `.meta` file. No new Nogova connector or terrain
-dependency is needed for this route. Update the terrain only when its own assets
-or navigation need an update. The owner then selects Nogova's existing exploration
-scenario and explicitly enables takeover as described above.
-
-After publishing, first verify on the test server that an authenticated player
-receives a prisoner, another player can join later, and both can reconnect and
-survive a scenario restart. Only then retire the separate connector from that
-deployment. Publication and RKS 2 configuration changes remain with the owner.
--- before/tests/test_terrain_sites.py
+++ after/tests/test_terrain_sites.py
@@ -3,7 +3,7 @@
base = ROOT/'addons/core/Scripts/Game/ERR/00_Core'
s = body(base/'ERR_GeneratedSites.c', 'protected bool SkipOptional(') + '\n' + body(base/'ERR_TerrainSites.c', 'override ERR_PrepareResult Tick(') + '\n' + body(base/'ERR_TerrainSites.c', 'protected bool PrepareAirports(')
for before, after in [('override ', ''), ('protected ', ''), ('ref ERR_SiteDefinition retained','Site* retained'),
- ('ERR_SiteDefinition site','Site* site'), ('site.', 'site->'), ('retained.','retained->'),
+ ('ERR_SiteDefinition site','Site* site'), ('foreach (', 'for ('), ('site.', 'site->'), ('retained.','retained->'),
('m_Definition.', 'm_Definition->'), ('m_Airports.', 'm_Airports->'), ('System.GetTickCount','Clock::Now'),
('Replication.IsServer','Server'), ('Math.RandomFloat','RandomFloat'),
('ERR_PrepareResult.','Result::'), ('ERR_LocationKind.','Kind::')]:
@@ -20,8 +20,9 @@
namespace Clock{int now=0;int Now(){return now++;}}
bool Server(){return true;}int randomCalls=0;float RandomFloat(float a,float b){randomCalls++;return (a+b)/2;}
template<class...T>void PrintFormat(T...){}
+void Print(string){}
namespace Kind{enum{PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
-struct Site{int Kind=Kind::PRISON;string Label="Test site",Id="test";vector Position=Vector(11,0,22);float Heading=0;bool PlacementResolved=false;};
+struct Site{int Kind=Kind::PRISON;string Label="Test site",Id="test";vector Position=Vector(11,0,22);float Heading=0;bool PlacementResolved=false,RequireAirfield=true;};
template<class T>struct Array:std::vector<T>{using std::vector<T>::operator=;bool IsEmpty(){return this->empty();}int Count(){return this->size();}int Find(T v){auto i=std::find(this->begin(),this->end(),v);return i==this->end()?-1:i-this->begin();}void Remove(int i){this->erase(this->begin()+i);}void InsertAt(T v,int i){this->insert(this->begin()+i,v);}};
struct Definition{Array<Site*> Sites;Site* required=nullptr;bool valid=true;bool Valid(){return valid&&(!required||Sites.Find(required)>=0);}};
struct Airports{Array<vector> m_Positions;Airports(){m_Positions.push_back(Vector(0,0,0));}bool complete=true;int updates=0;void Update(){updates++;}bool Complete(){return complete;}};
@@ -42,9 +43,11 @@
int main(){
{Scheduler s;s.m_Started=false;assert(s.Tick()==Result::FAILED);}
{Scheduler s;assert(s.Tick()==Result::READY);}
- // Airport discovery yields without spending the placement budget, then fails clearly when none exist.
- {Clock::now=0;Scheduler s;s.m_AirportsReady=false;s.airports.complete=false;assert(s.Tick()==Result::PENDING&&s.surveys==0);s.airports.complete=true;s.airports.m_Positions.clear();assert(s.Tick()==Result::FAILED&&!s.m_Error.empty()&&s.surveys==0);}
+ // Airport discovery yields without spending placement budget, then enables bounded clear-ground extraction.
+ {Clock::now=0;Scheduler s;s.m_AirportsReady=false;s.airports.complete=false;assert(s.Tick()==Result::PENDING&&s.surveys==0);s.airports.complete=true;s.airports.m_Positions.clear();Site extraction;extraction.Kind=Kind::EXTRACTION;s.m_Pending={&extraction};while(s.Tick()==Result::PENDING){}assert(s.m_Result==Result::READY&&!extraction.RequireAirfield&&extraction.PlacementResolved&&extraction.Label=="Extraction landing zone");}
{Clock::now=40000;Scheduler s;s.m_AirportsReady=false;Site a;s.m_Pending={&a};while(s.Tick()==Result::PENDING){}assert(s.m_Result==Result::READY&&a.PlacementResolved&&s.airports.updates==1);}
+ // Recognized airports retain their airfield requirement.
+ {Clock::now=0;Scheduler s;s.m_AirportsReady=false;Site extraction;extraction.Kind=Kind::EXTRACTION;s.m_Pending={&extraction};while(s.Tick()==Result::PENDING){}assert(s.m_Result==Result::READY&&extraction.RequireAirfield);}
// Off-origin bounds are sampled; authored Everon positions cannot be retained.
{Clock::now=0;Scheduler s;Site a,b;s.m_Pending={&a,&b};s.definition.Sites={&a,&b};while(s.Tick()==Result::PENDING){}assert(s.m_Result==Result::READY&&a.PlacementResolved&&b.PlacementResolved);assert(a.Position[0]==5000&&a.Position[2]==-1000&&a.Position[1]>42);int calls=s.surveys;assert(s.Tick()==Result::READY&&s.surveys==calls);}
// Pending navigation keeps the SAME candidate and yields before spinning or resampling.
@@ -93,3 +96,69 @@
}
''')
print('PASS: actual navigation readiness supports static meshes, yields only for loadable streaming tiles and refuses unreachable positions')
+
+# Execute sampling and clearance selection: an empty airport list must never be
+# indexed, and generic helicopter pads retain geometry/nav/physical validation.
+placement = body(base/'ERR_TerrainSites.c', 'protected vector SamplePosition(') + '\n' + body(base/'ERR_TerrainSites.c', 'protected bool Survey(')
+for before, after in [
+ ('protected ', ''), ('ERR_SiteDefinition site', 'Site* site'), ('site.', 'site->'),
+ ('out float floor', 'float& floor'), ('m_Airports.', 'm_Airports->'),
+ ('Math.', 'Math::'), ('ERR_LocationKind.', 'Kind::'),
+ ('ERR_Actors.', 'ERR_Actors::'), ('ERR_CampTerrain.', 'ERR_CampTerrain::'),
+ ('ERR_AirfieldPlacement.', 'ERR_AirfieldPlacement::')]:
+ placement = placement.replace(before, after)
+run(r'''
+#include <cassert>
+#include <cmath>
+#include <vector>
+struct vector {float a[3];float& operator[](int i){return a[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 Kind {enum {PRISON,EXTRACTION};}
+struct Site {int Kind=Kind::EXTRACTION,ResponseCount=0;bool RequireAirfield=false,Guarded=false;vector GuardOffset{},ResponseOffset{};};
+namespace Math {
+ int indexed=0;const float PI2=6.283185;
+ float RandomFloat(float a,float b){return (a+b)/2;}
+ int RandomInt(int a,int b){assert(b>a);indexed++;return a;}
+ 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_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;}
+}
+template<class T>struct Array:std::vector<T>{bool IsEmpty(){return this->empty();}int Count(){return this->size();}};
+struct Airports {Array<vector> m_Positions;};
+struct Terrain {
+ bool geometry=true,flat=true;
+ bool Site(::Site*,vector,float,float& floor){floor=42;return geometry;}
+ bool Clear(vector,float,float,float,float&){return flat;}
+};
+struct Placement {
+ Airports airports;Airports* m_Airports=&airports;Terrain m_Terrain;
+ vector m_Low=Vector(1000,0,-5000),m_High=Vector(9000,0,3000);
+ int m_GeometryRejected=0,m_NavigationRejected=0,m_PadRejected=0;
+ bool nav=true;bool Navigable(vector){return nav;}
+''' + placement + r'''
+};
+int main(){
+ Placement s;Site site;float floor=0;
+ auto p=s.SamplePosition(&site);assert(p[0]==5000&&p[2]==-1000&&Math::indexed==0);
+ s.airports.m_Positions.push_back(Vector(2000,0,3000));
+ p=s.SamplePosition(&site);assert(Math::indexed==1&&std::abs(p[0]-1775)<.01&&std::abs(p[2]-3000)<.01);
+ site.Kind=Kind::PRISON;p=s.SamplePosition(&site);assert(p[0]==5000&&p[2]==-1000&&Math::indexed==1);
+ site.Kind=Kind::EXTRACTION;
+ assert(s.Survey(&site,p,0,floor)&&floor==42);assert(ERR_AirfieldPlacement::groundChecks==1&&ERR_AirfieldPlacement::airportChecks==0);
+ 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);
+ ERR_AirfieldPlacement::airfield=true;assert(s.Survey(&site,p,0,floor));
+ site.RequireAirfield=false;s.m_Terrain.flat=false;assert(!s.Survey(&site,p,0,floor));
+ s.m_Terrain.flat=true;s.nav=false;assert(!s.Survey(&site,p,0,floor)&&s.m_NavigationRejected==1);
+ s.nav=true;s.m_Terrain.geometry=false;assert(!s.Survey(&site,p,0,floor)&&s.m_GeometryRejected==1);
+ assert(ERR_AirfieldPlacement::groundChecks==2&&ERR_AirfieldPlacement::airportChecks==2);
+}
+''')
+print('PASS: real sampling handles absent airports and extraction clearance preserves geometry/navigation/flatness while selecting the correct aircraft survey')
--- before/tests/test_terrain_takeover.py
+++ after/tests/test_terrain_takeover.py
@@ -92,7 +92,7 @@
#include <map>
using string=std::string;
struct SCR_MissionHeader {
- bool m_bERR_EnableEscape=false; string m_sERR_Airfields;
+ bool m_bERR_EnableEscape=true; string m_sERR_Airfields;
static SCR_MissionHeader* Cast(SCR_MissionHeader* header){return header;}
} header;
struct Game {SCR_MissionHeader* current=&header;SCR_MissionHeader* GetMissionHeader(){return current;}} game;
@@ -105,7 +105,8 @@
''' + settings_cpp(body(settings, 'static bool Enabled(')) +
settings_cpp(body(settings, 'static string Airfields(')) + r'''
int main(){
- assert(!Enabled());
+ assert(Enabled()&&Airfields().empty()); // Installing Escape requires no header or CLI flags.
+ header.m_bERR_EnableEscape=false;assert(!Enabled());
for(string bad:{"0","true","yes","01",""}){System::cli["errTerrainTakeover"]=bad;assert(!Enabled());}
System::cli["errTerrainTakeover"]="1";System::cli["errAirfields"]="1525 5075";
assert(Enabled()&&Airfields()=="1525 5075");
@@ -113,13 +114,19 @@
currentGame=nullptr;assert(!Enabled());currentGame=&game;
game.current=nullptr;assert(Enabled());game.current=&header;
header.m_bERR_EnableEscape=true;header.m_sERR_Airfields="10 20";
- System::cli["errTerrainTakeover"]="0";
- assert(Enabled()&&Airfields()=="10 20");
- header.m_sERR_Airfields="";assert(Airfields().empty()); // Header-selected automatic discovery wins.
- header.m_bERR_EnableEscape=false;System::cli.clear();assert(!Enabled());
-}
-''')
-print('PASS: explicit CLI opt-in, disabled/editor guards and mission-header airport precedence')
+ System::cli["errTerrainTakeover"]="0";assert(!Enabled()); // Explicit opt-out wins over the default header.
+ System::cli.erase("errTerrainTakeover");assert(Enabled());
+ assert(Airfields()=="1525 5075"); // Explicit CLI coordinates win.
+ System::cli.erase("errAirfields");assert(Airfields()=="10 20");
+ header.m_sERR_Airfields="";assert(Airfields().empty());
+ game.current=nullptr;System::cli.clear();assert(Enabled()&&Airfields().empty());
+}
+''')
+assert '[Attribute("1", desc: "Automatically start Escape' in settings.read_text()
+startup = body(takeover, 'protected override void OnWorldPostProcess(')
+assert '!ERR_GameMode.Get() && Replication.IsServer()' in startup
+assert startup.index('ERR_PreflightTerrainTakeover()') < startup.index('saves.SetSavingAllowed(false)')
+print('PASS: automatic default, explicit header/CLI opt-out, editor/authored-mode guards, optional airport override precedence and fresh-round saving guard')
client = core/'ERR_TerrainTakeoverClient.c'
managers = core/'ERR_TerrainTakeoverManagers.c'