diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeRoadblocksProbe.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeRoadblocksProbe.c index 2b4840d..b291c50 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeRoadblocksProbe.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeRoadblocksProbe.c @@ -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"); diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c index 8ce5eea..27a6344 100644 --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c @@ -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 units, array 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 resources = {COVER, BARRIER, OBSTACLE, SIGN, "{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", ERR_Resources.DefendWaypoint()}; + array 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 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 positions, array 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; diff --git a/docs/configuration.md b/docs/configuration.md index 5d1a7fd..8634f44 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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 diff --git a/docs/ordinary-game-startup.md b/docs/ordinary-game-startup.md index 7d05eb7..dc1753f 100644 --- a/docs/ordinary-game-startup.md +++ b/docs/ordinary-game-startup.md @@ -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. diff --git a/tests/test_roadblock_background.py b/tests/test_roadblock_background.py index d00c473..48d334b 100644 --- a/tests/test_roadblock_background.py +++ b/tests/test_roadblock_background.py @@ -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 #include @@ -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 m_BuildPositions,m_Positions;Arraym_BuildCovers;Arraym_BuildSpawned,m_Groups;Arraym_Roster={"one","two"}; @@ -136,31 +138,31 @@ struct Build{ void Rollback(Array&entities,Node*group){rolledBack+=entities.Count()+(group?1:0);} '''+s+r''' }; -int main(){Array positions={1,2,3,4};Arraycovers(10); +int main(){Array positions={1,2,3,4};Arraycovers(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;piecereplica.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;imissing=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;iprops;l.Layout(props);assert(props.size()==10); + l.m_Shoulder=std::max(width*.5+1.5,4.);Arrayprops;l.Layout(props);assert(props.size()==18); std::mapcounts;std::vectorbarriers; 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