Files
jakub d5464f7438
continuous-integration/drone/push Build is passing
Reorganize mission menu and separate design from logic
2026-10-17 18:27:31 +02:00

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(CORE/'ERR_AdminMenuFields.c',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')