The main menu warms every ready game's mods up into the assembly cache in the background (last played first), so picking a game only loads cached assemblies. Game picker and save browser are scenes on the shared MenuWindowShell (also used by the settings): caps title, close on the right, footer hint; the picker footer shows the warm-up state, cards get framed covers with a letter placeholder and Continue as the main action; the save browser shows 3x2 large cards with the game selector in the header. new_game waits for the warm-up; menu_flow takes --shots.
199 lines
7.7 KiB
C#
199 lines
7.7 KiB
C#
using System.Collections.Generic;
|
|
using Cthangover.Core.Games;
|
|
using Cthangover.Core.Scenes;
|
|
using Cthangover.Core.Settings;
|
|
using Cthangover.Core.UI;
|
|
using Cthangover.Core.UI.Menu;
|
|
using Cthangover.Core.UI.Theming;
|
|
using Cthangover.Core.UI.Tool;
|
|
using Cthangover.Core.Utils;
|
|
using Godot;
|
|
|
|
using Cthangover.Mods.Interface.Menu;
|
|
namespace Cthangover.BaseMenu
|
|
{
|
|
/// <summary>
|
|
/// Title screen main menu, fully driven by <see cref="MenuRegistry"/>. The scene
|
|
/// only defines named section containers (game, mods, exit, tools); every button —
|
|
/// built-in or mod-provided — is created from registry entries, so mods can add,
|
|
/// replace or re-order entries without touching this scene. Below the sections,
|
|
/// every registered tool appends its own launcher button via
|
|
/// <see cref="IToolProvider.AddMenuButton"/>, so no tool list is hardcoded here.
|
|
/// If GameLogger reports mod compilation errors, a popup lists them. The
|
|
/// SettingsMenu and SaveLoadMenu panels are instantiated at ready time but stay
|
|
/// hidden; they are registered in <see cref="MenuRegistry"/> as panels "settings"
|
|
/// and "save_load", so other mods can open them by id.
|
|
/// </summary>
|
|
public partial class MainMenu : Control
|
|
{
|
|
private SettingsMenu _settingsMenu;
|
|
private SaveLoadMenu _saveLoadMenu;
|
|
private GamePickerPanel _gamePicker;
|
|
private readonly Dictionary<Button, string> _btnKeys = new();
|
|
|
|
public override void _Ready()
|
|
{
|
|
try
|
|
{
|
|
BuildUi();
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
GameLogger.Log("MAIN_MENU",
|
|
$"_Ready failed: {ex.GetType().Name}: {ex.Message}{System.Environment.NewLine}{ex.StackTrace}",
|
|
LogLevel.Error);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Assembles the runtime UI: theme, hidden settings and save/load panels,
|
|
/// telegram link wiring, scene manager initialization and registry/tool
|
|
/// driven buttons.
|
|
/// </summary>
|
|
private void BuildUi()
|
|
{
|
|
CthangoverTheme.Apply(this);
|
|
// Panels of the session (pause menu, inventory, studio...) live on the
|
|
// persistent UI layer above every scene; none of them belongs on the title screen.
|
|
UiHost.Instance?.HideAll();
|
|
|
|
_settingsMenu = new ModScenePrefab("scenes/menu/settings_menu.tscn").Instantiate<SettingsMenu>();
|
|
_settingsMenu.Name = "SettingsPanel";
|
|
_settingsMenu.Visible = false;
|
|
_settingsMenu.LanguageChanged += RefreshButtons;
|
|
AddChild(_settingsMenu);
|
|
|
|
_saveLoadMenu = new ModScenePrefab("scenes/menu/save_load_menu.tscn").Instantiate<SaveLoadMenu>();
|
|
_saveLoadMenu.Name = "SaveLoadPanel";
|
|
_saveLoadMenu.Visible = false;
|
|
AddChild(_saveLoadMenu);
|
|
|
|
_gamePicker = new ModScenePrefab("scenes/menu/game_picker.tscn").Instantiate<GamePickerPanel>();
|
|
_gamePicker.Name = "GamePickerPanel";
|
|
AddChild(_gamePicker);
|
|
_gamePicker.SavesRequested += gameId =>
|
|
{
|
|
_gamePicker.Visible = false;
|
|
_saveLoadMenu.OpenForGame(gameId);
|
|
};
|
|
|
|
var telegramLink = GetNode<RichTextLabel>("TelegramLink");
|
|
telegramLink.MetaClicked += OnTelegramLinkClicked;
|
|
|
|
var sceneManager = GetNode<SceneManager>("/root/SceneManager");
|
|
sceneManager.Initialize();
|
|
|
|
BuildRegistryButtons();
|
|
BuildToolButtons();
|
|
CheckModCompilationErrors();
|
|
|
|
RefreshButtons();
|
|
StartWarmUp();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compiles the mods of every ready game into the assembly cache in the
|
|
/// background while the player looks at the menu (<see cref="GameSession.WarmUp"/>),
|
|
/// the last played game first, so Play / Continue / Load only load
|
|
/// cached assemblies instead of compiling behind the loading screen.
|
|
/// </summary>
|
|
private static void StartWarmUp()
|
|
{
|
|
var last = LastSessionStore.Load()?.GameId;
|
|
var ids = new List<string>();
|
|
foreach (var game in GameCatalog.Games())
|
|
{
|
|
if (!game.IsReady)
|
|
continue;
|
|
if (string.Equals(game.Collection.Id, last, System.StringComparison.OrdinalIgnoreCase))
|
|
ids.Insert(0, game.Collection.Id);
|
|
else
|
|
ids.Add(game.Collection.Id);
|
|
}
|
|
GameSession.WarmUp(ids);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates one button per <see cref="MenuRegistry"/> entry, routed to the
|
|
/// section container matching its <c>Section</c> ("game", "mods", "exit",
|
|
/// "tools"). Runs after <see cref="SceneManager.Initialize"/> because the
|
|
/// registry is built during boot and filled by mod initializers; the
|
|
/// defensive Initialize call keeps direct scene launches working. Each
|
|
/// button's translation key is registered so <see cref="RefreshButtons"/>
|
|
/// keeps it localized.
|
|
/// </summary>
|
|
private void BuildRegistryButtons()
|
|
{
|
|
var sections = new Dictionary<string, Container>
|
|
{
|
|
{ "game", GetNode<Container>("MenuContainer/GameButtons") },
|
|
{ MenuButtonEntry.SectionMods, GetNode<Container>("MenuContainer/ModMenuButtons") },
|
|
{ "exit", GetNode<Container>("MenuContainer/ExitButtons") },
|
|
{ "tools", GetNode<Container>("MenuContainer/ToolMenuButtons") },
|
|
};
|
|
MenuBuilder.Build(this, sections);
|
|
|
|
foreach (var container in sections.Values)
|
|
MenuBuilder.CollectLocaleKeys(container, _btnKeys);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lets every registered tool append its launcher button to the tool block
|
|
/// below Exit (<c>MenuContainer/ToolMenuButtons</c>). Tools register
|
|
/// themselves in <see cref="ToolFactory"/>, so the block stays empty when no
|
|
/// tools are installed and needs no changes when a mod introduces a new tool.
|
|
/// Each button carries its translation key in metadata, keeping
|
|
/// <see cref="RefreshButtons"/> aware of it.
|
|
/// </summary>
|
|
private void BuildToolButtons()
|
|
{
|
|
var container = GetNodeOrNull<VBoxContainer>("MenuContainer/ToolMenuButtons");
|
|
if (container == null)
|
|
return;
|
|
|
|
foreach (var tool in ToolFactory.Instance.GetAll())
|
|
tool.AddMenuButton(container);
|
|
|
|
MenuBuilder.CollectLocaleKeys(container, _btnKeys);
|
|
|
|
GameLogger.Log("MAIN_MENU", $"Built {container.GetChildCount()} tool button(s)");
|
|
}
|
|
|
|
private void CheckModCompilationErrors()
|
|
{
|
|
if (GameLogger.CompilationErrors.Count == 0)
|
|
return;
|
|
|
|
var errorText = string.Join("\r\n", GameLogger.CompilationErrors);
|
|
ShowModErrorPopup(errorText);
|
|
}
|
|
|
|
private void RefreshButtons()
|
|
{
|
|
foreach (var kv in _btnKeys)
|
|
kv.Key.Text = TranslationServer.Translate(kv.Value);
|
|
}
|
|
|
|
private void ShowModErrorPopup(string errorText)
|
|
{
|
|
var dialog = new AcceptDialog();
|
|
dialog.Title = "Mod Compilation Error";
|
|
dialog.DialogText = errorText;
|
|
dialog.OkButtonText = "OK";
|
|
dialog.Exclusive = true;
|
|
AddChild(dialog);
|
|
dialog.PopupCentered();
|
|
}
|
|
|
|
private void OnTelegramLinkClicked(Variant meta)
|
|
{
|
|
OS.ShellOpen($"https://{meta.AsString()}");
|
|
}
|
|
}
|
|
}
|
|
|
|
|