Make empty-server restarts configurable in the mission menu

Add persistent last-disconnect and idle-interval controls with enabled/five-hour defaults. Apply saved settings immediately, reset idle time while occupied, and cover migration, routing, layout and restart behavior.
This commit is contained in:
2026-10-14 14:05:46 +02:00
parent 0048527fe2
commit e4ca37dfc1
13 changed files with 242 additions and 61 deletions
+42 -3
View File
@@ -10,7 +10,7 @@ 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) == len(set(keys)) and set(settings) - set(keys) == {"automaticConvoyEnabled", "keepWeaponOnUnconscious", "planeReconEnabled"}
assert len(keys) == len(set(keys)) and set(settings) - set(keys) == {"automaticConvoyEnabled", "keepWeaponOnUnconscious", "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.
@@ -53,7 +53,7 @@ assert 'Apply_planeReconEnabled' not in names # Saved-state toggle pair, not a
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 ('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
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
@@ -100,7 +100,7 @@ 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={
{"aiDoctrine","mission doctrine"},{"aiShadow","mission aishadow"},{"automaticEquipmentChancePercent","hq equipment"},{"startMode","mission start"},{"radioDefenceSeconds","mission radio"},{"aiIntelChancePercent","intel carrying"},
{"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"},
@@ -135,6 +135,45 @@ assert 'm_Controller.ERR_AdminSubmit("mission keepweapon set " + retain, true)'
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 <cassert>
#include <string>
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'):