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
@@ -8,7 +8,7 @@ class ERR_AdminMenuFields
{
static array<string> Keys()
{
return {"aiDoctrine", "aiShadow", "automaticEquipmentChancePercent", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "startMode", "radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent", "heliTimerMinutes"};
return {"aiDoctrine", "aiShadow", "automaticEquipmentChancePercent", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "emptyRestartHours", "startMode", "radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent", "heliTimerMinutes"};
}
static bool Available(string key, bool hq, bool intel)
{
@@ -64,6 +64,7 @@ class ERR_AdminMenuFields
else if (key == "fieldCampCount") prefix = "mission camps";
else if (key == "fieldHospitalCount") prefix = "mission hospitals";
else if (key == "roadblockCount") prefix = "mission roadblocks";
else if (key == "emptyRestartHours") prefix = "mission emptyhours";
else if (key == "civilianTrafficCount") prefix = "mission traffic";
else if (key == "startMode") prefix = "mission start";
else if (key == "radioDefenceSeconds") prefix = "mission radio";
@@ -134,6 +135,7 @@ class ERR_AdminMenu : ChimeraMenuBase
names.Insert("RandomDay_startTimeMinutes");
names.Insert("Default_viewDistanceMetres");
names.Insert("OpenForces");
names.Insert("RestartEmptyOn"); names.Insert("RestartEmptyOff");
names.Insert("OpenGuards"); names.Insert("GuardsBack");
names.Insert("OpenSites"); names.Insert("SitesBack");
names.Insert("OpenPlane"); names.Insert("PlaneBack");
@@ -255,9 +257,11 @@ class ERR_AdminMenu : ChimeraMenuBase
if (page == 2) m_LogPoll = 1;
}
protected int m_KeepWeaponState = -1;
protected int m_RestartEmptyState = -1;
protected void UpdateActions()
{
ERR_SelectionStyle.Pair(GetRootWidget(), "KeepWeaponOn", "KeepWeaponOff", m_KeepWeaponState);
ERR_SelectionStyle.Pair(GetRootWidget(), "RestartEmptyOn", "RestartEmptyOff", m_RestartEmptyState);
foreach (SCR_ButtonBaseComponent button : m_Buttons)
{
string name = button.GetRootWidget().GetName();
@@ -272,6 +276,8 @@ class ERR_AdminMenu : ChimeraMenuBase
else if (name.IndexOf("GuardHQ_") == 0) button.SetEnabled(m_Ready && m_HQ && m_AvailableFields.Contains("objectGuardHQMode"));
else if (name == "RerollIntel" || name == "IntelSummary") button.SetEnabled(m_Ready && m_Intel);
else if (name == "LatestJournal") button.SetEnabled(m_Ready && m_HQ);
else if (name == "RestartEmptyOn") button.SetEnabled(m_Ready && m_RestartEmptyState == 0);
else if (name == "RestartEmptyOff") button.SetEnabled(m_Ready && m_RestartEmptyState == 1);
else if (name == "KeepWeaponOn") button.SetEnabled(m_Ready && m_KeepWeaponState == 0);
else if (name == "KeepWeaponOff") button.SetEnabled(m_Ready && m_KeepWeaponState == 1);
}
@@ -280,7 +286,7 @@ class ERR_AdminMenu : ChimeraMenuBase
{
if (!authorized) { Close(); return; }
if (!values) return;
m_HQ = false; m_Intel = false; m_AvailableFields.Clear(); m_KeepWeaponState = -1;
m_HQ = false; m_Intel = false; m_AvailableFields.Clear(); m_KeepWeaponState = -1; m_RestartEmptyState = -1;
foreach (string entry : values)
{
if (entry == "supports=hq") { m_HQ = true; continue; }
@@ -316,6 +322,14 @@ class ERR_AdminMenu : ChimeraMenuBase
ERR_SelectionStyle.Pair(GetRootWidget(), "PlaneOn", "PlaneOff", planeState);
m_AvailableFields.Insert(key); continue;
}
if (key == "restartOnEmpty")
{
m_RestartEmptyState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
string restartState = "Disabled";
if (m_RestartEmptyState == 1) restartState = "Enabled";
SetText("Saved_restartOnEmpty", "Saved: " + restartState);
continue;
}
if (key == "keepWeaponOnUnconscious")
{
m_KeepWeaponState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
@@ -464,6 +478,13 @@ class ERR_AdminMenu : ChimeraMenuBase
m_ClickDelay = 0.3; SetText("Feedback", "Saving reconnaissance plane selection for next round...");
m_Controller.ERR_AdminSubmit("mission plane set " + enabled, true); return;
}
if (name == "RestartEmptyOn" || name == "RestartEmptyOff")
{
if (m_RestartEmptyState < 0) return;
string restartEnabled = "0"; if (name == "RestartEmptyOn") restartEnabled = "1";
m_ClickDelay = 0.3; SetText("Feedback", "Saving restart after last disconnect...");
m_Controller.ERR_AdminSubmit("mission restartempty set " + restartEnabled, true); return;
}
if (name == "KeepWeaponOn" || name == "KeepWeaponOff")
{
string retain = "0"; if (name == "KeepWeaponOn") retain = "1";
@@ -23,6 +23,8 @@ class ERR_CommandArguments
if (section == "mission" && option == "traffic") return "civilianTrafficCount";
if (section == "mission" && option == "start") return "startMode";
if (section == "mission" && option == "keepweapon") return "keepWeaponOnUnconscious";
if (section == "mission" && option == "restartempty") return "restartOnEmpty";
if (section == "mission" && option == "emptyhours") return "emptyRestartHours";
if (section == "mission" && option == "radio") return "radioDefenceSeconds";
if (section == "intel" && option == "reveal") return "intelRevealChancePercent";
return "";
@@ -31,8 +33,8 @@ class ERR_CommandArguments
{
if (key == "aiDoctrine" && text == "current") { value = 0; return true; }
if (key == "aiDoctrine" && text == "coordinated") { value = 1; return true; }
if (key == "aiShadow" && text == "off") { value = 0; return true; }
if (key == "aiShadow" && text == "on") { value = 1; return true; }
if ((key == "aiShadow" || key == "restartOnEmpty") && text == "off") { value = 0; return true; }
if ((key == "aiShadow" || key == "restartOnEmpty") && text == "on") { value = 1; return true; }
if (key == "startMode" && text == "prison") { value = 0; return true; }
if (key == "startMode" && text == "crash") { value = 1; return true; }
if (text == "random" && ERR_Settings.Randomizable(key)) { value = -1; return true; }
@@ -179,6 +181,8 @@ class ERR_AdminService
if (key == "objectGuardCount") return "0..16 soldiers per factory/industrial, military or infrastructure site; default 8, zero off. Next round. HQ participation is configured separately; all report. Towers and chimneys use suitable platforms. Lost guards are not replaced.";
if (key == "hunterSpawnDistanceMetres") return "Opening UAZ and troop-truck patrol placement. Applies next round.";
if (key == "roadblockCount") return "Set 0..30 roadblocks (default 4; 0 disables). Applies next round. Each has four guards; unsuitable sites are skipped.";
if (key == "restartOnEmpty") return "On (1) restarts after the last disconnect; Off (0) preserves the round for reconnects. Default On. Applies immediately; normal round endings still apply.";
if (key == "emptyRestartHours") return "0 disables idle restarts; 1..168 real hours continuously empty. Default 5. Applies immediately, measured from mission start or the last disconnect. Connected players, including spectators, prevent idle restarts.";
if (key == "civilianTrafficCount") return "Set 0..6 civilian cars (default 3). Zero disables new traffic. Unclaimed cars retire only far from all players and out of sight; player-entered cars are preserved.";
if (key == "keepWeaponOnUnconscious") return "Set 1 to retain the held firearm during unconsciousness and equip it after recovery; 0 keeps normal dropping. Applies to the next knockout.";
if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent")
@@ -220,6 +224,7 @@ class ERR_AdminService
if (topic == "time" || topic == "mission")
{
replies.Insert("/esc mission radio show|set N|random | mission abandon");
replies.Insert("/esc mission restartempty show|set on|off | mission emptyhours show|set 0..168; applies now, zero hours disables idle restarts.");
replies.Insert("/esc mission doctrine show|set current|coordinated | mission aishadow show|set off|on; next round only.");
replies.Insert("/esc mission start show|set prison|crash: save the next round start; current round unchanged.");
replies.Insert("/esc mission startweapons show|set 0..12 | mission startammo show|set 0..48; shared backpack, next round.");
@@ -75,7 +75,7 @@ class ERR_GameMode : SCR_BaseGameMode
protected bool m_StartedPlayers;
protected bool m_HadConnectedPlayers;
protected bool m_EmptyRestartRequested;
protected int m_EmptyMissionStartedAt;
protected int m_EmptySince;
protected ref map<int, PlayerController> m_ExhaustedInitialAdmissions = new map<int, PlayerController>();
protected bool m_Stopped;
protected bool m_NativeEndHandled;
@@ -458,11 +458,12 @@ class ERR_GameMode : SCR_BaseGameMode
{
if (!Replication.IsServer() || m_Stopped || m_NativeEndHandled) return;
m_HadConnectedPlayers = true;
m_EmptySince = System.GetTickCount();
}
protected bool CheckEmptyServerRestart()
{
if (!Replication.IsServer() || m_Stopped || m_NativeEndHandled) return false;
if (!Replication.IsServer() || m_Stopped || m_NativeEndHandled || !m_Settings) return false;
if (m_EmptyRestartRequested) return true;
PlayerManager manager = GetGame().GetPlayerManager();
if (!manager) return false;
@@ -470,13 +471,16 @@ class ERR_GameMode : SCR_BaseGameMode
if (manager.GetPlayerCount() > 0)
{
m_HadConnectedPlayers = true;
m_EmptySince = System.GetTickCount();
return false;
}
// Real elapsed milliseconds: mission clock settings cannot accelerate this.
if (!m_HadConnectedPlayers && System.GetTickCount() - m_EmptyMissionStartedAt < 18000000) return false;
bool afterDisconnect = m_HadConnectedPlayers && m_Settings.Get("restartOnEmpty") == 1;
int hours = m_Settings.Get("emptyRestartHours");
if (!afterDisconnect && (hours == 0 || System.GetTickCount() - m_EmptySince < hours * 3600000)) return false;
m_EmptyRestartRequested = true;
m_EndReason = "The server has been empty for five hours. Restarting the mission.";
if (m_HadConnectedPlayers) m_EndReason = "All players disconnected. Restarting the mission.";
m_EndReason = string.Format("The server has been empty for %1 hours. Restarting the mission.", hours);
if (afterDisconnect) m_EndReason = "All players disconnected. Restarting the mission.";
PrintFormat("Reintroduced [round lifecycle]: %1", m_EndReason);
// Native termination also handles PREPARING, before Transition is available.
// OnGameModeEnd stops our services; vanilla owns the scenario reload.
@@ -492,7 +496,7 @@ class ERR_GameMode : SCR_BaseGameMode
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(BindLocalObserver, 250, true);
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(ApplyLocalViewDistance, 1000, true);
if (!Replication.IsServer()) return;
m_EmptyMissionStartedAt = System.GetTickCount();
m_EmptySince = System.GetTickCount();
m_Settings = new ERR_Settings(SettingsPath());
if (!System.IsCLIParam("errNoPerf")) m_Performance = new ERR_Performance();
PublishViewDistance();
@@ -9,7 +9,7 @@ class ERR_Settings
static array<string> Keys()
{
return {"aiDoctrine", "aiShadow", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "planeReconEnabled", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "startMode", "keepWeaponOnUnconscious", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticEquipmentChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
return {"aiDoctrine", "aiShadow", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "planeReconEnabled", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "startMode", "keepWeaponOnUnconscious", "restartOnEmpty", "emptyRestartHours", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticEquipmentChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
}
static bool Valid(string key, int value)
@@ -31,7 +31,8 @@ class ERR_Settings
if (key == "roadblockCount") return value >= 0 && value <= 30;
if (key == "civilianTrafficCount") return value >= 0 && value <= 6;
if (key == "startMode") return value == 0 || value == 1;
if (key == "keepWeaponOnUnconscious") return value == 0 || value == 1;
if (key == "keepWeaponOnUnconscious" || key == "restartOnEmpty") return value == 0 || value == 1;
if (key == "emptyRestartHours") return value >= 0 && value <= 168;
if (key == "heliTimerMinutes") return value >= 1 && value <= 1440;
if (key == "convoyCount") return value >= 0 && value <= 20;
if (key == "automaticConvoyEnabled") return value == 0 || value == 1;
@@ -132,7 +133,8 @@ class ERR_Settings
if (key == "roadblockCount") return 4;
if (key == "civilianTrafficCount") return 3;
if (key == "startMode") return 0;
if (key == "keepWeaponOnUnconscious") return 1;
if (key == "keepWeaponOnUnconscious" || key == "restartOnEmpty") return 1;
if (key == "emptyRestartHours") return 5;
if (key == "heliTimerMinutes") return 15;
if (key == "convoyCount") return 3;
if (key == "automaticConvoyEnabled") return 1;
@@ -167,6 +169,8 @@ class ERR_Settings
m_Values.Set("civilianTrafficCount", AddedDefault("civilianTrafficCount"));
m_Values.Set("startMode", AddedDefault("startMode"));
m_Values.Set("keepWeaponOnUnconscious", AddedDefault("keepWeaponOnUnconscious"));
m_Values.Set("restartOnEmpty", AddedDefault("restartOnEmpty"));
m_Values.Set("emptyRestartHours", AddedDefault("emptyRestartHours"));
m_Values.Set("reportIntervalSeconds", 10);
m_Values.Set("supportSquadLimit", 4);
m_Values.Set("responseRadiusMetres", 2000);
+41 -31
View File
@@ -21,37 +21,47 @@ FrameWidgetClass {
Color 0.035 0.05 0.065 0.98
}
FrameWidgetClass { Name "SettingsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
TextWidgetClass { Name "Label_startMode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 456 OffsetRight -484 SizeY 26 OffsetBottom -32 } Color 0.9 0.92 0.94 1 Text "Next start (Prison or Crash)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "startMode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 38 OffsetTop 38 SizeX 180 OffsetRight -208 SizeY 38 OffsetBottom -76 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_startMode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 38 OffsetTop 38 SizeX 100 OffsetRight -320 SizeY 38 OffsetBottom -76 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
TextWidgetClass { Name "Saved_startMode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 82 OffsetTop 82 SizeX 456 OffsetRight -484 SizeY 40 OffsetBottom -122 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Help_startMode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 125 OffsetTop 125 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -153 } Color 0.9 0.92 0.94 1 Text "Starting scenario for the next round." "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Label_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 6 OffsetTop 6 SizeX 456 OffsetRight -964 SizeY 26 OffsetBottom -32 } Color 0.9 0.92 0.94 1 Text "Radio defence (30-1800 seconds)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 38 OffsetTop 38 SizeX 180 OffsetRight -688 SizeY 38 OffsetBottom -76 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 700 OffsetLeft 700 PositionY 38 OffsetTop 38 SizeX 100 OffsetRight -800 SizeY 38 OffsetBottom -76 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Random_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 812 OffsetLeft 812 PositionY 38 OffsetTop 38 SizeX 140 OffsetRight -952 SizeY 38 OffsetBottom -76 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Random" } } }
TextWidgetClass { Name "Saved_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 82 OffsetTop 82 SizeX 456 OffsetRight -964 SizeY 24 OffsetBottom -106 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Help_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 109 OffsetTop 109 SizeX 456 OffsetRight -964 SizeY 36 OffsetBottom -145 } Color 0.9 0.92 0.94 1 Text "Time to defend the radio." "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Label_startWeaponCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 160 OffsetTop 160 SizeX 456 OffsetRight -484 SizeY 26 OffsetBottom -186 } Color 0.9 0.92 0.94 1 Text "Starting pistols (0-12)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "startWeaponCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 192 OffsetTop 192 SizeX 180 OffsetRight -208 SizeY 38 OffsetBottom -230 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_startWeaponCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 192 OffsetTop 192 SizeX 100 OffsetRight -320 SizeY 38 OffsetBottom -230 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
TextWidgetClass { Name "Saved_startWeaponCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 236 OffsetTop 236 SizeX 456 OffsetRight -484 SizeY 24 OffsetBottom -260 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Help_startWeaponCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 263 OffsetTop 263 SizeX 456 OffsetRight -484 SizeY 36 OffsetBottom -299 } Color 0.9 0.92 0.94 1 Text "Shared prison/crash backpack; next round." "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Label_startMagazineCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 160 OffsetTop 160 SizeX 456 OffsetRight -964 SizeY 26 OffsetBottom -186 } Color 0.9 0.92 0.94 1 Text "Starting spare magazines (0-48)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "startMagazineCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 192 OffsetTop 192 SizeX 180 OffsetRight -688 SizeY 38 OffsetBottom -230 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_startMagazineCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 700 OffsetLeft 700 PositionY 192 OffsetTop 192 SizeX 100 OffsetRight -800 SizeY 38 OffsetBottom -230 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
TextWidgetClass { Name "Saved_startMagazineCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 236 OffsetTop 236 SizeX 456 OffsetRight -964 SizeY 24 OffsetBottom -260 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Help_startMagazineCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 263 OffsetTop 263 SizeX 456 OffsetRight -964 SizeY 36 OffsetBottom -299 } Color 0.9 0.92 0.94 1 Text "Shared backpack; 8 rounds per magazine. Next round." "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "KeepWeaponLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 314 OffsetTop 314 SizeX 456 OffsetRight -484 SizeY 26 OffsetBottom -340 } Color 0.9 0.92 0.94 1 Text "Keep held weapon when unconscious" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "KeepWeaponOn" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 346 OffsetTop 346 SizeX 140 OffsetRight -168 SizeY 38 OffsetBottom -384 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Enabled" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "KeepWeaponOff" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 180 OffsetLeft 180 PositionY 346 OffsetTop 346 SizeX 152 OffsetRight -332 SizeY 38 OffsetBottom -384 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Disabled" } } }
TextWidgetClass { Name "Saved_keepWeaponOnUnconscious" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 390 OffsetTop 390 SizeX 456 OffsetRight -484 SizeY 24 OffsetBottom -414 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "MissionHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 417 OffsetTop 417 SizeX 456 OffsetRight -484 SizeY 40 OffsetBottom -457 } Color 0.9 0.92 0.94 1 Text "Applies next knockout; existing casualties keep their rule." "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Label_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 314 OffsetTop 314 SizeX 456 OffsetRight -964 SizeY 26 OffsetBottom -340 } Color 0.9 0.92 0.94 1 Text "Civilian traffic (0-6 cars; 0 off)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 346 OffsetTop 346 SizeX 180 OffsetRight -688 SizeY 38 OffsetBottom -384 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 700 OffsetLeft 700 PositionY 346 OffsetTop 346 SizeX 100 OffsetRight -800 SizeY 38 OffsetBottom -384 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
TextWidgetClass { Name "Saved_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 390 OffsetTop 390 SizeX 456 OffsetRight -964 SizeY 24 OffsetBottom -414 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Help_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 417 OffsetTop 417 SizeX 456 OffsetRight -964 SizeY 36 OffsetBottom -453 } Color 0.9 0.92 0.94 1 Text "0 disables new traffic. Player-used cars are kept." "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Label_startMode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 4 OffsetTop 4 SizeX 456 OffsetRight -484 SizeY 22 OffsetBottom -26 } Color 0.9 0.92 0.94 1 Text "Next start (Prison or Crash)" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "startMode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 28 OffsetTop 28 SizeX 180 OffsetRight -208 SizeY 32 OffsetBottom -60 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_startMode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 28 OffsetTop 28 SizeX 100 OffsetRight -320 SizeY 32 OffsetBottom -60 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
TextWidgetClass { Name "Saved_startMode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 64 OffsetTop 64 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -92 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Help_startMode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 92 OffsetTop 92 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -120 } Color 0.9 0.92 0.94 1 Text "Starting scenario for the next round." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Label_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 4 OffsetTop 4 SizeX 456 OffsetRight -964 SizeY 22 OffsetBottom -26 } Color 0.9 0.92 0.94 1 Text "Radio defence (30-1800 seconds)" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 28 OffsetTop 28 SizeX 180 OffsetRight -688 SizeY 32 OffsetBottom -60 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 700 OffsetLeft 700 PositionY 28 OffsetTop 28 SizeX 100 OffsetRight -800 SizeY 32 OffsetBottom -60 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Random_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 812 OffsetLeft 812 PositionY 28 OffsetTop 28 SizeX 140 OffsetRight -952 SizeY 32 OffsetBottom -60 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Random" } } }
TextWidgetClass { Name "Saved_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 64 OffsetTop 64 SizeX 456 OffsetRight -964 SizeY 28 OffsetBottom -92 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Help_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 92 OffsetTop 92 SizeX 456 OffsetRight -964 SizeY 28 OffsetBottom -120 } Color 0.9 0.92 0.94 1 Text "Time to defend the radio." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Label_startWeaponCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 124 OffsetTop 124 SizeX 456 OffsetRight -484 SizeY 22 OffsetBottom -146 } Color 0.9 0.92 0.94 1 Text "Starting pistols (0-12)" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "startWeaponCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 148 OffsetTop 148 SizeX 180 OffsetRight -208 SizeY 32 OffsetBottom -180 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_startWeaponCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 148 OffsetTop 148 SizeX 100 OffsetRight -320 SizeY 32 OffsetBottom -180 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
TextWidgetClass { Name "Saved_startWeaponCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 184 OffsetTop 184 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -212 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Help_startWeaponCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 212 OffsetTop 212 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -240 } Color 0.9 0.92 0.94 1 Text "Shared prison/crash backpack; next round." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Label_startMagazineCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 124 OffsetTop 124 SizeX 456 OffsetRight -964 SizeY 22 OffsetBottom -146 } Color 0.9 0.92 0.94 1 Text "Starting spare magazines (0-48)" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "startMagazineCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 148 OffsetTop 148 SizeX 180 OffsetRight -688 SizeY 32 OffsetBottom -180 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_startMagazineCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 700 OffsetLeft 700 PositionY 148 OffsetTop 148 SizeX 100 OffsetRight -800 SizeY 32 OffsetBottom -180 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
TextWidgetClass { Name "Saved_startMagazineCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 184 OffsetTop 184 SizeX 456 OffsetRight -964 SizeY 28 OffsetBottom -212 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Help_startMagazineCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 212 OffsetTop 212 SizeX 456 OffsetRight -964 SizeY 28 OffsetBottom -240 } Color 0.9 0.92 0.94 1 Text "Shared backpack; 8 rounds per magazine. Next round." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "KeepWeaponLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 244 OffsetTop 244 SizeX 456 OffsetRight -484 SizeY 22 OffsetBottom -266 } Color 0.9 0.92 0.94 1 Text "Keep held weapon when unconscious" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "KeepWeaponOn" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 268 OffsetTop 268 SizeX 140 OffsetRight -168 SizeY 32 OffsetBottom -300 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Enabled" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "KeepWeaponOff" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 180 OffsetLeft 180 PositionY 268 OffsetTop 268 SizeX 152 OffsetRight -332 SizeY 32 OffsetBottom -300 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Disabled" } } }
TextWidgetClass { Name "Saved_keepWeaponOnUnconscious" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 304 OffsetTop 304 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -332 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "MissionHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 332 OffsetTop 332 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -360 } Color 0.9 0.92 0.94 1 Text "Applies next knockout; existing casualties keep their rule." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Label_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 244 OffsetTop 244 SizeX 456 OffsetRight -964 SizeY 22 OffsetBottom -266 } Color 0.9 0.92 0.94 1 Text "Civilian traffic (0-6 cars; 0 off)" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 268 OffsetTop 268 SizeX 180 OffsetRight -688 SizeY 32 OffsetBottom -300 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 700 OffsetLeft 700 PositionY 268 OffsetTop 268 SizeX 100 OffsetRight -800 SizeY 32 OffsetBottom -300 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
TextWidgetClass { Name "Saved_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 304 OffsetTop 304 SizeX 456 OffsetRight -964 SizeY 28 OffsetBottom -332 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Help_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 332 OffsetTop 332 SizeX 456 OffsetRight -964 SizeY 28 OffsetBottom -360 } Color 0.9 0.92 0.94 1 Text "0 disables new traffic. Player-used cars are kept." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Label_restartOnEmpty" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 364 OffsetTop 364 SizeX 456 OffsetRight -484 SizeY 22 OffsetBottom -386 } Color 0.9 0.92 0.94 1 Text "Restart after last disconnect" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "RestartEmptyOn" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 388 OffsetTop 388 SizeX 140 OffsetRight -168 SizeY 32 OffsetBottom -420 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Enabled" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "RestartEmptyOff" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 180 OffsetLeft 180 PositionY 388 OffsetTop 388 SizeX 152 OffsetRight -332 SizeY 32 OffsetBottom -420 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Disabled" } } }
TextWidgetClass { Name "Saved_restartOnEmpty" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 424 OffsetTop 424 SizeX 456 OffsetRight -484 SizeY 24 OffsetBottom -448 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Help_restartOnEmpty" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 452 OffsetTop 452 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -480 } Color 0.9 0.92 0.94 1 Text "Applies now. Normal round endings still restart." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Label_emptyRestartHours" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 364 OffsetTop 364 SizeX 456 OffsetRight -964 SizeY 22 OffsetBottom -386 } Color 0.9 0.92 0.94 1 Text "Restart while empty (hours)" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "emptyRestartHours" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 388 OffsetTop 388 SizeX 180 OffsetRight -688 SizeY 32 OffsetBottom -420 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_emptyRestartHours" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 700 OffsetLeft 700 PositionY 388 OffsetTop 388 SizeX 100 OffsetRight -800 SizeY 32 OffsetBottom -420 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
TextWidgetClass { Name "Saved_emptyRestartHours" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 424 OffsetTop 424 SizeX 456 OffsetRight -964 SizeY 24 OffsetBottom -448 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
TextWidgetClass { Name "Help_emptyRestartHours" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 452 OffsetTop 452 SizeX 456 OffsetRight -964 SizeY 28 OffsetBottom -480 } Color 0.9 0.92 0.94 1 Text "0 off; 1-168 real hours empty. Default 5. Applies now." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
} }
FrameWidgetClass { Name "ForcesPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
TextWidgetClass {
+19
View File
@@ -60,6 +60,25 @@ requested totals instead of preventing mission startup. The admin Status page
also shows these totals. This is an interim checkpoint system, independent of the
existing HQ-directed road interceptions.
## Mission restarts
The admin menu **Mission** page includes two independent settings:
- **Restart after last disconnect**: Enabled or Disabled, default Enabled.
- **Restart while empty (hours)**: 0 disables the idle timer; 1–168 sets the
number of real hours continuously empty. Default 5. Press **Apply** to save.
Both apply immediately and persist in `$profile:Escapists.json` as
`mission.restartOnEmpty` and `mission.emptyRestartHours`. Older configurations
receive the enabled/five-hour defaults. The idle interval starts with a new
mission or the last disconnect and resets whenever players connect. Connected
spectators and players loading in also prevent an empty-server restart.
Changing the interval uses the time already spent empty. Normal victory/defeat
and no-survivor endings still apply when these options are disabled.
Chat equivalents are `/esc mission restartempty set on|off` and
`/esc mission emptyhours set N`; both also support `show` and `help`.
## One server configuration
All mission preferences now live in **`$profile:Escapists.json`** on the server.
+13 -5
View File
@@ -70,21 +70,29 @@ The server's scene deadline now reaches the existing owned-ticket rejection even
## Empty-server restarts (14 October 2026)
When the last player disconnects, the next server tick ends the mission through
By default, when the last player disconnects, the next server tick ends the mission through
the normal native end/reload sequence. This includes players who leave before
admission finishes. Living reconnect reservations do not keep a completely empty
mission running. While another player remains connected, the existing reserved
body and reconnect behavior still applies.
body and reconnect behavior still applies. Administrators can disable this
behavior using **Mission → Restart after last disconnect** to preserve the
round for reconnects, subject to the idle timer and normal round endings.
A fresh mission with no connections restarts after five hours of real elapsed
time. Each newly loaded mission starts a new five-hour window. Mission time and
time by default. **Mission → Restart while empty (hours)** accepts 0 to disable
the timer or 1–168 hours. The interval starts at mission initialization or the
last disconnect, and is reset while players are connected. Both controls save
immediately for this and future missions; interval changes use the elapsed empty
time rather than restarting the timer. Mission time and
time-multiplier settings do not change this interval. Connected players, including
spectators and players loading in, prevent the empty-server restart. The check
also runs during preparation; it stops with the mission's services. Both restart
reasons are written to the server log.
`test_empty_server_restart.py` executes the production restart check and tick
wrapper, covering the five-hour boundary, repeated empty rounds, last disconnect,
connections between ticks, preparation, authority and duplicate-end prevention.
wrapper, covering independent disabled/enabled settings, interval boundaries,
live changes, repeated empty rounds, last disconnect, connections between ticks,
preparation, authority and duplicate-end prevention. Settings and menu tests
cover migration defaults, persistence, invalid values, failed saves and routing.
The restart uses the existing native end/reload path. These offline checks do
not establish live dedicated-server disconnect/reload timing.
+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'):
+6
View File
@@ -13,6 +13,12 @@ run('#include <cassert>\n#include <string>\n'+rules+r'''
int main(){
assert(CoreSetting("mission","doctrine")=="aiDoctrine");
assert(CoreSetting("mission","aishadow")=="aiShadow");
assert(CoreSetting("mission","restartempty")=="restartOnEmpty");
assert(CoreSetting("mission","emptyhours")=="emptyRestartHours");
assert(Valid("restartOnEmpty",0)&&Valid("restartOnEmpty",1));
assert(!Valid("restartOnEmpty",-1)&&!Valid("restartOnEmpty",2));
assert(Valid("emptyRestartHours",0)&&Valid("emptyRestartHours",1)&&Valid("emptyRestartHours",5)&&Valid("emptyRestartHours",168));
assert(!Valid("emptyRestartHours",-1)&&!Valid("emptyRestartHours",169));
for(std::string key:{"aiDoctrine","aiShadow"}){ assert(Valid(key,0)&&Valid(key,1)); assert(!Valid(key,-1)&&!Valid(key,2)); }
assert(CoreSetting("mission","motorbikes")=="motorbikeCount");
assert(Valid("motorbikeCount",0)&&Valid("motorbikeCount",2)&&Valid("motorbikeCount",6));
+50 -8
View File
@@ -8,7 +8,8 @@ def cpp(source):
source = source.replace('protected ', '').replace('PlayerManager manager', 'PlayerManager* manager')
for name in ('Replication', 'System', 'SCR_GameModeEndData', 'EGameOverTypes'):
source = source.replace(name + '.', name + '::')
for name in ('GetGame()', 'manager', 'm_Performance'):
source = source.replace('string.Format', 'EnforceString::Format')
for name in ('GetGame()', 'manager', 'm_Performance', 'm_Settings'):
source = source.replace(name + '.', name + '->')
return source
@@ -28,9 +29,17 @@ struct Game {PlayerManager* manager=nullptr;PlayerManager* GetPlayerManager(){re
Game* GetGame(){return &game;}
template<class... T> void PrintFormat(const char*,T...){}
struct Performance {int starts=0,ends=0;int BeginTick(){return ++starts;}void EndTick(int){ends++;}};
struct EnforceString {
static std::string Format(std::string pattern,int value){pattern.replace(pattern.find("%1"),2,std::to_string(value));return pattern;}
};
struct Settings {
int restart=1,hours=5;
int Get(std::string key){if(key=="restartOnEmpty")return restart;assert(key=="emptyRestartHours");return hours;}
};
struct Mode {
Settings settings;Settings* m_Settings=&settings;
bool m_Stopped=false,m_NativeEndHandled=false,m_HadConnectedPlayers=false,m_EmptyRestartRequested=false;
int m_EmptyMissionStartedAt=System::now,phase=1,endCalls=0,measuredTicks=0;
int m_EmptySince=System::now,phase=1,endCalls=0,measuredTicks=0;
std::string m_EndReason;
Performance performance;Performance* m_Performance=&performance;
bool Active(){return !m_Stopped&&!m_NativeEndHandled&&phase>0&&phase<5;}
@@ -55,7 +64,7 @@ int main(){
int before=f.mode.measuredTicks;
f.tick(start+fiveHours,0);
assert(f.mode.endCalls==1&&f.mode.measuredTicks==before);
assert(f.mode.m_EndReason=="The server has been empty for five hours. Restarting the mission.");
assert(f.mode.m_EndReason=="The server has been empty for 5 hours. Restarting the mission.");
f.tick(start+fiveHours+1000,0);assert(f.mode.endCalls==1);
}
// All native connections block a scheduled restart, even without a spawned life.
@@ -82,21 +91,54 @@ int main(){
f.tick(disconnected?1000:fiveHours,0);
assert(f.mode.endCalls==1&&f.mode.measuredTicks==0&&f.mode.performance.starts==0);
}
// Client, stopped, already-ended and unavailable population must never reload.
for(int fault=0;fault<4;fault++){
// Independent controls: no idle restart at any age when both are disabled.
System::now=0;
{Fixture f;f.mode.settings.restart=0;f.mode.settings.hours=0;
f.tick(0,1);f.mode.ObservePlayerDisconnect();f.tick(1000,0);f.tick(169*3600000,0);
assert(f.mode.endCalls==0);}
// Last-disconnect restart remains available when the periodic interval is off.
System::now=0;
{Fixture f;f.mode.settings.hours=0;f.tick(1000,1);f.tick(2000,0);assert(f.mode.endCalls==1);}
// With immediate restart disabled, the full configured interval starts at
// the last disconnect even if the mission has been occupied for many hours.
for(int hours:{1,5,168}){
System::now=0;Fixture f;f.mode.settings.restart=0;f.mode.settings.hours=hours;
f.tick(20*3600000,1);System::now+=100;f.mode.ObservePlayerDisconnect();int since=System::now;
f.tick(since,0);f.tick(since+hours*3600000-1,0);assert(f.mode.endCalls==0);
f.tick(since+hours*3600000,0);assert(f.mode.endCalls==1);
}
// A new connection resets the idle interval, including a short unaudited visit.
System::now=0;
{Fixture f;f.mode.settings.restart=0;f.mode.settings.hours=1;
f.tick(3599999,1);System::now=3600000;f.mode.ObservePlayerDisconnect();
f.tick(7199999,0);assert(f.mode.endCalls==0);f.tick(7200000,0);assert(f.mode.endCalls==1);}
// Saved changes take effect on subsequent checks; disabling/enabling the
// interval does not forget elapsed empty time, and never restarts an occupied game.
System::now=0;
{Fixture f;f.mode.settings.restart=0;f.tick(2*3600000,0);assert(f.mode.endCalls==0);
f.mode.settings.hours=0;f.tick(6*3600000,0);assert(f.mode.endCalls==0);
f.mode.settings.hours=1;f.tick(6*3600000+1,1);assert(f.mode.endCalls==0);
f.tick(6*3600000+2,0);assert(f.mode.endCalls==0);
f.mode.settings.restart=1;f.tick(6*3600000+3,0);assert(f.mode.endCalls==1);}
System::now=0;
{Fixture f;f.mode.settings.restart=0;f.tick(2*3600000,0);f.mode.settings.hours=1;
f.tick(2*3600000+1,0);assert(f.mode.endCalls==1);}
// Client, stopped, already-ended and unavailable population/settings never reload.
for(int fault=0;fault<5;fault++){
System::now=0;Fixture f;
if(fault==0)Replication::server=false;
if(fault==1)f.mode.m_Stopped=true;
if(fault==2)f.mode.m_NativeEndHandled=true;
if(fault==3)game.manager=nullptr;
if(fault==4)f.mode.m_Settings=nullptr;
f.mode.ObservePlayerDisconnect();f.tick(fiveHours,0);assert(f.mode.endCalls==0);
}
}
''')
init = body(MODE, 'protected override void EOnInit(')
assert init.index('if (!Replication.IsServer()) return;') < init.index('m_EmptyMissionStartedAt = System.GetTickCount();')
assert init.index('m_EmptyMissionStartedAt = System.GetTickCount();') < init.index('CallLater(ServerTick, 1000, true)')
assert init.index('if (!Replication.IsServer()) return;') < init.index('m_EmptySince = System.GetTickCount();')
assert init.index('m_EmptySince = System.GetTickCount();') < init.index('CallLater(ServerTick, 1000, true)')
assert 'mode.ObservePlayerDisconnect();' in body(SPAWN, 'override void OnPlayerDisconnected_S(')
assert 'Remove(ServerTick)' in body(MODE, 'protected void StopServices(')
print('PASS: last disconnect, pending admission/spectator population, five-hour boundary, repeated empty rounds, preparation, authority and duplicate-end guards')
print('PASS: configurable independent restarts, idle clock reset and live setting changes; last disconnect, pending admission/spectator population, five-hour boundary, repeated empty rounds, preparation, authority and duplicate-end guards')
+1 -1
View File
@@ -5,7 +5,7 @@ from policy_harness import ROOT,body,run
spec=importlib.util.spec_from_file_location('import_settings',BASE/'tools/import_settings.py')
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
result,deferred=m.merge({'configVersion':1,'reportIntervalSeconds':20.0,'supportSquadLimit':2,'responseRadiusMetres':1500,'convoyCount':3},{'aiIntelChancePercent':0,'intelRevealChancePercent':100})
assert result==dict(aiDoctrine=0,aiShadow=0,planeAltitudeMetres=250,planeReplacementSeconds=60,planeSectorSeconds=120,planeReconEnabled=1,roadblockCount=4,civilianTrafficCount=3,startMode=0,keepWeaponOnUnconscious=1,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
assert result==dict(aiDoctrine=0,aiShadow=0,planeAltitudeMetres=250,planeReplacementSeconds=60,planeSectorSeconds=120,planeReconEnabled=1,roadblockCount=4,civilianTrafficCount=3,startMode=0,keepWeaponOnUnconscious=1,restartOnEmpty=1,emptyRestartHours=5,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
assert deferred==[]
for count in (0,1,3,20):
values,notes=m.merge(ai={'convoyCount':count})
+21
View File
@@ -55,6 +55,27 @@ int main(){
assert(!ERR_Settings::Randomizable("startMode")&&!ERR_Settings::ValidSelection("startMode",-1)&&!ERR_Settings::ValidSelection("startMode",-2));
disk["old"]={{"reportIntervalSeconds",10},{"supportSquadLimit",4},{"responseRadiusMetres",2000},{"aiIntelChancePercent",60},{"intelRevealChancePercent",15},{"radioDefenceSeconds",180}};
ERR_Settings settings;assert(settings.Read("old",settings.m_Values)&&settings.Get("startMode")==0);
// Older configurations gain the enabled/five-hour defaults. Each restart
// setting survives a save/reload, while invalid selections cannot change it.
for(string key:{"restartOnEmpty","emptyRestartHours"}){
int expected=key=="restartOnEmpty"?1:5;
assert(ERR_Settings::AddedDefault(key)==expected&&settings.Get(key)==expected);
assert(!ERR_Settings::Randomizable(key));
for(float bad:{-2.f,-1.f,.5f,169.f}){
disk["bad"]=disk["old"];disk["bad"][key]=bad;Values ignored;
assert(!settings.Read("bad",ignored));
}
int choice=key=="restartOnEmpty"?0:168;
assert(settings.Set(key,choice));
ERR_Settings restored;assert(restored.Read("settings",restored.m_Values));
assert(restored.Get(key)==choice);
failSave=true;assert(!settings.Set(key,expected));failSave=false;
assert(settings.Get(key)==choice);
assert(settings.Set(key,expected));
}
int restartSelection=-1;
assert(ERR_CommandArguments::Selection("restartOnEmpty","on",restartSelection)&&restartSelection==1);
assert(ERR_CommandArguments::Selection("restartOnEmpty","off",restartSelection)&&restartSelection==0);
for(float bad:{-1.f,-2.f,2.f,.5f}){disk["bad"]=disk["old"];disk["bad"]["startMode"]=bad;Values ignored;assert(!settings.Read("bad",ignored));}
// Newly introduced doctrine choices migrate safely and survive the same transaction.
+2
View File
@@ -13,6 +13,8 @@ SCHEMA={
'civilianTrafficCount':(3,0,6),
'startMode':(0,0,1),
'keepWeaponOnUnconscious':(1,0,1),
'restartOnEmpty':(1,0,1),
'emptyRestartHours':(5,0,168),
'reportIntervalSeconds':(10,5,60),
'supportSquadLimit':(4,0,8),
'responseRadiusMetres':(2000,500,5000),