Add configurable main-road checkpoints and reconnect replacements; record Workshop 0.0.136
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user