--- 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;} templatevoid 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;}; templatestruct Array:std::vector{using std::vector::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 Sites;Site* required=nullptr;bool valid=true;bool Valid(){return valid&&(!required||Sites.Find(required)>=0);}}; struct Airports{Array 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 +#include +#include +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;} +} +templatestruct Array:std::vector{bool IsEmpty(){return this->empty();}int Count(){return this->size();}}; +struct Airports {Array 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 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'