Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
Concurrent automatic-equipment production changes added automaticEquipmentChancePercent as a text-command-only setting plus the hq equipment alias. This test-only proposal keeps all 14 menu controls and the exact hidden-key set assertion, explicitly rejects the new key in menu Command/Available, and verifies the actual HQ text alias. No production bytes changed. Eleven settings/admin programs pass; ai_stinger_admin requires no change. Logs and run.py retained here. Proposal consists of test_admin_menu.py and test_admin_providers.py only.
|
||||
@@ -0,0 +1,11 @@
|
||||
import subprocess,os
|
||||
from pathlib import Path
|
||||
r=Path('/home/jakub/devel/escapistsrereintroduced');p=Path('/tmp/erri-mine-resupply-1724/compat');env=os.environ|{'PYTHONPATH':str(r/'tests'),'ERR_CONVOY_SETTINGS_ROOT':str(r)}
|
||||
names=sorted({x.name for x in (r/'tests').glob('test_admin*.py')}|{x.name for x in (r/'tests').glob('test*settings*.py')}|{'test_ai_stinger_admin.py','test_intel_admin.py'})
|
||||
failed=[]
|
||||
for name in names:
|
||||
f=p/'tests'/name
|
||||
if not f.exists():f=r/'tests'/name
|
||||
x=subprocess.run(['python',str(f)],env=env,text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT);(p/(name+'.log')).write_text(x.stdout);print(name,x.returncode)
|
||||
if x.returncode:failed.append(name);print(x.stdout[-2500:])
|
||||
assert not failed,failed
|
||||
@@ -0,0 +1,2 @@
|
||||
PASS: actual menu command routing covers all fourteen settings, rejects unsupported keys, gates companions, and matches unique authored widgets
|
||||
PASS: explicit weapon retention buttons use the same ready/authorized server command path
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
PASS: actual admin body authorization, god restoration, vanilla item deposit, callback completion/failure, timeout quarantine and stop/revocation paths
|
||||
PASS: same-body revoke/regrant preserves pending insertions and timeout quarantine; new-body binding isolates late callbacks and never inherits god mode
|
||||
@@ -0,0 +1 @@
|
||||
PASS: mission provider registration is idempotent, optional dispatch is ordered, status aggregates, journals preserve cursors, aliases stay with their addons and topic help is bounded
|
||||
@@ -0,0 +1 @@
|
||||
PASS: actual current-controller/admin/mode authorization, per-round anti-replay, token-rotation refusal, command/snapshot throttles and client scope; owner-only replies and player-tool cleanup wiring
|
||||
+1
@@ -0,0 +1 @@
|
||||
PASS: Core document-discovery alias, companion-only alias exclusion, strict integer parsing and setting boundaries
|
||||
+1
@@ -0,0 +1 @@
|
||||
PASS: actual session Tick + journal router delivers each grant completion and new HQ line once, without repeats on empty/unavailable feeds or unauthorized ticks
|
||||
@@ -0,0 +1 @@
|
||||
PASS: actual Stinger show/set/help state precise actor-roll effects; read-only help, bounded values, malformed syntax and failed-save preservation; no rerolls/gameplay edits and no native persistence claim
|
||||
+1
@@ -0,0 +1 @@
|
||||
PASS: actual read migrates old valid settings, preserves explicit disabled state, rejects invalid data; next-round feedback and fixed menu remain honest
|
||||
@@ -0,0 +1 @@
|
||||
PASS: actual Intel commands distinguish status/start/failure, preserve authorization gate and reject malformed requests; spent ledger is idempotent, server-only and bounded
|
||||
@@ -0,0 +1 @@
|
||||
PASS: random mode restrictions, full numeric ranges, independent calendar dates and strict daylight boundaries
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
PASS: legacy values preserved, unsupported fields reported, invalid data rejected, existing files protected, runtime bounds aligned
|
||||
PASS: failed initial settings promotion leaves defaults writable after restart; prior/fallback saves survive copy, readback and mismatch faults
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Exercise real menu routing/capability bodies and the authored control contract.
|
||||
|
||||
This does not claim native focus, rendering or multiplayer RPC coverage.
|
||||
"""
|
||||
import re,os
|
||||
from pathlib import Path
|
||||
BASE=Path(os.environ.get("ERR_CONVOY_SETTINGS_ROOT", Path(__file__).resolve().parents[1]))
|
||||
from policy_harness import ROOT, body, run
|
||||
source = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c'
|
||||
layout = ROOT / 'addons/core/UI/layouts/ERR/AdminMenu.layout'
|
||||
keys = re.findall(r'"([A-Za-z]+)"', body(source, 'static array<string> Keys('))
|
||||
settings = re.findall(r'"([A-Za-z]+)"', body(BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c', 'static array<string> Keys('))
|
||||
assert len(keys) == 14 and set(settings) - set(keys) == {"automaticConvoyEnabled", "heliTimerMinutes", "keepWeaponOnUnconscious", "automaticEquipmentChancePercent"}
|
||||
assert set(keys) <= set(settings) # New text-command setting does not alter the fixed native menu.
|
||||
# Native layouts place child widget declarations inside an anonymous Children
|
||||
# block. A missing brace can preserve most widget names yet prevent menu load.
|
||||
def validate_layout_structure(text):
|
||||
tokens = re.findall(r'"(?:[^"\\]|\\.)*"|[{}]|[^\s{}"]+', text)
|
||||
stack = []
|
||||
prefix = []
|
||||
for token in tokens:
|
||||
if token.endswith('WidgetClass'):
|
||||
assert not stack or stack[-1] == 'children', 'Widget outside parent Children block: ' + token
|
||||
if token == '{':
|
||||
kind = 'property'
|
||||
if any(part.endswith('WidgetClass') for part in prefix):
|
||||
kind = 'widget'
|
||||
elif not prefix:
|
||||
kind = 'children'
|
||||
stack.append(kind)
|
||||
prefix = []
|
||||
elif token == '}':
|
||||
assert stack, 'Unexpected closing brace'
|
||||
stack.pop()
|
||||
prefix = []
|
||||
else:
|
||||
prefix.append(token)
|
||||
assert not stack, 'Unclosed layout block'
|
||||
validate_layout_structure(layout.read_text())
|
||||
try:
|
||||
validate_layout_structure(layout.read_text().replace('Name "JournalState"', 'Name "JournalState" {', 1))
|
||||
except AssertionError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError('Malformed child structure must be rejected')
|
||||
names = re.findall(r'\bName "([^"]+)"', layout.read_text())
|
||||
assert 'RandomDay_startTimeMinutes' in names
|
||||
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, ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key, 'Help_' + key))
|
||||
for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'):
|
||||
assert name in names
|
||||
for name in re.findall(r'"([A-Za-z]+)"', body(source, 'override void OnMenuOpen(').split('foreach')[0]):
|
||||
assert name in names
|
||||
journal = body(source, 'static string JournalText(').replace('ERR_JournalText.Menu', 'ERR_JournalText::Menu')
|
||||
menu_format = body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_JournalText.c', 'static string Menu(')
|
||||
follow = body(source, 'static bool FollowJournal(')
|
||||
command = body(source, 'static string Command(')
|
||||
available = body(source, 'static bool Available(').replace('Keys().Contains(key)', 'Known(key)')
|
||||
known = ' || '.join('key == "' + key + '"' for key in keys)
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
struct string : std::string {
|
||||
using std::string::string;
|
||||
string(std::string s):std::string(s){}
|
||||
bool IsEmpty(){return empty();}
|
||||
int Length(){return size();}
|
||||
int IndexOf(string s){auto i=find(s);return i==npos?-1:int(i);}
|
||||
string Substring(int i,int n){return substr(i,n);}
|
||||
void Replace(string a,string b){size_t i=0;while((i=find(a,i))!=npos){replace(i,a.size(),b);i+=b.size();}}
|
||||
string Trim(){auto a=find_first_not_of(" \t\r\n");if(a==npos)return "";auto b=find_last_not_of(" \t\r\n");return substr(a,b-a+1);}
|
||||
};
|
||||
struct ERR_JournalText {
|
||||
''' + menu_format + r'''
|
||||
};
|
||||
struct Fields {
|
||||
static bool Known(string key) {return ''' + known + ';}\n' + command + '\n' + available + '\n' + follow + '\n' + journal + r'''
|
||||
};
|
||||
int main() {
|
||||
string raw="#24 t=7s M3 observe guard";
|
||||
assert(Fields::JournalText(raw)=="No. 24 t=7s M3 observe guard");
|
||||
assert(raw=="#24 t=7s M3 observe guard");
|
||||
assert(Fields::JournalText("unprefixed event")=="unprefixed event");
|
||||
assert(Fields::JournalText("#25 delivered #24")=="No. 25 delivered No. 24");
|
||||
assert(Fields::JournalText("").empty());
|
||||
assert(!Fields::FollowJournal(false,0.5,1000,400,false));
|
||||
assert(!Fields::FollowJournal(false,0,1000,400,false));
|
||||
assert(!Fields::FollowJournal(false,0.97,1000,400,false));
|
||||
assert(Fields::FollowJournal(false,1,1000,400,false));
|
||||
assert(Fields::FollowJournal(false,0,1000,400,true));
|
||||
assert(Fields::FollowJournal(false,0,200,400,false));
|
||||
assert(Fields::FollowJournal(true,0,0,400,false));
|
||||
std::vector<std::pair<string,string>> cases={
|
||||
{"radioDefenceSeconds","mission radio"},{"aiIntelChancePercent","intel carrying"},
|
||||
{"intelRevealChancePercent","intel reveal"},{"startTimeMinutes","mission time"},
|
||||
{"timeMultiplier","mission speed"},{"startDateCode","mission date"},
|
||||
{"weatherProfile","mission weather"},{"viewDistanceMetres","mission viewdistance"},{"reportIntervalSeconds","hq interval"},
|
||||
{"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");
|
||||
assert(Fields::Command(pair.first,"").empty());
|
||||
assert(Fields::Command(pair.first," \t ").empty());
|
||||
assert(Fields::Command(pair.first,string(33,'x')).empty());
|
||||
assert(Fields::Available(pair.first,true,true));
|
||||
}
|
||||
assert(Fields::Command("startTimeMinutes","14:25")=="mission time set 14:25");
|
||||
assert(Fields::Command("startDateCode","2026-09-19")=="mission date set 2026-09-19");
|
||||
assert(Fields::Command("weatherProfile","authored")=="mission weather set authored");
|
||||
for(string key:{"automaticEquipmentChancePercent","heliTimer","prisonDistance","stingerChance","mission abandon","","bad"}){
|
||||
assert(Fields::Command(key,"1").empty());assert(!Fields::Available(key,true,true));
|
||||
}
|
||||
for(string key:{"reportIntervalSeconds","supportSquadLimit","responseRadiusMetres","convoyCount","antiAirChancePercent","antiPersonnelMineChancePercent"}){
|
||||
assert(!Fields::Available(key,false,true));assert(Fields::Available(key,true,false));
|
||||
}
|
||||
assert(!Fields::Available("aiIntelChancePercent",true,false));
|
||||
assert(Fields::Available("aiIntelChancePercent",false,true));
|
||||
for(string key:{"intelRevealChancePercent","radioDefenceSeconds","startTimeMinutes","timeMultiplier","startDateCode","weatherProfile"}) assert(Fields::Available(key,false,false));
|
||||
}
|
||||
''')
|
||||
print('PASS: actual menu command routing covers all fourteen settings, rejects unsupported keys, gates companions, and matches unique authored widgets')
|
||||
|
||||
assert all(name in names for name in ('KeepWeaponOn', 'KeepWeaponOff', 'Saved_keepWeaponOnUnconscious'))
|
||||
click=body(source,'protected void OnButton(')
|
||||
assert 'm_Controller.ERR_AdminSubmit("mission keepweapon set " + retain, true)' in click
|
||||
assert click.index('!m_Ready') < click.index('"mission keepweapon set "')
|
||||
print('PASS: explicit weapon retention buttons use the same ready/authorized server command path')
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
"""Exercise actual registration/dispatch bodies without requiring optional addons.
|
||||
Native script compilation and authenticated RPC tests remain separate checks.
|
||||
"""
|
||||
from policy_harness import ROOT, body, run
|
||||
core = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c'
|
||||
hq = ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAdminProvider.c'
|
||||
intel = ROOT/'addons/core/Scripts/Game/ERR/20_Intel/ERR_IntelAdminProvider.c'
|
||||
source=core.read_text()
|
||||
assert 'ERR_HQ' not in source and 'ERR_IntelService' not in source
|
||||
assert 'modded class' not in (ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_AdminCommands.c').read_text()
|
||||
assert 'Admin().Register(new ERR_HQAdminProvider(hq))' in (ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c').read_text()
|
||||
assert 'Admin().Register(new ERR_IntelAdminProvider())' in (ROOT/'addons/core/Scripts/Game/ERR/20_Intel/ERR_IntelService.c').read_text()
|
||||
execute=body(core,'void Execute(')
|
||||
assert execute.index('if (!Replication.IsServer()') < execute.index('if (!authorized)') < execute.index('Extension(mode, arguments, replies)')
|
||||
assert 'arguments = "mission " + arguments' in execute
|
||||
|
||||
def translate(s):
|
||||
return (s.replace('protected ', '').replace('override ', '')
|
||||
.replace('array<string>', 'Array<string>&').replace('out int ', 'int& ')
|
||||
.replace('ERR_AdminProvider provider', 'Provider* provider').replace('ERR_GameMode mode','Mode* mode')
|
||||
.replace('foreach (Provider* provider : m_Providers)', 'for (Provider* provider : m_Providers)')
|
||||
.replace('ERR_CommandArguments.CoreSetting(', 'CoreSetting(')
|
||||
.replace('provider.', 'provider->').replace('m_Providers[i].', 'm_Providers[i]->')
|
||||
.replace('.IsEmpty()', '.empty()').replace('bool handled;', 'bool handled = false;'))
|
||||
methods='\n'.join(translate(body(core,sig)) for sig in ('void Register(', 'void Capabilities(', 'int JournalSequence(', 'bool ReadJournal(', 'protected string Setting(', 'protected bool Extension('))
|
||||
aliases='struct HQAliases{'+translate(body(hq,'override string Setting('))+'};\nstruct IntelAliases{'+translate(body(intel,'override string Setting('))+'};'
|
||||
help_bodies='struct HQHelp{'+translate(body(hq,'override bool Help('))+'};\nstruct IntelHelp{'+translate(body(intel,'override bool Help('))+'};'
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
using string=std::string;
|
||||
template<class T> struct Array:std::vector<T>{void Insert(T v){this->push_back(v);}void Clear(){this->clear();}int Count(){return this->size();}};
|
||||
struct Mode{};
|
||||
string CoreSetting(string section,string option){if(section=="intel"&&option=="reveal")return "intelRevealChancePercent";return "";}
|
||||
struct Provider{
|
||||
string id;int sequence=-1;int calls=0;bool accepts=false;int reads=0;
|
||||
string Id(){return id;}void Capabilities(Array<string>& values){values.Insert("supports="+id);}
|
||||
int JournalSequence(){return sequence;}
|
||||
bool ReadJournal(int after,Array<string>& lines,int& cursor,int limit=8){if(sequence<0)return false;reads++;cursor=std::min(sequence,after+limit);lines.Insert(id);return true;}
|
||||
string Setting(string section,string){return section==id?id+"Setting":"";}
|
||||
bool Execute(Mode*,string,Array<string>& replies){calls++;if(accepts)replies.Insert(id);return accepts;}
|
||||
};
|
||||
struct Admin{Array<Provider*>m_Providers;
|
||||
'''+methods+'};\n'+aliases+help_bodies+r'''
|
||||
int main(){
|
||||
Admin admin;Mode mode;Array<string> lines;int cursor=0;
|
||||
assert(admin.JournalSequence()==-1&&!admin.ReadJournal(0,lines,cursor));
|
||||
assert(admin.Setting("intel","reveal")=="intelRevealChancePercent"&&admin.Setting("hq","squads").empty());
|
||||
admin.Register(nullptr);Provider empty;admin.Register(&empty);assert(admin.m_Providers.empty());
|
||||
Provider hq;hq.id="hq";hq.sequence=12;hq.accepts=true;
|
||||
Provider intel;intel.id="intel";intel.accepts=true;
|
||||
admin.Register(&hq);admin.Register(&hq);admin.Register(&intel);assert(admin.m_Providers.size()==2);
|
||||
admin.Capabilities(lines);assert(lines.size()==2&&lines[0]=="supports=hq"&&lines[1]=="supports=intel");
|
||||
assert(admin.JournalSequence()==12);assert(admin.ReadJournal(2,lines,cursor,4)&&cursor==6&&lines.size()==1&&lines[0]=="hq");
|
||||
assert(hq.reads==1&&intel.reads==0);
|
||||
lines.clear();assert(admin.Extension(&mode,"status",lines)&&lines.size()==2&&hq.calls==1&&intel.calls==1);
|
||||
lines.clear();assert(admin.Extension(&mode,"hq show",lines)&&lines.size()==1&&hq.calls==2&&intel.calls==1);
|
||||
hq.accepts=false;intel.accepts=false;lines.clear();assert(!admin.Extension(&mode,"unknown",lines)&&lines.empty());
|
||||
Provider replacement;replacement.id="hq";replacement.sequence=20;
|
||||
admin.Register(&replacement);assert(admin.m_Providers.size()==2&&admin.JournalSequence()==20);
|
||||
HQAliases ha;IntelAliases ia;
|
||||
assert(ha.Setting("hq","equipment")=="automaticEquipmentChancePercent");
|
||||
assert(ha.Setting("hq","equipment")=="automaticEquipmentChancePercent");
|
||||
assert(ha.Setting("hq","interval")=="reportIntervalSeconds"&&ha.Setting("hq","squads")=="supportSquadLimit"&&ha.Setting("hq","radius")=="responseRadiusMetres");
|
||||
assert(ha.Setting("hq","aa")=="antiAirChancePercent"&&ha.Setting("hq","mines")=="antiPersonnelMineChancePercent");
|
||||
assert(ha.Setting("intel","carrying").empty()&&ha.Setting("hq","stinger")=="antiAirChancePercent");
|
||||
assert(ia.Setting("intel","chance")=="aiIntelChancePercent"&&ia.Setting("intel","carrying")=="aiIntelChancePercent");
|
||||
assert(ia.Setting("hq","chance").empty()&&ia.Setting("intel","spawn").empty());
|
||||
HQHelp hh;IntelHelp ih;lines.clear();assert(hh.Help("hq",lines)&&lines.size()<=5);lines.clear();assert(ih.Help("intel",lines)&&lines.size()<=4);
|
||||
lines.clear();assert(!hh.Help("unknown",lines)&&!ih.Help("unknown",lines)&&lines.empty());
|
||||
}
|
||||
''')
|
||||
print('PASS: mission provider registration is idempotent, optional dispatch is ordered, status aggregates, journals preserve cursors, aliases stay with their addons and topic help is bounded')
|
||||
Reference in New Issue
Block a user