Minecraft-Console-Client/MinecraftClient/Program.cs
BruceChen b757215dbb Temporarily disable classic mode TUI recommendation
Commented out the MaybePrintClassicModeTuiRecommendation function call to prevent its execution until the related issue is resolved. Reference to the issue is included for tracking purposes.
2026-03-29 01:34:10 +08:00

1039 lines
48 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Mapping.BlockPalettes;
using MinecraftClient.Mapping.EntityPalettes;
using MinecraftClient.Protocol;
using MinecraftClient.Protocol.Handlers.Forge;
using MinecraftClient.Protocol.ProfileKey;
using MinecraftClient.Protocol.Session;
using MinecraftClient.Scripting;
using MinecraftClient.WinAPI;
using Sentry;
using static MinecraftClient.Settings;
using static MinecraftClient.Settings.ConsoleConfigHealper.ConsoleConfig;
using static MinecraftClient.Settings.MainConfigHelper.MainConfig.AdvancedConfig;
using static MinecraftClient.Settings.MainConfigHelper.MainConfig.GeneralConfig;
namespace MinecraftClient
{
/// <summary>
/// Minecraft Console Client by the MCC Team (c) 2012-2022.
/// Allows to connect to any Minecraft server, send and receive text, automated scripts.
/// This source code is released under the CDDL 1.0 License.
/// </summary>
/// <remarks>
/// Typical steps to update MCC for a new Minecraft version
/// - Implement protocol changes (see Protocol18.cs)
/// - Handle new block types and states (see Material.cs)
/// - Add support for new entity types (see EntityType.cs)
/// - Add new item types for inventories (see ItemType.cs)
/// - Mark new version as handled (see ProtocolHandler.cs)
/// - Update MCHighestVersion field below (for versionning)
/// </remarks>
static class Program
{
private static McClient? client;
public static string[]? startupargs;
public static CultureInfo ActualCulture = CultureInfo.CurrentCulture;
public const string Version = MCHighestVersion;
public const string MCLowestVersion = "1.4.6";
public const string MCHighestVersion = "26.1";
public static readonly string? BuildInfo = null;
private static Tuple<Thread, CancellationTokenSource>? offlinePrompt = null;
private static IDisposable? _sentrySdk = null;
private static bool useMcVersionOnce = false;
private static string settingsIniPath = "MinecraftClient.ini";
// [SENTRY]
// Setting this string to an empty string will disable Sentry
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>
/// The main entry point of Minecraft Console Client
/// </summary>
static void Main(string[] args)
{
// [SENTRY] Initialize Sentry SDK only if the DSN is not empty
if (SentryDSN != string.Empty)
{
_sentrySdk = SentrySdk.Init(options =>
{
options.Dsn = SentryDSN;
options.AutoSessionTracking = true;
options.IsGlobalModeEnabled = true;
options.TracesSampleRate = 1.0;
options.SendDefaultPii = false;
});
AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
{
SentrySdk.CaptureException((Exception)eventArgs.ExceptionObject);
};
}
Task.Run(() =>
{
// "ToLower" require "CultureInfo" to be initialized on first run, which can take a lot of time.
_ = "a".ToLower();
//Take advantage of Windows 10 / Mac / Linux UTF-8 console
if (OperatingSystem.IsWindows())
{
// If we're on windows, check if our version is Win10 or greater.
if (OperatingSystem.IsWindowsVersionAtLeast(10))
Console.OutputEncoding = Console.InputEncoding = Encoding.UTF8;
}
else
{
// Apply to all other operating systems.
Console.OutputEncoding = Console.InputEncoding = Encoding.UTF8;
}
// Fix issue #2119
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
});
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-NoColor")
{
ConsoleIO.BasicIO_NoColor = true;
}
ConsoleIO.BasicIO = true;
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();
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();
}
if (!ProcessStartupState(startupState))
return;
// Wait for this issue to be fixed before enabling it: https://github.com/Consolonia/Consolonia/issues/602
// MaybePrintClassicModeTuiRecommendation();
RunStartupSequence(args);
}
/// <summary>
/// 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.
/// </summary>
/// <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)
{
WriteBackSettings(false);
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
{
WriteBackSettings(true);
if (!Config.Main.Advanced.Language.StartsWith("en"))
ConsoleIO.WriteLine(string.Format(Translations.mcc_help_us_translate, Settings.TranslationProjectUrl));
}
return true;
}
private static void MaybePrintClassicModeTuiRecommendation()
{
if (ConsoleIO.BasicIO
|| Config.Console.General.ConsoleMode != ConsoleModeType.classic
|| Console.IsInputRedirected)
{
return;
}
char cmdChar = Config.Main.Advanced.InternalCmdChar.ToChar();
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_console_mode_tui_recommendation, cmdChar));
}
/// <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
if (args.Length >= 1)
{
if (args.Contains("--help"))
{
Console.WriteLine("Command-Line Help:");
Console.WriteLine("MinecraftClient.exe <username> <password> <server>");
Console.WriteLine("MinecraftClient.exe <username> <password> <server> \"/mycommand\"");
Console.WriteLine("MinecraftClient.exe --setting=value [--other settings]");
Console.WriteLine("MinecraftClient.exe --section.setting=value [--other settings]");
Console.WriteLine("MinecraftClient.exe <settings-file.ini> [--other settings]");
return;
}
if (args.Contains("--upgrade"))
{
UpgradeHelper.HandleBlockingUpdate(forceUpgrade: false);
return;
}
if (args.Contains("--force-upgrade"))
{
UpgradeHelper.HandleBlockingUpdate(forceUpgrade: true);
return;
}
if (args.Contains("--generate"))
{
string dataGenerator = "";
string dataPath = "";
foreach (string argument in args)
{
if (argument.StartsWith("--") && !argument.Contains("--generate"))
{
if (!argument.Contains('='))
throw new ArgumentException(string.Format(Translations.error_setting_argument_syntax, argument));
string[] argParts = argument[2..].Split('=');
string argName = argParts[0].Trim();
string argValue = argParts[1].Replace("\"", "").Trim();
if (argName == "data-path")
{
Console.WriteLine(dataPath);
dataPath = argValue;
}
if (argName == "data-generator")
{
dataGenerator = argValue;
}
}
}
if (string.IsNullOrEmpty(dataGenerator) || !(Settings.ToLowerIfNeed(dataGenerator).Equals("entity") || Settings.ToLowerIfNeed(dataGenerator).Equals("item") || Settings.ToLowerIfNeed(dataGenerator).Equals("block")))
{
Console.WriteLine(Translations.error_generator_invalid);
Console.WriteLine(Translations.error_usage + " MinecraftClient.exe --data-generator=<entity|item|block> --data-path=\"<path to Translations.json>\"");
return;
}
if (string.IsNullOrEmpty(dataPath))
{
Console.WriteLine(string.Format(Translations.error_missing_argument, "--data-path"));
Console.WriteLine(Translations.error_usage + " MinecraftClient.exe --data-generator=<entity|item|block> --data-path=\"<path to Translations.json>\"");
return;
}
if (!File.Exists(dataPath))
{
Console.WriteLine(string.Format(Translations.error_generator_path, dataPath));
return;
}
if (!dataPath.EndsWith(".json"))
{
Console.WriteLine(string.Format(Translations.error_generator_json, dataPath));
return;
}
Console.WriteLine(string.Format(Translations.mcc_generator_generating, dataGenerator, dataPath));
switch (dataGenerator)
{
case "entity":
EntityPaletteGenerator.GenerateEntityTypes(dataPath);
break;
case "item":
ItemPaletteGenerator.GenerateItemType(dataPath);
break;
case "block":
BlockPaletteGenerator.GenerateBlockPalette(dataPath);
break;
}
Console.WriteLine(string.Format(Translations.mcc_generator_done, dataGenerator, dataPath));
return;
}
}
if (OperatingSystem.IsWindows() && !string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle))
{
InternalConfig.Username = "New Window";
Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle);
}
// Check for updates
UpgradeHelper.CheckUpdate();
// Load command-line arguments
if (args.Length >= 1)
{
try
{
Settings.LoadArguments(args);
}
catch (ArgumentException e)
{
InternalConfig.InteractiveMode = false;
HandleFailure(e.Message);
return;
}
}
//Test line to troubleshoot invisible colors
if (Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted(string.Format(Translations.debug_color_test, "[0123456789ABCDEF]: (4bit)[§00§11§22§33§44§55§66§77§88§99§aA§bB§cC§dD§eE§fF§r]"));
Random random = new();
{ // Test 8 bit color
StringBuilder sb = new();
sb.Append("[0123456789]: (vt100 8bit)[");
for (int i = 0; i < 10; ++i)
{
sb.Append(ColorHelper.GetColorEscapeCode((byte)random.Next(255),
(byte)random.Next(255),
(byte)random.Next(255),
true,
ConsoleColorModeType.vt100_8bit)).Append(i);
}
sb.Append(ColorHelper.GetResetEscapeCode()).Append(']');
ConsoleIO.WriteLine(string.Format(Translations.debug_color_test, sb));
}
{ // Test 24 bit color
StringBuilder sb = new();
sb.Append("[0123456789]: (vt100 24bit)[");
for (int i = 0; i < 10; ++i)
{
sb.Append(ColorHelper.GetColorEscapeCode((byte)random.Next(255),
(byte)random.Next(255),
(byte)random.Next(255),
true,
ConsoleColorModeType.vt100_24bit)).Append(i);
}
sb.Append(ColorHelper.GetResetEscapeCode()).Append(']');
ConsoleIO.WriteLine(string.Format(Translations.debug_color_test, sb));
}
}
//Load cached sessions from disk if necessary
if (Config.Main.Advanced.SessionCache == CacheType.disk)
{
bool cacheLoaded = SessionCache.InitializeDiskCache();
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8" + (cacheLoaded ? Translations.debug_session_cache_ok : Translations.debug_session_cache_fail), acceptnewlines: true);
}
// Setup exit cleaning code
ExitCleanUp.Add(() => { DoExit(); });
//Asking the user to type in missing data such as Username and Password
bool useBrowser = Config.Main.General.AccountType == LoginType.microsoft && Config.Main.General.Method == LoginMethod.browser;
bool useDeviceCode = Config.Main.General.AccountType == LoginType.microsoft && Config.Main.General.Method == LoginMethod.mcc;
bool skipPassword = useBrowser || useDeviceCode;
if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login) && !useBrowser)
{
ConsoleIO.WriteLine(ConsoleIO.BasicIO ? Translations.mcc_login_basic_io : Translations.mcc_login);
InternalConfig.Account.Login = ConsoleIO.ReadLine().Trim();
if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login))
{
HandleFailure(Translations.error_login_blocked, false, ChatBot.DisconnectReason.LoginRejected);
return;
}
}
InternalConfig.Username = InternalConfig.Account.Login;
if (string.IsNullOrWhiteSpace(InternalConfig.Account.Password) && !skipPassword &&
(Config.Main.Advanced.SessionCache == CacheType.none || !SessionCache.Contains(ToLowerIfNeed(InternalConfig.Account.Login))))
{
RequestPassword();
}
startupargs = args;
InitializeClient();
}
/// <summary>
/// Reduest user to submit password.
/// </summary>
private static void RequestPassword()
{
ConsoleIO.WriteLine(ConsoleIO.BasicIO ? string.Format(Translations.mcc_password_basic_io, InternalConfig.Account.Login) + "\n" : Translations.mcc_password_hidden);
string? password = ConsoleIO.BasicIO ? Console.ReadLine() : ConsoleIO.ReadPassword();
if (string.IsNullOrWhiteSpace(password))
InternalConfig.Account.Password = "-";
else
InternalConfig.Account.Password = password;
}
/// <summary>
/// Start a new Client
/// </summary>
private static void InitializeClient()
{
// Ensure that we use the provided Minecraft version if we can't connect automatically.
//
// useMcVersionOnce is set to true on HandleFailure()
// whenever we are unable to connect to the server and the user provides a version number.
if (!useMcVersionOnce)
InternalConfig.MinecraftVersion = Config.Main.Advanced.MinecraftVersion;
SessionToken session = new();
PlayerKeyPair? playerKeyPair = null;
ProtocolHandler.LoginResult result = ProtocolHandler.LoginResult.LoginRequired;
string loginLower = ToLowerIfNeed(InternalConfig.Account.Login);
if (InternalConfig.Account.Password == "-")
{
ConsoleIO.WriteLineFormatted("§8" + Translations.mcc_offline, acceptnewlines: true);
result = ProtocolHandler.LoginResult.Success;
session.PlayerID = "0";
session.PlayerName = InternalConfig.Username;
}
else
{
// Validate cached session or login new session.
if (Config.Main.Advanced.SessionCache != CacheType.none && SessionCache.Contains(loginLower) && Config.Main.General.AccountType != LoginType.yggdrasil)
{
session = SessionCache.Get(loginLower);
result = ProtocolHandler.GetTokenValidation(session);
if (result != ProtocolHandler.LoginResult.Success)
{
ConsoleIO.WriteLineFormatted("§8" + Translations.mcc_session_invalid, acceptnewlines: true);
// Try to refresh access token
if (!string.IsNullOrWhiteSpace(session.RefreshToken))
{
try
{
result = ProtocolHandler.MicrosoftLoginRefresh(session.RefreshToken, out session);
}
catch (Exception ex)
{
ConsoleIO.WriteLine("Refresh access token fail: " + ex.Message);
result = ProtocolHandler.LoginResult.InvalidResponse;
}
}
if (result != ProtocolHandler.LoginResult.Success
&& string.IsNullOrWhiteSpace(InternalConfig.Account.Password)
&& !(Config.Main.General.AccountType == LoginType.microsoft))
RequestPassword();
}
else ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_session_valid, session.PlayerName));
}
if (result != ProtocolHandler.LoginResult.Success)
{
ConsoleIO.WriteLine(string.Format(Translations.mcc_connecting, Config.Main.General.AccountType == LoginType.mojang ? "Minecraft.net" : (Config.Main.General.AccountType == LoginType.microsoft ? "Microsoft" : Config.Main.General.AuthServer.Host)));
result = ProtocolHandler.GetLogin(InternalConfig.Account.Login, InternalConfig.Account.Password, Config.Main.General.AccountType, out session);
}
if (result == ProtocolHandler.LoginResult.Success && Config.Main.Advanced.SessionCache != CacheType.none)
SessionCache.Store(loginLower, session);
if (result == ProtocolHandler.LoginResult.Success)
session.SessionPreCheckTask = Task.Factory.StartNew(() => session.SessionPreCheck(Config.Main.General.AccountType));
}
if (result == ProtocolHandler.LoginResult.Success)
{
InternalConfig.Username = session.PlayerName;
bool isRealms = false;
if (OperatingSystem.IsWindows() && !string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle))
Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle);
if (Config.Main.Advanced.PlayerHeadAsIcon && OperatingSystem.IsWindows())
ConsoleIcon.SetPlayerIconAsync(InternalConfig.Username);
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLine(string.Format(Translations.debug_session_id, session.ID));
List<string> availableWorlds = new();
if (Config.Main.Advanced.MinecraftRealms && !String.IsNullOrEmpty(session.ID))
availableWorlds = ProtocolHandler.RealmsListWorlds(InternalConfig.Username, session.PlayerID, session.ID);
if (InternalConfig.ServerIP == string.Empty)
{
ConsoleIO.WriteLine(Translations.mcc_ip);
string addressInput = ConsoleIO.ReadLine();
if (addressInput.StartsWith("realms:"))
{
if (Config.Main.Advanced.MinecraftRealms)
{
if (availableWorlds.Count == 0)
{
HandleFailure(Translations.error_realms_access_denied, false, ChatBot.DisconnectReason.LoginRejected);
return;
}
string worldId = addressInput.Split(':')[1];
if (!availableWorlds.Contains(worldId) && int.TryParse(worldId, NumberStyles.Any, CultureInfo.CurrentCulture, out int worldIndex) && worldIndex < availableWorlds.Count)
worldId = availableWorlds[worldIndex];
if (availableWorlds.Contains(worldId))
{
string realmsAddress = ProtocolHandler.GetRealmsWorldServerAddress(worldId, InternalConfig.Username, session.PlayerID, session.ID);
if (realmsAddress != "")
{
addressInput = realmsAddress;
isRealms = true;
InternalConfig.MinecraftVersion = MCHighestVersion;
}
else
{
HandleFailure(Translations.error_realms_server_unavailable, false, ChatBot.DisconnectReason.LoginRejected);
return;
}
}
else
{
HandleFailure(Translations.error_realms_server_id, false, ChatBot.DisconnectReason.LoginRejected);
return;
}
}
else
{
HandleFailure(Translations.error_realms_disabled);
return;
}
}
Config.Main.SetServerIP(new MainConfigHelper.MainConfig.ServerInfoConfig(addressInput), true);
}
//Get server version
int protocolversion = 0;
ForgeInfo? forgeInfo = null;
if (InternalConfig.MinecraftVersion != "" && Settings.ToLowerIfNeed(InternalConfig.MinecraftVersion) != "auto")
{
protocolversion = ProtocolHandler.MCVer2ProtocolVersion(InternalConfig.MinecraftVersion);
if (protocolversion != 0)
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_version, InternalConfig.MinecraftVersion, protocolversion));
else
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_unknown_version, InternalConfig.MinecraftVersion));
if (useMcVersionOnce)
{
useMcVersionOnce = false;
InternalConfig.MinecraftVersion = "";
}
}
//Retrieve server info if version is not manually set OR if need to retrieve Forge information
if (!isRealms && (protocolversion == 0 || (Config.Main.Advanced.EnableForge == ForgeConfigType.auto) ||
((Config.Main.Advanced.EnableForge == ForgeConfigType.force) && !ProtocolHandler.ProtocolMayForceForge(protocolversion))))
{
if (protocolversion != 0)
ConsoleIO.WriteLine(Translations.mcc_forge);
else
ConsoleIO.WriteLine(Translations.mcc_retrieve);
if (!ProtocolHandler.GetServerInfo(InternalConfig.ServerIP, InternalConfig.ServerPort, ref protocolversion, ref forgeInfo))
{
HandleFailure(Translations.error_ping, true, ChatBot.DisconnectReason.ConnectionLost);
return;
}
}
if ((Config.Main.General.AccountType == LoginType.microsoft || Config.Main.General.AccountType == LoginType.yggdrasil)
&& InternalConfig.Account.Password != "-"
&& Config.Signature.LoginWithSecureProfile
&& protocolversion >= 759 /* 1.19 and above */
&& !string.IsNullOrWhiteSpace(session.ID))
{
// Load cached profile key from disk if necessary
if (Config.Main.Advanced.ProfileKeyCache == CacheType.disk)
{
bool cacheKeyLoaded = KeysCache.InitializeDiskCache();
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8" + (cacheKeyLoaded ? Translations.debug_keys_cache_ok : Translations.debug_keys_cache_fail), acceptnewlines: true);
}
if (Config.Main.Advanced.ProfileKeyCache != CacheType.none && KeysCache.Contains(loginLower))
{
playerKeyPair = KeysCache.Get(loginLower);
if (playerKeyPair.NeedRefresh())
ConsoleIO.WriteLineFormatted("§8" + Translations.mcc_profile_key_invalid, acceptnewlines: true);
else
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_profile_key_valid, session.PlayerName));
}
if (playerKeyPair is null || playerKeyPair.NeedRefresh())
{
ConsoleIO.WriteLineFormatted(Translations.mcc_fetching_key, acceptnewlines: true);
playerKeyPair = KeyUtils.GetNewProfileKeys(session.ID, Config.Main.General.AccountType == LoginType.yggdrasil);
if (Config.Main.Advanced.ProfileKeyCache != CacheType.none && playerKeyPair is not null)
{
KeysCache.Store(loginLower, playerKeyPair);
}
}
}
//Force-enable Forge support?
if (!isRealms && (Config.Main.Advanced.EnableForge == ForgeConfigType.force) && forgeInfo is null)
{
if (ProtocolHandler.ProtocolMayForceForge(protocolversion))
{
ConsoleIO.WriteLine(Translations.mcc_forgeforce);
forgeInfo = ProtocolHandler.ProtocolForceForge(protocolversion);
}
else
{
HandleFailure(Translations.error_forgeforce, true, ChatBot.DisconnectReason.ConnectionLost);
return;
}
}
//Proceed to server login
if (protocolversion != 0)
{
try
{
//Start the main TCP client
client = new McClient(session, playerKeyPair, InternalConfig.ServerIP, InternalConfig.ServerPort, protocolversion, forgeInfo);
//Update console title
if (OperatingSystem.IsWindows() && !string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle))
Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle);
}
catch (NotSupportedException)
{
HandleFailure(Translations.error_unsupported, true);
}
catch (NotImplementedException)
{
throw;
}
catch (Exception e)
{
// [SENTRY]
SentrySdk.CaptureException(e);
ConsoleIO.WriteLine(e.Message);
ConsoleIO.WriteLine(e.StackTrace ?? "");
HandleFailure(); // Other error
}
}
else HandleFailure(Translations.error_determine, true);
}
else
{
string failureMessage = Translations.error_login;
string failureReason = result switch
{
#pragma warning disable format // @formatter:off
ProtocolHandler.LoginResult.AccountMigrated => Translations.error_login_migrated,
ProtocolHandler.LoginResult.ServiceUnavailable => Translations.error_login_server,
ProtocolHandler.LoginResult.WrongPassword => Translations.error_login_blocked,
ProtocolHandler.LoginResult.InvalidResponse => Translations.error_login_response,
ProtocolHandler.LoginResult.NotPremium => Translations.error_login_premium,
ProtocolHandler.LoginResult.OtherError => Translations.error_login_network,
ProtocolHandler.LoginResult.SSLError => Translations.error_login_ssl,
ProtocolHandler.LoginResult.UserCancel => Translations.error_login_cancel,
ProtocolHandler.LoginResult.WrongSelection => Translations.error_login_blocked,
_ => Translations.error_login_unknown,
#pragma warning restore format // @formatter:on
};
failureMessage += failureReason;
HandleFailure(failureMessage, false, ChatBot.DisconnectReason.LoginRejected);
}
}
/// <summary>
/// Reloads settings
/// </summary>
public static void ReloadSettings(bool keepAccountAndServerSettings = false)
{
var result = Settings.LoadFromFile(settingsIniPath, keepAccountAndServerSettings);
if (result.Success)
ConsoleIO.WriteLine(string.Format(Translations.config_load, settingsIniPath));
}
/// <summary>
/// Write-back settings
/// </summary>
public static void WriteBackSettings(bool enableBackup = true)
{
Settings.WriteToFile(settingsIniPath, enableBackup);
}
/// <summary>
/// Disconnect the current client from the server and restart it
/// </summary>
/// <param name="delaySeconds">Optional delay, in seconds, before restarting</param>
/// <param name="keepAccountAndServerSettings">Optional, keep account and server settings</param>
public static void Restart(int delaySeconds = 0, bool keepAccountAndServerSettings = false)
{
ConsoleIO.Backend.StopReadThread();
new Thread(new ThreadStart(delegate
{
if (client is not null) { client.Disconnect(); ConsoleIO.Reset(); }
if (offlinePrompt is not null)
{
ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler;
offlinePrompt.Item2.Cancel(); offlinePrompt.Item1.Join(); offlinePrompt = null; ConsoleIO.Reset();
}
if (delaySeconds > 0)
{
ConsoleIO.WriteLine(string.Format(Translations.mcc_restart_delay, delaySeconds));
Thread.Sleep(delaySeconds * 1000);
}
ConsoleIO.WriteLine(Translations.mcc_restart);
ReloadSettings(keepAccountAndServerSettings);
InitializeClient();
})).Start();
}
public static void DoExit(int exitcode = 0)
{
WriteBackSettings();
ConsoleIO.WriteLineFormatted("§a" + string.Format(Translations.config_saving, settingsIniPath));
if (client is not null) { client.Disconnect(); ConsoleIO.Reset(); }
if (offlinePrompt is not null)
{
ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler;
offlinePrompt.Item2.Cancel();
if (Thread.CurrentThread != offlinePrompt.Item1)
offlinePrompt.Item1.Join(1000);
offlinePrompt = null;
ConsoleIO.Reset();
}
if (Config.Main.Advanced.PlayerHeadAsIcon) { ConsoleIcon.RevertToMCCIcon(); }
ConsoleIO.Backend?.Shutdown();
Environment.Exit(exitcode);
}
/// <summary>
/// Disconnect the current client from the server and exit the app
/// </summary>
public static void Exit(int exitcode = 0)
{
new Thread(() => { DoExit(exitcode); }).Start();
}
/// <summary>
/// Handle fatal errors such as ping failure, login failure, server disconnection, and so on.
/// Allows AutoRelog to perform on fatal errors, prompt for server version, and offline commands.
/// </summary>
/// <param name="errorMessage">Error message to display and optionally pass to AutoRelog bot</param>
/// <param name="versionError">Specify if the error is related to an incompatible or unkown server version</param>
/// <param name="disconnectReason">If set, the error message will be processed by the AutoRelog bot</param>
public static void HandleFailure(string? errorMessage = null, bool versionError = false, ChatBot.DisconnectReason? disconnectReason = null)
{
if (!string.IsNullOrEmpty(errorMessage))
{
ConsoleIO.Reset();
if (ConsoleIO.Backend is not Tui.TuiConsoleBackend)
{
try
{
while (Console.KeyAvailable)
Console.ReadKey(true);
}
catch { }
}
ConsoleIO.WriteLine(errorMessage);
if (disconnectReason.HasValue)
{
if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage))
return; //AutoRelog is triggering a restart of the client
}
}
if (InternalConfig.InteractiveMode)
{
if (versionError)
{
ConsoleIO.WriteLine(Translations.mcc_server_version);
InternalConfig.MinecraftVersion = ConsoleIO.ReadLine();
if (InternalConfig.MinecraftVersion != "")
{
useMcVersionOnce = true;
Restart(0, true);
return;
}
}
if (disconnectReason.HasValue)
{
if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage!))
return; //AutoRelog is triggering a restart of the client, don't turn on the offline prompt
}
if (offlinePrompt is null)
{
ConsoleIO.Backend.StopReadThread();
ConsoleIO.Backend.OnInputChange += ConsoleIO.OfflineAutocompleteHandler;
var cancellationTokenSource = new CancellationTokenSource();
offlinePrompt = new(new Thread(new ThreadStart(delegate
{
bool exitThread = false;
string command = " ";
ConsoleIO.WriteLine(string.Empty);
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_disconnected, 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);
while (!cancellationTokenSource.IsCancellationRequested)
{
if (exitThread)
return;
command = ConsoleIO.ReadLine().Trim();
if (command.Length == 0)
{
if (ConsoleIO.Backend is not Tui.TuiConsoleBackend)
Commands.Exit.DoExit(Config.AppVar.ExpandVars(command));
continue;
}
string message = "";
if (Config.Main.Advanced.InternalCmdChar.ToChar() != ' '
&& command[0] == Config.Main.Advanced.InternalCmdChar.ToChar())
command = command[1..];
if (command.StartsWith("reco"))
{
message = Commands.Reco.DoReconnect(Config.AppVar.ExpandVars(command));
if (message == "")
{
exitThread = true;
continue;
}
}
else if (command.StartsWith("connect"))
{
message = Commands.Connect.DoConnect(Config.AppVar.ExpandVars(command));
if (message == "")
{
exitThread = true;
continue;
}
}
else if (command.StartsWith("exit") || command.StartsWith("quit"))
{
message = Commands.Exit.DoExit(Config.AppVar.ExpandVars(command));
}
else if (command.StartsWith("help"))
{
ConsoleIO.WriteLineFormatted("§8MCC: " +
Config.Main.Advanced.InternalCmdChar.ToLogString() +
new Commands.Reco().GetCmdDescTranslated());
ConsoleIO.WriteLineFormatted("§8MCC: " +
Config.Main.Advanced.InternalCmdChar.ToLogString() +
new Commands.Connect().GetCmdDescTranslated());
}
else
ConsoleIO.WriteLineFormatted(string.Format(Translations.icmd_unknown, command.Split(' ')[0]));
if (message != "")
ConsoleIO.WriteLineFormatted("§8MCC: " + message);
}
})), cancellationTokenSource);
offlinePrompt.Item1.Start();
}
}
else
{
// Not in interactive mode, just exit and let the calling script handle the failure
if (disconnectReason.HasValue)
{
// Return distinct exit codes for known failures.
if (disconnectReason.Value == ChatBot.DisconnectReason.UserLogout) Exit(1);
if (disconnectReason.Value == ChatBot.DisconnectReason.InGameKick) Exit(2);
if (disconnectReason.Value == ChatBot.DisconnectReason.ConnectionLost) Exit(3);
if (disconnectReason.Value == ChatBot.DisconnectReason.LoginRejected) Exit(4);
}
Exit();
}
}
/// <summary>
/// Enumerate types in namespace through reflection
/// </summary>
/// <param name="nameSpace">Namespace to process</param>
/// <param name="assembly">Assembly to use. Default is Assembly.GetExecutingAssembly()</param>
/// <returns></returns>
public static Type[] GetTypesInNamespace(string nameSpace, Assembly? assembly = null)
{
if (assembly is null) { assembly = Assembly.GetExecutingAssembly(); }
return assembly.GetTypes().Where(t => string.Equals(t.Namespace, nameSpace, StringComparison.Ordinal)).ToArray();
}
/// <summary>
/// Static initialization of build information, read from assembly information
/// </summary>
static Program()
{
if (typeof(Program)
.Assembly
.GetCustomAttributes(typeof(AssemblyConfigurationAttribute), false)
.FirstOrDefault() is AssemblyConfigurationAttribute attribute)
BuildInfo = attribute.Configuration;
}
}
}