Integrate faction and random settings menus, generated objectives, scouts, convoys, air support, mortar stations and body-intel recovery. Add identified-body aggression, finite hunter UAZ deployment, dedicated objective defence and maximum AI skill with separate reduced aim accuracy. Include policy tests, native probes, evidence and documentation.
118 lines
6.2 KiB
Python
118 lines
6.2 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) == 11 and set(settings) - set(keys) == {"stingerChanceOneIn", "automaticConvoyEnabled", "heliTimerMinutes"}
|
|
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, 'Random_' + 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"},{"convoyCount","hq convoy count"}};
|
|
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:{"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"}){
|
|
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 eleven settings, rejects unsupported keys, gates companions, and matches unique authored widgets')
|