Expand roadblocks with defensive bays and checkpoint equipment

This commit is contained in:
2026-10-16 17:34:11 +02:00
parent f15c915e54
commit 96f471ef4a
6 changed files with 55 additions and 39 deletions
@@ -162,7 +162,7 @@ modded class ERR_GameMode
}
if (posts.ERR_ProbeBackgroundPending()) return;
if (!posts.ERR_ProbeCompleteRoadblocks() || posts.ERR_ProbePostCount() <= m_ERRRoadblockInitial) { ERR_RoadblockProbeFail("physical four-guard posts, replicated props, ledger and HQ admission"); return; }
PrintFormat("ERR SMOKE PASS: roadblocks %1 physical posts / %2 guards / %3 replicated checkpoint props with distance streaming disabled; verified model footprints and centre lane; background completed after prison unlock, no extra posts on repeat tick", posts.ERR_ProbePostCount(), posts.ERR_ProbePostCount() * 4, posts.ERR_ProbePostCount() * 10);
PrintFormat("ERR SMOKE PASS: roadblocks %1 physical posts / %2 guards / %3 replicated checkpoint props with distance streaming disabled; verified model footprints and centre lane; background completed after prison unlock, no extra posts on repeat tick", posts.ERR_ProbePostCount(), posts.ERR_ProbePostCount() * 4, posts.ERR_ProbePostCount() * ERR_RoadblockRules.PropCount());
ERR_StopRoadblockProbe();
Transition(ERR_Phase.DEFEAT);
Print("ERR SMOKE COMPLETE: roadblocks background completion on Everon; scripted local fixture");
@@ -4,6 +4,7 @@ class ERR_RoadblockRules
{
// Admission never waits longer for a high count. Remaining posts are built
// by the same safe placement path in background, including after prison unlock.
static int PropCount() { return 18; }
static int SearchMilliseconds() { return 12000; }
static int SearchAttempts() { return 300; }
static bool Road(float width, float length, float heightDifference)
@@ -62,6 +63,8 @@ class ERR_Roadblocks
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";
static const ResourceName SHELTER = "{01AE5FD77A9A4C21}Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et";
static const ResourceName TABLE = "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.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); }
@@ -90,7 +93,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, BARRIER, OBSTACLE, SIGN, "{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", ERR_Resources.DefendWaypoint()};
array<ResourceName> resources = {COVER, BARRIER, OBSTACLE, SIGN, SHELTER, TABLE, "{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", ERR_Resources.DefendWaypoint()};
foreach (ResourceName actor : m_Roster) resources.Insert(actor);
foreach (ResourceName prefab : resources)
{
@@ -187,15 +190,23 @@ class ERR_Roadblocks
// 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.
// Two front walls, an approach wall and an outer return form
// a sheltered bay for each pair. The rear stays open for guard access.
AddProp(covers, COVER, side * m_Shoulder, -2, m_Right, 1.9, 0.7, 1);
AddProp(covers, COVER, side * m_Shoulder, 2, m_Right, 1.9, 0.7, 1);
AddProp(covers, COVER, side * (m_Shoulder + 2.8), side * 4.8, m_Forward, 1.9, 0.7, 1);
AddProp(covers, COVER, side * (m_Shoulder + 5.6), side * 2, m_Right, 1.9, 0.7, 1);
// Paired stop-line barriers funnel traffic into a >=4m centre passage.
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, BARRIER, side * 5.9, side * 8, m_Forward, 1.2, 0.4, 1.3);
AddProp(covers, OBSTACLE, side * (m_Shoulder + 4.5), 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);
}
// A small inspection/rest area behind one bay, away from the vehicle lane.
AddProp(covers, TABLE, m_Shoulder + 3, -4.8, m_Forward, 0.9, 0.7, 1.1);
AddProp(covers, SHELTER, m_Shoulder + 3, -8.5, m_Forward, 1.2, 2.4, 1.4);
}
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);
@@ -209,7 +220,7 @@ class ERR_Roadblocks
// Start a transaction; physical pieces are created separately in BuildPost.
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() != 10) return false;
if (!PlayersClear() || m_Mode.Forces().Count() >= 250 || m_BuildGroup || positions.Count() != 4 || covers.Count() != ERR_RoadblockRules.PropCount()) 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);
@@ -225,20 +236,20 @@ class ERR_Roadblocks
m_BuildPositions.Clear(); m_BuildCovers.Clear();
}
// A free Game Master camera does not force-stream bare, non-editable props
// with the nearby editable guards. Keep this bounded bank (<=300 objects) on
// with the nearby editable guards. Keep this bounded bank (<=540 props) on
// clients, including late joiners, rather than tying it to the player's body.
// Do not report a completed post if any physical prop lacks a network node.
protected bool ShareProps()
{
if (m_BuildCovers.Count() != 10 || m_BuildSpawned.Count() != 15) return false;
for (int i = 4; i < 14; i++)
if (m_BuildCovers.Count() != ERR_RoadblockRules.PropCount() || m_BuildSpawned.Count() != m_BuildPositions.Count() + m_BuildCovers.Count() + 1) return false;
for (int i = m_BuildPositions.Count(); i < m_BuildPositions.Count() + m_BuildCovers.Count(); i++)
{
IEntity entity = m_BuildSpawned[i];
RplComponent component;
if (entity) component = RplComponent.Cast(entity.FindComponent(RplComponent));
if (!component || component.Id() == RplId.Invalid()) return false;
}
for (int i = 4; i < 14; i++)
for (int i = m_BuildPositions.Count(); i < m_BuildPositions.Count() + m_BuildCovers.Count(); i++)
{
RplComponent component = RplComponent.Cast(m_BuildSpawned[i].FindComponent(RplComponent));
component.EnableStreaming(false);
@@ -290,7 +301,7 @@ 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 props=10 networkedProps=10 propStreaming=always roadPassage=open HQ=registered", id, m_Centre);
PrintFormat("Reintroduced [roadblocks]: %1 at %2 guards=4 props=%3 networkedProps=%3 propStreaming=always roadPassage=open HQ=registered", id, m_Centre, m_BuildCovers.Count());
m_BuildSpawned.Clear(); m_BuildGroup = null; m_BuildHold = null;
m_BuildPositions.Clear(); m_BuildCovers.Clear();
m_NextSearch = System.GetTickCount() + 2000;
+8 -5
View File
@@ -44,17 +44,20 @@ in `Escapists.json`: **0–30**, default **4**; **0 disables** them. The chat eq
is `/esc mission roadblocks set N` (also `show` and `help`). Changes apply on the
next round/restart, leaving current units and cover in place.
These are simple roadside checkpoints: two sandbag positions and four soldiers
from the selected enemy faction. They use normal equipment rules and physical
Each checkpoint has two defensive bays built from eight sandbag walls, four
approach barriers, two anti-vehicle obstacles, two stop signs, a small rest tent
and an inspection table. Four soldiers come from the selected enemy faction. They use normal equipment rules and physical
radio reporting to VirtualHQ; HQ can send their squad to respond to contacts.
They do not reveal player locations to HQ merely by being created. Soldiers and
cover are owned by the round; killed guards are not replaced.
Placement uses the loaded terrain's road network and Soldiers navmesh before
player admission. Posts stay at least 600 m apart, 700 m from the prison and any
Placement uses the loaded terrain's road network and Soldiers navmesh after
terrain/site preparation. The initial search yields after 12 seconds; unfinished
placement continues in small background steps while players are connected,
including while the prison is locked and after escape. Posts stay at least 600 m apart, 700 m from the prison and any
controlled player body, 150 m from mission sites and outside settlement centres.
Elevated bridge segments, narrow roads, steep or obstructed shoulders are rejected.
Cover stays outside the travelled road to preserve vehicle passage. The requested
All prop footprints preserve a straight vehicle passage at least four metres wide. The requested
count is a maximum: a bounded search skips unsuitable posts and logs placed versus
requested totals instead of preventing mission startup. The admin Status page
also shows these totals. This is an interim checkpoint system, independent of the
+2 -2
View File
@@ -121,7 +121,7 @@ Before requesting `System.MakeScreenshot("$logs:err-ordinary-prison")`, the help
Roadblock props remain replicated independently of player distance so an admin
viewing the island through Game Master can see the same barriers as nearby
players. This is limited to the ten props per post (at most 300); guard and
vehicle streaming is unchanged. A post is counted only after all ten props have
players. This is limited to the 18 props per post (at most 540); guard and
vehicle streaming is unchanged. A post is counted only after all 18 props have
valid replication nodes. Local Workbench validation does not by itself prove
remote-client visibility.
+16 -14
View File
@@ -109,7 +109,8 @@ s=s.replace('m_BuildGroup.', 'm_BuildGroup->').replace('m_BuildHold.', 'm_BuildH
s=s.replace('IEntity entity', 'Node* entity').replace('RplComponent component;', 'Replica* component=nullptr;').replace('RplComponent component =', 'Replica* component =')
s=s.replace('RplComponent.Cast', 'Replica::Cast').replace('FindComponent(RplComponent)', 'FindComponent()').replace('RplId.Invalid()', '-1')
s=s.replace('entity.FindComponent', 'entity->FindComponent').replace('m_BuildSpawned[i].FindComponent', 'm_BuildSpawned[i]->FindComponent').replace('component.', 'component->')
for ns in ('ERR_Actors','ERR_Resources','ERR_ForceKind'):s=s.replace(ns+'.',ns+'::')
for ns in ('ERR_Actors','ERR_Resources','ERR_ForceKind','ERR_RoadblockRules'):s=s.replace(ns+'.',ns+'::')
rule=body(p,'static int PropCount(')
run(r'''
#include <cassert>
#include <string>
@@ -128,6 +129,7 @@ struct Mode{Ledger ledger;int owned=0,groups=0;Ledger& Forces(){return ledger;}i
struct Placement{bool clear=true;bool Clear(int,void*){return clear;}};
struct Mining{int registered=0;void Register(Node*,Node*,int){registered++;}};
struct Prop{string Prefab="cover";int Position=5,Angles=0;};
namespace ERR_RoadblockRules{'''+rule+r'''}
struct Build{
Mode m_Mode;Placement m_Placement;Mining m_Mining;Node* m_BuildGroup=nullptr;Node* m_BuildHold=nullptr;
Array<int> m_BuildPositions,m_Positions;Array<Prop>m_BuildCovers;Array<Node*>m_BuildSpawned,m_Groups;Array<string>m_Roster={"one","two"};
@@ -136,31 +138,31 @@ struct Build{
void Rollback(Array<Node*>&entities,Node*group){rolledBack+=entities.Count()+(group?1:0);}
'''+s+r'''
};
int main(){Array<int> positions={1,2,3,4};Array<Prop>covers(10);
int main(){Array<int> positions={1,2,3,4};Array<Prop>covers(ERR_RoadblockRules::PropCount());const int pieces=4+covers.Count()+1;
{Build b;int start=ERR_Actors::calls;assert(b.SpawnPost(positions,covers));assert(ERR_Actors::calls==start+1);
for(int piece=0;piece<15;piece++){int calls=ERR_Actors::calls;b.BuildPost();assert(ERR_Actors::calls==calls+1&&b.m_Positions.Count()==0&&b.m_Mode.owned==0);}
auto shared=b.m_BuildSpawned;for(int i=4;i<14;i++)assert(shared[i]->replica.streaming);
b.BuildPost();for(int i=4;i<14;i++)assert(!shared[i]->replica.streaming);assert(!b.m_BuildGroup&&b.m_Positions.Count()==1&&b.m_Groups.Count()==1&&b.m_Mode.owned==16&&b.m_Mode.groups==1&&b.m_Mining.registered==1&&b.m_NextSearch==2100);
for(int piece=0;piece<pieces;piece++){int calls=ERR_Actors::calls;b.BuildPost();assert(ERR_Actors::calls==calls+1&&b.m_Positions.Count()==0&&b.m_Mode.owned==0);}
auto shared=b.m_BuildSpawned;for(int i=4;i<4+covers.Count();i++)assert(shared[i]->replica.streaming);
b.BuildPost();for(int i=4;i<4+covers.Count();i++)assert(!shared[i]->replica.streaming);assert(!b.m_BuildGroup&&b.m_Positions.Count()==1&&b.m_Groups.Count()==1&&b.m_Mode.owned==pieces+1&&b.m_Mode.groups==1&&b.m_Mining.registered==1&&b.m_NextSearch==2100);
}
// A server-only or unregistered prop must never count as a complete roadblock.
for(int bad=4;bad<14;bad++)for(bool missing:{false,true}){
Build b;b.SpawnPost(positions,covers);for(int i=0;i<15;i++)b.BuildPost();
auto pieces=b.m_BuildSpawned;pieces[bad]->missing=missing;pieces[bad]->replica.id=-1;
b.BuildPost();assert(b.m_Positions.Count()==0&&!b.m_BuildGroup&&b.m_Mode.owned==0&&b.rolledBack==16);
for(int i=4;i<14;i++)assert(pieces[i]->replica.streaming); // reject before changing any streaming rule
for(int bad=4;bad<4+covers.Count();bad++)for(bool missing:{false,true}){
Build b;b.SpawnPost(positions,covers);for(int i=0;i<pieces;i++)b.BuildPost();
auto spawned=b.m_BuildSpawned;spawned[bad]->missing=missing;spawned[bad]->replica.id=-1;
b.BuildPost();assert(b.m_Positions.Count()==0&&!b.m_BuildGroup&&b.m_Mode.owned==0&&b.rolledBack==pieces+1);
for(int i=4;i<4+covers.Count();i++)assert(spawned[i]->replica.streaming); // reject before changing any streaming rule
}
// Player approaches at any stage: no registered post and every owned piece removed.
for(int stage=0;stage<=15;stage++){Build b;assert(b.SpawnPost(positions,covers));for(int i=0;i<stage;i++)b.BuildPost();b.playersClear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.m_Positions.Count()==0&&b.m_Mode.owned==0&&b.rolledBack==stage+1);}
for(int stage=0;stage<=pieces;stage++){Build b;assert(b.SpawnPost(positions,covers));for(int i=0;i<stage;i++)b.BuildPost();b.playersClear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.m_Positions.Count()==0&&b.m_Mode.owned==0&&b.rolledBack==stage+1);}
// Any native entity creation failure rolls back rather than consuming a post.
for(int stage=0;stage<16;stage++){Build b;ERR_Actors::failAt=ERR_Actors::calls+stage+1;bool begun=b.SpawnPost(positions,covers);
for(int stage=0;stage<pieces+1;stage++){Build b;ERR_Actors::failAt=ERR_Actors::calls+stage+1;bool begun=b.SpawnPost(positions,covers);
if(stage==0)assert(!begun);else{for(int i=0;i<stage;i++)b.BuildPost();assert(b.rolledBack==stage);}
assert(!b.m_BuildGroup&&b.m_Positions.Count()==0&&b.m_Mode.owned==0);ERR_Actors::failAt=0;
}
{Build b;b.SpawnPost(positions,covers);b.m_Placement.clear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==1);}
{Build b;b.SpawnPost(positions,covers);for(int i=0;i<4;i++)b.BuildPost();b.coversClear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==5);}
{Build b;b.SpawnPost(positions,covers);for(int i=0;i<15;i++)b.BuildPost();b.m_Mode.ledger.accept=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==16&&b.m_Positions.Count()==0);}
{Build b;b.SpawnPost(positions,covers);for(int i=0;i<pieces;i++)b.BuildPost();b.m_Mode.ledger.accept=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==pieces+1&&b.m_Positions.Count()==0);}
{Build b;b.SpawnPost(positions,covers);b.BuildPost();b.CancelPost();assert(!b.m_BuildGroup&&b.rolledBack==2&&b.m_Mode.owned==0);}
for(auto*n:ERR_Actors::all)delete n;
}
''')
print('PASS: actual staged post creation spawns at most one entity per callback, commits only all four guards/ten props/waypoint, and rolls back player approach, obstructions, spawn/replication/ledger failure and cancellation; all ten committed props bypass distance streaming')
print('PASS: actual staged post creation spawns at most one entity per callback, commits only all four guards/all planned props/waypoint, and rolls back player approach, obstructions, spawn/replication/ledger failure and cancellation; all committed props bypass distance streaming')
+5 -5
View File
@@ -20,14 +20,14 @@ struct Prop{string Prefab;Vec Position,Forward,Right;double HalfWidth,HalfDepth,
Prop(string p,Vec v,Vec f,double w,double d,double h):Prefab(p),Position(v),Forward(f),Right({f.z,0,-f.x}),HalfWidth(w),HalfDepth(d),Height(h){}
};
struct Checkpoint{Vec m_Centre={1000,50,2000},m_Forward,m_Right;double m_Shoulder;
string COVER="cover",BARRIER="barrier",OBSTACLE="obstacle",SIGN="sign";
string COVER="cover",BARRIER="barrier",OBSTACLE="obstacle",SIGN="sign",TABLE="table",SHELTER="shelter";
'''+s+r'''
};
int main(){
for(double width:{4.,6.,10.,16.})for(int degrees=0;degrees<360;degrees+=15){
Checkpoint l;double angle=degrees*3.141592653589793/180;
l.m_Forward={std::sin(angle),0,std::cos(angle)};l.m_Right={std::cos(angle),0,-std::sin(angle)};
l.m_Shoulder=std::max(width*.5+1.5,4.);Array<Prop*>props;l.Layout(props);assert(props.size()==10);
l.m_Shoulder=std::max(width*.5+1.5,4.);Array<Prop*>props;l.Layout(props);assert(props.size()==18);
std::map<string,int>counts;std::vector<double>barriers;
for(auto*p:props){counts[p->Prefab]++;
Vec delta=p->Position-l.m_Centre;double across=dot(delta,l.m_Right),along=dot(delta,l.m_Forward);
@@ -40,11 +40,11 @@ int main(){
for(int side:{-1,1})for(int row:{-1,1}){
Vec guard=l.m_Centre+l.m_Right*(side*(l.m_Shoulder+2.5))+l.m_Forward*(side*(row-1.5));
Vec rel=guard-p->Position;
assert(std::abs(dot(rel,p->Right))>p->HalfWidth+.45 || std::abs(dot(rel,p->Forward))>p->HalfDepth+.45);
assert(std::abs(dot(rel,p->Right))>p->HalfWidth+.6 || std::abs(dot(rel,p->Forward))>p->HalfDepth+.6);
}
}
assert(counts["cover"]==4&&counts["barrier"]==2&&counts["obstacle"]==2&&counts["sign"]==2);
assert(barriers.size()==2&&std::abs(barriers[0]-barriers[1])>15.9);
assert(counts["cover"]==8&&counts["barrier"]==4&&counts["obstacle"]==2&&counts["sign"]==2&&counts["table"]==1&&counts["shelter"]==1);
assert(barriers.size()==4&&std::abs(barriers[0]-barriers[2])>15.9);
// No physical prop footprint intersects another (separating axes).
for(size_t i=0;i<props.size();i++)for(size_t j=i+1;j<props.size();j++){
auto*a=props[i];auto*b=props[j];bool separate=false;