113 lines
5.8 KiB
Python
113 lines
5.8 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
|
|
from policy_harness import ROOT, body, run
|
|
source = ROOT / 'addons/core/Scripts/Game/ERR/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(ROOT / 'addons/core/Scripts/Game/ERR/ERR_Settings.c', 'static array<string> Keys('))
|
|
assert len(keys) == 10 and set(keys) == set(settings)
|
|
# 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')
|