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:
+63
-10
@@ -6,12 +6,12 @@ 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'
|
||||
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<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) == 13 and set(settings) - set(keys) == {"automaticConvoyEnabled", "heliTimerMinutes", "keepWeaponOnUnconscious"}
|
||||
assert set(keys) <= set(settings) # New text-command setting does not alter the fixed native menu.
|
||||
assert len(keys) == len(set(keys)) and set(settings) - set(keys) == {"automaticConvoyEnabled", "keepWeaponOnUnconscious", "planeReconEnabled"}
|
||||
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):
|
||||
@@ -45,9 +45,16 @@ 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<string> Pages(', 'static array<string> 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, 'Random_' + key, 'Help_' + key))
|
||||
assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key, 'Help_' + key))
|
||||
if key not in ('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]):
|
||||
@@ -93,11 +100,11 @@ int main() {
|
||||
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"},
|
||||
{"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"},{"reportIntervalSeconds","hq interval"},
|
||||
{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"},{"convoyCount","hq convoy count"},{"antiAirChancePercent","hq aa"},{"antiPersonnelMineChancePercent","hq mines"}};
|
||||
{"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");
|
||||
@@ -112,7 +119,7 @@ int main() {
|
||||
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","antiAirChancePercent","antiPersonnelMineChancePercent"}){
|
||||
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));
|
||||
@@ -120,10 +127,56 @@ int main() {
|
||||
for(string key:{"intelRevealChancePercent","radioDefenceSeconds","startTimeMinutes","timeMultiplier","startDateCode","weatherProfile"}) assert(Fields::Available(key,false,false));
|
||||
}
|
||||
''')
|
||||
print('PASS: actual menu command routing covers all thirteen settings, rejects unsupported keys, gates companions, and matches unique authored widgets')
|
||||
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')
|
||||
|
||||
# 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 <cassert>
|
||||
#include <cstdint>
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user