diff --git a/MinecraftClient/CommandHandler/ArgumentType/BotNameArgumentType.cs b/MinecraftClient/CommandHandler/ArgumentType/BotNameArgumentType.cs index 1f49da98..ef33fd66 100644 --- a/MinecraftClient/CommandHandler/ArgumentType/BotNameArgumentType.cs +++ b/MinecraftClient/CommandHandler/ArgumentType/BotNameArgumentType.cs @@ -19,7 +19,7 @@ namespace MinecraftClient.CommandHandler.ArgumentType McClient? client = CmdResult.currentHandler; if (client != null) { - var botList = client.GetLoadedChatBots(); + Scripting.ChatBot[] botList = client.GetLoadedChatBots(); foreach (var bot in botList) builder.Suggest(bot.GetType().Name); } diff --git a/MinecraftClient/CommandHandler/ArgumentType/MapBotMapIdArgumentType.cs b/MinecraftClient/CommandHandler/ArgumentType/MapBotMapIdArgumentType.cs index bd1ffee6..585ad59e 100644 --- a/MinecraftClient/CommandHandler/ArgumentType/MapBotMapIdArgumentType.cs +++ b/MinecraftClient/CommandHandler/ArgumentType/MapBotMapIdArgumentType.cs @@ -21,15 +21,18 @@ namespace MinecraftClient.CommandHandler.ArgumentType McClient? client = CmdResult.currentHandler; if (client != null) { - var bot = (Map?)client.GetLoadedChatBots().Find(bot => bot.GetType().Name == "Map"); - if (bot != null) + foreach (var bot in client.GetLoadedChatBots()) { - var mapList = bot.cachedMaps; - foreach (var map in mapList) + if (bot.GetType() == typeof(Map)) { - string mapName = map.Key.ToString(); - if (mapName.StartsWith(builder.RemainingLowerCase, StringComparison.InvariantCultureIgnoreCase)) - builder.Suggest(mapName); + var mapList = ((Map)bot).cachedMaps; + foreach (var map in mapList) + { + string mapName = map.Key.ToString(); + if (mapName.StartsWith(builder.RemainingLowerCase, StringComparison.InvariantCultureIgnoreCase)) + builder.Suggest(mapName); + } + break; } } } diff --git a/MinecraftClient/Commands/Bots.cs b/MinecraftClient/Commands/Bots.cs index 375c05c3..92b5e397 100644 --- a/MinecraftClient/Commands/Bots.cs +++ b/MinecraftClient/Commands/Bots.cs @@ -53,7 +53,7 @@ namespace MinecraftClient.Commands private int DoListBot(CmdResult r) { McClient handler = CmdResult.currentHandler!; - int length = handler.GetLoadedChatBots().Count; + int length = handler.GetLoadedChatBots().Length; if (length == 0) return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_bots_noloaded); @@ -73,7 +73,7 @@ namespace MinecraftClient.Commands McClient handler = CmdResult.currentHandler!; if (botName.ToLower().Equals("all", StringComparison.OrdinalIgnoreCase)) { - if (handler.GetLoadedChatBots().Count == 0) + if (handler.GetLoadedChatBots().Length == 0) return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_bots_noloaded); else { @@ -83,12 +83,20 @@ namespace MinecraftClient.Commands } else { - ChatBot? bot = handler.GetLoadedChatBots().Find(bot => bot.GetType().Name.ToLower() == botName.ToLower()); - if (bot == null) + ChatBot? target = null; + foreach (ChatBot bot in handler.GetLoadedChatBots()) + { + if (bot.GetType().Name.Equals(botName, StringComparison.InvariantCultureIgnoreCase)) + { + target = bot; + break; + } + } + if (target == null) return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_bots_notfound, botName)); else { - handler.BotUnLoad(bot).Wait(); + handler.BotUnLoad(target).Wait(); return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_bots_unloaded, botName)); } } diff --git a/MinecraftClient/Commands/Connect.cs b/MinecraftClient/Commands/Connect.cs index c8691340..c74094b4 100644 --- a/MinecraftClient/Commands/Connect.cs +++ b/MinecraftClient/Commands/Connect.cs @@ -47,7 +47,7 @@ namespace MinecraftClient.Commands if (Settings.Config.Main.SetServerIP(new Settings.MainConfigHealper.MainConfig.ServerInfoConfig(server), true)) { - Program.Restart(keepAccountAndServerSettings: true); + Program.SetRestart(keepAccountAndServerSettings: true); return r.SetAndReturn(Status.Done); } else @@ -64,7 +64,7 @@ namespace MinecraftClient.Commands if (Settings.Config.Main.SetServerIP(new Settings.MainConfigHealper.MainConfig.ServerInfoConfig(args[0]), true)) { - Program.Restart(keepAccountAndServerSettings: true); + Program.SetRestart(keepAccountAndServerSettings: true); return string.Empty; } else diff --git a/MinecraftClient/Commands/Exit.cs b/MinecraftClient/Commands/Exit.cs index 77ecdd6e..ad3b7c1a 100644 --- a/MinecraftClient/Commands/Exit.cs +++ b/MinecraftClient/Commands/Exit.cs @@ -45,7 +45,7 @@ namespace MinecraftClient.Commands private int DoExit(CmdResult r, int code = 0) { - Program.Exit(code); + Program.SetExit(code); return r.SetAndReturn(CmdResult.Status.Done); } } diff --git a/MinecraftClient/Commands/Reco.cs b/MinecraftClient/Commands/Reco.cs index 4baea4e2..e4830d36 100644 --- a/MinecraftClient/Commands/Reco.cs +++ b/MinecraftClient/Commands/Reco.cs @@ -47,7 +47,7 @@ namespace MinecraftClient.Commands if (!Settings.Config.Main.Advanced.SetAccount(account)) return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_connect_unknown, account)); } - Program.Restart(keepAccountAndServerSettings: true); + Program.SetRestart(keepAccountAndServerSettings: true); return r.SetAndReturn(CmdResult.Status.Done); } @@ -62,7 +62,7 @@ namespace MinecraftClient.Commands return string.Format(Translations.cmd_connect_unknown, account); } } - Program.Restart(keepAccountAndServerSettings: true); + Program.SetRestart(keepAccountAndServerSettings: true); return string.Empty; } } diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 9ef89440..683e3c00 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -103,9 +103,9 @@ namespace MinecraftClient private double sampleSum = 0; // ChatBot + private ChatBot[] chatbots = Array.Empty(); + private static ChatBot[] botsOnHold = Array.Empty(); private bool OldChatBotUpdateTrigger = false; - private readonly List bots = new(); - private static readonly List botsOnHold = new(); private bool networkPacketCaptureEnabled = false; public int GetServerPort() { return port; } @@ -131,17 +131,33 @@ namespace MinecraftClient public int GetProtocolVersion() { return protocolversion; } public ILogger GetLogger() { return Log; } public int GetPlayerEntityID() { return playerEntityID; } - public List GetLoadedChatBots() { return new List(bots); } + public ChatBot[] GetLoadedChatBots() { return chatbots; } private TcpClient? tcpClient; private IMinecraftCom? handler; - private CancellationTokenSource? cmdprompt; - private readonly CancellationToken CancelToken; - - private Task TimeoutDetectorTask = Task.CompletedTask; + private readonly CancellationTokenSource CancelTokenSource; public ILogger Log; + public static void LoadCommandsAndChatbots() + { + /* Load commands from the 'Commands' namespace */ + Type[] cmds_classes = Program.GetTypesInNamespace("MinecraftClient.Commands"); + foreach (Type type in cmds_classes) + { + if (type.IsSubclassOf(typeof(Command))) + { + Command cmd = (Command)Activator.CreateInstance(type)!; + cmd.RegisterCommand(dispatcher); + } + } + + /* Load ChatBots */ + botsOnHold = GetBotsToRegister(); + foreach (ChatBot bot in botsOnHold) + bot.Initialize(); + } + /// /// Starts the main chat client, wich will login to the server using the MinecraftCom class. /// @@ -151,11 +167,10 @@ namespace MinecraftClient /// The server port to use /// Minecraft protocol version to use /// ForgeInfo item stating that Forge is enabled - public McClient(CancellationToken cancelToken, string serverHost, ushort serverPort) + public McClient(string serverHost, ushort serverPort, CancellationTokenSource cancelTokenSource) { - CancelToken = cancelToken; + CancelTokenSource = cancelTokenSource; - dispatcher = new(); CmdResult.currentHandler = this; terrainAndMovementsEnabled = Config.Main.Advanced.TerrainAndMovements; inventoryHandlingEnabled = Config.Main.Advanced.InventoryHandling; @@ -179,12 +194,6 @@ namespace MinecraftClient Log.ChatEnabled = Config.Logging.ChatMessages; Log.WarnEnabled = Config.Logging.WarningMessages; Log.ErrorEnabled = Config.Logging.ErrorMessages; - - /* Load commands from Commands namespace */ - LoadCommands(); - - if (botsOnHold.Count == 0) - RegisterBots(); } public async Task Login(HttpClient httpClient, SessionToken session, PlayerKeyPair? playerKeyPair, int protocolversion, ForgeInfo? forgeInfo) @@ -204,26 +213,22 @@ namespace MinecraftClient tcpClient.ReceiveBufferSize = 1024 * 1024; tcpClient.ReceiveTimeout = Config.Main.Advanced.TcpTimeout * 1000; // Default: 30 seconds - handler = ProtocolHandler.GetProtocolHandler(CancelToken, tcpClient, protocolversion, forgeInfo, this); + handler = ProtocolHandler.GetProtocolHandler(CancelTokenSource.Token, tcpClient, protocolversion, forgeInfo, this); Log.Info(Translations.mcc_version_supported); - TimeoutDetectorTask = Task.Run(TimeoutDetector, CancelToken); + _ = Task.Run(TimeoutDetector, CancelTokenSource.Token); try { if (await handler.Login(httpClient, this.playerKeyPair, session)) { - foreach (ChatBot bot in botsOnHold) - BotLoad(bot, false); - - botsOnHold.Clear(); - - Log.Info(string.Format(Translations.mcc_joined, Config.Main.Advanced.InternalCmdChar.ToLogString())); - - cmdprompt = new CancellationTokenSource(); - ConsoleInteractive.ConsoleReader.BeginReadThread(cmdprompt); - ConsoleInteractive.ConsoleReader.MessageReceived += ConsoleReaderOnMessageReceived; - ConsoleInteractive.ConsoleReader.OnInputChange += ConsoleIO.AutocompleteHandler; + chatbots = botsOnHold; + botsOnHold = Array.Empty(); + foreach (ChatBot bot in chatbots) + { + bot.SetHandler(this); + bot.AfterGameJoined(); + } return; } @@ -249,14 +254,11 @@ namespace MinecraftClient Log.Info(string.Format(Translations.mcc_reconnect, ReconnectionAttemptsLeft)); Thread.Sleep(5000); ReconnectionAttemptsLeft--; - Program.Restart(); + Program.SetRestart(); } - else if (InternalConfig.InteractiveMode) + else { - ConsoleInteractive.ConsoleReader.StopReadThread(); - ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived; - ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler; - Program.Exit(); + Program.SetExit(); } throw new Exception("Initialization failed."); @@ -264,38 +266,55 @@ namespace MinecraftClient public async Task StartUpdating() { + Log.Info(string.Format(Translations.mcc_joined, Config.Main.Advanced.InternalCmdChar.ToLogString())); + + ConsoleInteractive.ConsoleReader.MessageReceived += ConsoleReaderOnMessageReceived; + ConsoleInteractive.ConsoleReader.OnInputChange += ConsoleIO.AutocompleteHandler; + ConsoleInteractive.ConsoleReader.BeginReadThread(); + await handler!.StartUpdating(); + + ConsoleInteractive.ConsoleReader.StopReadThread(); + ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler; + ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived; + + ConsoleIO.CancelAutocomplete(); + ConsoleIO.WriteLine(string.Empty); } /// /// Register bots /// - private void RegisterBots(bool reload = false) + private static ChatBot[] GetBotsToRegister(bool reload = false) { - if (Config.ChatBot.Alerts.Enabled) { BotLoad(new Alerts()); } - if (Config.ChatBot.AntiAFK.Enabled) { BotLoad(new AntiAFK()); } - if (Config.ChatBot.AutoAttack.Enabled) { BotLoad(new AutoAttack()); } - if (Config.ChatBot.AutoCraft.Enabled) { BotLoad(new AutoCraft()); } - if (Config.ChatBot.AutoDig.Enabled) { BotLoad(new AutoDig()); } - if (Config.ChatBot.AutoDrop.Enabled) { BotLoad(new AutoDrop()); } - if (Config.ChatBot.AutoEat.Enabled) { BotLoad(new AutoEat()); } - if (Config.ChatBot.AutoFishing.Enabled) { BotLoad(new AutoFishing()); } - if (Config.ChatBot.AutoRelog.Enabled) { BotLoad(new AutoRelog()); } - if (Config.ChatBot.AutoRespond.Enabled) { BotLoad(new AutoRespond()); } - if (Config.ChatBot.ChatLog.Enabled) { BotLoad(new ChatLog()); } - if (Config.ChatBot.DiscordBridge.Enabled) { BotLoad(new DiscordBridge()); } - if (Config.ChatBot.Farmer.Enabled) { BotLoad(new Farmer()); } - if (Config.ChatBot.FollowPlayer.Enabled) { BotLoad(new FollowPlayer()); } - if (Config.ChatBot.HangmanGame.Enabled) { BotLoad(new HangmanGame()); } - if (Config.ChatBot.Mailer.Enabled) { BotLoad(new Mailer()); } - if (Config.ChatBot.Map.Enabled) { BotLoad(new Map()); } - if (Config.ChatBot.PlayerListLogger.Enabled) { BotLoad(new PlayerListLogger()); } - if (Config.ChatBot.RemoteControl.Enabled) { BotLoad(new RemoteControl()); } - if (Config.ChatBot.ReplayCapture.Enabled && reload) { BotLoad(new ReplayCapture()); } - if (Config.ChatBot.ScriptScheduler.Enabled) { BotLoad(new ScriptScheduler()); } - if (Config.ChatBot.TelegramBridge.Enabled) { BotLoad(new TelegramBridge()); } + List chatbotList = new(); + + if (Config.ChatBot.Alerts.Enabled) { chatbotList.Add(new Alerts()); } + if (Config.ChatBot.AntiAFK.Enabled) { chatbotList.Add(new AntiAFK()); } + if (Config.ChatBot.AutoAttack.Enabled) { chatbotList.Add(new AutoAttack()); } + if (Config.ChatBot.AutoCraft.Enabled) { chatbotList.Add(new AutoCraft()); } + if (Config.ChatBot.AutoDig.Enabled) { chatbotList.Add(new AutoDig()); } + if (Config.ChatBot.AutoDrop.Enabled) { chatbotList.Add(new AutoDrop()); } + if (Config.ChatBot.AutoEat.Enabled) { chatbotList.Add(new AutoEat()); } + if (Config.ChatBot.AutoFishing.Enabled) { chatbotList.Add(new AutoFishing()); } + if (Config.ChatBot.AutoRelog.Enabled) { chatbotList.Add(new AutoRelog()); } + if (Config.ChatBot.AutoRespond.Enabled) { chatbotList.Add(new AutoRespond()); } + if (Config.ChatBot.ChatLog.Enabled) { chatbotList.Add(new ChatLog()); } + if (Config.ChatBot.DiscordBridge.Enabled) { chatbotList.Add(new DiscordBridge()); } + if (Config.ChatBot.Farmer.Enabled) { chatbotList.Add(new Farmer()); } + if (Config.ChatBot.FollowPlayer.Enabled) { chatbotList.Add(new FollowPlayer()); } + if (Config.ChatBot.HangmanGame.Enabled) { chatbotList.Add(new HangmanGame()); } + if (Config.ChatBot.Mailer.Enabled) { chatbotList.Add(new Mailer()); } + if (Config.ChatBot.Map.Enabled) { chatbotList.Add(new Map()); } + if (Config.ChatBot.PlayerListLogger.Enabled) { chatbotList.Add(new PlayerListLogger()); } + if (Config.ChatBot.RemoteControl.Enabled) { chatbotList.Add(new RemoteControl()); } + // if (Config.ChatBot.ReplayCapture.Enabled && reload) { chatbotList.Add(new ReplayCapture()); } + if (Config.ChatBot.ScriptScheduler.Enabled) { chatbotList.Add(new ScriptScheduler()); } + if (Config.ChatBot.TelegramBridge.Enabled) { chatbotList.Add(new TelegramBridge()); } // Add your ChatBot here by uncommenting and adapting // BotLoad(new ChatBots.YourBot()); + + return chatbotList.ToArray(); } /// @@ -304,10 +323,13 @@ namespace MinecraftClient /// private async Task TrySendMessageToServer() { - while (nextMessageSendTime < DateTime.Now && chatQueue.TryDequeue(out string? text)) + if (handler != null) { - await handler!.SendChatMessage(text, playerKeyPair); - nextMessageSendTime = DateTime.Now + TimeSpan.FromSeconds(Config.Main.Advanced.MessageCooldown); + while (nextMessageSendTime < DateTime.Now && chatQueue.TryDequeue(out string? text)) + { + await handler.SendChatMessage(text, playerKeyPair); + nextMessageSendTime = DateTime.Now + TimeSpan.FromSeconds(Config.Main.Advanced.MessageCooldown); + } } } @@ -317,7 +339,7 @@ namespace MinecraftClient public async Task OnUpdate() { OldChatBotUpdateTrigger = !OldChatBotUpdateTrigger; - foreach (ChatBot bot in bots.ToArray()) + foreach (ChatBot bot in chatbots) { await bot.OnClientTickAsync(); if (OldChatBotUpdateTrigger) @@ -390,15 +412,24 @@ namespace MinecraftClient private async Task TimeoutDetector() { UpdateKeepAlive(); - var periodicTimer = new PeriodicTimer(TimeSpan.FromSeconds(Config.Main.Advanced.TcpTimeout)); - while (await periodicTimer.WaitForNextTickAsync(CancelToken) && !CancelToken.IsCancellationRequested) + using PeriodicTimer periodicTimer = new(TimeSpan.FromSeconds(Config.Main.Advanced.TcpTimeout)); + try { - if (lastKeepAlive.AddSeconds(Config.Main.Advanced.TcpTimeout) < DateTime.Now) + while (await periodicTimer.WaitForNextTickAsync(CancelTokenSource.Token) && !CancelTokenSource.IsCancellationRequested) { - OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, Translations.error_timeout); - return; + if (lastKeepAlive.AddSeconds(Config.Main.Advanced.TcpTimeout) < DateTime.Now) + { + OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, Translations.error_timeout); + return; + } } } + catch (AggregateException e) + { + if (e.InnerException is not OperationCanceledException) + throw; + } + catch (OperationCanceledException) { } } /// @@ -414,10 +445,10 @@ namespace MinecraftClient /// public void Disconnect() { - DispatchBotEvent(bot => bot.OnDisconnect(ChatBot.DisconnectReason.UserLogout, "")); + DispatchBotEvent(bot => bot.OnDisconnect(ChatBot.DisconnectReason.UserLogout, string.Empty)); - botsOnHold.Clear(); - botsOnHold.AddRange(bots); + botsOnHold = chatbots; + chatbots = Array.Empty(); if (handler != null) { @@ -425,15 +456,7 @@ namespace MinecraftClient handler.Dispose(); } - if (cmdprompt != null) - { - cmdprompt.Cancel(); - cmdprompt = null; - } - tcpClient?.Close(); - - TimeoutDetectorTask.Wait(); } /// @@ -441,12 +464,6 @@ namespace MinecraftClient /// public void OnConnectionLost(ChatBot.DisconnectReason reason, string message) { - ConsoleInteractive.ConsoleReader.StopReadThread(); - ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived; - ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler; - - ConsoleIO.CancelAutocomplete(); - switch (reason) { case ChatBot.DisconnectReason.ConnectionLost: @@ -469,8 +486,8 @@ namespace MinecraftClient } // Process AutoRelog last to make sure other bots can perform their cleanup tasks first (issue #1517) - List onDisconnectBotList = bots.Where(bot => bot is not AutoRelog).ToList(); - onDisconnectBotList.AddRange(bots.Where(bot => bot is AutoRelog)); + List onDisconnectBotList = chatbots.Where(bot => bot is not AutoRelog).ToList(); + onDisconnectBotList.AddRange(chatbots.Where(bot => bot is AutoRelog)); int restartDelay = -1; foreach (ChatBot bot in onDisconnectBotList) @@ -490,9 +507,9 @@ namespace MinecraftClient } if (restartDelay < 0) - Program.Exit(handleFailure: true); + Program.SetExit(handleFailure: true); else - Program.Restart(restartDelay, true); + Program.SetRestart(restartDelay, true); handler!.Dispose(); @@ -601,11 +618,11 @@ namespace MinecraftClient { dispatcher.Execute(parse); - foreach (ChatBot bot in bots.ToArray()) + foreach (ChatBot bot in chatbots) { try { - bot.OnInternalCommand(command, string.Join(" ", Command.GetArgs(command)), result); + bot.OnInternalCommand(command, string.Join(' ', Command.GetArgs(command)), result); } catch (Exception e) { @@ -634,32 +651,6 @@ namespace MinecraftClient } } - public void LoadCommands() - { - /* Load commands from the 'Commands' namespace */ - - if (!commandsLoaded) - { - Type[] cmds_classes = Program.GetTypesInNamespace("MinecraftClient.Commands"); - foreach (Type type in cmds_classes) - { - if (type.IsSubclassOf(typeof(Command))) - { - try - { - Command cmd = (Command)Activator.CreateInstance(type)!; - cmd.RegisterCommand(dispatcher); - } - catch (Exception e) - { - Log.Warn(e.Message); - } - } - } - commandsLoaded = true; - } - } - /// /// Reload settings and bots /// @@ -676,10 +667,16 @@ namespace MinecraftClient public async Task ReloadBots() { await UnloadAllBots(); - RegisterBots(true); - if (tcpClient!.Client.Connected) - bots.ForEach(bot => bot.AfterGameJoined()); + ChatBot[] bots = GetBotsToRegister(true); + foreach (ChatBot bot in bots) + { + bot.SetHandler(this); + bot.Initialize(); + if (handler != null) + bot.AfterGameJoined(); + } + chatbots = bots; } /// @@ -687,8 +684,11 @@ namespace MinecraftClient /// public async Task UnloadAllBots() { - foreach (ChatBot bot in GetLoadedChatBots()) - await BotUnLoad(bot); + foreach (ChatBot bot in chatbots) + bot.OnUnload(); + chatbots = Array.Empty(); + registeredBotPluginChannels.Clear(); + await Task.CompletedTask; } #endregion @@ -698,14 +698,14 @@ namespace MinecraftClient /// /// Load a new bot /// - public void BotLoad(ChatBot b, bool init = true) + public void BotLoad(ChatBot bot, bool init = true) { - b.SetHandler(this); - bots.Add(b); + bot.SetHandler(this); + chatbots = new List(chatbots) { bot }.ToArray(); if (init) - DispatchBotEvent(bot => bot.Initialize(), new ChatBot[] { b }); + bot.Initialize(); if (handler != null) - DispatchBotEvent(bot => bot.AfterGameJoined(), new ChatBot[] { b }); + bot.AfterGameJoined(); } /// @@ -715,7 +715,11 @@ namespace MinecraftClient { b.OnUnload(); - bots.RemoveAll(item => ReferenceEquals(item, b)); + List botList = new(); + botList.AddRange(from bot in chatbots + where !ReferenceEquals(bot, b) + select bot); + chatbots = botList.ToArray(); // ToList is needed to avoid an InvalidOperationException from modfiying the list while it's being iterated upon. var botRegistrations = registeredBotPluginChannels.Where(entry => entry.Value.Contains(b)).ToList(); @@ -725,14 +729,6 @@ namespace MinecraftClient } } - /// - /// Clear bots - /// - public void BotClear() - { - bots.Clear(); - } - /// /// Get Terrain and Movements status. /// @@ -2156,18 +2152,8 @@ namespace MinecraftClient /// Only fire the event for the specified bot list (default: all bots) private void DispatchBotEvent(Action action, IEnumerable? botList = null) { - ChatBot[] selectedBots; - - if (botList != null) - { - selectedBots = botList.ToArray(); - } - else - { - selectedBots = bots.ToArray(); - } - - foreach (ChatBot bot in selectedBots) + botList ??= chatbots; + foreach (ChatBot bot in botList) { try { @@ -3000,9 +2986,8 @@ namespace MinecraftClient /// Latency public void OnLatencyUpdate(Guid uuid, int latency) { - if (onlinePlayers.ContainsKey(uuid)) + if (onlinePlayers.TryGetValue(uuid, out PlayerInfo? player)) { - PlayerInfo player = onlinePlayers[uuid]; player.Ping = latency; string playerName = player.Name; foreach (KeyValuePair ent in entities) diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 35ca056c..3f4156ae 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -59,7 +59,9 @@ namespace MinecraftClient private static int CurrentThreadId; private static bool RestartKeepSettings = false; private static int RestartAfter = -1, Exitcode = 0; - private static CancellationTokenSource McClientCancelToken = new(); + + private static Task McClientInit = Task.CompletedTask; + private static CancellationTokenSource McClientCancelTokenSource = new(); /// /// The main entry point of Minecraft Console Client @@ -126,6 +128,11 @@ namespace MinecraftClient } } + // Setup exit cleaning code + ExitCleanUp.Add(() => { DoExit(); }); + + McClientInit = Task.Run(McClient.LoadCommandsAndChatbots); + if (!string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle)) { InternalConfig.Username = "New Window"; @@ -167,9 +174,6 @@ namespace MinecraftClient } } - // Setup exit cleaning code - ExitCleanUp.Add(() => { DoExit(); }); - ConsoleIO.SuppressPrinting(true); // 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; @@ -233,8 +237,6 @@ namespace MinecraftClient McClient = null; } - ConsoleInteractive.ConsoleReader.StopReadThread(); - if (RestartAfter < 0 && FailureInfo.hasFailure) RestartAfter = HandleFailure(); @@ -255,7 +257,7 @@ namespace MinecraftClient RestartAfter = -1; RestartKeepSettings = false; FailureInfo.hasFailure = false; - McClientCancelToken = new CancellationTokenSource(); + McClientCancelTokenSource = new CancellationTokenSource(); } DoExit(); @@ -721,7 +723,6 @@ namespace MinecraftClient var loginTask = LoginAsync(loginHttpClient); var getServerInfoTask = GetServerInfoAsync(loginHttpClient, loginTask); var refreshPlayerKeyTask = RefreshPlayerKeyPair(loginHttpClient, loginTask); - var initMcClientTask = Task.Run(() => { return new McClient(McClientCancelToken.Token, InternalConfig.ServerIP, InternalConfig.ServerPort); }); (result, session, playerKeyPair) = await loginTask; if (result == ProtocolHandler.LoginResult.Success && session != null) @@ -732,7 +733,7 @@ namespace MinecraftClient Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle); if (Config.Main.Advanced.PlayerHeadAsIcon && OperatingSystem.IsWindows()) - _ = Task.Run(async () => { await ConsoleIcon.SetPlayerIconAsync(loginHttpClient, InternalConfig.Username); }); + _ = Task.Run(async () => { await ConsoleIcon.SetPlayerIconAsync(loginHttpClient, InternalConfig.Username); }); if (Config.Logging.DebugMessages) ConsoleIO.WriteLine(string.Format(Translations.debug_session_id, session.ID)); @@ -754,8 +755,10 @@ namespace MinecraftClient { try { + await McClientInit; + McClient = new McClient(InternalConfig.ServerIP, InternalConfig.ServerPort, McClientCancelTokenSource); + // Start the main TCP client - McClient = await initMcClientTask; await McClient.Login(loginHttpClient, session, playerKeyPair, protocolversion, forgeInfo); } catch (NotSupportedException) @@ -835,23 +838,23 @@ namespace MinecraftClient /// Disconnect the current client from the server and restart it /// /// Optional delay, in seconds, before restarting - public static void Restart(int delayMilliseconds = 0, bool keepAccountAndServerSettings = false) + public static void SetRestart(int delayMilliseconds = 0, bool keepAccountAndServerSettings = false) { RestartAfter = Math.Max(0, delayMilliseconds); RestartKeepSettings = keepAccountAndServerSettings; - McClientCancelToken.Cancel(); + McClientCancelTokenSource.Cancel(); } /// /// Disconnect the current client from the server and exit the app /// - public static void Exit(int exitcode = 0, bool handleFailure = false) + public static void SetExit(int exitcode = 0, bool handleFailure = false) { - McClientCancelToken.Cancel(); - Exitcode = exitcode; RestartAfter = -1; + Exitcode = exitcode; if (handleFailure) FailureInfo.Record(); + McClientCancelTokenSource.Cancel(); } public static void DoExit() @@ -904,7 +907,6 @@ namespace MinecraftClient } } - ConsoleIO.WriteLine(string.Empty); ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_disconnected, Config.Main.Advanced.InternalCmdChar.ToLogString())); ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true); diff --git a/MinecraftClient/Protocol/Handlers/Protocol16.cs b/MinecraftClient/Protocol/Handlers/Protocol16.cs index 1b70865e..098a5f61 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol16.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol16.cs @@ -540,30 +540,27 @@ namespace MinecraftClient.Protocol.Handlers { ConsoleIO.WriteLine(Translations.mcc_session); - bool needCheckSession = true; - string serverHash = CryptoHandler.GetServerHash(serverIDhash, serverPublicKey, secretKey); if (session.SessionPreCheckTask != null && session.ServerInfoHash != null && serverHash == session.ServerInfoHash) { - try + (bool preCheckResult, string? error) = await session.SessionPreCheckTask; + if (!preCheckResult) { - bool preCheckResult = await session.SessionPreCheckTask; - if (preCheckResult) // PreCheck Successed - needCheckSession = false; + handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, + string.IsNullOrEmpty(error) ? Translations.mcc_session_fail : $"{Translations.mcc_session_fail} Error: {error}."); + return false; } - catch (HttpRequestException) { } + session.SessionPreCheckTask = null; } - - if (needCheckSession) + else { - var sessionCheck = await ProtocolHandler.SessionCheckAsync(httpClient, uuid, sessionID, serverHash); + (bool sessionCheck, string? error) = await ProtocolHandler.SessionCheckAsync(httpClient, uuid, sessionID, serverHash); if (sessionCheck) - { SessionCache.StoreServerInfo($"{InternalConfig.ServerIP}:{InternalConfig.ServerPort}", serverIDhash, serverPublicKey); - } else { - handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, Translations.mcc_session_fail); + handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, + string.IsNullOrEmpty(error) ? Translations.mcc_session_fail : $"{Translations.mcc_session_fail} Error: {error}."); return false; } } diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 6a65f4d4..e78e50ba 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -95,7 +95,7 @@ namespace MinecraftClient.Protocol.Handlers private readonly CancellationToken CancelToken; - public Protocol18Handler(CancellationToken cancelToken, TcpClient Client, int protocolVersion, IMinecraftComHandler handler, ForgeInfo? forgeInfo) + public Protocol18Handler(TcpClient Client, int protocolVersion, IMinecraftComHandler handler, ForgeInfo? forgeInfo, CancellationToken cancelToken) { CancelToken = cancelToken; ConsoleIO.SetAutoCompleteEngine(this); @@ -213,23 +213,32 @@ namespace MinecraftClient.Protocol.Handlers private async Task MainTicker() { - var periodicTimer = new PeriodicTimer(TimeSpan.FromMilliseconds(1000 / 20)); - while (await periodicTimer.WaitForNextTickAsync(CancelToken) && !CancelToken.IsCancellationRequested) + using PeriodicTimer periodicTimer = new(TimeSpan.FromMilliseconds(1000 / 20)); + try { - try + while (await periodicTimer.WaitForNextTickAsync(CancelToken) && !CancelToken.IsCancellationRequested) { - await handler.OnUpdate(); - } - catch (Exception e) - { - if (Config.Logging.DebugMessages) + try { - ConsoleIO.WriteLine($"{e.GetType().Name} when ticking: {e.Message}"); - if (e.StackTrace != null) - ConsoleIO.WriteLine(e.StackTrace); + await handler.OnUpdate(); + } + catch (Exception e) + { + if (Config.Logging.DebugMessages) + { + ConsoleIO.WriteLine($"{e.GetType().Name} when ticking: {e.Message}"); + if (e.StackTrace != null) + ConsoleIO.WriteLine(e.StackTrace); + } } } } + catch (AggregateException e) + { + if (e.InnerException is not OperationCanceledException) + throw; + } + catch (OperationCanceledException) { } } /// @@ -252,6 +261,11 @@ namespace MinecraftClient.Protocol.Handlers catch (ObjectDisposedException) { break; } catch (OperationCanceledException) { break; } catch (NullReferenceException) { break; } + catch (AggregateException e) + { + if (e.InnerException is TaskCanceledException) + break; + } } if (!CancelToken.IsCancellationRequested) @@ -1760,14 +1774,7 @@ namespace MinecraftClient.Protocol.Handlers /// /// Disconnect from the server, cancel network reading. /// - public void Dispose() - { - try - { - socketWrapper.Disconnect(); - } - catch { } - } + public void Dispose() { } /// /// Send a packet to the server. Packet ID, compression, and encryption will be handled automatically. @@ -1934,31 +1941,27 @@ namespace MinecraftClient.Protocol.Handlers { log.Info(Translations.mcc_session); - bool needCheckSession = true; - string serverHash = CryptoHandler.GetServerHash(serverIDhash, serverPublicKey, secretKey); if (session.SessionPreCheckTask != null && session.ServerInfoHash != null && serverHash == session.ServerInfoHash) { - try + (bool preCheckResult, string? error) = await session.SessionPreCheckTask; + if (!preCheckResult) { - bool preCheckResult = await session.SessionPreCheckTask; - if (preCheckResult) // PreCheck Successed - needCheckSession = false; + handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, + string.IsNullOrEmpty(error) ? Translations.mcc_session_fail : $"{Translations.mcc_session_fail} Error: {error}."); + return false; } - catch (HttpRequestException) { } session.SessionPreCheckTask = null; } - - if (needCheckSession) + else { - var sessionCheck = await ProtocolHandler.SessionCheckAsync(httpClient, uuid, sessionID, serverHash); + (bool sessionCheck, string? error) = await ProtocolHandler.SessionCheckAsync(httpClient, uuid, sessionID, serverHash); if (sessionCheck) - { SessionCache.StoreServerInfo($"{InternalConfig.ServerIP}:{InternalConfig.ServerPort}", serverIDhash, serverPublicKey); - } else { - handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, Translations.mcc_session_fail); + handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, + string.IsNullOrEmpty(error) ? Translations.mcc_session_fail : $"{Translations.mcc_session_fail} Error: {error}."); return false; } } diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index 34e868ab..11f931f5 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; +using System.Net.Http.Json; using System.Net.Security; using System.Net.Sockets; using System.Runtime.CompilerServices; @@ -20,6 +21,7 @@ using MinecraftClient.Protocol.Handlers.Forge; using MinecraftClient.Protocol.Session; using MinecraftClient.Proxy; using PInvoke; +using static MinecraftClient.Json; using static MinecraftClient.Settings; using static MinecraftClient.Settings.MainConfigHealper.MainConfig.GeneralConfig; @@ -139,7 +141,7 @@ namespace MinecraftClient.Protocol int[] supportedVersions_Protocol18 = { 4, 5, 47, 107, 108, 109, 110, 210, 315, 316, 335, 338, 340, 393, 401, 404, 477, 480, 485, 490, 498, 573, 575, 578, 735, 736, 751, 753, 754, 755, 756, 757, 758, 759, 760 }; if (Array.IndexOf(supportedVersions_Protocol18, ProtocolVersion) > -1) - return new Protocol18Handler(cancelToken, Client, ProtocolVersion, Handler, forgeInfo); + return new Protocol18Handler(Client, ProtocolVersion, Handler, forgeInfo, cancelToken); throw new NotSupportedException(string.Format(Translations.exception_version_unsupport, ProtocolVersion)); } @@ -743,6 +745,12 @@ namespace MinecraftClient.Protocol public string? serverId { init; get; } } + private record SessionCheckFailResult + { + public string? error { init; get; } + public string? path { init; get; } + } + /// /// Check session using Mojang's Yggdrasil authentication scheme. Allows to join an online-mode server /// @@ -750,7 +758,7 @@ namespace MinecraftClient.Protocol /// Session ID /// Server ID /// TRUE if session was successfully checked - public static async Task SessionCheckAsync(HttpClient httpClient, string uuid, string accesstoken, string serverhash) + public static async Task> SessionCheckAsync(HttpClient httpClient, string uuid, string accesstoken, string serverhash) { SessionCheckPayload payload = new() { @@ -759,24 +767,33 @@ namespace MinecraftClient.Protocol serverId = serverhash, }; - using HttpRequestMessage request = new(HttpMethod.Post, "https://sessionserver.mojang.com/session/minecraft/join"); + try + { + using HttpRequestMessage request = new(HttpMethod.Post, "https://sessionserver.mojang.com/session/minecraft/join"); - request.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); + request.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); - request.Headers.UserAgent.Clear(); - request.Headers.UserAgent.ParseAdd($"MCC/{Program.Version}"); + request.Headers.UserAgent.Clear(); + request.Headers.UserAgent.ParseAdd($"MCC/{Program.Version}"); - using HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + using HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); - return response.IsSuccessStatusCode; - - //try - //{ - // string json_request = $"{{\"accessToken\":\"{accesstoken}\",\"selectedProfile\":\"{uuid}\",\"serverId\":\"{serverhash}\"}}"; - // (int code, _) = await DoHTTPSPost("sessionserver.mojang.com", "/session/minecraft/join", json_request); - // return (code >= 200 && code < 300); - //} - //catch { return false; } + if (response.IsSuccessStatusCode) + return new(true, null); + else + { + SessionCheckFailResult jsonData = (await response.Content.ReadFromJsonAsync())!; + return new(false, jsonData.error); + } + } + catch (HttpRequestException e) + { + return new(false, $"HttpRequestException: {e.Message}"); + } + catch (JsonException e) + { + return new(false, $"JsonException: {e.Message}"); + } } /// diff --git a/MinecraftClient/Protocol/Session/SessionToken.cs b/MinecraftClient/Protocol/Session/SessionToken.cs index 852e1be3..dac4e6b7 100644 --- a/MinecraftClient/Protocol/Session/SessionToken.cs +++ b/MinecraftClient/Protocol/Session/SessionToken.cs @@ -35,7 +35,7 @@ namespace MinecraftClient.Protocol.Session public string? ServerInfoHash = null; [JsonIgnore] - public Task? SessionPreCheckTask = null; + public Task>? SessionPreCheckTask = null; public SessionToken() { diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index 8dd815ae..56c643ce 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -38,7 +38,7 @@ namespace MinecraftClient.Scripting public void SetHandler(McClient handler) { _handler = handler; } protected void SetMaster(ChatBot master) { this.master = master; } protected void LoadBot(ChatBot bot) { Handler.BotUnLoad(bot).Wait(); Handler.BotLoad(bot); } - protected List GetLoadedChatBots() { return Handler.GetLoadedChatBots(); } + protected ChatBot[] GetLoadedChatBots() { return Handler.GetLoadedChatBots(); } protected void UnLoadBot(ChatBot bot) { Handler.BotUnLoad(bot).Wait(); } private McClient? _handler = null; private ChatBot? master = null; @@ -945,7 +945,7 @@ namespace MinecraftClient.Scripting ConsoleIO.WriteLogLine(string.Format(Translations.chatbot_reconnect, botName)); } McClient.ReconnectionAttemptsLeft = ExtraAttempts; - Program.Restart(delaySeconds * 10, keepAccountAndServerSettings); + Program.SetRestart(delaySeconds * 10, keepAccountAndServerSettings); } /// @@ -953,7 +953,7 @@ namespace MinecraftClient.Scripting /// protected void DisconnectAndExit() { - Program.Exit(); + Program.SetExit(); } ///