diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 4bcbf5be..759e69ed 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -58,6 +58,17 @@ namespace MinecraftClient // Setting this string to an empty string will disable Sentry private const string SentryDSN = ""; + /// + /// Snapshot of all state collected before the console backend is initialized. + /// Passed to once the backend is ready. + /// + internal sealed class StartupState + { + public Settings.ConfigLoadResult ConfigResult { get; init; } + public bool NewlyGenerated { get; init; } + public bool SentryEnabled { get; init; } + } + /// /// The main entry point of Minecraft Console Client /// @@ -103,7 +114,6 @@ namespace MinecraftClient Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); }); - //Setup ConsoleIO ConsoleIO.LogPrefix = "§8[MCC] "; if (args.Length >= 1 && args[^1] == "BasicIO" || args.Length >= 1 && args[^1] == "BasicIO-NoColor") { @@ -115,133 +125,178 @@ namespace MinecraftClient args = args.Where(o => !Object.ReferenceEquals(o, args[^1])).ToArray(); } + //Debug input ? + if (args.Length == 1 && args[0] == "--keyboard-debug") + { + if (!ConsoleIO.BasicIO) + { + ConsoleIO.Backend = new ClassicConsoleBackend(); + ConsoleIO.Backend.Init(); + } + ConsoleIO.WriteLine("Keyboard debug mode: Press any key to display info"); + ConsoleIO.DebugReadInput(); + } + + // --- Load config as early as possible (no printing yet) --- + Settings.ConfigLoadResult configResult; + bool newlyGenerated = false; + + if (args.Length >= 1 && File.Exists(args[0]) && Settings.ToLowerIfNeed(Path.GetExtension(args[0])) == ".ini") + { + configResult = Settings.LoadFromFile(args[0]); + settingsIniPath = args[0]; + + List args_tmp = args.ToList(); + args_tmp.RemoveAt(0); + args = args_tmp.ToArray(); + } + else if (File.Exists("MinecraftClient.ini")) + { + configResult = Settings.LoadFromFile("MinecraftClient.ini"); + } + else + { + configResult = new Settings.ConfigLoadResult { Success = true, NeedWriteDefault = true }; + newlyGenerated = true; + } + + if (configResult.NeedWriteDefault) + { + Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage(); + WriteBackSettings(false); + } + else if (configResult.Success) + { + WriteBackSettings(true); + } + + if (!Config.Main.Advanced.EnableSentry) + _sentrySdk?.Dispose(); + + var startupState = new StartupState + { + ConfigResult = configResult, + NewlyGenerated = newlyGenerated, + SentryEnabled = SentryDSN != string.Empty, + }; + + // --- Determine console mode and initialize backend --- + if (!ConsoleIO.BasicIO && Config.Console.General.ConsoleMode == ConsoleModeType.tui) + { + ConsoleIO.Backend?.Shutdown(); + var tuiBackend = new Tui.TuiConsoleBackend(); + ConsoleIO.Backend = tuiBackend; + tuiBackend.RunTuiMainLoop(args, startupState); + return; + } + + // Classic mode: init backend, then print and process startup state. if (!ConsoleIO.BasicIO) { ConsoleIO.Backend = new ClassicConsoleBackend(); ConsoleIO.Backend.Init(); } - ConsoleIO.WriteLine($"Minecraft Console Client v{Version} - for MC {MCLowestVersion} to {MCHighestVersion} - Github.com/MCCTeam"); - - //Build information to facilitate processing of bug reports - if (BuildInfo is not null) - ConsoleIO.WriteLineFormatted("§8" + BuildInfo); - - //Debug input ? - if (args.Length == 1 && args[0] == "--keyboard-debug") - { - ConsoleIO.WriteLine("Keyboard debug mode: Press any key to display info"); - ConsoleIO.DebugReadInput(); - } - - //Process ini configuration file - { - bool loadSucceed, needWriteDefaultSetting, newlyGenerated = false; - if (args.Length >= 1 && File.Exists(args[0]) && Settings.ToLowerIfNeed(Path.GetExtension(args[0])) == ".ini") - { - (loadSucceed, needWriteDefaultSetting) = Settings.LoadFromFile(args[0]); - settingsIniPath = args[0]; - - //remove ini configuration file from arguments array - List args_tmp = args.ToList(); - args_tmp.RemoveAt(0); - args = args_tmp.ToArray(); - } - else if (File.Exists("MinecraftClient.ini")) - { - (loadSucceed, needWriteDefaultSetting) = Settings.LoadFromFile("MinecraftClient.ini"); - } - else - { - loadSucceed = true; - needWriteDefaultSetting = true; - newlyGenerated = true; - } - - if (needWriteDefaultSetting) - { - Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage(); - WriteBackSettings(false); - if (newlyGenerated) - ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_settings_generated); - ConsoleIO.WriteLine(Translations.mcc_run_with_default_settings); - - // Only show the Sentry message if the DSN is not empty - // as Sentry will not be initialized if the DSN is empty - if (SentryDSN != string.Empty) - { - ConsoleIO.WriteLine(Translations.mcc_sentry_logging); - } - } - else if (!loadSucceed) - { - ConsoleIO.Backend?.StopReadThread(); - string command = " "; - while (command.Length > 0) - { - ConsoleIO.WriteLine(string.Empty); - ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_invaild_config, Config.Main.Advanced.InternalCmdChar.ToLogString())); - if (ConsoleIO.Backend is Tui.TuiConsoleBackend) - ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_quit_to_exit, Config.Main.Advanced.InternalCmdChar.ToLogString())); - else - ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true); - command = ConsoleIO.ReadLine().Trim(); - if (command.Length > 0) - { - if (Config.Main.Advanced.InternalCmdChar.ToChar() != ' ' - && command[0] == Config.Main.Advanced.InternalCmdChar.ToChar()) - command = command[1..]; - - if (command.StartsWith("exit") || command.StartsWith("quit")) - { - return; - } - else if (command.StartsWith("new")) - { - Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage(); - WriteBackSettings(true); - ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_gen_new_config, settingsIniPath)); - return; - } - } - else - { - return; - } - } - return; - } - else - { - //Load external translation file. Should be called AFTER settings loaded - if (!Config.Main.Advanced.Language.StartsWith("en")) - ConsoleIO.WriteLine(string.Format(Translations.mcc_help_us_translate, Settings.TranslationProjectUrl)); - WriteBackSettings(true); // format - } - - if (!Config.Main.Advanced.EnableSentry) - _sentrySdk?.Dispose(); - } - - // Switch to TUI mode if configured (must happen after config load) - if (!ConsoleIO.BasicIO && Config.Console.General.ConsoleMode == ConsoleModeType.tui) - { - ConsoleIO.Backend?.Shutdown(); - var tuiBackend = new Tui.TuiConsoleBackend(); - ConsoleIO.Backend = tuiBackend; - tuiBackend.RunTuiMainLoop(args); + if (!ProcessStartupState(startupState)) return; - } - ContinueAfterTuiInit(args); + RunStartupSequence(args); } /// - /// Continues MCC startup after console mode has been determined. - /// Called directly from Main for classic/basic mode, or from a background - /// thread for TUI mode (after the Avalonia UI loop has started). + /// Prints the application banner and processes the startup state collected before + /// the console backend was ready. Called once from classic mode or from TUI after + /// the view is initialized. /// - internal static void ContinueAfterTuiInit(string[] args) + /// True if startup can continue; false if config load failed and user chose to exit. + internal static bool ProcessStartupState(StartupState state) + { + ConsoleIO.WriteLine($"Minecraft Console Client v{Version} - for MC {MCLowestVersion} to {MCHighestVersion} - Github.com/MCCTeam"); + if (BuildInfo is not null) + ConsoleIO.WriteLineFormatted("§8" + BuildInfo); + + var cfg = state.ConfigResult; + + if (cfg.NeedWriteDefault) + { + if (cfg.IsLegacyUpgrade) + { + ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_use_new_config); + ConsoleIO.WriteLineFormatted("§c" + string.Format(Translations.mcc_backup_old_config, cfg.LegacyBackupPath)); + } + + if (state.NewlyGenerated) + ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_settings_generated); + + ConsoleIO.WriteLine(Translations.mcc_run_with_default_settings); + + if (state.SentryEnabled) + ConsoleIO.WriteLine(Translations.mcc_sentry_logging); + } + else if (!cfg.Success) + { + ConsoleIO.WriteLineFormatted("§c" + Translations.config_load_fail); + if (cfg.ErrorMessage is not null) + ConsoleIO.WriteLine(cfg.ErrorMessage); + HandleConfigLoadFailure(); + return false; + } + else + { + if (!Config.Main.Advanced.Language.StartsWith("en")) + ConsoleIO.WriteLine(string.Format(Translations.mcc_help_us_translate, Settings.TranslationProjectUrl)); + } + + return true; + } + + /// + /// Handles a failed config load by prompting the user to fix or regenerate the config file. + /// + internal static void HandleConfigLoadFailure() + { + ConsoleIO.Backend?.StopReadThread(); + string command = " "; + while (command.Length > 0) + { + ConsoleIO.WriteLine(string.Empty); + ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_invaild_config, Config.Main.Advanced.InternalCmdChar.ToLogString())); + if (ConsoleIO.Backend is Tui.TuiConsoleBackend) + ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_quit_to_exit, Config.Main.Advanced.InternalCmdChar.ToLogString())); + else + ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true); + command = ConsoleIO.ReadLine().Trim(); + if (command.Length > 0) + { + if (Config.Main.Advanced.InternalCmdChar.ToChar() != ' ' + && command[0] == Config.Main.Advanced.InternalCmdChar.ToChar()) + command = command[1..]; + + if (command.StartsWith("exit") || command.StartsWith("quit")) + { + return; + } + else if (command.StartsWith("new")) + { + Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage(); + WriteBackSettings(true); + ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_gen_new_config, settingsIniPath)); + return; + } + } + else + { + return; + } + } + } + + /// + /// Runs the main startup sequence: CLI argument processing, auth, and connection. + /// Called from Main() for classic/basic mode, or from TuiConsoleBackend on a + /// background thread after the Avalonia UI loop has started. + /// + internal static void RunStartupSequence(string[] args) { //Other command-line arguments if (args.Length >= 1) @@ -732,7 +787,8 @@ namespace MinecraftClient /// public static void ReloadSettings(bool keepAccountAndServerSettings = false) { - if (Settings.LoadFromFile(settingsIniPath, keepAccountAndServerSettings).Item1) + var result = Settings.LoadFromFile(settingsIniPath, keepAccountAndServerSettings); + if (result.Success) ConsoleIO.WriteLine(string.Format(Translations.config_load, settingsIniPath)); } diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 79e314d0..a3368c8e 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -136,7 +136,22 @@ namespace MinecraftClient } - public static Tuple LoadFromFile(string filepath, bool keepAccountAndServerSettings = false) + /// + /// Structured result returned by . + /// + public readonly struct ConfigLoadResult + { + public bool Success { get; init; } + public bool NeedWriteDefault { get; init; } + /// True when a pre-TOML legacy config was detected, backed up, and a fresh default is needed. + public bool IsLegacyUpgrade { get; init; } + /// Non-null when the load failed due to a parse/IO error (not a legacy upgrade). + public string? ErrorMessage { get; init; } + /// Path where the old config was backed up (legacy upgrade case). + public string? LegacyBackupPath { get; init; } + } + + public static ConfigLoadResult LoadFromFile(string filepath, bool keepAccountAndServerSettings = false) { bool keepAccountSettings = InternalConfig.KeepAccountSettings; bool keepServerSettings = InternalConfig.KeepServerSettings; @@ -157,21 +172,27 @@ namespace MinecraftClient Thread.CurrentThread.CurrentCulture = Program.ActualCulture; try { - // The old configuration file has been backed up as A. string configString = File.ReadAllText(filepath); if (configString.Contains("Some settings missing here after an upgrade?")) { string newFilePath = Path.ChangeExtension(filepath, ".old.ini"); File.Copy(filepath, newFilePath, true); - ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_use_new_config); - ConsoleIO.WriteLineFormatted("§c" + string.Format(Translations.mcc_backup_old_config, newFilePath)); - return new(false, true); + return new ConfigLoadResult + { + Success = false, + NeedWriteDefault = true, + IsLegacyUpgrade = true, + LegacyBackupPath = newFilePath + }; } } catch { } - ConsoleIO.WriteLineFormatted("§c" + Translations.config_load_fail); - ConsoleIO.WriteLine(ex.GetFullMessage()); - return new(false, false); + return new ConfigLoadResult + { + Success = false, + NeedWriteDefault = false, + ErrorMessage = ex.GetFullMessage() + }; } finally { @@ -180,7 +201,7 @@ namespace MinecraftClient if (!keepServerSettings) InternalConfig.KeepServerSettings = false; } - return new(true, false); + return new ConfigLoadResult { Success = true, NeedWriteDefault = false }; } public static void WriteToFile(string filepath, bool backupOldFile) diff --git a/MinecraftClient/Tui/TuiConsoleBackend.cs b/MinecraftClient/Tui/TuiConsoleBackend.cs index 64d5af86..bee59751 100644 --- a/MinecraftClient/Tui/TuiConsoleBackend.cs +++ b/MinecraftClient/Tui/TuiConsoleBackend.cs @@ -25,14 +25,18 @@ namespace MinecraftClient.Tui internal static TuiConsoleBackend? Instance { get; private set; } + private Program.StartupState? _pendingStartupState; + private readonly ManualResetEventSlim _viewReady = new(false); + /// /// Initializes the Avalonia app and starts the main UI loop. /// This blocks the calling thread until the TUI exits. /// Before blocking, it starts MCC's remaining initialization on a background thread. /// - public void RunTuiMainLoop(string[] args) + internal void RunTuiMainLoop(string[] args, Program.StartupState startupState) { Instance = this; + _pendingStartupState = startupState; AppDomain.CurrentDomain.ProcessExit += (_, _) => RestoreTerminalState(); @@ -46,7 +50,7 @@ namespace MinecraftClient.Tui new Thread(() => { - Thread.Sleep(500); + _viewReady.Wait(); ContinueMccStartup(args); }) { Name = "MCC-Main", IsBackground = true }.Start(); @@ -113,7 +117,15 @@ namespace MinecraftClient.Tui { try { - Program.ContinueAfterTuiInit(args); + var instance = Instance; + if (instance?._pendingStartupState is { } state) + { + instance._pendingStartupState = null; + if (!Program.ProcessStartupState(state)) + return; + } + + Program.RunStartupSequence(args); } catch (Exception ex) { @@ -124,6 +136,7 @@ namespace MinecraftClient.Tui internal void SetView(MainTuiView view) { _view = view; + _viewReady.Set(); } internal MainTuiView? GetView() => _view;