Capture current mission, AI, map and administration development

Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
2026-10-13 22:52:53 +02:00
parent a951a96edc
commit 6a667d8deb
8104 changed files with 2124945 additions and 2061 deletions
@@ -0,0 +1,45 @@
{
"status": "working scratch core, measured resource dependency pending; not integrated or native-qualified",
"scope": "Default prison or independently authored static crash aftermath; no aircraft control or alternate admission/loadout.",
"implementation": [
"Once-only server settings choice frozen before first scenario Prepare, replicated int StartMode; invalid choice refuses preparation and later saved edits cannot change this round.",
"Guarded PREPARING->RADIO_SEARCH requires frozen crash choice and successful full scenario/extraction/environment readiness. Existing default phase edges retained. Both initial exit paths reset dormant force clocks; production prison AI release/listeners receive actual RADIO_SEARCH.",
"Fresh runtime single-start definition becomes owned empty crash site+one static wreck prop, preserving finite container/stock/map/site ID and internal PRISON start-slot role. Removes only start guards/route/responders/old props; no new forces or grants.",
"Crash transformation follows WB-only definition overrides. It forces regular generated survey; terrain-independent scheduler already calls same common survey. Both keep full original start/cache square plus padded measured wreck volume, rotated around actual heading.",
"Explicit prison escape service mode guard; standard spawn/one-life/reconnect/preload unchanged. Scenario normal catalog/generated allowlist includes crash-start while exact latest patrol-hq helper remains byte-for-byte."
],
"resource_contract": {
"owner": "convoy_supply_pool",
"class": "ERR_CrashSiteResources",
"methods": [
"SitePrefab()",
"WreckPrefab()",
"Bounds(out vector minimum,out vector maximum)"
],
"measurement": "No guessed constants. Current resource draft Bounds returns false until root measured exact native replicated wrapper. Core Configure and survey fail closed if false.",
"geometry": "Measured local bounds must be finite/nondegenerate, coordinates and extents bounded30m; offset places nearest hullX at12m; overall offset<=40m. Rounded measured footprint +1m padding, halfsize<=20m, vertical extent+1<=20m. These are refusal caps, not invented dimensions. Existing original16spawn slots and cache remain in original10m survey."
},
"verification": {
"new_test": "tests/test_crash_start.py",
"exit": 0,
"actual_methods": [
"ERR_StartRules.Valid/Geometry",
"ERR_StartSetup.IsCrash/Geometry/Configure",
"ERR_GameMode.FreezeStartMode",
"ERR_GameMode.Transition admission prefix",
"ERR_MissionRules.Terminal/CanTransition",
"ERR_CampTerrain.Site"
],
"boundaries": "Resource measurement, definition.Valid, native actor allocation and trace/terrain primitives doubled; source-order checks bind full startup, replicated getter, prison guard and both placement callers. Actual native geometry/replication/crash route remain pending.",
"baseline_red": "Not claimed: this adds an absent feature; new-class behavior tested directly."
},
"unmodified": [
"ERR_SpawnLogic / roster / inventory admission",
"SiteService finite stock and owned cleanup",
"TerrainSites (shared survey used without edits)",
"PrisonAI production hold/release",
"latest patrol-hq finite responders and runtime radius/queue fixture",
"settings/admin/UI files owned by separate agent",
"live successor and original"
]
}
@@ -0,0 +1,357 @@
--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c
+++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c
@@ -26,6 +26,20 @@
m_SelectedPlayerFaction = player; m_SelectedEnemyFaction = enemy; Replication.BumpMe();
}
protected ref ERR_Settings m_Settings;
+ [RplProp()] protected int m_StartMode;
+ protected bool m_StartModeFrozen;
+ protected bool m_StartPrepared;
+ int StartMode() { return m_StartMode; }
+ protected bool FreezeStartMode()
+ {
+ if (!Replication.IsServer() || m_Stopped || m_NativeEndHandled || m_Phase != ERR_Phase.PREPARING || !m_Settings) return false;
+ if (m_StartModeFrozen) return true;
+ int selected = m_Settings.Get("startMode");
+ if (!ERR_StartRules.Valid(selected)) return false;
+ m_StartMode = selected; m_StartModeFrozen = true;
+ Replication.BumpMe();
+ return true;
+ }
[RplProp(onRplName: "ApplyLocalViewDistance")]
protected int m_MissionViewDistance;
protected ref ERR_ViewDistance m_LocalViewDistance = new ERR_ViewDistance();
@@ -313,10 +327,11 @@
}
bool Transition(ERR_Phase phase)
{
- if (!Replication.IsServer() || !m_PreparationError.IsEmpty() || !ERR_MissionRules.CanTransition(m_Phase, phase)) return false;
+ if (!Replication.IsServer() || !m_PreparationError.IsEmpty() || !ERR_MissionRules.CanTransition(m_Phase, phase, m_StartModeFrozen && m_StartPrepared && m_StartMode == 1)) return false;
+ if (m_Phase == ERR_Phase.PREPARING && !m_StartPrepared) return false;
ERR_Phase previous = m_Phase;
m_Phase = phase;
- if (previous == ERR_Phase.PRISON && phase == ERR_Phase.RADIO_SEARCH)
+ if ((previous == ERR_Phase.PRISON || previous == ERR_Phase.PREPARING) && phase == ERR_Phase.RADIO_SEARCH)
{
// Dormant patrols must not catch up the time spent waiting in prison.
for (int i = 0; i < m_Forces.Count(); i++)
@@ -475,6 +490,7 @@
if (!m_FactionSelection || !m_FactionSelection.Prepare(this)) { FailPreparation("Three enabled faction profiles are required for the mission roles."); return; }
if (!m_Factions || !m_Factions.WorldReady()) { FailPreparation("The configured player and enemy factions or required assets are missing, invalid or not hostile."); return; }
if (!m_Content || !m_Content.PrepareFactions(this)) { FailPreparation("The content registry or enabled faction profiles are invalid. Check faction keys, guard groups and the mission faction manager."); return; }
+ if (!FreezeStartMode()) { FailPreparation("The selected mission start is invalid."); return; }
// Scenario content is created through a separate authoring component.
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent));
ERR_PrepareResult prepared = ERR_PrepareResult.FAILED;
@@ -498,7 +514,9 @@
}
if (!m_Environment.Start(this)) { FailPreparation("The selected date or vanilla weather profile could not be initialized."); return; }
if (!m_Time.Start(this)) { FailPreparation("The world clock could not be initialized."); return; }
- Transition(ERR_Phase.PRISON);
+ m_StartPrepared = true;
+ if (m_StartMode == 1) Transition(ERR_Phase.RADIO_SEARCH);
+ else Transition(ERR_Phase.PRISON);
}
protected void ExtractionResolved(ERR_ExtractionState result)
--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_GeneratedSites.c
+++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_GeneratedSites.c
@@ -25,7 +25,7 @@
vector rotation[3]; Math3D.AnglesToMatrix(Vector(heading, 0, 0), rotation);
return Vector(vector.Dot(offset, rotation[0]), offset[1], vector.Dot(offset, rotation[2]));
}
- bool Clear(vector center, float heading, float halfSize, float maximumRange, out float lowest)
+ bool Clear(vector center, float heading, float halfSize, float maximumRange, out float lowest, float clearanceHeight = 4)
{
lowest = float.MAX; float highest = -float.MAX;
for (int x = -1; x <= 1; x++)
@@ -49,7 +49,7 @@
vector cell = center + Rotate(Vector(cx, 0, cz), heading);
float ground = GetGame().GetWorld().GetSurfaceY(cell[0], cell[2]);
TraceBox trace = new TraceBox();
- trace.Start = Vector(cell[0], ground + 0.12, cell[2]); trace.End = trace.Start + "0 4 0";
+ trace.Start = Vector(cell[0], ground + 0.12, cell[2]); trace.End = trace.Start + Vector(0, clearanceHeight, 0);
// Bounding the rotated cell conservatively also protects the exit corridor.
trace.Mins = "-1.42 0 -1.42"; trace.Maxs = "1.42 0.1 1.42";
trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
@@ -70,6 +70,13 @@
aiWorld.GetRoadNetworkManager().GetClosestRoad(position, road, distance, skipNavlinks: true);
if (road && distance < 12) return false;
if (!Clear(position, heading, 10, 0.6, baseHeight)) return false;
+ if (ERR_StartSetup.IsCrash(site))
+ {
+ vector offset, center; float halfSize, height;
+ if (!ERR_StartSetup.Geometry(offset, center, halfSize, height)) return false;
+ float wreckFloor;
+ if (!Clear(position + Rotate(center, heading), heading, halfSize, 0.6, wreckFloor, height)) return false;
+ }
float guardHeight;
if (site.Guarded && !Clear(position + Rotate(site.GuardOffset, heading), heading, 6, 0.6, guardHeight)) return false;
if (site.ResponseCount > 0 && !Clear(position + Rotate(site.ResponseOffset, heading), heading, 6, 0.6, guardHeight)) return false;
--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_MissionRules.c
+++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_MissionRules.c
@@ -17,11 +17,15 @@
return phase == ERR_Phase.VICTORY || phase == ERR_Phase.DEFEAT;
}
- static bool CanTransition(int from, int to)
+ static bool CanTransition(int from, int to, bool crashStart = false)
{
if (Terminal(from) || from == to) return false;
if (to == ERR_Phase.DEFEAT) return from >= ERR_Phase.PRISON && from <= ERR_Phase.EXTRACTION;
- if (from == ERR_Phase.PREPARING) return to == ERR_Phase.PRISON;
+ if (from == ERR_Phase.PREPARING)
+ {
+ if (crashStart) return to == ERR_Phase.RADIO_SEARCH;
+ return to == ERR_Phase.PRISON;
+ }
if (from == ERR_Phase.PRISON) return to == ERR_Phase.RADIO_SEARCH;
if (from == ERR_Phase.RADIO_SEARCH) return to == ERR_Phase.RADIO_DEFENCE;
if (from == ERR_Phase.RADIO_DEFENCE) return to == ERR_Phase.EXTRACTION;
--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_PrisonEscapeService.c
+++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_PrisonEscapeService.c
@@ -94,7 +94,7 @@
m_Samples.Clear();
if (!Replication.IsServer() || m_Stopped || current != ERR_Phase.PRISON || m_Registered) return;
m_RegistrationReason = "no unique owned authored prison";
- if (!m_Mode || !m_Mode.Sites()) return;
+ if (!m_Mode || m_Mode.StartMode() != 0 || !m_Mode.Sites()) return;
array<ERR_LocationComponent> locations = {};
m_Mode.Locations(locations);
IEntity prison;
@@ -146,7 +146,7 @@
}
void Tick(float now)
{
- if (!Replication.IsServer() || m_Stopped || !m_Mode || m_Mode.Phase() != ERR_Phase.PRISON || !m_Mode.Active() || !m_Registered) return;
+ if (!Replication.IsServer() || m_Stopped || !m_Mode || m_Mode.Phase() != ERR_Phase.PRISON || m_Mode.StartMode() != 0 || !m_Mode.Active() || !m_Registered) return;
ref map<UUID, ref ERR_PrisonEscapeSample> next = new map<UUID, ref ERR_PrisonEscapeSample>();
array<ref ERR_PlayerLife> lives = {};
m_Mode.Players().Participants(lives);
--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c
+++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c
@@ -46,7 +46,7 @@
m_Started = true;
#ifdef WORKBENCH
string historicalFixture;
- if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "normal-escape-route" && historicalFixture != "mounted-ammo" && historicalFixture != "alarm-hearing" && historicalFixture != "optional-site-skip" && historicalFixture != "infantry-combat" && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
+ if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "normal-escape-route" && historicalFixture != "crash-start" && historicalFixture != "mounted-ammo" && historicalFixture != "alarm-hearing" && historicalFixture != "optional-site-skip" && historicalFixture != "infantry-combat" && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
#endif
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
@@ -54,10 +54,11 @@
#ifdef WORKBENCH
// Existing deterministic probes intentionally use authored coordinates.
string fixture;
- if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "normal-escape-route" && fixture != "mounted-ammo" && fixture != "alarm-hearing" && fixture != "optional-site-skip" && fixture != "infantry-combat" && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
+ if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "normal-escape-route" && fixture != "crash-start" && fixture != "mounted-ammo" && fixture != "alarm-hearing" && fixture != "optional-site-skip" && fixture != "infantry-combat" && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
if (!ERR_ConfigurePatrolHQFixture(fixture)) { m_Failed = true; return ERR_PrepareResult.FAILED; }
#endif
+ if (!ERR_StartSetup.Configure(m_RuntimeDefinition, mode.StartMode())) { m_Failed = true; return ERR_PrepareResult.FAILED; }
if (m_TerrainIndependent) m_Generated = new ERR_TerrainSites();
else m_Generated = new ERR_GeneratedSites();
if (!m_Generated.Begin(m_RuntimeDefinition)) { m_Failed = true; return ERR_PrepareResult.FAILED; }
--- /dev/null
+++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_StartRules.c
@@ -0,0 +1,23 @@
+// Start selection is fixed once per mission. Geometry uses measured native
+// model bounds only; it never substitutes guessed helicopter dimensions.
+class ERR_StartRules
+{
+ static bool Valid(int mode) { return mode == 0 || mode == 1; }
+ static bool Geometry(vector minimum, vector maximum, out vector offset, out vector center, out float halfSize, out float height)
+ {
+ offset = vector.Zero; center = vector.Zero; halfSize = 0; height = 0;
+ for (int axis = 0; axis < 3; axis++)
+ if (!ERR_ForceRules.Finite(minimum[axis]) || !ERR_ForceRules.Finite(maximum[axis]) || Math.AbsFloat(minimum[axis]) > 30 || Math.AbsFloat(maximum[axis]) > 30 || minimum[axis] >= maximum[axis] || maximum[axis] - minimum[axis] > 30) return false;
+ // Keep the measured wreck at least 12m east of the start anchor: all sixteen
+ // standing slots and the existing cache remain inside the original survey.
+ vector placed = Vector(12 - minimum[0], -minimum[1], 0);
+ float radius = Math.Ceil(Math.Max(maximum[0] - minimum[0], maximum[2] - minimum[2]) * 0.5 + 1);
+ float clearance = Math.Ceil(maximum[1] - minimum[1] + 1);
+ if (placed.Length() > 40 || radius > 20 || clearance > 20) return false;
+ offset = placed;
+ center = placed + (minimum + maximum) * 0.5;
+ center[1] = 0;
+ halfSize = radius; height = clearance;
+ return true;
+ }
+}
--- /dev/null
+++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_StartSetup.c
@@ -0,0 +1,47 @@
+// PRISON remains the internal single-start-slot role for existing catalog,
+// map-stock and terrain-spacing rules. Crash gameplay never enters PRISON.
+class ERR_StartSetup
+{
+ static bool IsCrash(ERR_SiteDefinition site)
+ {
+ return site && site.Kind == ERR_LocationKind.PRISON && site.Prefab == ERR_CrashSiteResources.SitePrefab();
+ }
+ static bool Geometry(out vector offset, out vector center, out float halfSize, out float height)
+ {
+ offset = vector.Zero; center = vector.Zero; halfSize = 0; height = 0;
+ vector minimum, maximum;
+ return ERR_CrashSiteResources.Bounds(minimum, maximum) && ERR_StartRules.Geometry(minimum, maximum, offset, center, halfSize, height);
+ }
+ static bool Configure(ERR_ScenarioDefinition definition, int mode)
+ {
+ if (!ERR_StartRules.Valid(mode) || !definition) return false;
+ if (mode == 0) return true;
+ ERR_SiteDefinition start;
+ int count;
+ foreach (ERR_SiteDefinition site : definition.Sites)
+ {
+ if (!site || site.Kind != ERR_LocationKind.PRISON) continue;
+ start = site; count++;
+ }
+ if (count != 1 || !start || start.Container.IsEmpty()) return false;
+ vector offset, center; float halfSize, height;
+ if (!Geometry(offset, center, halfSize, height)) return false;
+ for (int axis = 0; axis < 3; axis++)
+ if (!ERR_ForceRules.Finite(start.ContainerOffset[axis])) return false;
+ if (Math.AbsFloat(start.ContainerOffset[0]) > 8 || Math.AbsFloat(start.ContainerOffset[2]) > 8 || Math.AbsFloat(start.ContainerOffset[1]) > 1) return false;
+ start.Prefab = ERR_CrashSiteResources.SitePrefab();
+ start.Label = "Crash site";
+ start.RootHeight = 0;
+ start.Guarded = false; start.Guards = ""; start.GuardCount = 0;
+ start.ResponseCount = 0; start.AllowFactionPool = false;
+ start.Patrol = null;
+ start.RandomClear = true;
+ start.Props = new array<ref ERR_SiteProp>();
+ ERR_SiteProp wreck = new ERR_SiteProp();
+ wreck.Prefab = ERR_CrashSiteResources.WreckPrefab();
+ wreck.Offset = offset;
+ start.Props.Insert(wreck);
+ // Preserve the original finite cache, stock, radio clues, map and site ID.
+ return definition.Valid();
+ }
+}
--- /dev/null
+++ b/tests/test_crash_start.py
@@ -0,0 +1,124 @@
+"""Actual start policy/setup/freeze methods; engine/resource/terrain boundaries doubled."""
+import os,sys
+from pathlib import Path
+ROOT=Path(os.environ.get('ERR_REPO','/home/jakub/devel/escapistsrereintroduced'))
+P=Path(os.environ.get('ERR_CRASH_CORE_ROOT',ROOT))
+sys.path.insert(0,str(ROOT/'tests'))
+from policy_harness import body,run
+C='addons/core/Scripts/Game/ERR/00_Core/'
+def source(name):return P/C/name
+
+def cpp(s):
+ for a,b in [('protected ',''),('ERR_ForceRules.','ERR_ForceRules::'),('ERR_StartRules.','Rules::'),('ERR_CrashSiteResources.','Resources::'),('ERR_LocationKind.','Kind::'),('Math.','Math::'),('vector.Zero','vector{}'),('out vector ','vector& '),('out float ','float& ')]:s=s.replace(a,b)
+ return s
+rules='\n'.join(cpp(body(source('ERR_StartRules.c'),sig)) for sig in ['static bool Valid(','static bool Geometry('])
+setup='\n'.join(cpp(body(source('ERR_StartSetup.c'),sig)) for sig in ['static bool IsCrash(','static bool Geometry(','static bool Configure('])
+for a,b in [('ERR_SiteDefinition site','Site* site'),('ERR_SiteDefinition start;','Site* start=nullptr;'),('ERR_ScenarioDefinition definition','Definition* definition'),('ERR_SiteProp wreck','Prop* wreck'),('ref Prop*','Prop*'),('new ERR_SiteProp()','new Prop()'),('new array<ref ERR_SiteProp>()','new Array<Prop*>()'),('foreach (','for ('),('int count;','int count=0;')]:setup=setup.replace(a,b)
+for var in ['site','start','definition','wreck']:setup=setup.replace(var+'.',var+'->')
+setup=setup.replace('start->Props.Insert','start->Props->Insert').replace('.IsEmpty()','.empty()')
+run(r'''
+#include <cassert>
+#include <cmath>
+#include <limits>
+#include <vector>
+#include <string>
+#define null nullptr
+struct vector{float a[3]{};float&operator[](int i){return a[i];}float operator[](int i)const{return a[i];}float Length(){return std::sqrt(a[0]*a[0]+a[1]*a[1]+a[2]*a[2]);}};
+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]);}vector operator*(vector a,float b){return Vector(a[0]*b,a[1]*b,a[2]*b);}
+namespace ERR_ForceRules{bool Finite(float x){return std::isfinite(x);}}
+namespace Math{float Ceil(float x){return std::ceil(x);}float Max(float x,float y){return std::fmax(x,y);}float AbsFloat(float x){return std::fabs(x);}}
+struct Rules{RULES};
+namespace Kind{constexpr int PRISON=0;}
+template<class T>struct Array:std::vector<T>{using std::vector<T>::operator=;void Insert(T x){this->push_back(x);}};
+struct Prop{std::string Prefab;vector Offset;};
+struct Site{int Kind=0,GuardCount=6,ResponseCount=2;bool Guarded=true,AllowFactionPool=true,RandomClear=false;std::string Prefab="prison",Label="Prison",Guards="guards",Container="finite-bag",Id="original";float RootHeight=2;int*Patrol=(int*)1;void*Stock=(void*)2;vector ContainerOffset=Vector(-4,0,0);Array<Prop*>*Props=nullptr;};
+struct Definition{Array<Site*>Sites;bool valid=true;int calls=0;bool Valid(){calls++;return valid;}};
+namespace Resources{bool ready=true;vector low=Vector(-4,-1,-3),high=Vector(6,4,3);std::string SitePrefab(){return"crash-site";}std::string WreckPrefab(){return"wreck";}bool Bounds(vector&lo,vector&hi){lo=low;hi=high;return ready;}}
+struct Setup{SETUP};
+int main(){
+ assert(Rules::Valid(0)&&Rules::Valid(1)&&!Rules::Valid(-1)&&!Rules::Valid(2));
+ vector offset,center;float half,height;
+ assert(Rules::Geometry(Resources::low,Resources::high,offset,center,half,height));assert(offset[0]+Resources::low[0]==12&&offset[1]==1&&half==6&&height==6);
+ for(int a=0;a<3;a++)for(float bad:{std::numeric_limits<float>::infinity(),std::numeric_limits<float>::quiet_NaN()}){vector lo=Resources::low;lo[a]=bad;assert(!Rules::Geometry(lo,Resources::high,offset,center,half,height)&&half==0&&height==0&&offset[0]==0);}
+ assert(!Rules::Geometry(Vector(0,0,100),Vector(1,1,101),offset,center,half,height));assert(!Rules::Geometry(Vector(0,0,0),Vector(0,1,1),offset,center,half,height));assert(!Rules::Geometry(Vector(-50,0,0),Vector(-49,1,1),offset,center,half,height));assert(!Rules::Geometry(Vector(0,0,0),Vector(31,1,1),offset,center,half,height));assert(!Rules::Geometry(Vector(0,0,0),Vector(1,21,1),offset,center,half,height));
+ Site prison;Definition original;original.Sites={&prison};Resources::ready=false;assert(Setup::Configure(&original,0)&&prison.Guarded&&prison.Prefab=="prison"&&prison.ResponseCount==2&&prison.RootHeight==2&&original.calls==0);
+ assert(!Setup::Configure(&original,1)&&prison.Prefab=="prison");Resources::ready=true;
+ assert(Setup::Configure(&original,1)&&original.calls==1&&Setup::IsCrash(&prison));assert(!prison.Guarded&&prison.Guards.empty()&&prison.GuardCount==0&&prison.ResponseCount==0&&!prison.Patrol&&!prison.AllowFactionPool&&prison.RandomClear&&prison.RootHeight==0);
+ assert(prison.Stock==(void*)2&&prison.Container=="finite-bag"&&prison.Id=="original"&&prison.ContainerOffset[0]==-4&&prison.Props->size()==1&&(*prison.Props)[0]->Prefab=="wreck"&&(*prison.Props)[0]->Offset[0]==16);
+ for(int fault=0;fault<7;fault++){Site a,b;Definition d;d.Sites={&a};if(fault==0)d.Sites.clear();if(fault==1)d.Sites.push_back(&b);if(fault==2)a.Container="";if(fault==3)a.ContainerOffset[0]=9;if(fault==4)a.ContainerOffset[1]=2;if(fault==5)d.valid=false;assert(!Setup::Configure(fault==6?nullptr:&d,1));}
+ assert(!Setup::Configure(&original,-1));
+}
+'''.replace('RULES',rules).replace('SETUP',setup))
+# Actual once-only server freeze, including failed settings and restart/new owner.
+freeze=body(source('ERR_GameMode.c'),'protected bool FreezeStartMode(').replace('protected ','').replace('Replication.','Replication::').replace('ERR_Phase.','Phase::').replace('ERR_StartRules.','Rules::').replace('m_Settings.','m_Settings->')
+run(r'''
+#include <cassert>
+#include <string>
+namespace Replication{bool server=true;int bumps=0;bool IsServer(){return server;}void BumpMe(){bumps++;}}
+namespace Phase{constexpr int PREPARING=0;}
+namespace Rules{bool Valid(int n){return n==0||n==1;}}
+struct Settings{int value=0,reads=0;int Get(std::string key){assert(key=="startMode");reads++;return value;}};
+struct Mode{bool m_Stopped=false,m_NativeEndHandled=false,m_StartModeFrozen=false;int m_Phase=0,m_StartMode=0;Settings*m_Settings=nullptr;METHOD};
+int main(){Settings s;Mode p;p.m_Settings=&s;assert(p.FreezeStartMode()&&p.m_StartMode==0);s.value=1;assert(p.FreezeStartMode()&&p.m_StartMode==0&&s.reads==1);Mode restart;restart.m_Settings=&s;assert(restart.FreezeStartMode()&&restart.m_StartMode==1);for(int f=0;f<6;f++){Mode x;x.m_Settings=&s;if(f==0)Replication::server=false;if(f==1)x.m_Stopped=true;if(f==2)x.m_NativeEndHandled=true;if(f==3)x.m_Phase=1;if(f==4)x.m_Settings=nullptr;if(f==5)s.value=2;assert(!x.FreezeStartMode()&&!x.m_StartModeFrozen);Replication::server=true;s.value=1;}}
+'''.replace('METHOD',freeze))
+# Actual transition policy retains all default edges; crash authorization adds
+# only initial RADIO_SEARCH and cannot add a terminal or downstream shortcut.
+policy='\n'.join(body(source('ERR_MissionRules.c'),sig) for sig in ['static bool Terminal(','static bool CanTransition(']).replace('ERR_Phase.','Phase::')
+run(r'''
+#include <cassert>
+namespace Phase{enum{PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
+POLICY
+int main(){for(int from=0;from<=6;from++)for(int to=0;to<=6;to++){bool old=(from<5&&to==from+1)||(from>=1&&from<=4&&to==6);assert(CanTransition(from,to)==old);assert(CanTransition(from,to,true)==(from==0?to==2:old));}}
+'''.replace('POLICY',policy))
+mode=source('ERR_GameMode.c').read_text();scenario=source('ERR_ScenarioComponent.c').read_text()
+transition=body(source('ERR_GameMode.c'),'bool Transition(')
+assert 'm_StartModeFrozen && m_StartPrepared && m_StartMode == 1' in transition
+assert 'm_Phase == ERR_Phase.PREPARING && !m_StartPrepared' in transition
+start=body(source('ERR_GameMode.c'),'protected void StartScenario(')
+assert start.index('FreezeStartMode()')<start.index('scenario.Prepare()')<start.index('m_StartPrepared = true;')<start.index('Transition(ERR_Phase.RADIO_SEARCH)')
+assert start.index('m_Time.Start(this)')<start.index('m_StartPrepared = true;')
+assert 'previous == ERR_Phase.PREPARING' in transition
+assert '[RplProp()] protected int m_StartMode;' in mode
+assert scenario.index('ERR_ConfigurePatrolHQFixture(fixture)')<scenario.index('ERR_StartSetup.Configure(')<scenario.index('m_Generated.Begin(')
+assert 'historicalFixture != "crash-start"' in scenario and 'fixture != "crash-start"' in scenario
+assert 'm_Mode.StartMode() != 0' in body(source('ERR_PrisonEscapeService.c'),'void PhaseChanged(')
+# Both generated and terrain-independent placement dispatch through this exact
+# shared Site method; actual Clear is native trace geometry (not simulated here).
+survey=body(source('ERR_GeneratedSites.c'),'bool Site(')
+assert 'ERR_StartSetup.IsCrash(site)' in survey and 'ERR_StartSetup.Geometry(offset, center, halfSize, height)' in survey
+assert 'Clear(position + Rotate(center, heading), heading, halfSize, 0.6, wreckFloor, height)' in survey
+assert 'm_Terrain.Site(site, position, heading, floor)' in (ROOT/C/'ERR_TerrainSites.c').read_text()
+print('PASS: actual crash geometry, detached definition setup, frozen server choice and guarded phase policy; integration/source ordering checked')
+# Execute actual GameMode transition admission prefix (effects after acceptance
+# are existing mission hooks); callers cannot use selected crash mode before
+# ordinary scenario/extraction/environment preparation has completed.
+admit=body(source('ERR_GameMode.c'),'bool Transition(').split('\n\t\tERR_Phase previous',1)[0]+'\n return true;\n}'
+admit=admit.replace('ERR_Phase phase','int phase').replace('Replication.','Replication::').replace('ERR_MissionRules.','Policy::').replace('ERR_Phase.','Phase::').replace('m_PreparationError.IsEmpty()','m_PreparationError.empty()')
+run(r'''
+#include <cassert>
+#include <string>
+namespace Replication{bool server=true;bool IsServer(){return server;}}
+namespace Phase{enum{PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
+namespace Policy{POLICY}
+struct Mode{int m_Phase=0,m_StartMode=1;bool m_StartModeFrozen=false,m_StartPrepared=false;std::string m_PreparationError;ADMIT};
+int main(){Mode crash;assert(!crash.Transition(2));crash.m_StartModeFrozen=true;assert(!crash.Transition(2));crash.m_StartPrepared=true;assert(crash.Transition(2)&&!crash.Transition(1));crash.m_PreparationError="failed";assert(!crash.Transition(2));crash.m_PreparationError="";Replication::server=false;assert(!crash.Transition(2));Replication::server=true;Mode prison;prison.m_StartMode=0;prison.m_StartModeFrozen=true;prison.m_StartPrepared=true;assert(prison.Transition(1)&&!prison.Transition(2));prison.m_Phase=1;assert(prison.Transition(2));}
+'''.replace('POLICY',policy).replace('ADMIT',admit))
+# Execute actual common site survey to prove both measured wreck and start/cache
+# areas are required and that unknown measured bounds cannot fall back to10m.
+site=body(source('ERR_GeneratedSites.c'),'bool Site(')
+for a,b in [('ERR_SiteDefinition site','SiteData* site'),('site.','site->'),('foreach (','for ('),('ERR_LocationKind.','Kind::'),('ERR_StartSetup.','StartSetup::'),('SCR_AIWorld aiWorld = SCR_AIWorld.Cast(GetGame().GetAIWorld());','AIWorld* aiWorld = &world;'),('aiWorld.GetRoadNetworkManager()','aiWorld->GetRoadNetworkManager()'),('GetRoadNetworkManager().','GetRoadNetworkManager()->'),('BaseRoad road; float distance;','Road* road=nullptr; float distance=0;'),('skipNavlinks: true','true'),('out float ','float& '),('vector.DistanceXZ','DistanceXZ')]:site=site.replace(a,b)
+run(r'''
+#include <cassert>
+#include <cmath>
+#include <vector>
+#define null nullptr
+struct vector{float x=0,y=0,z=0;};vector operator+(vector a,vector b){return{a.x+b.x,a.y+b.y,a.z+b.z};}float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
+namespace Kind{constexpr int PRISON=0;}
+struct SiteData{int Kind=0,ResponseCount=0;bool Guarded=false,crash=false;vector GuardOffset,ResponseOffset;};
+namespace StartSetup{bool measured=true;bool IsCrash(SiteData*s){return s->crash;}bool Geometry(vector&offset,vector&center,float&half,float&height){offset={16,1,0};center={17,0,0};half=6;height=6;return measured;}}
+struct Road{};struct RoadManager{void GetClosestRoad(vector,Road*&,float&,bool){}};struct AIWorld{RoadManager roads;RoadManager*GetRoadNetworkManager(){return &roads;}};
+struct Call{vector p;float half,height;};
+struct Survey{AIWorld world;std::vector<vector>m_Airfields;std::vector<Call>calls;int refuse=0;vector Rotate(vector p,float h){assert(h==90);return{p.z,p.y,-p.x};}bool Clear(vector p,float,float half,float,float&floor,float height=4){calls.push_back({p,half,height});floor=42;return int(calls.size())!=refuse;}SITE};
+int main(){SiteData a;float floor=0;Survey 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')
@@ -0,0 +1,2 @@
PASS: actual crash geometry, detached definition setup, frozen server choice and guarded phase policy; integration/source ordering checked
PASS: actual GameMode readiness admission and common full-footprint survey require crash authorization and both surveyed areas
@@ -0,0 +1,11 @@
--- a/tests/test_crash_start.py
+++ b/tests/test_crash_start.py
@@ -1,7 +1,7 @@
"""Actual start policy/setup/freeze methods; engine/resource/terrain boundaries doubled."""
import os,sys
from pathlib import Path
-ROOT=Path(os.environ.get('ERR_REPO','/home/jakub/devel/escapistsrereintroduced'))
+ROOT=Path(os.environ.get('ERR_REPO',Path(__file__).resolve().parents[1]))
P=Path(os.environ.get('ERR_CRASH_CORE_ROOT',ROOT))
sys.path.insert(0,str(ROOT/'tests'))
from policy_harness import body,run
@@ -0,0 +1,2 @@
PASS: actual crash geometry, detached definition setup, frozen server choice and guarded phase policy; integration/source ordering checked
PASS: actual GameMode readiness admission and common full-footprint survey require crash authorization and both surveyed areas
@@ -0,0 +1,2 @@
PASS: actual crash geometry, detached definition setup, frozen server choice and guarded phase policy; integration/source ordering checked
PASS: actual GameMode readiness admission and common full-footprint survey require crash authorization and both surveyed areas
@@ -0,0 +1,20 @@
{
"file": "tests/test_crash_start.py",
"baseline_sha256": "22ec3e0627a3c2f8ef79a39dafc933ed3489b24acca4bb53b136c56a98d56d6b",
"proposed_sha256": "b65da31d60b5cdaacc8e729155b05eb1388284c9a6241549eaac49d59e1f4552",
"patch_sha256": "814432d578ffeed5e4aa11cf1975d44e22285ce67d8e7a3f6202cd84f2118646",
"original_proposal_preserved": true,
"tests": {
"scratch_override": {
"exit": 0,
"log": "d01e24f45cb53afede40230165f9f2aba874fdb0c34de14f893c9a136b9d2edb"
},
"isolated_default_no_environment_overrides": {
"exit": 0,
"log": "d01e24f45cb53afede40230165f9f2aba874fdb0c34de14f893c9a136b9d2edb"
}
},
"compiler_work_finished": true,
"live_edits": false,
"native_runs": 0
}
@@ -0,0 +1,27 @@
{
"code_review_approved": true,
"validation_scope": "Read-only bounded source integration review; no tests/compiler/native run",
"proposal_patch_sha256": "82d2f6c481caef7dc777fd6bf3d92258a4af4b9bf87b207aa7bc1ff4c3aabead",
"reviewed_source_hashes": {
"addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c": "9a0536377f0e93f70972838de6480dfbdb44bfda198f70daaa541616eb046fd6",
"addons/core/Scripts/Game/ERR/00_Core/ERR_MissionRules.c": "17d1f48878b02a457eebbe5ff87c4267488d68d2a9b3121dda77a8bc8e69c8c6",
"addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c": "d7c953fc8617c21e03f7d501c4a4c00a034b6375060072fd03c751fc2970feeb",
"addons/core/Scripts/Game/ERR/00_Core/ERR_StartRules.c": "ab7c21ef700343cbe769d8d51d91f167db37285b466d6af9873e121b3adf4ade",
"addons/core/Scripts/Game/ERR/00_Core/ERR_StartSetup.c": "58a6b617b22910d0ce9255f82091dc11cd1284417a66adf1cdaa7b661c1ed01c",
"addons/core/Scripts/Game/ERR/00_Core/ERR_GeneratedSites.c": "addd7850e58a2f5c4a0ca6672033f2b42f5879e9a1a6a7ab86aa730419cef97b",
"addons/core/Scripts/Game/ERR/00_Core/ERR_PrisonEscapeService.c": "ed180448cb516bc4ec521aedd366810a47787628c7f5ee9c9afdbd185d980751"
},
"settings_manifest_sha256": "9295723e6653bea750b438f22366e3a9f3bc62e1645766ba42047bc1337cb5ee",
"findings": [
"Settings startMode0/1 missing-default0 and replicated int GameMode.StartMode match. Freeze runs before first Scenario.Prepare and is idempotent across pending preparation retries. Saved mid-round edits cannot change frozen selector; restart/new GameMode reads saved choice.",
"Transition grants PREPARING->RADIO_SEARCH only with frozen crash choice and readiness latch set after scenario, extraction, environment and clock initialization. Default prison edge and downstream phase rules remain separate.",
"Fresh runtime scenario instance is transformed before generated-site Begin. Internal PRISON site role remains the one start-anchor/catalog slot, without entering PRISON gameplay phase in crash mode.",
"Existing SpawnLogic selects surviving canonical actors after RADIO_SEARCH and falls back to original sixteen start-anchor slots for first arrival. Life, reconnection, reserved positions and finalization-ticket guards remain unchanged.",
"Prison escape service explicitly excludes crash mode; existing SiteService.PhaseChanged reveals intended clue/supply locations on initial RADIO_SEARCH. Force time reset is generalized to the authorized direct initial phase."
],
"limits": [
"Geometry constants depend on independent installed-resource/native bounds proof; current review does not establish physical terrain clearance, spawn safety or native static wreck support.",
"Source fixture and tests use doubled native boundaries; parent owns subsequent full-suite and engine validation.",
"No manual human/MP, crash physics, flight or search/escape route qualification."
]
}
@@ -0,0 +1,52 @@
{
"files": {
"addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c": {
"baseline_sha256": "89a29c03ba225acbbb9fa3c58a721aced91a77bc0c77046d697005a6d7c48bb9",
"proposed_sha256": "9a0536377f0e93f70972838de6480dfbdb44bfda198f70daaa541616eb046fd6",
"new_file": false
},
"addons/core/Scripts/Game/ERR/00_Core/ERR_GeneratedSites.c": {
"baseline_sha256": "4a23058f27e3ab074df6f8f7bb6f08a8fd26fbadb8943c6fb8d64dcc9c221751",
"proposed_sha256": "addd7850e58a2f5c4a0ca6672033f2b42f5879e9a1a6a7ab86aa730419cef97b",
"new_file": false
},
"addons/core/Scripts/Game/ERR/00_Core/ERR_MissionRules.c": {
"baseline_sha256": "f7abc38ad8fdc375ce7fa63a65f0bade027d22b37e09de5e3f94ba8c3c63cc41",
"proposed_sha256": "17d1f48878b02a457eebbe5ff87c4267488d68d2a9b3121dda77a8bc8e69c8c6",
"new_file": false
},
"addons/core/Scripts/Game/ERR/00_Core/ERR_PrisonEscapeService.c": {
"baseline_sha256": "535cf17e21fc8e62716c7d9da88d976ee2f526501c6a4752b822f1252e742bf4",
"proposed_sha256": "ed180448cb516bc4ec521aedd366810a47787628c7f5ee9c9afdbd185d980751",
"new_file": false
},
"addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c": {
"baseline_sha256": "7fe3e8ee04a15a50fe6d665027a5ef0ff945b4c587113713b9d462382b0eccd1",
"proposed_sha256": "d7c953fc8617c21e03f7d501c4a4c00a034b6375060072fd03c751fc2970feeb",
"new_file": false
},
"addons/core/Scripts/Game/ERR/00_Core/ERR_StartRules.c": {
"baseline_sha256": null,
"proposed_sha256": "ab7c21ef700343cbe769d8d51d91f167db37285b466d6af9873e121b3adf4ade",
"new_file": true
},
"addons/core/Scripts/Game/ERR/00_Core/ERR_StartSetup.c": {
"baseline_sha256": null,
"proposed_sha256": "58a6b617b22910d0ce9255f82091dc11cd1284417a66adf1cdaa7b661c1ed01c",
"new_file": true
},
"tests/test_crash_start.py": {
"baseline_sha256": null,
"proposed_sha256": "22ec3e0627a3c2f8ef79a39dafc933ed3489b24acca4bb53b136c56a98d56d6b",
"new_file": true
}
},
"patch_sha256": "82d2f6c481caef7dc777fd6bf3d92258a4af4b9bf87b207aa7bc1ff4c3aabead",
"finding_sha256": "090260582f37f01ffa7984ee5da743f670e51c231164d6db82d75d800ebf0d85",
"test_log_sha256": "d01e24f45cb53afede40230165f9f2aba874fdb0c34de14f893c9a136b9d2edb",
"test_exit": 0,
"compilers_finished": true,
"pending": "Rawls native-measured resource bounds and root full integration/native validation",
"live_edits": false,
"native_runs": 0
}