Files
EscapistsReintroduced/tests/test_admin_menu.py
T
jakub cab998fb64 Combine Workshop addon and fix prison startup
Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
2026-09-20 16:42:52 +02:00

116 lines
6.0 KiB
Python

"""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) == 10 and set(settings) - set(keys) == {"stingerChanceOneIn", "automaticConvoyEnabled"}
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 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))
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(')
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();}
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 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"},{"reportIntervalSeconds","hq interval"},
{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"}};
for(auto pair:cases){
assert(Fields::Command(pair.first," 42 ")==pair.second+" set 42");
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:{"heliTimer","convoyCount","prisonDistance","stingerChance","mission abandon","","bad"}){
assert(Fields::Command(key,"1").empty());assert(!Fields::Available(key,true,true));
}
for(string key:{"reportIntervalSeconds","supportSquadLimit","responseRadiusMetres"}){
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 ten settings, rejects unsupported keys, gates companions, and matches unique authored widgets')