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 { /// /// Title screen main menu, fully driven by . 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 /// , 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 as panels "settings" /// and "save_load", so other mods can open them by id. /// public partial class MainMenu : Control { private SettingsMenu _settingsMenu; private SaveLoadMenu _saveLoadMenu; private GamePickerPanel _gamePicker; private readonly Dictionary _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; } } /// /// Assembles the runtime UI: theme, hidden settings and save/load panels, /// telegram link wiring, scene manager initialization and registry/tool /// driven buttons. /// 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.Name = "SettingsPanel"; _settingsMenu.Visible = false; _settingsMenu.LanguageChanged += RefreshButtons; AddChild(_settingsMenu); _saveLoadMenu = new ModScenePrefab("scenes/menu/save_load_menu.tscn").Instantiate(); _saveLoadMenu.Name = "SaveLoadPanel"; _saveLoadMenu.Visible = false; AddChild(_saveLoadMenu); _gamePicker = new ModScenePrefab("scenes/menu/game_picker.tscn").Instantiate(); _gamePicker.Name = "GamePickerPanel"; AddChild(_gamePicker); _gamePicker.SavesRequested += gameId => { _gamePicker.Visible = false; _saveLoadMenu.OpenForGame(gameId); }; var telegramLink = GetNode("TelegramLink"); telegramLink.MetaClicked += OnTelegramLinkClicked; var sceneManager = GetNode("/root/SceneManager"); sceneManager.Initialize(); BuildRegistryButtons(); BuildToolButtons(); CheckModCompilationErrors(); RefreshButtons(); StartWarmUp(); } /// /// Compiles the mods of every ready game into the assembly cache in the /// background while the player looks at the menu (), /// the last played game first, so Play / Continue / Load only load /// cached assemblies instead of compiling behind the loading screen. /// private static void StartWarmUp() { var last = LastSessionStore.Load()?.GameId; var ids = new List(); 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); } /// /// Creates one button per entry, routed to the /// section container matching its Section ("game", "mods", "exit", /// "tools"). Runs after 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 /// keeps it localized. /// private void BuildRegistryButtons() { var sections = new Dictionary { { "game", GetNode("MenuContainer/GameButtons") }, { MenuButtonEntry.SectionMods, GetNode("MenuContainer/ModMenuButtons") }, { "exit", GetNode("MenuContainer/ExitButtons") }, { "tools", GetNode("MenuContainer/ToolMenuButtons") }, }; MenuBuilder.Build(this, sections); foreach (var container in sections.Values) MenuBuilder.CollectLocaleKeys(container, _btnKeys); } /// /// Lets every registered tool append its launcher button to the tool block /// below Exit (MenuContainer/ToolMenuButtons). Tools register /// themselves in , 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 /// aware of it. /// private void BuildToolButtons() { var container = GetNodeOrNull("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()}"); } } }