Add persistent last-disconnect and idle-interval controls with enabled/five-hour defaults. Apply saved settings immediately, reset idle time while occupied, and cover migration, routing, layout and restart behavior.
172 lines
13 KiB
Python
172 lines
13 KiB
Python
"""Actual start setting parse/save and admin command bodies; native IO/UI are doubled.
|
|
Frozen-mode lifecycle itself is tested by the separate core start fixture.
|
|
"""
|
|
import os,re
|
|
from pathlib import Path
|
|
from policy_harness import ROOT,body,run
|
|
BASE=Path(os.environ.get('ERR_START_SETTINGS_ROOT',ROOT));CORE=BASE/'addons/core/Scripts/Game/ERR/00_Core'
|
|
S=Path(os.environ.get('ERR_SETTINGS_SOURCE', CORE/'ERR_Settings.c'));A=CORE/'ERR_AdminService.c';M=CORE/'ERR_AdminMenu.c'
|
|
def cpp(s):
|
|
s=s.replace('protected ','').replace('out int ','int& ').replace('out map<string, int> values','Values& values').replace('new map<string, int>()','Values()').replace('map<string, int>','Values')
|
|
s=s.replace('array<string> parts','Array<string> parts').replace('array<string> replies','Array<string>& replies').replace('array<string>','Array<string>').replace('foreach (','for (')
|
|
s=s.replace('JsonLoadContext json = new JsonLoadContext();','JsonLoadContext json;').replace('JsonSaveContext json = new JsonSaveContext();','JsonSaveContext json;').replace('JsonLoadContext json,','JsonLoadContext& json,')
|
|
s=s.replace('ERR_GameMode mode','Mode* mode').replace('mode.','mode->').replace('Settings().','Settings()->').replace('Math.Round','std::round')
|
|
for n in ['ERR_Settings','ERR_ProfileConfig','ERR_CommandArguments','ERR_AdminMenuFields','ERR_TimeService','ERR_EnvironmentService','Replication','FileIO']:s=s.replace(n+'.',n+'::')
|
|
s=s.replace('"0123456789".IndexOf','string("0123456789").IndexOf')
|
|
for n in ['value','saved','current']:s=s.replace(n+'.ToString()','string(std::to_string('+n+'))')
|
|
return s
|
|
settings='\n'.join(cpp(body(S,sig)) for sig in ['static array<string> Keys(','static bool Valid(','static bool Randomizable(','static bool ValidSelection(','static string SelectionText(','static int AddedDefault(','protected bool Read(','static bool Parse(','bool Set('])
|
|
args='\n'.join(cpp(body(A,sig)) for sig in ['static bool Integer(','static bool Selection('])
|
|
fields='\n'.join(cpp(body(M,sig)) for sig in ['static string Display(','static string StartSummary('])
|
|
command=cpp(body(A,'protected bool StartCommand('))
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <map>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}int Length(){return size();}int ToInt(){return std::stoi(*this);}int IndexOf(string x){auto n=find(x);return n==npos?-1:int(n);}string Substring(int a,int n){return substr(a,n);}};
|
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}};
|
|
struct Values{std::map<string,int> data;int Get(string k){return data.at(k);}void Set(string k,int v){data[k]=v;}void Remove(string k){data.erase(k);}};
|
|
std::map<string,std::map<string,float>> disk;bool failSave=false;
|
|
struct JsonLoadContext{std::map<string,float> data;bool DoesKeyExist(string k){return data.count(k);}bool ReadValue(string k,float& v){if(!data.count(k))return false;v=data[k];return true;}};
|
|
struct JsonSaveContext{std::map<string,float> data;void EnableTypeDiscriminator(bool){}void WriteValue(string k,int v){data[k]=v;}bool SaveToFile(string p){if(failSave)return false;disk[p]=data;return true;}};
|
|
namespace ERR_ProfileConfig{bool Load(string p,JsonLoadContext& j){if(!disk.count(p))return false;j.data=disk[p];return true;}bool Save(string p,JsonSaveContext&j){return j.SaveToFile(p);}bool Exists(string p){return disk.count(p);}}
|
|
namespace FileIO{bool CopyFile(string a,string b){if(!disk.count(a))return false;disk[b]=disk[a];return true;}void DeleteFile(string p){disk.erase(p);}}
|
|
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
|
struct ERR_Settings{static constexpr auto PATH="settings";string m_Path=PATH;bool m_ReadOnly=false;Values m_Values,m_Resolved;int Get(string k){return m_Values.Get(k);}
|
|
'''+settings+r'''
|
|
};
|
|
namespace ERR_TimeService{bool ParseClock(string,int&){return false;}string Clock(int){return "clock";}}
|
|
namespace ERR_EnvironmentService{bool ParseDate(string,int&){return false;}bool ParseProfile(string,int&){return false;}string Date(int){return "date";}string ProfileName(int){return "weather";}}
|
|
struct ERR_CommandArguments{
|
|
'''+args+r'''
|
|
};
|
|
struct ERR_AdminMenuFields{
|
|
'''+fields+r'''
|
|
};
|
|
struct Mode{ERR_Settings* settings;int frozen;ERR_Settings*Settings(){return settings;}int StartMode(){return frozen;}};
|
|
struct Admin{
|
|
'''+command+r'''
|
|
};
|
|
int main(){
|
|
assert(ERR_Settings::AddedDefault("startMode")==0);
|
|
assert(!ERR_Settings::Randomizable("startMode")&&!ERR_Settings::ValidSelection("startMode",-1)&&!ERR_Settings::ValidSelection("startMode",-2));
|
|
disk["old"]={{"reportIntervalSeconds",10},{"supportSquadLimit",4},{"responseRadiusMetres",2000},{"aiIntelChancePercent",60},{"intelRevealChancePercent",15},{"radioDefenceSeconds",180}};
|
|
ERR_Settings settings;assert(settings.Read("old",settings.m_Values)&&settings.Get("startMode")==0);
|
|
// Older configurations gain the enabled/five-hour defaults. Each restart
|
|
// setting survives a save/reload, while invalid selections cannot change it.
|
|
for(string key:{"restartOnEmpty","emptyRestartHours"}){
|
|
int expected=key=="restartOnEmpty"?1:5;
|
|
assert(ERR_Settings::AddedDefault(key)==expected&&settings.Get(key)==expected);
|
|
assert(!ERR_Settings::Randomizable(key));
|
|
for(float bad:{-2.f,-1.f,.5f,169.f}){
|
|
disk["bad"]=disk["old"];disk["bad"][key]=bad;Values ignored;
|
|
assert(!settings.Read("bad",ignored));
|
|
}
|
|
int choice=key=="restartOnEmpty"?0:168;
|
|
assert(settings.Set(key,choice));
|
|
ERR_Settings restored;assert(restored.Read("settings",restored.m_Values));
|
|
assert(restored.Get(key)==choice);
|
|
failSave=true;assert(!settings.Set(key,expected));failSave=false;
|
|
assert(settings.Get(key)==choice);
|
|
assert(settings.Set(key,expected));
|
|
}
|
|
int restartSelection=-1;
|
|
assert(ERR_CommandArguments::Selection("restartOnEmpty","on",restartSelection)&&restartSelection==1);
|
|
assert(ERR_CommandArguments::Selection("restartOnEmpty","off",restartSelection)&&restartSelection==0);
|
|
for(float bad:{-1.f,-2.f,2.f,.5f}){disk["bad"]=disk["old"];disk["bad"]["startMode"]=bad;Values ignored;assert(!settings.Read("bad",ignored));}
|
|
|
|
// Newly introduced doctrine choices migrate safely and survive the same transaction.
|
|
for(string key:{"aiDoctrine","aiShadow"}){
|
|
assert(ERR_Settings::AddedDefault(key)==0&&settings.Get(key)==0);
|
|
assert(!ERR_Settings::Randomizable(key));
|
|
for(float bad:{-2.f,-1.f,2.f,.5f}){
|
|
disk["bad"]=disk["old"];disk["bad"][key]=bad;Values ignored;
|
|
assert(!settings.Read("bad",ignored));
|
|
}
|
|
assert(settings.Set(key,1));ERR_Settings restored;
|
|
assert(restored.Read("settings",restored.m_Values)&&restored.Get(key)==1);
|
|
failSave=true;assert(!settings.Set(key,0)&&settings.Get(key)==1);failSave=false;
|
|
assert(!settings.Set(key,2)&&!settings.Set(key,-1)&&settings.Get(key)==1);
|
|
assert(settings.Set(key,0));
|
|
}
|
|
int doctrine=-1;
|
|
assert(ERR_CommandArguments::Selection("aiDoctrine","current",doctrine)&&doctrine==0);
|
|
assert(ERR_CommandArguments::Selection("aiDoctrine","coordinated",doctrine)&&doctrine==1);
|
|
assert(ERR_CommandArguments::Selection("aiShadow","off",doctrine)&&doctrine==0);
|
|
assert(ERR_CommandArguments::Selection("aiShadow","on",doctrine)&&doctrine==1);
|
|
for(string key:{"aiDoctrine","aiShadow"})
|
|
for(string invalid:{"random","random-day","enabled","currentjunk","coordinatedjunk","1.0","-1"})
|
|
assert(!ERR_CommandArguments::Selection(key,invalid,doctrine));
|
|
assert(ERR_AdminMenuFields::Display("aiDoctrine","0")=="Current");
|
|
assert(ERR_AdminMenuFields::Display("aiDoctrine","1")=="Coordinated");
|
|
assert(ERR_AdminMenuFields::Display("aiShadow","0")=="Off");
|
|
assert(ERR_AdminMenuFields::Display("aiShadow","1")=="On");
|
|
assert(settings.Get("fieldCampCount")==12&&settings.Get("fieldHospitalCount")==6);
|
|
for(string key:{"fieldCampCount","fieldHospitalCount"}){
|
|
int maximum=key=="fieldCampCount"?16:8;
|
|
assert(!settings.Set(key,0)&&!settings.Set(key,maximum+1));
|
|
assert(settings.Set(key,maximum));ERR_Settings savedSites;
|
|
assert(savedSites.Read("settings",savedSites.m_Values)&&savedSites.Get(key)==maximum);
|
|
}
|
|
assert(settings.Get("startWeaponCount")==5&&settings.Get("startMagazineCount")==8);
|
|
for(string key:{"startWeaponCount","startMagazineCount"}){
|
|
int maximum=key=="startWeaponCount"?12:48;
|
|
assert(!ERR_Settings::Randomizable(key));
|
|
for(float bad:{-1.f,float(maximum+1),.5f}){
|
|
disk["bad"]=disk["old"];disk["bad"][key]=bad;Values ignored;assert(!settings.Read("bad",ignored));
|
|
}
|
|
assert(!settings.Set(key,-1)&&!settings.Set(key,maximum+1));
|
|
for(int count:{0,1,maximum}){
|
|
assert(settings.Set(key,count));ERR_Settings restored;
|
|
assert(restored.Read("settings",restored.m_Values)&&restored.Get(key)==count);
|
|
}
|
|
failSave=true;assert(!settings.Set(key,0)&&settings.Get(key)==maximum);failSave=false;
|
|
}
|
|
assert(settings.Get("objectGuardCount")==8);
|
|
assert(!ERR_Settings::Randomizable("objectGuardCount"));
|
|
assert(!settings.Set("objectGuardCount",-1)&&!settings.Set("objectGuardCount",17));
|
|
for(int guards:{0,1,8,16}){
|
|
assert(settings.Set("objectGuardCount",guards));
|
|
ERR_Settings savedGuards;assert(savedGuards.Read("settings",savedGuards.m_Values)&&savedGuards.Get("objectGuardCount")==guards);
|
|
}
|
|
failSave=true;assert(!settings.Set("objectGuardCount",8)&&settings.Get("objectGuardCount")==16);failSave=false;
|
|
assert(settings.Get("hunterSpawnDistanceMetres")==1000);
|
|
assert(!ERR_Settings::Randomizable("hunterSpawnDistanceMetres"));
|
|
assert(!settings.Set("hunterSpawnDistanceMetres",799)&&!settings.Set("hunterSpawnDistanceMetres",3001));
|
|
assert(settings.Set("hunterSpawnDistanceMetres",1800));
|
|
ERR_Settings savedRadius;assert(savedRadius.Read("settings",savedRadius.m_Values)&&savedRadius.Get("hunterSpawnDistanceMetres")==1800);
|
|
failSave=true;assert(!settings.Set("hunterSpawnDistanceMetres",1000)&&settings.Get("hunterSpawnDistanceMetres")==1800);failSave=false;
|
|
assert(ERR_Settings::AddedDefault("objectGuardEvery")==3 && ERR_Settings::AddedDefault("objectGuardHQMode")==1);
|
|
for(int every=1;every<=4;every++) assert(settings.Set("objectGuardEvery",every));
|
|
for(int mode=0;mode<=2;mode++) assert(settings.Set("objectGuardHQMode",mode));
|
|
assert(!settings.Set("objectGuardEvery",0)&&!settings.Set("objectGuardEvery",5));
|
|
assert(!settings.Set("objectGuardHQMode",-1)&&!settings.Set("objectGuardHQMode",3));
|
|
ERR_Settings guardsReloaded;assert(guardsReloaded.Read("settings",guardsReloaded.m_Values));
|
|
assert(guardsReloaded.Get("objectGuardEvery")==4&&guardsReloaded.Get("objectGuardHQMode")==2);
|
|
Mode mode{&settings,0};Admin admin;Array<string> replies;
|
|
assert(!admin.StartCommand(&mode,{"mission","radio","show"},replies));
|
|
assert(admin.StartCommand(&mode,{"mission","start","set","crash"},replies));
|
|
assert(settings.Get("startMode")==1&&mode.StartMode()==0&&disk["settings"]["startMode"]==1);
|
|
assert(replies.back().find("Next start: Crash | Current round: Prison")!=string::npos);
|
|
assert(replies.back().find("Saved for next round")!=string::npos);
|
|
ERR_Settings restarted;assert(restarted.Read("settings",restarted.m_Values));Mode next{&restarted,restarted.Get("startMode")};assert(next.StartMode()==1);
|
|
auto saved=disk["settings"];failSave=true;admin.StartCommand(&mode,{"mission","start","set","prison"},replies);assert(settings.Get("startMode")==1&&disk["settings"]==saved&&mode.StartMode()==0);failSave=false;
|
|
for(string bad:{"random","random-day","2","-1","0.5","crashjunk"}){admin.StartCommand(&mode,{"mission","start","set",bad},replies);assert(settings.Get("startMode")==1&&disk["settings"]==saved);}
|
|
admin.StartCommand(&mode,{"mission","start","set","prison","extra"},replies);assert(settings.Get("startMode")==1);
|
|
Replication::server=false;admin.StartCommand(&mode,{"mission","start","set","prison"},replies);assert(settings.Get("startMode")==1);Replication::server=true;
|
|
settings.m_ReadOnly=true;admin.StartCommand(&mode,{"mission","start","set","prison"},replies);assert(settings.Get("startMode")==1);settings.m_ReadOnly=false;
|
|
admin.StartCommand(&mode,{"mission","start","set","0"},replies);assert(settings.Get("startMode")==0&&mode.StartMode()==0);
|
|
admin.StartCommand(&next,{"mission","start","show"},replies);assert(replies.back()=="Next start: Crash | Current round: Crash");
|
|
}
|
|
''')
|
|
assert 'm_Values.Set("startMode", AddedDefault("startMode"));' in S.read_text()
|
|
execute=body(A,'void Execute(');assert execute.index('if (!authorized)')<execute.index('StartCommand(mode, parts, replies)')
|
|
assert 'StartSummary(' in body(M,'void ReceiveSnapshot(')
|
|
assert 'm_Mode.StartMode()' in body(M,'void ReceiveSnapshot(')
|
|
assert 'ERR_Settings.Randomizable(key)' in body(M,'override void OnMenuOpen(')
|
|
assert 'mission start show|set prison|crash' in body(A,'protected void Help(')
|
|
print('PASS: actual start defaults/parse/save, failure retention and authorized command wiring; current/next feedback, aliases and no random choice; native UI and core mode freezing are separate')
|