--- 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 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 next = new map(); array 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(); + 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()','new Array()'),('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 +#include +#include +#include +#include +#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;} +templatestruct Array:std::vector{using std::vector::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*Props=nullptr;}; +struct Definition{ArraySites;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::infinity(),std::numeric_limits::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 +#include +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 +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()') +#include +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 +#include +#include +#define null nullptr +struct vector{float x=0,y=0,z=0;};vector operator+(vector a,vector b){return{a.x+b.x,a.y+b.y,a.z+b.z};}float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);} +namespace Kind{constexpr int PRISON=0;} +struct SiteData{int Kind=0,ResponseCount=0;bool Guarded=false,crash=false;vector GuardOffset,ResponseOffset;}; +namespace StartSetup{bool measured=true;bool IsCrash(SiteData*s){return s->crash;}bool Geometry(vector&offset,vector¢er,float&half,float&height){offset={16,1,0};center={17,0,0};half=6;height=6;return measured;}} +struct Road{};struct RoadManager{void GetClosestRoad(vector,Road*&,float&,bool){}};struct AIWorld{RoadManager roads;RoadManager*GetRoadNetworkManager(){return &roads;}}; +struct Call{vector p;float half,height;}; +struct Survey{AIWorld world;std::vectorm_Airfields;std::vectorcalls;int refuse=0;vector Rotate(vector p,float h){assert(h==90);return{p.z,p.y,-p.x};}bool Clear(vector p,float,float half,float,float&floor,float height=4){calls.push_back({p,half,height});floor=42;return int(calls.size())!=refuse;}SITE}; +int main(){SiteData a;float floor=0;Survey 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')