Allow leaving failed-start screens; record Workshop 0.0.122
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-10-25 14:55:36 +01:00
parent 6397fe89b6
commit 9233320fd1
21 changed files with 3315 additions and 3 deletions
+5
View File
@@ -1,5 +1,10 @@
MenuManager {
MenuPresets {
MenuPreset ERR_PreparationFailure {
Layout "{28DD8E27B1CF40C5}UI/layouts/ERR/PreparationFailure.layout"
ActionContext "MenuContext"
Class "ERR_PreparationFailureMenu"
}
MenuPreset ERR_AdminPanel {
Layout "{AE761C0439D25FB8}UI/layouts/ERR/AdminMenu.layout"
ActionContext "MenuContext"
@@ -0,0 +1,92 @@
modded enum ChimeraMenuPreset
{
ERR_PreparationFailure
}
// The native loading placeholder is not a menu and has no focus/input lifetime.
// Use a real local menu on failure; never grant admission or change server state.
class ERR_PreparationFailureMenu : ChimeraMenuBase
{
protected ERR_GameMode m_Mode;
protected SCR_ButtonTextComponent m_ReturnButton, m_PauseButton;
protected TextWidget m_Description;
protected string m_LastError;
protected bool m_Bound, m_Leaving;
static bool Failed(ERR_GameMode mode)
{
return mode && mode == ERR_GameMode.Get() && mode.Phase() == ERR_Phase.PREPARING && !mode.PreparationError().IsEmpty();
}
static bool Show(ERR_GameMode mode)
{
if (System.IsConsoleApp() || !Failed(mode) || !GetGame() || !GetGame().GetMenuManager()) return false;
MenuManager manager = GetGame().GetMenuManager();
// Leave existing menus and exit confirmation dialogs in control of focus.
if (manager.IsAnyMenuOpen() || manager.IsAnyDialogOpen()) return true;
if (manager.OpenMenu(ChimeraMenuPreset.ERR_PreparationFailure)) return true;
// A missing custom layout must not trap the player on the loading overlay.
GetGame().OpenPauseMenu(false, true);
return manager.FindMenuByPreset(ChimeraMenuPreset.PauseMenu) != null;
}
override void OnMenuOpen()
{
super.OnMenuOpen();
m_Mode = ERR_GameMode.Get();
if (!Failed(m_Mode)) { Close(); return; }
m_Description = TextWidget.Cast(GetRootWidget().FindAnyWidget("FailureDescription"));
m_ReturnButton = SCR_ButtonTextComponent.GetButtonText("ReturnToMainMenu", GetRootWidget());
m_PauseButton = SCR_ButtonTextComponent.GetButtonText("OpenPauseMenu", GetRootWidget());
if (m_ReturnButton) m_ReturnButton.m_OnClicked.Insert(OnReturn);
if (m_PauseButton) m_PauseButton.m_OnClicked.Insert(OnPause);
GetGame().GetInputManager().AddActionListener(UIConstants.MENU_ACTION_BACK, EActionTrigger.PRESSED, OnBack);
m_Bound = true;
Refresh();
}
protected void Refresh()
{
string detail = ERR_ObjectiveText.Description(m_Mode, false);
if (m_Description && detail != m_LastError) { m_Description.SetText(detail); m_LastError = detail; }
}
override void OnMenuUpdate(float tDelta)
{
super.OnMenuUpdate(tDelta);
if (!Failed(m_Mode)) { Close(); return; }
if (!m_Leaving) Refresh();
}
protected void OpenPause()
{
if (!IsFocused() || m_Leaving || !Failed(m_Mode)) return;
GetGame().OpenPauseMenu(false, true);
}
protected void OnBack() { OpenPause(); }
protected void OnPause(SCR_ButtonBaseComponent button) { OpenPause(); }
protected void OnReturn(SCR_ButtonBaseComponent button)
{
if (!IsFocused() || m_Leaving || !Failed(m_Mode)) return;
m_Leaving = true;
if (m_ReturnButton) m_ReturnButton.SetEnabled(false);
if (m_PauseButton) m_PauseButton.SetEnabled(false);
ChimeraWorld world = GetGame().GetWorld();
if (world) world.PauseGameTime(false);
// Same local exit transition as vanilla's pause-menu exit. No mission RPC.
GameStateTransitions.RequestGameplayEndTransition();
}
override void OnMenuClose()
{
if (m_Bound && GetGame() && GetGame().GetInputManager())
GetGame().GetInputManager().RemoveActionListener(UIConstants.MENU_ACTION_BACK, EActionTrigger.PRESSED, OnBack);
m_Bound = false;
if (m_ReturnButton) m_ReturnButton.m_OnClicked.Remove(OnReturn);
if (m_PauseButton) m_PauseButton.m_OnClicked.Remove(OnPause);
m_ReturnButton = null; m_PauseButton = null; m_Description = null; m_Mode = null;
super.OnMenuClose();
}
#ifdef WORKBENCH
static bool Visible(ERR_GameMode mode)
{
if (!Failed(mode)) return false;
ERR_PreparationFailureMenu menu = ERR_PreparationFailureMenu.Cast(GetGame().GetMenuManager().FindMenuByPreset(ChimeraMenuPreset.ERR_PreparationFailure));
return menu && menu.m_Mode == mode && menu.GetRootWidget().IsVisible() && menu.m_Description && menu.m_ReturnButton && menu.m_PauseButton && menu.m_Description.GetText() == ERR_ObjectiveText.Description(mode, false);
}
#endif
}
@@ -109,6 +109,10 @@ modded class SCR_RespawnSystemComponent
if (m_ERRPreparationSpinner) m_ERRPreparationSpinner.SetVisible(m_ERRPreparationSpinnerWasVisible);
m_ERRPreparationSpinnerHidden = false;
}
// The placeholder's high-Z black layer would obscure even the pause menu.
// Hide it only after an interactive menu is available; native updates restore
// it if preparation recovers. The failed round still admits no players.
if (failed && ERR_PreparationFailureMenu.Show(mode)) m_wLoadingPlaceholder.SetVisible(false);
}
override void DestroyLoadingPlaceholder()
@@ -130,6 +134,7 @@ modded class SCR_RespawnSystemComponent
// Inspect the actual production-created widgets, without creating or changing them.
bool ERR_ProbePreparationVisible(ERR_GameMode mode, bool failed)
{
if (failed && ERR_PreparationFailureMenu.Visible(mode)) return true;
if (!mode || mode != ERR_GameMode.Get() || m_pGameMode != mode || !m_wLoadingPlaceholder || !m_wLoadingPlaceholder.IsVisible()) return false;
if (!m_ERRPreparationRoot || m_ERRPreparationParent != m_wLoadingPlaceholder || m_ERRPreparationRoot.GetParent() != m_wLoadingPlaceholder || !m_ERRPreparationRoot.IsVisible()) return false;
if (!ERR_PreparationWidgetVisible(m_ERRPreparationTitle) || !ERR_PreparationWidgetVisible(m_ERRPreparationDescription) || !ERR_PreparationWidgetVisible(m_ERRPreparationRoot)) return false;
@@ -0,0 +1,43 @@
FrameWidgetClass {
Name "ERR_PreparationFailure"
Slot FrameWidgetSlot { Anchor 0 0 1 1 OffsetLeft 0 OffsetTop 0 OffsetRight 0 OffsetBottom 0 }
{
ImageWidgetClass {
Name "Background"
Slot FrameWidgetSlot { Anchor 0 0 1 1 OffsetLeft 0 OffsetTop 0 OffsetRight 0 OffsetBottom 0 }
Color 0.025 0.035 0.04 1
}
FrameWidgetClass {
Name "FailurePanel"
Slot FrameWidgetSlot { Anchor 0.5 0.5 0.5 0.5 OffsetLeft 0 OffsetTop 0 SizeX 960 OffsetRight -960 SizeY 620 OffsetBottom -620 Alignment 0.5 0.5 }
{
TextWidgetClass {
Name "FailureTitle"
Slot FrameWidgetSlot { Anchor 0 0 1 0 OffsetLeft 0 OffsetTop 0 SizeY 58 OffsetBottom -58 }
Color 1 1 1 1
Text "Mission could not start"
"Font Size" 32
"Min Font Size" 32
}
TextWidgetClass {
Name "FailureDescription"
Slot FrameWidgetSlot { Anchor 0 0 1 0 OffsetLeft 0 OffsetTop 80 SizeY 400 OffsetBottom -480 }
Color 1 1 1 1
"Font Size" 22
"Min Font Size" 22
Wrap 1
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "ReturnToMainMenu"
Slot FrameWidgetSlot { Anchor 0 0 0 0 OffsetLeft 0 OffsetTop 520 SizeX 400 OffsetRight -400 SizeY 56 OffsetBottom -576 }
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Return to main menu" } }
}
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
Name "OpenPauseMenu"
Slot FrameWidgetSlot { Anchor 0 0 0 0 OffsetLeft 430 OffsetTop 520 SizeX 400 OffsetRight -830 SizeY 56 OffsetBottom -576 }
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Pause menu (Esc / Back)" } }
}
}
}
}
}
@@ -0,0 +1,11 @@
MetaFileClass {
Name "{28DD8E27B1CF40C5}UI/layouts/ERR/PreparationFailure.layout"
Configurations {
LayoutResourceClass PC { }
LayoutResourceClass HEADLESS : PC { }
LayoutResourceClass XBOX_ONE : PC { }
LayoutResourceClass XBOX_SERIES : PC { }
LayoutResourceClass PS4 : PC { }
LayoutResourceClass PS5 : PC { }
}
}