Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
358 lines
27 KiB
Diff
358 lines
27 KiB
Diff
--- 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¢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::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')
|