Implement startup state management and enhance configuration loading

- Introduced a new `StartupState` class to encapsulate the state collected before the console backend initialization.
- Updated the configuration loading process to handle legacy upgrades and provide detailed feedback on configuration status.
- Enhanced the TUI backend to utilize the new startup state for improved initialization flow.
- Refactored the `LoadFromFile` method in `Settings` to return a structured result, improving error handling and clarity.
This commit is contained in:
BruceChen 2026-03-27 00:29:01 +08:00
parent b33ee7e4a1
commit fe7ab9f373
3 changed files with 219 additions and 129 deletions

View file

@ -58,6 +58,17 @@ namespace MinecraftClient
// Setting this string to an empty string will disable Sentry // Setting this string to an empty string will disable Sentry
private const string SentryDSN = ""; private const string SentryDSN = "";
/// <summary>
/// Snapshot of all state collected before the console backend is initialized.
/// Passed to <see cref="ProcessStartupState"/> once the backend is ready.
/// </summary>
internal sealed class StartupState
{
public Settings.ConfigLoadResult ConfigResult { get; init; }
public bool NewlyGenerated { get; init; }
public bool SentryEnabled { get; init; }
}
/// <summary> /// <summary>
/// The main entry point of Minecraft Console Client /// The main entry point of Minecraft Console Client
/// </summary> /// </summary>
@ -103,7 +114,6 @@ namespace MinecraftClient
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}); });
//Setup ConsoleIO
ConsoleIO.LogPrefix = "§8[MCC] "; ConsoleIO.LogPrefix = "§8[MCC] ";
if (args.Length >= 1 && args[^1] == "BasicIO" || args.Length >= 1 && args[^1] == "BasicIO-NoColor") 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(); 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<string> args_tmp = args.ToList<string>();
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) if (!ConsoleIO.BasicIO)
{ {
ConsoleIO.Backend = new ClassicConsoleBackend(); ConsoleIO.Backend = new ClassicConsoleBackend();
ConsoleIO.Backend.Init(); ConsoleIO.Backend.Init();
} }
ConsoleIO.WriteLine($"Minecraft Console Client v{Version} - for MC {MCLowestVersion} to {MCHighestVersion} - Github.com/MCCTeam"); if (!ProcessStartupState(startupState))
//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<string> args_tmp = args.ToList<string>();
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);
return; return;
}
ContinueAfterTuiInit(args); RunStartupSequence(args);
} }
/// <summary> /// <summary>
/// Continues MCC startup after console mode has been determined. /// Prints the application banner and processes the startup state collected before
/// Called directly from Main for classic/basic mode, or from a background /// the console backend was ready. Called once from classic mode or from TUI after
/// thread for TUI mode (after the Avalonia UI loop has started). /// the view is initialized.
/// </summary> /// </summary>
internal static void ContinueAfterTuiInit(string[] args) /// <returns>True if startup can continue; false if config load failed and user chose to exit.</returns>
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;
}
/// <summary>
/// Handles a failed config load by prompting the user to fix or regenerate the config file.
/// </summary>
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;
}
}
}
/// <summary>
/// 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.
/// </summary>
internal static void RunStartupSequence(string[] args)
{ {
//Other command-line arguments //Other command-line arguments
if (args.Length >= 1) if (args.Length >= 1)
@ -732,7 +787,8 @@ namespace MinecraftClient
/// </summary> /// </summary>
public static void ReloadSettings(bool keepAccountAndServerSettings = false) 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)); ConsoleIO.WriteLine(string.Format(Translations.config_load, settingsIniPath));
} }

View file

@ -136,7 +136,22 @@ namespace MinecraftClient
} }
public static Tuple<bool, bool> LoadFromFile(string filepath, bool keepAccountAndServerSettings = false) /// <summary>
/// Structured result returned by <see cref="LoadFromFile(string, bool)"/>.
/// </summary>
public readonly struct ConfigLoadResult
{
public bool Success { get; init; }
public bool NeedWriteDefault { get; init; }
/// <summary>True when a pre-TOML legacy config was detected, backed up, and a fresh default is needed.</summary>
public bool IsLegacyUpgrade { get; init; }
/// <summary>Non-null when the load failed due to a parse/IO error (not a legacy upgrade).</summary>
public string? ErrorMessage { get; init; }
/// <summary>Path where the old config was backed up (legacy upgrade case).</summary>
public string? LegacyBackupPath { get; init; }
}
public static ConfigLoadResult LoadFromFile(string filepath, bool keepAccountAndServerSettings = false)
{ {
bool keepAccountSettings = InternalConfig.KeepAccountSettings; bool keepAccountSettings = InternalConfig.KeepAccountSettings;
bool keepServerSettings = InternalConfig.KeepServerSettings; bool keepServerSettings = InternalConfig.KeepServerSettings;
@ -157,21 +172,27 @@ namespace MinecraftClient
Thread.CurrentThread.CurrentCulture = Program.ActualCulture; Thread.CurrentThread.CurrentCulture = Program.ActualCulture;
try try
{ {
// The old configuration file has been backed up as A.
string configString = File.ReadAllText(filepath); string configString = File.ReadAllText(filepath);
if (configString.Contains("Some settings missing here after an upgrade?")) if (configString.Contains("Some settings missing here after an upgrade?"))
{ {
string newFilePath = Path.ChangeExtension(filepath, ".old.ini"); string newFilePath = Path.ChangeExtension(filepath, ".old.ini");
File.Copy(filepath, newFilePath, true); File.Copy(filepath, newFilePath, true);
ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_use_new_config); return new ConfigLoadResult
ConsoleIO.WriteLineFormatted("§c" + string.Format(Translations.mcc_backup_old_config, newFilePath)); {
return new(false, true); Success = false,
NeedWriteDefault = true,
IsLegacyUpgrade = true,
LegacyBackupPath = newFilePath
};
} }
} }
catch { } catch { }
ConsoleIO.WriteLineFormatted("§c" + Translations.config_load_fail); return new ConfigLoadResult
ConsoleIO.WriteLine(ex.GetFullMessage()); {
return new(false, false); Success = false,
NeedWriteDefault = false,
ErrorMessage = ex.GetFullMessage()
};
} }
finally finally
{ {
@ -180,7 +201,7 @@ namespace MinecraftClient
if (!keepServerSettings) if (!keepServerSettings)
InternalConfig.KeepServerSettings = false; InternalConfig.KeepServerSettings = false;
} }
return new(true, false); return new ConfigLoadResult { Success = true, NeedWriteDefault = false };
} }
public static void WriteToFile(string filepath, bool backupOldFile) public static void WriteToFile(string filepath, bool backupOldFile)

View file

@ -25,14 +25,18 @@ namespace MinecraftClient.Tui
internal static TuiConsoleBackend? Instance { get; private set; } internal static TuiConsoleBackend? Instance { get; private set; }
private Program.StartupState? _pendingStartupState;
private readonly ManualResetEventSlim _viewReady = new(false);
/// <summary> /// <summary>
/// Initializes the Avalonia app and starts the main UI loop. /// Initializes the Avalonia app and starts the main UI loop.
/// This blocks the calling thread until the TUI exits. /// This blocks the calling thread until the TUI exits.
/// Before blocking, it starts MCC's remaining initialization on a background thread. /// Before blocking, it starts MCC's remaining initialization on a background thread.
/// </summary> /// </summary>
public void RunTuiMainLoop(string[] args) internal void RunTuiMainLoop(string[] args, Program.StartupState startupState)
{ {
Instance = this; Instance = this;
_pendingStartupState = startupState;
AppDomain.CurrentDomain.ProcessExit += (_, _) => RestoreTerminalState(); AppDomain.CurrentDomain.ProcessExit += (_, _) => RestoreTerminalState();
@ -46,7 +50,7 @@ namespace MinecraftClient.Tui
new Thread(() => new Thread(() =>
{ {
Thread.Sleep(500); _viewReady.Wait();
ContinueMccStartup(args); ContinueMccStartup(args);
}) })
{ Name = "MCC-Main", IsBackground = true }.Start(); { Name = "MCC-Main", IsBackground = true }.Start();
@ -113,7 +117,15 @@ namespace MinecraftClient.Tui
{ {
try 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) catch (Exception ex)
{ {
@ -124,6 +136,7 @@ namespace MinecraftClient.Tui
internal void SetView(MainTuiView view) internal void SetView(MainTuiView view)
{ {
_view = view; _view = view;
_viewReady.Set();
} }
internal MainTuiView? GetView() => _view; internal MainTuiView? GetView() => _view;