"""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 = BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c' layout = BASE / 'addons/core/UI/layouts/ERR/AdminMenu.layout' keys = re.findall(r'"([A-Za-z]+)"', body(source, 'static array Keys(')) settings = re.findall(r'"([A-Za-z]+)"', body(BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c', 'static array Keys(')) assert len(keys) == len(set(keys)) and set(settings) - set(keys) == {"automaticConvoyEnabled", "keepWeaponOnUnconscious", "selfEpinephrine", "planeReconEnabled", "restartOnEmpty"} assert set(keys) <= set(settings) # Every menu field must have a server setting. # 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 'PlaneOn' in names and 'PlaneOff' in names assert all(n in names for n in ('OpenPlane','PlaneBack','PlanePage','Selected_OpenPlane')) for signature in ('static array Pages(', 'static array Navigation('): for n in re.findall(r'"([A-Za-z]+)"', body(source, signature)): assert n in names assert 'Apply_planeReconEnabled' not in names # Saved-state toggle pair, not a numeric edit. 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, 'Help_' + key)) if key not in ('emptyRestartHours','aiDoctrine','aiShadow','motorbikeCount','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','fieldCampCount','fieldHospitalCount','startMode','civilianTrafficCount','roadblockCount','hunterSpawnDistanceMetres','objectGuardCount','objectGuardEvery','objectGuardHQMode','startWeaponCount','startMagazineCount'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names else: assert 'Random_startMode' not in names 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 #include #include 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> cases={ {"emptyRestartHours","mission emptyhours"},{"aiDoctrine","mission doctrine"},{"aiShadow","mission aishadow"},{"automaticEquipmentChancePercent","hq equipment"},{"startMode","mission start"},{"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"}, {"planeAltitudeMetres","mission planealtitude"},{"planeReplacementSeconds","mission planereplacement"},{"planeSectorSeconds","mission planesector"},{"fieldCampCount","mission camps"},{"fieldHospitalCount","mission hospitals"},{"objectGuardCount","mission objectguards"},{"objectGuardEvery","mission objectevery"},{"objectGuardHQMode","mission objecthq"},{"hunterSpawnDistanceMetres","mission hunterdistance"},{"roadblockCount","mission roadblocks"},{"civilianTrafficCount","mission traffic"},{"heliTimerMinutes","hq heli timer"},{"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:{"heliTimer","prisonDistance","stingerChance","mission abandon","","bad"}){ assert(Fields::Command(key,"1").empty());assert(!Fields::Available(key,true,true)); } for(string key:{"aiDoctrine","aiShadow","planeAltitudeMetres","planeReplacementSeconds","planeSectorSeconds","objectGuardCount","heliTimerMinutes","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 menu 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') assert all(name in names for name in ('RestartEmptyOn', 'RestartEmptyOff', 'Saved_restartOnEmpty')) assert click.index('!m_Ready') < click.index('"mission restartempty set "') toggle = body(source, 'if (name == "RestartEmptyOn" || name == "RestartEmptyOff")') toggle = toggle.replace('m_Controller.', 'm_Controller->') run(r''' #include #include using string=std::string; struct Controller {int calls=0;string last;void ERR_AdminSubmit(string command,bool menu){assert(menu);calls++;last=command;}}; struct Menu { int m_RestartEmptyState=-1;float m_ClickDelay=0;Controller controller;Controller* m_Controller=&controller; void SetText(string,string){} void Click(string name){ ''' + toggle + r''' } }; int main(){ Menu m;m.Click("RestartEmptyOn");assert(m.controller.calls==0); m.m_RestartEmptyState=0;m.Click("RestartEmptyOn"); assert(m.controller.last=="mission restartempty set 1"&&m.controller.calls==1); m.m_RestartEmptyState=1;m.Click("RestartEmptyOff"); assert(m.controller.last=="mission restartempty set 0"&&m.controller.calls==2); } ''') # All four rows remain inside the visible Mission page with no overlapping controls. page = body(layout, 'FrameWidgetClass { Name "SettingsPage"') rectangles = [] for line in page.splitlines()[1:]: if not line.startswith(('TextWidgetClass', 'ButtonWidgetClass')): continue name = re.search(r'Name "([^"]+)"', line)[1] x, y, width, height = (int(re.search(r'\b' + prop + r' (\d+)', line)[1]) for prop in ('PositionX', 'PositionY', 'SizeX', 'SizeY')) assert x + width <= 1000 and y + height <= 486, name for other, ox, oy, ow, oh in rectangles: assert x + width <= ox or ox + ow <= x or y + height <= oy or oy + oh <= y, (name, other) rectangles.append((name, x, y, width, height)) print('PASS: restart toggle submits authorized settings commands; Mission controls fit without overlap') # The edit page must carry every control bound by the extension, with a valid row. items_menu = (ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ServerItemsMenu.c').read_text() for name in ('ItemsMod','ItemsShow','ItemsEnableAll','ItemsDisableAll','ItemsPrevious','ItemsNext','ItemsPresets','ItemsPresetLoad','ItemsPresetDelete','ItemsPresetName','ItemsPresetSave','ItemsStorage','ItemsContent'): assert name in names, name validate_layout_structure((ROOT/'addons/core/UI/layouts/ERR/ItemRow.layout').read_text()) print('PASS: item editor, preset controls and dynamic item-row layout are structurally valid') # Run the production selected-state styling: retain selected color at disabled # opacity 1, while unselected controls remain neutral rather than looking active. choice=body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_SelectionStyle.c','static void Choice(').replace('SCR_ButtonTextComponent button','SCR_ButtonTextComponent* button').replace('button.','button->').replace('Color.FromInt','Color::FromInt') run(r''' #include #include struct Color{uint32_t value;static Color FromInt(uint32_t v){return {v};}}; struct SCR_ButtonTextComponent{ float opacity=0;bool border=false;Color background{0}; void SetDisabledOpacity(float v){opacity=v;} void SetBackgroundColors(Color normal,Color,Color selected,Color,Color){background=normal;assert(normal.value==selected.value);} void ColorizeBackground(bool animate){assert(!animate);} void ShowBorder(bool selected,bool animate){assert(!animate);border=selected;} }; '''+choice+r''' int main(){ Choice(nullptr,true); SCR_ButtonTextComponent button; Choice(&button,true,true);assert(button.background.value==0xFF246B3C&&button.opacity==1&&button.border); Choice(&button,false,true);assert(button.background.value==0xFF454B52&&!button.border); Choice(&button,true,false);assert(button.background.value==0xFF8F2929&&button.opacity==1&&button.border); Choice(&button,false,false);assert(button.background.value==0xFF454B52&&!button.border); } ''') print('PASS: selected choices remain green/red and fully visible when disabled; unselected choices return to neutral') # Probability controls share the authenticated item editor and use a distinct row. weight_menu=(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ServerItemsWeightsMenu.c').read_text() weight_layout=(ROOT/'addons/core/UI/layouts/ERR/ItemWeightRow.layout').read_text() validate_layout_structure(weight_layout) weight_names=re.findall(r'\bName "([^"]+)"', weight_layout) assert len(weight_names)==len(set(weight_names)) for name in ('EquipmentNavigation','EquipmentOverview','OpenAllowedItems','AllowedItemsPage','OpenItemPresets','ItemPresetsPage','OpenWeights','WeightsPage','WeightsMod','WeightsShow','WeightsPrevious','WeightsNext','ModWeight','ModWeightApply','ModWeightNormal','ModWeightHigh','ModWeightSaved','WeightsChance','WeightsPageState','WeightsContent','WeightsScroll'): assert name in names, name for name in ('WeightLabel','WeightState','WeightValue','WeightApply','WeightNormal'): assert name in weight_names, name assert 'SubmitItems("items weight "' in weight_menu print('PASS: probability editor and weighted item-row layout have all bound controls')