Build realistic roadblocks with staggered barriers and guarded positions

This commit is contained in:
2026-10-15 12:39:04 +02:00
parent 7b13231f29
commit 3b920e0715
6 changed files with 267 additions and 40 deletions
@@ -17,11 +17,79 @@ modded class ERR_Roadblocks
m_Mode.Settings().Set("roadblockCount", 30);
return super.Prepare(definition);
}
protected int m_ERRLayoutPosts;
protected bool m_ERRLayoutFailed, m_ERRLayoutCaptured;
protected SCR_ManualCamera m_ERRLayoutCamera;
override protected void BuildPost()
{
string outcome;
if (!System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "roadblocks") { super.BuildPost(); return; }
// Inspect the actual spawned models before the transaction clears its refs.
if (m_BuildHold)
{
bool valid = m_BuildCovers.Count() == 10 && m_BuildSpawned.Count() == 15;
for (int i = 0; i < m_BuildCovers.Count() && valid; i++)
{
ERR_RoadblockProp prop = m_BuildCovers[i];
IEntity entity = m_BuildSpawned[i + 4];
vector low, high; entity.GetBounds(low, high);
if (m_ERRLayoutPosts == 0) PrintFormat("ERR ROADBLOCK PROP NATIVE: prefab=%1 low=%2 high=%3", prop.Prefab, low, high);
if (Math.Max(Math.AbsFloat(low[0]), Math.AbsFloat(high[0])) > prop.HalfWidth
|| Math.Max(Math.AbsFloat(low[2]), Math.AbsFloat(high[2])) > prop.HalfDepth || high[1] > prop.Height) valid = false;
// Every actual prop corner must stay outside the four-metre centre lane.
for (int x = 0; x < 2; x++)
for (int z = 0; z < 2; z++)
{
vector corner = low;
if (x) corner[0] = high[0];
if (z) corner[2] = high[2];
vector delta = entity.CoordToParent(corner) - m_Centre;
if (Math.AbsFloat(vector.Dot(delta, m_Right)) < 2) valid = false;
}
}
if (!valid) { m_ERRLayoutFailed = true; Print("ERR SMOKE FAIL: roadblock prop geometry or centre lane", LogLevel.ERROR); }
if (!m_ERRLayoutCaptured && m_Mode.Active())
{
m_ERRLayoutCaptured = true;
EntitySpawnParams params = new EntitySpawnParams();
m_ERRLayoutCamera = SCR_ManualCamera.Cast(GetGame().SpawnEntity(SCR_ManualCamera, GetGame().GetWorld(), params));
if (m_ERRLayoutCamera)
{
vector camera[4];
vector eye = m_Centre - m_Forward * 18 + m_Right * 12 + "0 13 0";
Math3D.DirectionAndUpMatrix(vector.Direction(eye, m_Centre), "0 1 0", camera);
camera[3] = eye; m_ERRLayoutCamera.SetWorldTransform(camera); m_ERRLayoutCamera.SetVerticalFOV(65);
GetGame().GetCameraManager().SetCamera(m_ERRLayoutCamera);
GetGame().GetCallqueue().CallLater(ERR_ProbeCaptureLayout, 3000, false);
}
}
m_ERRLayoutPosts++;
}
super.BuildPost();
}
protected void ERR_ProbeCaptureLayout()
{
System.MakeScreenshot("$logs:err-roadblock-layout");
GetGame().GetCallqueue().CallLater(ERR_ProbeCloseLayoutCamera, 1000, false);
}
protected void ERR_ProbeCloseLayoutCamera()
{
if (!m_ERRLayoutCamera) return;
GetGame().GetCameraManager().SetCamera(GetGame().GetPlayerController().GetPlayerCamera());
m_ERRLayoutCamera.Terminate(); delete m_ERRLayoutCamera; m_ERRLayoutCamera = null;
}
override void Stop()
{
GetGame().GetCallqueue().Remove(ERR_ProbeCaptureLayout);
GetGame().GetCallqueue().Remove(ERR_ProbeCloseLayoutCamera);
ERR_ProbeCloseLayoutCamera();
super.Stop();
}
int ERR_ProbePostCount() { return m_Positions.Count(); }
bool ERR_ProbeBackgroundPending() { return m_Background; }
bool ERR_ProbeCompleteRoadblocks()
{
if (m_Target != 30 || m_Positions.Count() != 30 || m_Background || !m_Ready) return false;
if (m_ERRLayoutFailed || m_ERRLayoutPosts < 30 || m_Target != 30 || m_Positions.Count() != 30 || m_Background || !m_Ready) return false;
foreach (SCR_AIGroup group : m_Groups)
{
ERR_ForceRecord record = m_Mode.Forces().FindGroup(group);
@@ -58,7 +126,7 @@ modded class ERR_GameMode
super.EOnInit(owner);
string outcome;
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "roadblocks") return;
m_ERRRoadblockDeadline = System.GetTickCount() + 600000;
m_ERRRoadblockDeadline = System.GetTickCount() + 900000;
GetGame().GetCallqueue().CallLater(ERR_RoadblockProbeTick, 500, true);
OnStop().Insert(ERR_StopRoadblockProbe);
}
@@ -85,7 +153,7 @@ modded class ERR_GameMode
}
if (posts.ERR_ProbeBackgroundPending()) return;
if (!posts.ERR_ProbeCompleteRoadblocks() || posts.ERR_ProbePostCount() <= m_ERRRoadblockInitial) { ERR_RoadblockProbeFail("30 physical four-guard posts, ledger and HQ admission"); return; }
Print("ERR SMOKE PASS: roadblocks 30 physical posts / 120 guards; background completed after prison unlock, no extra posts on repeat tick");
Print("ERR SMOKE PASS: roadblocks 30 physical posts / 120 guards / 300 checkpoint props; verified model footprints and centre lane; background completed after prison unlock, no extra posts on repeat tick");
ERR_StopRoadblockProbe();
Transition(ERR_Phase.DEFEAT);
Print("ERR SMOKE COMPLETE: roadblocks background completion on Everon; scripted local fixture");
@@ -22,6 +22,20 @@ class ERR_RoadblockRules
}
}
// A small, staged composition: each prop has its own heading and clearance box.
class ERR_RoadblockProp
{
ResourceName Prefab;
vector Position, Angles, Right, Forward;
float HalfWidth, HalfDepth, Height;
void ERR_RoadblockProp(ResourceName prefab, vector position, vector forward, float halfWidth, float halfDepth, float height)
{
Prefab = prefab; Position = position; Forward = forward;
Right = Vector(forward[2], 0, -forward[0]); Angles = forward.VectorToAngles();
HalfWidth = halfWidth; HalfDepth = halfDepth; Height = height;
}
}
class ERR_Roadblocks
{
protected ERR_GameMode m_Mode;
@@ -39,11 +53,15 @@ class ERR_Roadblocks
protected SCR_AIGroup m_BuildGroup;
protected AIWaypoint m_BuildHold;
protected ref array<IEntity> m_BuildSpawned = {};
protected ref array<vector> m_BuildPositions = {}, m_BuildCovers = {};
protected ref array<vector> m_BuildPositions = {};
protected ref array<ref ERR_RoadblockProp> m_BuildCovers = {};
protected int m_BuildActors, m_BuildCoverCount;
protected ref array<ResourceName> m_Roster = {};
protected ref array<ref Resource> m_Loaded = {};
static const ResourceName COVER = "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et";
static const ResourceName COVER = "{21D83950DF36F250}Prefabs/Props/Military/Sandbags/Sandbag_01_long_burlap.et";
static const ResourceName BARRIER = "{64C9C1D1C2C1B38D}Prefabs/Props/Infrastructure/Roadblocks/RoadBlock_01.et";
static const ResourceName OBSTACLE = "{A46D45922F8CAD6C}Prefabs/Props/Military/Fortification/CzechHedgehog_01_painted.et";
static const ResourceName SIGN = "{C1977A326F2AA011}Prefabs/Structures/Signs/Military/SignCheckpoint_01_stop.et";
void ERR_Roadblocks(ERR_GameMode mode) { m_Mode = mode; m_Mining = new ERR_RoadblockMining(mode); }
void MineTick(array<ref ERR_HQUnit> units, array<IEntity> attempted, float now) { if (!m_Stopped) m_Mining.Tick(units, attempted, now); }
bool Contains(SCR_AIGroup group) { return group && m_Groups.Contains(group); }
@@ -72,7 +90,7 @@ class ERR_Roadblocks
if (profile && profile.IsValid()) source = profile.GetResource().ToEntitySource();
if (!source || !source.Get("m_aUnitPrefabSlots", m_Roster) || m_Roster.IsEmpty()) return false;
// Preload before physical creation. A failed prefab never leaves half a post.
array<ResourceName> resources = {COVER, "{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", ERR_Resources.DefendWaypoint()};
array<ResourceName> resources = {COVER, BARRIER, OBSTACLE, SIGN, "{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", ERR_Resources.DefendWaypoint()};
foreach (ResourceName actor : m_Roster) resources.Insert(actor);
foreach (ResourceName prefab : resources)
{
@@ -119,8 +137,8 @@ class ERR_Roadblocks
if (m_Forward.Length() < 0.1) return false;
m_Forward.Normalize();
m_Right = Vector(m_Forward[2], 0, -m_Forward[0]);
// Keep all physical cover outside the travelled road. Even a narrow road has
// a >=6m centre gap between these compact sandbag positions.
// Protected guard positions sit on the shoulders; approach barriers narrow
// wider roads while preserving a straight vehicle passage on every width.
m_Shoulder = Math.Max(road.GetWidth() * 0.5 + 1.5, 4);
m_Candidates++; m_CandidateStarted = System.GetTickCount();
return true;
@@ -135,32 +153,68 @@ class ERR_Roadblocks
}
return true;
}
protected bool CoverClear(vector point)
protected bool CoverClear(ERR_RoadblockProp prop)
{
vector point = prop.Position;
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)) return false;
for (int x = -1; x <= 1; x++)
for (int z = -1; z <= 1; z++)
{
vector sample = ERR_Actors.Ground(point + Vector(x * 1.5, 0, z * 1.5));
vector sample = ERR_Actors.Ground(point + prop.Right * (x * prop.HalfWidth) + prop.Forward * (z * prop.HalfDepth));
if (Math.AbsFloat(sample[1] - point[1]) > 0.3) return false;
}
// Planned props have separate local footprints. Exclude our own transaction
// from the enclosing world-axis trace, which otherwise falsely overlaps
// adjacent pieces at diagonal road headings. Still reject a moving guard.
foreach (IEntity builtEntity : m_BuildSpawned)
{
if (!ChimeraCharacter.Cast(builtEntity)) continue;
vector delta = builtEntity.GetOrigin() - point;
if (Math.AbsFloat(vector.Dot(delta, prop.Right)) < prop.HalfWidth + 0.6
&& Math.AbsFloat(vector.Dot(delta, prop.Forward)) < prop.HalfDepth + 0.6 && Math.AbsFloat(delta[1]) < 2) return false;
}
// Conservative world-axis enclosure of the rotated prop, including its feet.
float extentX = Math.AbsFloat(prop.Right[0]) * prop.HalfWidth + Math.AbsFloat(prop.Forward[0]) * prop.HalfDepth;
float extentZ = Math.AbsFloat(prop.Right[2]) * prop.HalfWidth + Math.AbsFloat(prop.Forward[2]) * prop.HalfDepth;
TraceBox trace = new TraceBox(); trace.Start = point + "0 0.15 0"; trace.End = trace.Start + "0 0.01 0";
trace.Mins = "-1.5 0 -1.5"; trace.Maxs = "1.5 2 1.5"; trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
trace.Mins = Vector(-extentX, 0, -extentZ); trace.Maxs = Vector(extentX, prop.Height, extentZ); trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
trace.ExcludeArray = m_BuildSpawned;
return GetGame().GetWorld().TraceMove(trace, null) == 1;
}
protected void Layout(out array<ref ERR_RoadblockProp> covers)
{
covers.Clear();
// Footprints enclose the native model bounds, including small placement margins.
for (int side = -1; side <= 1; side += 2)
{
// Two long sandbag walls shelter each pair from the road and its approach.
AddProp(covers, COVER, side * m_Shoulder, 0, m_Right, 1.9, 0.7, 1);
AddProp(covers, COVER, side * (m_Shoulder + 2.8), side * 2.8, m_Forward, 1.9, 0.7, 1);
// Opposing, staggered stop lines: the inner ends leave a >=4m centre lane.
AddProp(covers, BARRIER, side * 3.3, side * 8, m_Forward, 1.2, 0.4, 1.3);
AddProp(covers, OBSTACLE, side * (m_Shoulder + 4), side * 8, m_Forward, 1, 1, 1.6);
AddProp(covers, SIGN, side * 3, side * 12, m_Forward * (side * 1.0), 0.45, 0.4, 0.8);
}
}
protected void AddProp(array<ref ERR_RoadblockProp> covers, ResourceName prefab, float across, float along, vector forward, float halfWidth, float halfDepth, float height)
{
vector point = ERR_Actors.Ground(m_Centre + m_Right * across + m_Forward * along);
covers.Insert(new ERR_RoadblockProp(prefab, point, forward, halfWidth, halfDepth, height));
}
protected void Rollback(array<IEntity> spawned, SCR_AIGroup group)
{
foreach (IEntity entity : spawned) ERR_Actors.DeleteSpawned(entity);
ERR_Actors.DeleteSpawned(group);
}
// Start a transaction; physical pieces are created separately in BuildPost.
protected bool SpawnPost(array<vector> positions, array<vector> covers)
protected bool SpawnPost(array<vector> positions, array<ref ERR_RoadblockProp> covers)
{
if (!PlayersClear() || m_Mode.Forces().Count() >= 250 || m_BuildGroup || positions.Count() != 4 || covers.Count() != 2) return false;
if (!PlayersClear() || m_Mode.Forces().Count() >= 250 || m_BuildGroup || positions.Count() != 4 || covers.Count() != 10) return false;
m_BuildGroup = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", m_Centre));
if (!m_BuildGroup) return false;
m_BuildGroup.SetFaction(m_Mode.EnemyFaction()); m_BuildGroup.SetSpawnImmediately(false); m_BuildGroup.SetDeleteWhenEmpty(false); m_BuildGroup.SetPermanentLOD(0);
m_BuildPositions.Copy(positions); m_BuildCovers.Copy(covers);
m_BuildPositions.Copy(positions); m_BuildCovers.Clear();
foreach (ERR_RoadblockProp plannedProp : covers) m_BuildCovers.Insert(plannedProp);
m_BuildActors = 0; m_BuildCoverCount = 0;
return true;
}
@@ -188,9 +242,9 @@ class ERR_Roadblocks
}
if (m_BuildCoverCount < m_BuildCovers.Count())
{
vector coverPosition = m_BuildCovers[m_BuildCoverCount];
if (!CoverClear(coverPosition)) { CancelPost(); return; }
IEntity cover = ERR_Actors.Spawn(COVER, coverPosition, m_Forward.VectorToAngles());
ERR_RoadblockProp prop = m_BuildCovers[m_BuildCoverCount];
if (!CoverClear(prop)) { CancelPost(); return; }
IEntity cover = ERR_Actors.Spawn(prop.Prefab, prop.Position, prop.Angles);
if (!cover) { CancelPost(); return; }
m_BuildSpawned.Insert(cover); m_BuildCoverCount++;
return;
@@ -210,26 +264,26 @@ class ERR_Roadblocks
m_BuildGroup.AddWaypoint(m_BuildHold);
m_Mode.RegisterGroup(m_BuildGroup); m_BuildGroup.ActivateAI();
m_Mining.Register(m_BuildGroup, m_BuildHold, m_Right);
PrintFormat("Reintroduced [roadblocks]: %1 at %2 guards=4 roadPassage=open HQ=registered", id, m_Centre);
PrintFormat("Reintroduced [roadblocks]: %1 at %2 guards=4 props=10 roadPassage=open HQ=registered", id, m_Centre);
m_BuildSpawned.Clear(); m_BuildGroup = null; m_BuildHold = null;
m_BuildPositions.Clear(); m_BuildCovers.Clear();
m_NextSearch = System.GetTickCount() + 2000;
}
// 0: reject, 1: awaiting native tile, 2: ready.
protected int Plan(out array<vector> positions, out array<vector> covers)
protected int Plan(out array<vector> positions, out array<ref ERR_RoadblockProp> covers)
{
positions.Clear(); covers.Clear();
NavmeshWorldComponent mesh = GetGame().GetAIWorld().GetNavmeshWorldComponent("Soldiers");
if (!mesh || !PlayersClear()) return 0;
Layout(covers);
foreach (ERR_RoadblockProp prop : covers)
if (!CoverClear(prop)) return 0;
map<int, vector> reserved = new map<int, vector>();
for (int side = -1; side <= 1; side += 2)
{
vector cover = ERR_Actors.Ground(m_Centre + m_Right * (side * m_Shoulder));
if (!CoverClear(cover)) return 0;
covers.Insert(cover);
for (int row = -1; row <= 1; row += 2)
{
vector point = ERR_Actors.Ground(m_Centre + m_Right * (side * (m_Shoulder + 3)) + m_Forward * (row * 3));
vector point = ERR_Actors.Ground(m_Centre + m_Right * (side * (m_Shoulder + 2.5)) + m_Forward * (side * (row - 1.5)));
if (!mesh.IsTileLoaded(point)) { mesh.LoadTileIn(point); return 1; }
vector reachable;
if (!mesh.GetReachablePoint(point, 1, reachable) || vector.DistanceXZ(point, reachable) > 1 || !m_Placement.Clear(point, reserved)) return 0;
@@ -260,7 +314,7 @@ class ERR_Roadblocks
{
m_Ready = true; m_Background = true; m_Definition = definition;
m_NextReport = System.GetTickCount() + 60000;
GetGame().GetCallqueue().CallLater(BackgroundStep, 500, true);
GetGame().GetCallqueue().CallLater(BackgroundStep, 200, true);
ERR_HQ.Event("ROADBLOCKS", Status() + " Initial search yielded; remaining posts will continue in background.");
return ERR_PrepareResult.READY;
}
@@ -285,7 +339,8 @@ class ERR_Roadblocks
}
return;
}
array<vector> positions = {}, covers = {};
array<vector> positions = {};
array<ref ERR_RoadblockProp> covers = {};
int plan = Plan(positions, covers);
if (plan == 1 && System.GetTickCount() - m_CandidateStarted < 2000) return;
if (plan == 2)
@@ -318,7 +373,7 @@ class ERR_Roadblocks
if (now >= m_NextReport)
{
m_NextReport = now + 60000;
PrintFormat("Reintroduced [roadblocks background]: placed=%1/%2 attempts=%3 steps=%4 workMs=%5 maxStepMs=%6; still searching safe locations", m_Positions.Count(), m_Target, m_Attempts, m_BackgroundSteps, m_BackgroundWork, m_MaxBackgroundStep);
PrintFormat("Reintroduced [roadblocks background]: placed=%1/%2 attempts=%3 steps=%4 workMs=%5 maxStepMs=%6 candidates=%7 plans=%8; still searching safe locations", m_Positions.Count(), m_Target, m_Attempts, m_BackgroundSteps, m_BackgroundWork, m_MaxBackgroundStep, m_Candidates, m_Planned);
}
}
void Stop()