Add configurable main-road checkpoints and reconnect replacements; record Workshop 0.0.136
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-11-01 18:22:38 +01:00
parent 0e1e7790f9
commit b8dfa9184e
40 changed files with 2818 additions and 112 deletions
+2 -2
View File
@@ -55,7 +55,7 @@ assert 'Apply_planeReconEnabled' not in names # Saved-state toggle pair, not a
assert len(names) == len(set(names)), 'Menu widget names must be unique'
for key in keys:
assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key, 'Help_' + key))
if key not in ('radioIntelSiteCount','droneReconCount','droneCooldownSeconds','sarkaAngerPerCar','emptyRestartHours','aiDoctrine','aiShadow','motorbikeCount','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','fieldCampCount','fieldHospitalCount','startMode','garageVehicleCount','garageChancePercent','garageRadiusMetres','garageSpawnIntervalSeconds','civilianTrafficCount','roadblockCount','hunterSpawnDistanceMetres','objectGuardCount','objectGuardEvery','objectGuardHQMode','startWeaponCount','startMagazineCount'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names
if key not in ('radioIntelSiteCount','droneReconCount','droneCooldownSeconds','sarkaAngerPerCar','emptyRestartHours','aiDoctrine','aiShadow','motorbikeCount','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','fieldCampCount','fieldHospitalCount','startMode','garageVehicleCount','garageChancePercent','garageRadiusMetres','garageSpawnIntervalSeconds','civilianTrafficCount','roadblockCount','largeRoadblockCount','mainRoadWidthMetres','hunterSpawnDistanceMetres','objectGuardCount','objectGuardEvery','objectGuardHQMode','startWeaponCount','startMagazineCount'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names
else: assert 'Random_startMode' not in names
for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'):
assert name in names
@@ -106,7 +106,7 @@ int main() {
{"intelRevealChancePercent","intel reveal"},{"radioIntelSiteCount","intel radiosites"},{"startTimeMinutes","mission time"},
{"timeMultiplier","mission speed"},{"startDateCode","mission date"},
{"weatherProfile","mission weather"},{"viewDistanceMetres","mission viewdistance"},{"reportIntervalSeconds","hq interval"},
{"planeAltitudeMetres","mission planealtitude"},{"planeReplacementSeconds","mission planereplacement"},{"planeSectorSeconds","mission planesector"},{"fieldCampCount","mission camps"},{"fieldHospitalCount","mission hospitals"},{"objectGuardCount","mission objectguards"},{"objectGuardEvery","mission objectevery"},{"objectGuardHQMode","mission objecthq"},{"hunterSpawnDistanceMetres","mission hunterdistance"},{"roadblockCount","mission roadblocks"},{"civilianTrafficCount","mission traffic"},{"heliTimerMinutes","hq heli timer"},{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"},{"convoyCount","hq convoy count"},{"antiAirChancePercent","hq aa"},{"antiPersonnelMineChancePercent","hq mines"}};
{"planeAltitudeMetres","mission planealtitude"},{"planeReplacementSeconds","mission planereplacement"},{"planeSectorSeconds","mission planesector"},{"fieldCampCount","mission camps"},{"fieldHospitalCount","mission hospitals"},{"objectGuardCount","mission objectguards"},{"objectGuardEvery","mission objectevery"},{"objectGuardHQMode","mission objecthq"},{"hunterSpawnDistanceMetres","mission hunterdistance"},{"roadblockCount","mission roadblocks"},{"largeRoadblockCount","mission largeroadblocks"},{"mainRoadWidthMetres","mission mainroadwidth"},{"civilianTrafficCount","mission traffic"},{"heliTimerMinutes","hq heli timer"},{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"},{"convoyCount","hq convoy count"},{"antiAirChancePercent","hq aa"},{"antiPersonnelMineChancePercent","hq mines"}};
for(auto pair:cases){
assert(Fields::Command(pair.first," 42 ")==pair.second+" set 42");
assert(Fields::Command(pair.first,"Random")==pair.second+" set Random");
+4
View File
@@ -67,6 +67,10 @@ int main(){
bool recognized=(section=="intel"&&option=="reveal")||(section=="mission"&&option=="radio");
assert(CoreSetting(section,option).empty()==!recognized);
}
assert(CoreSetting("mission","largeroadblocks")=="largeRoadblockCount");
assert(CoreSetting("mission","mainroadwidth")=="mainRoadWidthMetres");
assert(Valid("largeRoadblockCount",0)&&Valid("largeRoadblockCount",30)&&!Valid("largeRoadblockCount",31)&&!Valid("largeRoadblockCount",-1));
assert(Valid("mainRoadWidthMetres",5)&&Valid("mainRoadWidthMetres",6)&&Valid("mainRoadWidthMetres",12)&&!Valid("mainRoadWidthMetres",4)&&!Valid("mainRoadWidthMetres",13));
int parsed=-1;
for(std::string malformed:{"", "-1", "+1", "1.0", "10junk", "1e2", "0x10", " 50", "50 ", "1234567"})
assert(!Integer(malformed,parsed));
+37 -9
View File
@@ -1,16 +1,19 @@
"""Production reconnect reservation/handoff with opaque native engine boundaries."""
from policy_harness import ROOT,body,run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerReconnect.c'
methods='\n'.join(body(p,k) for k in ['protected override void HandleDataExpiery(', 'override int GetReconnectTimeout(', 'override bool HandlePlayerDisconnect(', 'override bool HandlePlayerReconnect('])
methods='\n'.join(body(p,k) for k in ['protected void PrepareReplacement(', 'protected override void HandleDataExpiery(', 'override int GetReconnectTimeout(', 'override bool HandlePlayerDisconnect(', 'override bool HandlePlayerReconnect('])
methods=methods.replace('protected ', '').replace('notnull ', '').replace('override ', '').replace('super.', 'Native::').replace('null','nullptr')
for kind in ['ERR_GameMode','ERR_PlayerLife','SCR_ReconnectData','IEntity']:
import re
methods=re.sub(r'\b'+kind+r' (\w+)\b',kind+r'* \1',methods)
for obj in ['mode','life','data']:methods=methods.replace(obj+'.',obj+'->')
for kind in ['ERR_GameMode','ERR_ReconnectRules','SCR_PlayerIdentityUtils']:methods=methods.replace(kind+'.',kind+'::')
for obj in ['mode','life','data','orphan']:methods=methods.replace(obj+'.',obj+'->')
for kind in ['ERR_GameMode','ERR_ReconnectRules','SCR_PlayerIdentityUtils','ERR_PlayerLifeRules','ERR_Actors']:methods=methods.replace(kind+'.',kind+'::')
methods=methods.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('GetPlayerManager().','GetPlayerManager()->').replace('Players().','Players()->')
# Value-initialize locals as Enforce does.
methods=methods.replace('int owner;', 'int owner=0;')
roster_path=p.with_name('ERR_PlayerRoster.c')
replacement='\n'.join(body(roster_path,k) for k in ['bool AllowReconnectReplacement(', 'void Spawned(', 'void Disconnect('])
replacement=replacement.replace('IEntity actor','IEntity* actor').replace('Replication.IsServer()', 'true').replace('ERR_PlayerLife life', 'ERR_PlayerLife* life').replace('life.', 'life->').replace('ERR_PlayerLifeRules.', 'ERR_PlayerLifeRules::').replace('ERR_Actors.', 'ERR_Actors::').replace('null','nullptr').replace('m_OnChanged.Invoke(playerId);','').replace('m_Connections.Remove(playerId);','')
rule=body(p,'static int Timeout(').replace('Math.Max','std::max')
run(r'''
#include <cassert>
@@ -20,9 +23,11 @@ struct UUID {int value=0;bool IsNull(){return value==0;}bool operator==(UUID b)c
struct Time {int seconds=0;Time PlusSeconds(int s){return {seconds+s};}};
struct IEntity {bool alive=true;int equipment=937,health=41,seat=3;};
struct SCR_ReconnectData {Time m_ValidUntil;IEntity* m_ReservedEntity=nullptr;};
struct ERR_PlayerLife {UUID AccountId{7};int ConnectionId=1;bool Connected=true,UsedLife=true;IEntity* Actor=nullptr;};
struct Roster {ERR_PlayerLife life;bool valid=true;ERR_PlayerLife* CurrentActor(IEntity* a){return valid&&a&&life.Actor==a&&life.Connected&&life.UsedLife?&life:nullptr;}ERR_PlayerLife* Get(int){return valid?&life:nullptr;}};
struct ERR_GameMode {bool active=true;Roster roster;static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}bool Active(){return active;}Roster* Players(){return &roster;}};
struct ERR_PlayerLife {UUID AccountId{7};int ConnectionId=1;bool Connected=true,UsedLife=true,ReconnectPending=false;IEntity* Actor=nullptr;};
struct ERR_PlayerLifeRules {static bool CanEnter(bool used,int phase){return !used&&phase>=1&&phase<=3;}};
struct ERR_Actors {static bool Alive(IEntity* a){return a&&a->alive;}};
struct Roster {ERR_PlayerLife life;bool valid=true;ERR_PlayerLife* CurrentActor(IEntity* a){return valid&&a&&life.Actor==a&&life.Connected&&life.UsedLife?&life:nullptr;}ERR_PlayerLife* Get(int){return valid?&life:nullptr;}'''+replacement+r'''};
struct ERR_GameMode {bool active=true;int phase=1;int Phase(){return phase;}Roster roster;static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}bool Active(){return active;}Roster* Players(){return &roster;}};
ERR_GameMode* ERR_GameMode::current;
struct Game {IEntity* controlled=nullptr;int owner=0;Time now{100};Game* GetPlayerManager(){return this;}Game* GetWorld(){return this;}Time GetTimestamp(){return now;}
IEntity* GetPlayerControlledEntity(int){return controlled;}int GetPlayerIdFromControlledEntity(IEntity*){return owner;}} game;
@@ -52,16 +57,39 @@ int main(){
SCR_PlayerIdentityUtils::id={9};assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.m_mReconnectData.values.size()==1);SCR_PlayerIdentityUtils::id={7};
game.owner=2;assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.deleted==0);game.owner=0; // never steal/delete another controlled body
game.controlled=&body;assert(reconnect.HandlePlayerDisconnect(1,0));game.controlled=&foreign;assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.m_mReconnectData.values.size()==1);game.controlled=nullptr;
reconnect.apply=false;assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.m_mReconnectData.values.size()==1&&reconnect.data.m_ValidUntil.seconds==1000);
reconnect.apply=true;game.now.seconds=1001;assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.deleted==1&&reconnect.m_mReconnectData.values.empty());
mode.roster.life.ReconnectPending=true;reconnect.apply=false;assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.m_mReconnectData.values.size()==1&&reconnect.data.m_ValidUntil.seconds==1000);assert(mode.roster.life.UsedLife&&mode.roster.life.ReconnectPending);
mode.roster.life.ReconnectPending=true;reconnect.apply=true;game.now.seconds=1001;assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.deleted==1&&reconnect.m_mReconnectData.values.empty());
assert(!mode.roster.life.UsedLife&&!mode.roster.life.ReconnectPending&&mode.roster.life.Actor==nullptr);
mode.roster.life.UsedLife=true;mode.roster.life.ReconnectPending=true;mode.roster.life.Actor=&body;
body.alive=true;game.controlled=&body;assert(reconnect.HandlePlayerDisconnect(1,0));game.controlled=nullptr;body.alive=false;
assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.deleted==1); // dead body left for corpse/loot systems, never revived
assert(!mode.roster.life.UsedLife&&!mode.roster.life.ReconnectPending);
mode.roster.life.UsedLife=true;mode.roster.life.Actor=&body;
body.alive=true;game.controlled=&body;assert(reconnect.HandlePlayerDisconnect(1,0));game.controlled=nullptr;mode.roster.life.Actor=&foreign;
assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.deleted==1); // stale round/body rejected
mode.roster.life.Actor=&body;game.controlled=&body;mode.roster.life.UsedLife=false;assert(!reconnect.HandlePlayerDisconnect(1,0));mode.roster.life.UsedLife=true;
assert(reconnect.HandlePlayerDisconnect(1,0));game.controlled=nullptr;mode.active=false;assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.GetReconnectTimeout()==120);
mode.active=true;game.owner=1;int deleted=reconnect.deleted;reconnect.HandleDataExpiery(&reconnect.data);assert(reconnect.deleted==deleted&&body.alive);game.owner=0;
mode.active=true;reconnect.configured=0;game.controlled=&body;assert(reconnect.HandlePlayerDisconnect(1,0));assert(reconnect.data.m_ValidUntil.seconds==0);
// Production roster lifecycle arms only disconnected accounts, clears on assignment.
mode.roster.Spawned(1,&body);assert(!mode.roster.life.ReconnectPending);
mode.roster.Disconnect(1);assert(mode.roster.life.ReconnectPending&&!mode.roster.life.Connected);
assert(!mode.roster.AllowReconnectReplacement(1,1));mode.roster.life.Connected=true;
mode.roster.Spawned(1,&body);assert(!mode.roster.life.ReconnectPending);
// Missing reservation grants exactly one new admission, only on reconnect.
reconnect.m_mReconnectData.values.clear();game.controlled=nullptr;mode.roster.life.Actor=nullptr;
assert(!reconnect.HandlePlayerReconnect(1));assert(mode.roster.life.UsedLife); // duplicate audit after death
mode.roster.life.ReconnectPending=true;
mode.phase=4;assert(!reconnect.HandlePlayerReconnect(1));assert(mode.roster.life.UsedLife); // extraction
mode.phase=1;assert(!reconnect.HandlePlayerReconnect(1));assert(!mode.roster.life.UsedLife);
assert(!reconnect.HandlePlayerReconnect(1));assert(!mode.roster.life.ReconnectPending);
// A living body without a reservation is cleaned up before granting new admission.
mode.roster.life.UsedLife=true;mode.roster.life.ReconnectPending=true;mode.roster.life.Actor=&body;body.alive=true;
game.owner=2;assert(!reconnect.HandlePlayerReconnect(1));assert(body.alive&&mode.roster.life.UsedLife);
game.owner=0;deleted=reconnect.deleted;assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.deleted==deleted+1&&!mode.roster.life.UsedLife);
// Lost entity inside an existing reservation is also eligible.
mode.roster.life.UsedLife=true;mode.roster.life.ReconnectPending=true;reconnect.data.m_ReservedEntity=nullptr;
reconnect.m_mReconnectData.Set({7},&reconnect.data);assert(!reconnect.HandlePlayerReconnect(1));assert(!mode.roster.life.UsedLife);
}
''')
print('PASS: original body/equipment retained past old timeout; expiry, death, canonical account/body, occupied controller, failed handoff and no replacement life')
print('PASS: original body/equipment retained past old timeout; expiry, death, canonical account/body, occupied controller, failed handoff, missing/dead/expired body replacement and extraction/duplicate-audit guards')
+23 -18
View File
@@ -8,6 +8,7 @@ s=s.replace('protected ', '').replace('ERR_ScenarioDefinition definition', 'Defi
s=s.replace('array<vector> positions = {};', 'Array<int> positions;').replace('array<ref ERR_RoadblockProp> covers = {};', 'Array<int> covers;')
s=s.replace('GetGame().GetCallqueue().CallLater(BackgroundStep, 200, true)', 'queue.Schedule(200)').replace('GetGame().GetCallqueue().Remove(BackgroundStep)', 'queue.Remove()')
s=s.replace('GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High)', 'Bounds(m_Low, m_High)').replace('GetGame().GetPlayerManager().GetPlayerCount()', 'players')
s=s.replace('m_Mode.Settings().Get("largeRoadblockCount")', 'm_Mode->largeTarget').replace('m_Mode.Settings().Get("mainRoadWidthMetres")', '6')
s=s.replace('m_Mode.Settings().Get("roadblockCount")', 'm_Mode->target').replace('m_Mode.Forces().Count()', 'm_Mode->ledger')
s=s.replace('m_Mode.', 'm_Mode->').replace('System.GetTickCount()', 'Now()').replace('Replication.IsServer()', 'server').replace('ERR_GameMode.Get()', 'current')
s=s.replace('ERR_PrepareResult.', 'Result::').replace('ERR_Phase.', 'Phase::').replace('ERR_RoadblockRules.', 'Rules::').replace('ERR_TerrainSites.Bounds', 'ValidBounds').replace('ERR_HQ.Event','Event').replace('Math.Max','std::max').replace('null','nullptr')
@@ -28,14 +29,14 @@ int Now(){return clockMs;}bool GetGame(){return true;}void Bounds(int&,int&){}bo
template<class...T>void PrintFormat(T...){}void Event(string,string){}
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}void Clear(){this->clear();}};
struct Queue{int scheduled=0,removed=0;bool active=false;void Schedule(int ms){assert(ms==200);scheduled++;active=true;}void Remove(){removed++;active=false;}};
struct Mode{int target=30,ledger=0,phase=Phase::PRISON;bool active=true;int Phase(){return phase;}bool Active(){return active;}};Mode* current=nullptr;
struct Mode{int target=30,largeTarget=0,ledger=0,phase=Phase::PRISON;bool active=true;int Phase(){return phase;}bool Active(){return active;}};Mode* current=nullptr;
struct Definition{};
struct Mining{bool stopped=false;void Stop(){stopped=true;}};
struct Scheduler{
Mode* m_Mode;Definition* m_Definition=nullptr;Queue queue;Mining m_Mining;
bool m_BuildGroup=false;void BuildPost(){if(m_Mode->ledger>=250)CancelPost();}void CancelPost(){m_BuildGroup=false;}
bool m_Begun=false,m_Ready=false,m_Stopped=false,m_HaveCandidate=false,m_Background=false;
int m_Target=0,m_Attempts=0,m_Started=0,m_CandidateStarted=0,m_Candidates=0,m_Planned=0;
int m_SmallTarget=0,m_LargeTarget=0,m_MainRoadWidth=0,m_Target=0,m_Attempts=0,m_Started=0,m_CandidateStarted=0,m_Candidates=0,m_Planned=0;
int m_NextSearch=0,m_NextReport=0,m_BackgroundSteps=0,m_BackgroundWork=0,m_MaxBackgroundStep=0,m_Low=0,m_High=0;
Array<int>m_Loaded,m_Positions,m_Groups;
int candidateCost=0,candidateCalls=0,planCalls=0,spawnCalls=0;bool accept=false,spawn=true,roster=true;int plan=2;
@@ -110,7 +111,7 @@ s=s.replace('IEntity entity', 'Node* entity').replace('RplComponent 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','ERR_RoadblockRules'):s=s.replace(ns+'.',ns+'::')
rule=body(p,'static int PropCount(')
rule=body(p,'static int PropCount(')+body(p,'static int GuardCount(')
run(r'''
#include <cassert>
#include <string>
@@ -124,13 +125,14 @@ struct Node{Replica replica;bool missing=false;Replica* FindComponent(){return m
namespace ERR_Actors{int calls=0,failAt=0;Array<Node*> all;Node* Spawn(string,int,int=0){calls++;if(calls==failAt)return nullptr;auto*n=new Node;all.Insert(n);return n;}bool SetFaction(Node*,int){return true;}}
namespace ERR_Resources{string DefendWaypoint(){return "hold";}}
namespace ERR_ForceKind{int CHECKPOINT=1;}
struct Ledger{int count=0,registered=0;bool accept=true;int Count(){return count;}bool Register(string,int,int,int strength,void*,Node*,int){assert(strength==4);if(!accept)return false;registered++;return true;}};
struct Ledger{int count=0,registered=0;bool accept=true;int Count(){return count;}bool Register(string,int,int,int strength,void*,Node*,int){assert(strength==4||strength==8);if(!accept)return false;registered++;return true;}};
struct Mode{Ledger ledger;int owned=0,groups=0;Ledger& Forces(){return ledger;}int EnemyFaction(){return 1;}void Own(Node*n){assert(n);owned++;}void RegisterGroup(Node*){groups++;}};
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{
bool m_Large=false;int m_SmallPlaced=0,m_LargePlaced=0;
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"};
int m_BuildActors=0,m_BuildCoverCount=0,m_Centre=10,m_Right=0,m_NextSearch=0,rolledBack=0;string COVER="cover";bool playersClear=true,coversClear=true;
@@ -138,31 +140,34 @@ 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(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);
int main(){for(bool large:{false,true}){
const int guards=ERR_RoadblockRules::GuardCount(large);Array<int> positions(guards,1);Array<Prop>covers(ERR_RoadblockRules::PropCount(large));const int pieces=guards+covers.Count()+1;
{Build b;b.m_Large=large;int start=ERR_Actors::calls;assert(b.SpawnPost(positions,covers));assert(ERR_Actors::calls==start+1);
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);
auto shared=b.m_BuildSpawned;for(int i=guards;i<guards+covers.Count();i++)assert(shared[i]->replica.streaming);
b.BuildPost();for(int i=guards;i<guards+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);
assert(b.m_SmallPlaced==(large?0:1)&&b.m_LargePlaced==(large?1:0));
}
// A server-only or unregistered prop must never count as a complete roadblock.
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();
for(int bad=guards;bad<guards+covers.Count();bad++)for(bool missing:{false,true}){
Build b;b.m_Large=large;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
for(int i=guards;i<guards+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<=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);}
for(int stage=0;stage<=pieces;stage++){Build b;b.m_Large=large;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<pieces+1;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;b.m_Large=large;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<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);}
{Build b;b.m_Large=large;b.SpawnPost(positions,covers);b.m_Placement.clear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==1);}
{Build b;b.m_Large=large;b.SpawnPost(positions,covers);for(int i=0;i<guards;i++)b.BuildPost();b.coversClear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==guards+1);}
{Build b;b.m_Large=large;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.m_Large=large;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/all planned props/waypoint, and rolls back player approach, obstructions, spawn/replication/ledger failure and cancellation; all committed props bypass distance streaming')
print('PASS: actual staged post creation spawns at most one entity per callback, commits only all four/eight guards, all planned props and waypoint, and rolls back player approach, obstructions, spawn/replication/ledger failure and cancellation; all committed props bypass distance streaming')
+10 -9
View File
@@ -22,15 +22,15 @@ template<class T>struct Array:std::vector<T>{void Clear(){for(auto*p:*this)delet
struct Prop{string Prefab;Vec Position,Forward,Right;double HalfWidth,HalfDepth,Height;
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;
struct Checkpoint{Vec m_Centre={1000,50,2000},m_Forward,m_Right;double m_Shoulder;bool m_Large=false;
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;
for(bool large:{false,true})for(double width:{4.,6.,10.,16.})for(int degrees=0;degrees<360;degrees+=15){
Checkpoint l;l.m_Large=large;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='''+shoulder+r''';Array<Prop*>props;l.Layout(props);assert(props.size()==8);
l.m_Shoulder='''+shoulder+r''';Array<Prop*>props;l.Layout(props);assert(props.size()==(large?14:8));
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,18 +40,19 @@ int main(){
// Bound the compact footprint, including both road shoulders.
assert(std::abs(across)+reach <= std::max(width*.5+1.2,4.5)+1e-6);
double depth=std::abs(dot(p->Right,l.m_Forward))*p->HalfWidth+std::abs(dot(p->Forward,l.m_Forward))*p->HalfDepth;
assert(std::abs(along)+depth <= 8.9+1e-6);
assert(std::abs(along)+depth <= (large?21.4:8.9)+1e-6);
if(p->Prefab=="barrier")barriers.push_back(along);
if(p->Prefab=="sign")assert(dot(p->Forward,l.m_Forward)*along>0); // both approaches face outward
// Guard starting volumes cannot intersect any prop in the planned post.
for(int side:{-1,1})for(int row:{-1,1}){
for(int side:{-1,1})for(int row=0;row<(large?4:2);row++){
Vec guard=l.GuardPosition(side,row);
Vec rel=guard-p->Position;
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["sign"]==2&&counts.size()==3);
assert(barriers.size()==2&&std::abs(barriers[0]-barriers[1])>11.9);
assert(counts["cover"]==(large?8:4)&&counts["barrier"]==(large?4:2)&&counts["sign"]==2&&counts.size()==3);
assert(barriers.size()==(large?4:2));
if(!large)assert(std::abs(barriers[0]-barriers[1])>11.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;
@@ -66,7 +67,7 @@ int main(){
}
}
''')
print('PASS: shipped checkpoint layout preserves vehicle lane, opposing signs, staggered barriers, clear guard slots and separate props across 4–16m roads and all tested headings')
print('PASS: both shipped checkpoint layouts preserve vehicle lane, opposing signs, staggered barriers, clear guard slots and separate props across 4–16m roads and all tested headings')
# Execute the actual clearance path: owned pieces are excluded only after
# checking guards; terrain and foreign obstructions remain authoritative.
+20
View File
@@ -47,3 +47,23 @@ assert 'if (m_Target == 0) return Finish("disabled")' in s
assert 'm_Positions.Count() >= m_Target' in s
assert 'roadblockCount' not in body(settings,'static bool Randomizable(')
print('PASS: road width/bridge/length checks, prison and post spacing, setting bounds/default, finite startup and owned HQ admission')
classes = body(p, 'static bool LargeRoad(') + body(p, 'static bool Needed(')
run('#include <cassert>\n' + classes + r"""
int main(){
assert(!LargeRoad(5.99,6) && LargeRoad(6,6) && LargeRoad(16,6));
assert(!LargeRoad(6,8) && LargeRoad(8,8));
assert(Needed(false,0,2,1,1) && !Needed(true,0,2,1,1));
assert(!Needed(false,2,2,0,1) && Needed(true,2,2,0,1));
assert(!Needed(false,0,0,0,1) && Needed(true,0,0,0,1));
assert(Needed(false,0,1,0,0) && !Needed(true,0,1,0,0));
assert(!Needed(false,0,0,0,0) && !Needed(true,0,0,0,0));
}
""")
assert 'ERR_RoadblockRules.Needed(m_Large, m_SmallPlaced, m_SmallTarget, m_LargePlaced, m_LargeTarget)' in body(p,'protected bool Candidate(')
print('PASS: exact configurable width cutoff, separate quotas and independent disabling')
keys=set(re.findall(r'"([A-Za-z0-9]+)"',body(settings,'static array<string> Keys(')))
initialized=set(re.findall(r'm_Values.Set\("([A-Za-z0-9]+)"',body(settings,'void ERR_Settings(')))
assert keys <= initialized, keys-initialized
print('PASS: every saved setting has a fresh-profile constructor default')
+1 -1
View File
@@ -5,7 +5,7 @@ from policy_harness import ROOT,body,run
spec=importlib.util.spec_from_file_location('import_settings',BASE/'tools/import_settings.py')
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
result,deferred=m.merge({'configVersion':1,'reportIntervalSeconds':20.0,'supportSquadLimit':2,'responseRadiusMetres':1500,'convoyCount':3},{'aiIntelChancePercent':0,'intelRevealChancePercent':100})
assert result==dict(telemetryEnabled=1,droneReconCount=0,droneCooldownSeconds=300,aiDoctrine=0,aiShadow=0,planeAltitudeMetres=250,planeReplacementSeconds=60,planeSectorSeconds=120,planeReconEnabled=1,roadblockCount=4,civilianTrafficCount=3,garageVehicleCount=16,garageChancePercent=70,garageRadiusMetres=1000,garageSpawnIntervalSeconds=5,startMode=0,keepWeaponOnUnconscious=1,selfEpinephrine=0,restartOnEmpty=1,emptyRestartHours=5,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioIntelSiteCount=5,showAllSites=0,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
assert result==dict(telemetryEnabled=1,droneReconCount=0,droneCooldownSeconds=300,aiDoctrine=0,aiShadow=0,planeAltitudeMetres=250,planeReplacementSeconds=60,planeSectorSeconds=120,planeReconEnabled=1,roadblockCount=4,largeRoadblockCount=4,mainRoadWidthMetres=6,civilianTrafficCount=3,garageVehicleCount=16,garageChancePercent=70,garageRadiusMetres=1000,garageSpawnIntervalSeconds=5,startMode=0,keepWeaponOnUnconscious=1,selfEpinephrine=0,restartOnEmpty=1,emptyRestartHours=5,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioIntelSiteCount=5,showAllSites=0,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
assert deferred==[]
for count in (0,1,3,20):
values,notes=m.merge(ai={'convoyCount':count})