diff --git a/.gitignore b/.gitignore index 22033cc81f..e272e69a1c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ /MinecraftClient.userprefs /Other/ /.vs/ -SessionCache.ini .* !/.github /packages diff --git a/.gitmodules b/.gitmodules index 5ef4ed0f99..66e56c893e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = ConsoleInteractive url = https://github.com/breadbyte/ConsoleInteractive branch = main +[submodule "MinecraftProtocolLibrary"] + path = MinecraftProtocolLibrary + url = https://github.com/BruceChenQAQ/Minecraft-Protocol-Library.git diff --git a/MinecraftClient.sln b/MinecraftClient.sln index 8f0049d85b..cd117ea10f 100644 --- a/MinecraftClient.sln +++ b/MinecraftClient.sln @@ -4,9 +4,14 @@ Microsoft Visual Studio Solution File, Format Version 12.00 VisualStudioVersion = 17.3.32901.215 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinecraftClient", "MinecraftClient\MinecraftClient.csproj", "{1E2FACE4-F5CA-4323-9641-740C6A551770}" + ProjectSection(ProjectDependencies) = postProject + {B6EA6A06-0EF8-4931-93EA-68EB02D69FE9} = {B6EA6A06-0EF8-4931-93EA-68EB02D69FE9} + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleInteractive", "ConsoleInteractive\ConsoleInteractive\ConsoleInteractive\ConsoleInteractive.csproj", "{93DA4D71-EFAD-4493-BE21-A105AF663660}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinecraftProtocolLibrary", "MinecraftProtocolLibrary\MinecraftProtocolLibrary\MinecraftProtocolLibrary.csproj", "{B6EA6A06-0EF8-4931-93EA-68EB02D69FE9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,13 +26,17 @@ Global {93DA4D71-EFAD-4493-BE21-A105AF663660}.Debug|Any CPU.Build.0 = Debug|Any CPU {93DA4D71-EFAD-4493-BE21-A105AF663660}.Release|Any CPU.ActiveCfg = Release|Any CPU {93DA4D71-EFAD-4493-BE21-A105AF663660}.Release|Any CPU.Build.0 = Release|Any CPU + {B6EA6A06-0EF8-4931-93EA-68EB02D69FE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6EA6A06-0EF8-4931-93EA-68EB02D69FE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6EA6A06-0EF8-4931-93EA-68EB02D69FE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6EA6A06-0EF8-4931-93EA-68EB02D69FE9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - RESX_ShowErrorsInErrorList = False - SolutionGuid = {6DED60F4-9CF4-4DB3-8966-582B2EBE8487} RESX_SortFileContentOnSave = False + SolutionGuid = {6DED60F4-9CF4-4DB3-8966-582B2EBE8487} + RESX_ShowErrorsInErrorList = False EndGlobalSection EndGlobal diff --git a/MinecraftClient/AsyncTaskHandler.cs b/MinecraftClient/AsyncTaskHandler.cs new file mode 100644 index 0000000000..8491d9af87 --- /dev/null +++ b/MinecraftClient/AsyncTaskHandler.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MinecraftClient.Protocol.ProfileKey; +using MinecraftClient.Protocol.Session; + +namespace MinecraftClient +{ + internal static class AsyncTaskHandler + { + private static readonly List Tasks = new(); + + internal static Task CheckUpdate = Task.CompletedTask; + + internal static Task CacheSessionReader = Task.CompletedTask; + + internal static Task SaveSessionToDisk = Task.CompletedTask; + + internal static Task WritebackSettingFile = Task.CompletedTask; + + internal static async void ExitCleanUp() + { + await WritebackSettingFile; + + await CacheSessionReader; + + await SaveSessionToDisk; + + foreach (var task in Tasks) + { + await task; + } + + Tasks.Clear(); + } + } +} diff --git a/MinecraftClient/ChatBots/AutoAttack.cs b/MinecraftClient/ChatBots/AutoAttack.cs index 8cd8ee2f5b..85129b4bc9 100644 --- a/MinecraftClient/ChatBots/AutoAttack.cs +++ b/MinecraftClient/ChatBots/AutoAttack.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using MinecraftClient.Mapping; +using MinecraftClient.EntityHandler; using MinecraftClient.Scripting; using Tomlet.Attributes; diff --git a/MinecraftClient/ChatBots/AutoDig.cs b/MinecraftClient/ChatBots/AutoDig.cs index cddb6ffd39..6e2b511435 100644 --- a/MinecraftClient/ChatBots/AutoDig.cs +++ b/MinecraftClient/ChatBots/AutoDig.cs @@ -437,10 +437,9 @@ public override void OnDeath() StopDigging(); } - public override bool OnDisconnect(DisconnectReason reason, string message) + public override int OnDisconnect(DisconnectReason reason, string message) { StopDigging(); - return base.OnDisconnect(reason, message); } } diff --git a/MinecraftClient/ChatBots/AutoFishing.cs b/MinecraftClient/ChatBots/AutoFishing.cs index 09ee140f19..8c4d8ffe2a 100644 --- a/MinecraftClient/ChatBots/AutoFishing.cs +++ b/MinecraftClient/ChatBots/AutoFishing.cs @@ -5,6 +5,7 @@ using Brigadier.NET.Builder; using MinecraftClient.CommandHandler; using MinecraftClient.CommandHandler.Patch; +using MinecraftClient.EntityHandler; using MinecraftClient.Inventory; using MinecraftClient.Mapping; using MinecraftClient.Scripting; @@ -535,7 +536,7 @@ public override void OnDeath() StopFishing(); } - public override bool OnDisconnect(DisconnectReason reason, string message) + public override int OnDisconnect(DisconnectReason reason, string message) { StopFishing(); diff --git a/MinecraftClient/ChatBots/AutoRelog.cs b/MinecraftClient/ChatBots/AutoRelog.cs index c384eb7c9f..6d72c16c66 100644 --- a/MinecraftClient/ChatBots/AutoRelog.cs +++ b/MinecraftClient/ChatBots/AutoRelog.cs @@ -97,8 +97,9 @@ private void _Initialize() } } - public override bool OnDisconnect(DisconnectReason reason, string message) + public override int OnDisconnect(DisconnectReason reason, string message) { + bool triggerReco = false; if (reason == DisconnectReason.UserLogout) { LogDebugToConsole(Translations.bot_autoRelog_ignore_user_logout); @@ -113,44 +114,77 @@ public override bool OnDisconnect(DisconnectReason reason, string message) if (Config.Ignore_Kick_Message) { Configs._BotRecoAttempts++; - LaunchDelayedReconnection(null); - return true; + LogDebugToConsole(Translations.bot_autoRelog_reconnect_always); + triggerReco = true; } - - foreach (string msg in Config.Kick_Messages) + else { - if (comp.Contains(msg)) + foreach (string msg in Config.Kick_Messages) { - Configs._BotRecoAttempts++; - LaunchDelayedReconnection(msg); - return true; + if (comp.Contains(msg)) + { + Configs._BotRecoAttempts++; + LogDebugToConsole(string.Format(Translations.bot_autoRelog_reconnect, msg)); + triggerReco = true; + break; + } } - } - LogDebugToConsole(Translations.bot_autoRelog_reconnect_ignore); + if (!triggerReco) + LogDebugToConsole(Translations.bot_autoRelog_reconnect_ignore); + } } - return false; + if (triggerReco) + { + double delay = random.NextDouble() * (Config.Delay.max - Config.Delay.min) + Config.Delay.min; + LogToConsole(string.Format(Translations.bot_autoRelog_wait, delay)); + return (int)Math.Floor(delay * 1000); + } + else + { + return -1; + } } - private void LaunchDelayedReconnection(string? msg) + public static int OnDisconnectStatic(DisconnectReason reason, string message) { - double delay = random.NextDouble() * (Config.Delay.max - Config.Delay.min) + Config.Delay.min; - LogDebugToConsole(string.Format(string.IsNullOrEmpty(msg) ? Translations.bot_autoRelog_reconnect_always : Translations.bot_autoRelog_reconnect, msg)); - LogToConsole(string.Format(Translations.bot_autoRelog_wait, delay)); - System.Threading.Thread.Sleep((int)Math.Floor(delay * 1000)); - ReconnectToTheServer(); - } + bool triggerReco = false; + if (Config.Enabled + && reason != DisconnectReason.UserLogout + && (Config.Retries < 0 || Configs._BotRecoAttempts < Config.Retries)) + { + message = GetVerbatim(message); + string comp = message.ToLower(); - public static bool OnDisconnectStatic(DisconnectReason reason, string message) - { - if (Config.Enabled) + if (Config.Ignore_Kick_Message) + { + Configs._BotRecoAttempts++; + triggerReco = true; + } + else + { + foreach (string msg in Config.Kick_Messages) + { + if (comp.Contains(msg)) + { + Configs._BotRecoAttempts++; + triggerReco = true; + break; + } + } + } + } + + if (triggerReco) + { + double delay = random.NextDouble() * (Config.Delay.max - Config.Delay.min) + Config.Delay.min; + return (int)Math.Floor(delay * 1000); + } + else { - AutoRelog bot = new(); - bot.Initialize(); - return bot.OnDisconnect(reason, message); + return -1; } - return false; } } } diff --git a/MinecraftClient/ChatBots/Farmer.cs b/MinecraftClient/ChatBots/Farmer.cs index b035e22ca7..58158c33d3 100644 --- a/MinecraftClient/ChatBots/Farmer.cs +++ b/MinecraftClient/ChatBots/Farmer.cs @@ -244,10 +244,10 @@ public override void AfterGameJoined() running = false; } - public override bool OnDisconnect(DisconnectReason reason, string message) + public override int OnDisconnect(DisconnectReason reason, string message) { running = false; - return true; + return base.OnDisconnect(reason, message); } private void MainPorcess() diff --git a/MinecraftClient/ChatBots/FollowPlayer.cs b/MinecraftClient/ChatBots/FollowPlayer.cs index 0561bf0fa7..6833ca14bc 100644 --- a/MinecraftClient/ChatBots/FollowPlayer.cs +++ b/MinecraftClient/ChatBots/FollowPlayer.cs @@ -4,6 +4,7 @@ using Brigadier.NET.Builder; using MinecraftClient.CommandHandler; using MinecraftClient.CommandHandler.Patch; +using MinecraftClient.EntityHandler; using MinecraftClient.Mapping; using MinecraftClient.Scripting; using Tomlet.Attributes; diff --git a/MinecraftClient/ChatBots/Map.cs b/MinecraftClient/ChatBots/Map.cs index fef48a90b9..5462fec374 100644 --- a/MinecraftClient/ChatBots/Map.cs +++ b/MinecraftClient/ChatBots/Map.cs @@ -356,11 +356,12 @@ private static void RenderInConsole(McMap map) for (int base_y = 0; base_y < map.Height; base_y += scale) { - int last_R = -1, last_G = -1, last_B = -1; + string lastFg = string.Empty, lagtBg = string.Empty; for (int base_x = 0; base_x < map.Width; base_x += scale) { - int RL = 0, GL = 0, BL = 0, RR = 0, GR = 0, BR = 0; - double mid_dx = (double)(scale - 1) / 2; + int RUL = 0, GUL = 0, BUL = 0, RUR = 0, GUR = 0, BUR = 0; + int RDL = 0, GDL = 0, BDL = 0, RDR = 0, GDR = 0, BDR = 0; + double mid = (double)(scale - 1) / 2; for (int dy = 0; dy < scale; ++dy) { for (int dx = 0; dx < scale; ++dx) @@ -368,40 +369,57 @@ private static void RenderInConsole(McMap map) int x = Math.Min(base_x + dx, map.Width - 1); int y = Math.Min(base_y + dy, map.Height - 1); ColorRGBA color = MapColors.ColorByteToRGBA(map.Colors![x + y * map.Width]); - if (dx <= mid_dx) + if (dx <= mid) { - RL += color.R; GL += color.G; BL += color.B; + if (dy <= mid) + { + RUL += color.R; GUL += color.G; BUL += color.B; + } + if (dy >= mid) + { + RDL += color.R; GDL += color.G; BDL += color.B; + } } - if (dx >= mid_dx) + if (dx >= mid) { - RR += color.R; GR += color.G; BR += color.B; + if (dy <= mid) + { + RUR += color.R; GUR += color.G; BUR += color.B; + } + if (dy >= mid) + { + RDR += color.R; GDR += color.G; BDR += color.B; + } } } } - int pixel_cnt = ((scale + 1) / 2) * scale; - RL = (int)Math.Round((double)RL / pixel_cnt); - GL = (int)Math.Round((double)GL / pixel_cnt); - BL = (int)Math.Round((double)BL / pixel_cnt); - RR = (int)Math.Round((double)RR / pixel_cnt); - GR = (int)Math.Round((double)GR / pixel_cnt); - BR = (int)Math.Round((double)BR / pixel_cnt); - - if (RL == last_R && GL == last_G && BL == last_B) - sb.Append(' '); - else - { - sb.Append(ColorHelper.GetColorEscapeCode((byte)RL, (byte)GL, (byte)BL, false)).Append(' '); - last_R = RL; last_G = GL; last_B = BL; - } - - if (RR == last_R && GR == last_G && BR == last_B) - sb.Append(' '); - else - { - sb.Append(ColorHelper.GetColorEscapeCode((byte)RR, (byte)GR, (byte)BR, false)).Append(' '); - last_R = RR; last_G = GR; last_B = BR; - } + int pixel_cnt = ((scale + 1) / 2) * ((scale + 1) / 2); + RDL = (int)Math.Round((double)RDL / pixel_cnt); + GDL = (int)Math.Round((double)GDL / pixel_cnt); + BDL = (int)Math.Round((double)BDL / pixel_cnt); + RDR = (int)Math.Round((double)RDR / pixel_cnt); + GDR = (int)Math.Round((double)GDR / pixel_cnt); + BDR = (int)Math.Round((double)BDR / pixel_cnt); + + RUL = (int)Math.Round((double)RUL / pixel_cnt); + GUL = (int)Math.Round((double)GUL / pixel_cnt); + BUL = (int)Math.Round((double)BUL / pixel_cnt); + RUR = (int)Math.Round((double)RUR / pixel_cnt); + GUR = (int)Math.Round((double)GUR / pixel_cnt); + BUR = (int)Math.Round((double)BUR / pixel_cnt); + + string colorCode = ColorHelper.GetColorEscapeCode((byte)RUL, (byte)GUL, (byte)BUL, true); + if (lastFg != colorCode) { sb.Append(colorCode); lastFg = colorCode; } + colorCode = ColorHelper.GetColorEscapeCode((byte)RDL, (byte)GDL, (byte)BDL, false); + if (lagtBg != colorCode) { sb.Append(colorCode); lagtBg = colorCode; } + sb.Append('▀'); + + colorCode = ColorHelper.GetColorEscapeCode((byte)RUR, (byte)GUR, (byte)BUR, true); + if (lastFg != colorCode) { sb.Append(colorCode); lastFg = colorCode; } + colorCode = ColorHelper.GetColorEscapeCode((byte)RDR, (byte)GDR, (byte)BDR, false); + if (lagtBg != colorCode) { sb.Append(colorCode); lagtBg = colorCode; } + sb.Append('▀'); } if (base_y >= map.Height - scale) sb.Append(ColorHelper.GetResetEscapeCode()); diff --git a/MinecraftClient/ChatBots/ReplayCapture.cs b/MinecraftClient/ChatBots/ReplayCapture.cs index eea749119f..24d1d728f5 100644 --- a/MinecraftClient/ChatBots/ReplayCapture.cs +++ b/MinecraftClient/ChatBots/ReplayCapture.cs @@ -134,7 +134,7 @@ public override void Update() } } - public override bool OnDisconnect(DisconnectReason reason, string message) + public override int OnDisconnect(DisconnectReason reason, string message) { replay!.OnShutDown(); return base.OnDisconnect(reason, message); diff --git a/MinecraftClient/ChatBots/Script.cs b/MinecraftClient/ChatBots/Script.cs index 8ed7cb4229..010566c6a5 100644 --- a/MinecraftClient/ChatBots/Script.cs +++ b/MinecraftClient/ChatBots/Script.cs @@ -130,7 +130,7 @@ public override void Initialize() //Load the given file from the startup parameters if (LookForScript(ref file!)) { - lines = System.IO.File.ReadAllLines(file, Encoding.UTF8); + lines = File.ReadAllLines(file, Encoding.UTF8); csharp = file.EndsWith(".cs"); thread = null; diff --git a/MinecraftClient/ChatBots/ScriptScheduler.cs b/MinecraftClient/ChatBots/ScriptScheduler.cs index bf8680a4f3..f9b0e9364a 100644 --- a/MinecraftClient/ChatBots/ScriptScheduler.cs +++ b/MinecraftClient/ChatBots/ScriptScheduler.cs @@ -236,10 +236,10 @@ public override void Update() } } - public override bool OnDisconnect(DisconnectReason reason, string message) + public override int OnDisconnect(DisconnectReason reason, string message) { serverlogin_done = false; - return false; + return base.OnDisconnect(reason, message); } private static string Task2String(TaskConfig task) diff --git a/MinecraftClient/ChatBots/TestBot.cs b/MinecraftClient/ChatBots/TestBot.cs index 8b10353564..f1a8e62221 100644 --- a/MinecraftClient/ChatBots/TestBot.cs +++ b/MinecraftClient/ChatBots/TestBot.cs @@ -1,4 +1,6 @@ -using MinecraftClient.Scripting; +using System; +using System.Threading.Tasks; +using MinecraftClient.Scripting; namespace MinecraftClient.ChatBots { @@ -8,6 +10,19 @@ namespace MinecraftClient.ChatBots public class TestBot : ChatBot { + //public override Tuple>[]? InitializeEventCallbacks() + //{ + // return new Tuple>[] + // { + // new(McClientEventType.ClientTick, async (object? o) => + // { + // await Task.CompletedTask; + // LogToConsole("test aaa"); + // throw new Exception("dwadwa"); + // }) + // }; + //} + public override void GetText(string text) { string message = ""; diff --git a/MinecraftClient/Command.cs b/MinecraftClient/Command.cs index e606f0dd36..1725d10b1d 100644 --- a/MinecraftClient/Command.cs +++ b/MinecraftClient/Command.cs @@ -29,14 +29,17 @@ public abstract class Command /// Get the translated version of command description. /// /// Translated command description - public string GetCmdDescTranslated() + public string GetCmdDescTranslated(bool ListAllUsage = true) { char cmdChar = Settings.Config.Main.Advanced.InternalCmdChar.ToChar(); StringBuilder sb = new(); string s = (string.IsNullOrEmpty(CmdUsage) || string.IsNullOrEmpty(CmdDesc)) ? string.Empty : ": "; // If either one is empty, no colon : - sb.Append("§e").Append(cmdChar).Append(CmdUsage).Append("§r").Append(s).AppendLine(CmdDesc); - sb.Append(McClient.dispatcher.GetAllUsageString(CmdName, false)); + sb.Append("§e").Append(cmdChar).Append(CmdUsage).Append("§r").Append(s); + if (ListAllUsage) + sb.AppendLine(CmdDesc).Append(McClient.dispatcher.GetAllUsageString(CmdName, false)); + else + sb.Append(CmdDesc); return sb.ToString(); } diff --git a/MinecraftClient/CommandHandler/ArgumentType/BotNameArgumentType.cs b/MinecraftClient/CommandHandler/ArgumentType/BotNameArgumentType.cs index 1f49da9808..ef33fd6688 100644 --- a/MinecraftClient/CommandHandler/ArgumentType/BotNameArgumentType.cs +++ b/MinecraftClient/CommandHandler/ArgumentType/BotNameArgumentType.cs @@ -19,7 +19,7 @@ public override Task ListSuggestions(CommandContext public override WindowActionType Parse(IStringReader reader) { reader.SkipWhitespace(); - string inputStr = reader.ReadString(); - foreach (var action in SupportActions) + string inputStr = reader.ReadString().ToLower(); + return inputStr switch { - string actionStr = action.ToString(); - if (string.Compare(inputStr, actionStr, true) == 0) - return action; - } - throw CommandSyntaxException.BuiltInExceptions.LiteralIncorrect().CreateWithContext(reader, inputStr); + "left" => WindowActionType.LeftClick, + "leftclick" => WindowActionType.LeftClick, + "right" => WindowActionType.RightClick, + "rightclick" => WindowActionType.RightClick, + "mid" => WindowActionType.MiddleClick, + "middle" => WindowActionType.MiddleClick, + "middleclick" => WindowActionType.MiddleClick, + "shift" => WindowActionType.ShiftClick, + "shiftclick" => WindowActionType.ShiftClick, + _ => throw CommandSyntaxException.BuiltInExceptions.LiteralIncorrect().CreateWithContext(reader, inputStr) + }; } public override Task ListSuggestions(CommandContext context, SuggestionsBuilder builder) diff --git a/MinecraftClient/CommandHandler/ArgumentType/MapBotMapIdArgumentType.cs b/MinecraftClient/CommandHandler/ArgumentType/MapBotMapIdArgumentType.cs index bd1ffee630..585ad59e99 100644 --- a/MinecraftClient/CommandHandler/ArgumentType/MapBotMapIdArgumentType.cs +++ b/MinecraftClient/CommandHandler/ArgumentType/MapBotMapIdArgumentType.cs @@ -21,15 +21,18 @@ public override Task ListSuggestions(CommandContext 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/CommandHandler/ArgumentType/PlayerNameArgumentType.cs b/MinecraftClient/CommandHandler/ArgumentType/PlayerNameArgumentType.cs index b5092251ee..6bd0bd56ef 100644 --- a/MinecraftClient/CommandHandler/ArgumentType/PlayerNameArgumentType.cs +++ b/MinecraftClient/CommandHandler/ArgumentType/PlayerNameArgumentType.cs @@ -4,7 +4,7 @@ using Brigadier.NET.ArgumentTypes; using Brigadier.NET.Context; using Brigadier.NET.Suggestion; -using MinecraftClient.Mapping; +using MinecraftClient.EntityHandler; namespace MinecraftClient.CommandHandler.ArgumentType { diff --git a/MinecraftClient/CommandHandler/MccArguments.cs b/MinecraftClient/CommandHandler/MccArguments.cs index c659ee3e04..fa0f0a21c1 100644 --- a/MinecraftClient/CommandHandler/MccArguments.cs +++ b/MinecraftClient/CommandHandler/MccArguments.cs @@ -1,6 +1,7 @@ using System; using Brigadier.NET.Context; using MinecraftClient.CommandHandler.ArgumentType; +using MinecraftClient.EntityHandler; namespace MinecraftClient.CommandHandler { @@ -31,9 +32,9 @@ public static EntityTypeArgumentType EntityType() return new EntityTypeArgumentType(); } - public static Mapping.EntityType GetEntityType(CommandContext context, string name) + public static EntityType GetEntityType(CommandContext context, string name) { - return context.GetArgument(name); + return context.GetArgument(name); } public static ItemTypeArgumentType ItemType() diff --git a/MinecraftClient/Commands/Animation.cs b/MinecraftClient/Commands/Animation.cs index dab51b6f90..5cad8e176f 100644 --- a/MinecraftClient/Commands/Animation.cs +++ b/MinecraftClient/Commands/Animation.cs @@ -49,7 +49,7 @@ private int GetUsage(CmdResult r, string? cmd) private static int DoAnimation(CmdResult r, bool mainhand) { McClient handler = CmdResult.currentHandler!; - return r.SetAndReturn(handler.DoAnimation(mainhand ? 1 : 0)); + return r.SetAndReturn(handler.DoAnimationAsync(mainhand ? 1 : 0).Result); } } } diff --git a/MinecraftClient/Commands/Bed.cs b/MinecraftClient/Commands/Bed.cs index f3d4b995fb..9c76438a5b 100644 --- a/MinecraftClient/Commands/Bed.cs +++ b/MinecraftClient/Commands/Bed.cs @@ -57,7 +57,7 @@ private int GetUsage(CmdResult r, string? cmd) private static int DoLeaveBed(CmdResult r) { McClient handler = CmdResult.currentHandler!; - return r.SetAndReturn(Translations.cmd_bed_leaving, handler.SendEntityAction(Protocol.EntityActionType.LeaveBed)); + return r.SetAndReturn(Translations.cmd_bed_leaving, handler.SendEntityActionAsync(Protocol.EntityActionType.LeaveBed).Result); } private static int DoSleepBedWithRadius(CmdResult r, double radius) @@ -113,7 +113,7 @@ private static int DoSleepBedWithRadius(CmdResult r, double radius) return r.SetAndReturn(Status.FailChunkNotLoad, string.Format(Translations.cmd_move_chunk_not_loaded, bedLocation.X, bedLocation.Y, bedLocation.Z)); - if (handler.MoveTo(bedLocation)) + if (handler.MoveToAsync(bedLocation).Result) { Task.Factory.StartNew(() => { @@ -137,7 +137,7 @@ private static int DoSleepBedWithRadius(CmdResult r, double radius) handler.Log.Info(string.Format(Translations.cmd_bed_moving, bedLocation.X, bedLocation.Y, bedLocation.Z)); - bool res = handler.PlaceBlock(bedLocation, Direction.Down); + bool res = handler.PlaceBlockAsync(bedLocation, Direction.Down).Result; handler.Log.Info(string.Format( Translations.cmd_bed_trying_to_use, @@ -174,7 +174,7 @@ private static int DoSleepBedWithLocation(CmdResult r, Location block) blockCenter.X, blockCenter.Y, blockCenter.Z, - handler.PlaceBlock(block, Direction.Down) ? Translations.cmd_bed_in : Translations.cmd_bed_not_in + handler.PlaceBlockAsync(block, Direction.Down).Result ? Translations.cmd_bed_in : Translations.cmd_bed_not_in )); } } diff --git a/MinecraftClient/Commands/Bots.cs b/MinecraftClient/Commands/Bots.cs index 73574a14b4..92b5e397c5 100644 --- a/MinecraftClient/Commands/Bots.cs +++ b/MinecraftClient/Commands/Bots.cs @@ -53,7 +53,7 @@ private int GetUsage(CmdResult r, string? cmd) 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,22 +73,30 @@ private int DoUnloadBot(CmdResult r, string botName) 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 { - handler.UnloadAllBots(); + handler.UnloadAllBots().Wait(); return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_bots_unloaded_all); } } 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); + handler.BotUnLoad(target).Wait(); return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_bots_unloaded, botName)); } } diff --git a/MinecraftClient/Commands/ChangeSlot.cs b/MinecraftClient/Commands/ChangeSlot.cs index acbe2e8d3f..20641cb6d6 100644 --- a/MinecraftClient/Commands/ChangeSlot.cs +++ b/MinecraftClient/Commands/ChangeSlot.cs @@ -44,7 +44,7 @@ private int DoChangeSlot(CmdResult r, int slot) if (!handler.GetInventoryEnabled()) return r.SetAndReturn(Status.FailNeedInventory); - if (handler.ChangeSlot((short)(slot - 1))) + if (handler.ChangeSlotAsync((short)(slot - 1)).Result) return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_changeSlot_changed, slot)); else return r.SetAndReturn(Status.Fail, Translations.cmd_changeSlot_fail); diff --git a/MinecraftClient/Commands/Connect.cs b/MinecraftClient/Commands/Connect.cs index c8691340db..c74094b42b 100644 --- a/MinecraftClient/Commands/Connect.cs +++ b/MinecraftClient/Commands/Connect.cs @@ -47,7 +47,7 @@ private int DoConnect(CmdResult r, string server, string account) 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 @@ internal static string DoConnect(string command) 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/Dig.cs b/MinecraftClient/Commands/Dig.cs index 3a30619a15..6d0af7a053 100644 --- a/MinecraftClient/Commands/Dig.cs +++ b/MinecraftClient/Commands/Dig.cs @@ -54,7 +54,7 @@ private int DigAt(CmdResult r, Location blockToBreak) Block block = handler.GetWorld().GetBlock(blockToBreak); if (block.Type == Material.Air) return r.SetAndReturn(Status.Fail, Translations.cmd_dig_no_block); - else if (handler.DigBlock(blockToBreak)) + else if (handler.DigBlockAsync(blockToBreak).Result) { blockToBreak = blockToBreak.ToCenter(); return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_dig_dig, blockToBreak.X, blockToBreak.Y, blockToBreak.Z, block.GetTypeString())); @@ -74,7 +74,7 @@ private int DigLookAt(CmdResult r) return r.SetAndReturn(Status.Fail, Translations.cmd_dig_too_far); else if (block.Type == Material.Air) return r.SetAndReturn(Status.Fail, Translations.cmd_dig_no_block); - else if (handler.DigBlock(blockLoc, lookAtBlock: false)) + else if (handler.DigBlockAsync(blockLoc, lookAtBlock: false).Result) return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_dig_dig, blockLoc.X, blockLoc.Y, blockLoc.Z, block.GetTypeString())); else return r.SetAndReturn(Status.Fail, Translations.cmd_dig_fail); diff --git a/MinecraftClient/Commands/DropItem.cs b/MinecraftClient/Commands/DropItem.cs index 38cfc4b68b..92ed576b1d 100644 --- a/MinecraftClient/Commands/DropItem.cs +++ b/MinecraftClient/Commands/DropItem.cs @@ -58,7 +58,7 @@ private int DoDropItem(CmdResult r, ItemType itemType) var p = inventories[inventoryId]; int[] targetItems = p.SearchItem(itemType); foreach (int slot in targetItems) - handler.DoWindowAction(inventoryId, slot, WindowActionType.DropItemStack); + handler.DoWindowActionAsync(inventoryId, slot, WindowActionType.DropItemStack).Wait(); return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_dropItem_dropped, Item.GetTypeString(itemType), inventoryId)); } diff --git a/MinecraftClient/Commands/Enchant.cs b/MinecraftClient/Commands/Enchant.cs index 813c734f18..339540cea6 100644 --- a/MinecraftClient/Commands/Enchant.cs +++ b/MinecraftClient/Commands/Enchant.cs @@ -99,7 +99,7 @@ private int DoEnchant(CmdResult r, int slotId) return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_enchant_no_levels, handler.GetLevel(), requiredLevel)); else { - if (handler.ClickContainerButton(enchantingTable.ID, slotId)) + if (handler.ClickContainerButton(enchantingTable.ID, slotId).Result) return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_enchant_clicked); else return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_enchant_not_clicked); diff --git a/MinecraftClient/Commands/Entitycmd.cs b/MinecraftClient/Commands/Entitycmd.cs index 7cd4d8b4c0..b00b7b933f 100644 --- a/MinecraftClient/Commands/Entitycmd.cs +++ b/MinecraftClient/Commands/Entitycmd.cs @@ -5,6 +5,7 @@ using Brigadier.NET; using Brigadier.NET.Builder; using MinecraftClient.CommandHandler; +using MinecraftClient.EntityHandler; using MinecraftClient.Inventory; using MinecraftClient.Mapping; using static MinecraftClient.CommandHandler.CmdResult; @@ -166,12 +167,12 @@ private int OperateWithType(CmdResult r, bool near, EntityType entityType, Actio { if (action == ActionType.Attack) { - handler.InteractEntity(entity2.Key, InteractType.Attack); + handler.InteractEntityAsync(entity2.Key, InteractType.Attack).Wait(); actionst = Translations.cmd_entityCmd_attacked; } else if (action == ActionType.Use) { - handler.InteractEntity(entity2.Key, InteractType.Interact); + handler.InteractEntityAsync(entity2.Key, InteractType.Interact).Wait(); actionst = Translations.cmd_entityCmd_used; } actioncount++; @@ -251,7 +252,7 @@ private static string GetEntityInfoDetailed(McClient handler, Entity entity) { sb.Append($"\n [MCC] {Translations.cmd_entityCmd_latency}: {latency}"); } - else if (type == EntityType.Item || type == EntityType.ItemFrame || type == Mapping.EntityType.EyeOfEnder || type == Mapping.EntityType.Egg || type == Mapping.EntityType.EnderPearl || type == Mapping.EntityType.Potion || type == Mapping.EntityType.Fireball || type == Mapping.EntityType.FireworkRocket) + else if (type == EntityType.Item || type == EntityType.ItemFrame || type == EntityType.EyeOfEnder || type == EntityType.Egg || type == EntityType.EnderPearl || type == EntityType.Potion || type == EntityType.Fireball || type == EntityType.FireworkRocket) { string? displayName = item.DisplayName; if (string.IsNullOrEmpty(displayName)) @@ -311,10 +312,10 @@ private static string InteractionWithEntity(McClient handler, Entity entity, Act switch (action) { case ActionType.Attack: - handler.InteractEntity(entity.ID, InteractType.Attack); + handler.InteractEntityAsync(entity.ID, InteractType.Attack).Wait(); return Translations.cmd_entityCmd_attacked; case ActionType.Use: - handler.InteractEntity(entity.ID, InteractType.Interact); + handler.InteractEntityAsync(entity.ID, InteractType.Interact).Wait(); return Translations.cmd_entityCmd_used; case ActionType.List: return GetEntityInfoDetailed(handler, entity); diff --git a/MinecraftClient/Commands/Exit.cs b/MinecraftClient/Commands/Exit.cs index d2ac389eb0..ad3b7c1aba 100644 --- a/MinecraftClient/Commands/Exit.cs +++ b/MinecraftClient/Commands/Exit.cs @@ -45,14 +45,8 @@ private int GetUsage(CmdResult r, string? cmd) private int DoExit(CmdResult r, int code = 0) { - Program.Exit(code); + Program.SetExit(code); return r.SetAndReturn(CmdResult.Status.Done); } - - internal static string DoExit(string command) - { - Program.Exit(); - return string.Empty; - } } } diff --git a/MinecraftClient/Commands/Inventory.cs b/MinecraftClient/Commands/Inventory.cs index 9b8325ea05..db5cea1ffc 100644 --- a/MinecraftClient/Commands/Inventory.cs +++ b/MinecraftClient/Commands/Inventory.cs @@ -159,7 +159,7 @@ private int DoCreativeGive(CmdResult r, int slot, ItemType itemType, int count) if (handler.GetGamemode() == 1) { - if (handler.DoCreativeGive(slot, itemType, count, null)) + if (handler.DoCreativeGiveAsync(slot, itemType, count, null).Result) return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_inventory_creative_done, itemType, count, slot)); else return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_inventory_creative_fail); @@ -178,7 +178,7 @@ private int DoCreativeDelete(CmdResult r, int slot) if (handler.GetGamemode() == 1) { - if (handler.DoCreativeGive(slot, ItemType.Null, 0, null)) + if (handler.DoCreativeGiveAsync(slot, ItemType.Null, 0, null).Result) return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_inventory_creative_delete, slot)); else return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_inventory_creative_fail); @@ -279,7 +279,7 @@ private int DoCloseAction(CmdResult r, int? inventoryId) if (inventory == null) return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_inventory_not_exist, inventoryId)); - if (handler.CloseInventory(inventoryId.Value)) + if (handler.CloseInventoryAsync(inventoryId.Value).Result) return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_inventory_close, inventoryId)); else return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_inventory_close_fail, inventoryId)); @@ -355,7 +355,9 @@ private int DoClickAction(CmdResult r, int? inventoryId, int slot, WindowActionT }; handler.Log.Info(string.Format(Translations.cmd_inventory_clicking, keyName, slot, inventoryId)); - return r.SetAndReturn(handler.DoWindowAction(inventoryId.Value, slot, actionType)); + var task = handler.DoWindowActionAsync(inventoryId.Value, slot, actionType); + task.Wait(); + return r.SetAndReturn(task.Result); } private int DoDropAction(CmdResult r, int? inventoryId, int slot, WindowActionType actionType) @@ -379,7 +381,7 @@ private int DoDropAction(CmdResult r, int? inventoryId, int slot, WindowActionTy if (!inventory.Items.ContainsKey(slot)) return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_inventory_no_item, slot)); - if (handler.DoWindowAction(inventoryId.Value, slot, actionType)) + if (handler.DoWindowActionAsync(inventoryId.Value, slot, actionType).Result) { if (actionType == WindowActionType.DropItemStack) return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_inventory_drop_stack, slot)); diff --git a/MinecraftClient/Commands/Move.cs b/MinecraftClient/Commands/Move.cs index fa94a06b44..87ac60d15e 100644 --- a/MinecraftClient/Commands/Move.cs +++ b/MinecraftClient/Commands/Move.cs @@ -145,7 +145,7 @@ private int MoveToCenter(CmdResult r) Location current = handler.GetCurrentLocation(); Location currentCenter = new(Math.Floor(current.X) + 0.5, current.Y, Math.Floor(current.Z) + 0.5); - handler.MoveTo(currentCenter, allowDirectTeleport: true); + handler.MoveToAsync(currentCenter, allowDirectTeleport: true).Wait(); return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_move_walk, currentCenter, current)); } @@ -162,7 +162,7 @@ private int MoveOnDirection(CmdResult r, Direction direction, bool takeRisk) if (Movement.CanMove(handler.GetWorld(), handler.GetCurrentLocation(), direction)) { - if (handler.MoveTo(goal, allowUnsafe: takeRisk)) + if (handler.MoveToAsync(goal, allowUnsafe: takeRisk).Result) return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_move_moving, direction.ToString())); else return r.SetAndReturn(Status.Fail, takeRisk ? Translations.cmd_move_dir_fail : Translations.cmd_move_suggestforce); @@ -188,8 +188,8 @@ private int MoveToLocation(CmdResult r, Location goal, bool takeRisk) if (takeRisk || Movement.PlayerFitsHere(handler.GetWorld(), goal)) { if (current.ToFloor() == goal.ToFloor()) - handler.MoveTo(goal, allowDirectTeleport: true); - else if (!handler.MoveTo(goal, allowUnsafe: takeRisk)) + handler.MoveToAsync(goal, allowDirectTeleport: true).Wait(); + else if (!handler.MoveToAsync(goal, allowUnsafe: takeRisk).Result) return r.SetAndReturn(Status.Fail, takeRisk ? string.Format(Translations.cmd_move_fail, goal) : string.Format(Translations.cmd_move_suggestforce, goal)); return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_move_walk, goal, current)); } diff --git a/MinecraftClient/Commands/Reco.cs b/MinecraftClient/Commands/Reco.cs index 482b4e0b5f..e4830d367c 100644 --- a/MinecraftClient/Commands/Reco.cs +++ b/MinecraftClient/Commands/Reco.cs @@ -47,7 +47,7 @@ private int DoReconnect(CmdResult r, string account) 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,8 +62,8 @@ internal static string DoReconnect(string command) return string.Format(Translations.cmd_connect_unknown, account); } } - Program.Restart(keepAccountAndServerSettings: true); - return String.Empty; + Program.SetRestart(keepAccountAndServerSettings: true); + return string.Empty; } } } diff --git a/MinecraftClient/Commands/Reload.cs b/MinecraftClient/Commands/Reload.cs index 953591595b..edf3994ee6 100644 --- a/MinecraftClient/Commands/Reload.cs +++ b/MinecraftClient/Commands/Reload.cs @@ -40,7 +40,7 @@ private int DoReload(CmdResult r) { McClient handler = CmdResult.currentHandler!; handler.Log.Info(Translations.cmd_reload_started); - handler.ReloadSettings(); + handler.ReloadSettings().Wait(); handler.Log.Warn(Translations.cmd_reload_warning1); handler.Log.Warn(Translations.cmd_reload_warning2); handler.Log.Warn(Translations.cmd_reload_warning3); diff --git a/MinecraftClient/Commands/Respawn.cs b/MinecraftClient/Commands/Respawn.cs index f8dd542a88..f1f6bf80b5 100644 --- a/MinecraftClient/Commands/Respawn.cs +++ b/MinecraftClient/Commands/Respawn.cs @@ -39,7 +39,7 @@ private int GetUsage(CmdResult r, string? cmd) private int DoRespawn(CmdResult r) { McClient handler = CmdResult.currentHandler!; - handler.SendRespawnPacket(); + handler.SendRespawnPacketAsync().Wait(); return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_respawn_done); } } diff --git a/MinecraftClient/Commands/Script.cs b/MinecraftClient/Commands/Script.cs index d2f310ddda..31336e4597 100644 --- a/MinecraftClient/Commands/Script.cs +++ b/MinecraftClient/Commands/Script.cs @@ -41,7 +41,7 @@ private int GetUsage(CmdResult r, string? cmd) private int DoExecuteScript(CmdResult r, string command, Dictionary? localVars) { McClient handler = CmdResult.currentHandler!; - handler.BotLoad(new ChatBots.Script(command.Trim(), null, localVars)); + handler.BotLoad(new ChatBots.Script(command.Trim(), null, localVars)).Wait(); return r.SetAndReturn(CmdResult.Status.Done); } } diff --git a/MinecraftClient/Commands/Send.cs b/MinecraftClient/Commands/Send.cs index cf6964eca4..9ec6bff580 100644 --- a/MinecraftClient/Commands/Send.cs +++ b/MinecraftClient/Commands/Send.cs @@ -37,7 +37,7 @@ private int GetUsage(CmdResult r, string? cmd) private int DoSendText(CmdResult r, string command) { McClient handler = CmdResult.currentHandler!; - handler.SendText(command); + handler.SendTextAsync(command).Wait(); return r.SetAndReturn(CmdResult.Status.Done); } } diff --git a/MinecraftClient/Commands/Sneak.cs b/MinecraftClient/Commands/Sneak.cs index 01d470fafc..b98eeff753 100644 --- a/MinecraftClient/Commands/Sneak.cs +++ b/MinecraftClient/Commands/Sneak.cs @@ -42,7 +42,7 @@ private int DoSneak(CmdResult r) McClient handler = CmdResult.currentHandler!; if (sneaking) { - var result = handler.SendEntityAction(Protocol.EntityActionType.StopSneaking); + var result = handler.SendEntityActionAsync(Protocol.EntityActionType.StopSneaking).Result; if (result) sneaking = false; if (result) @@ -52,7 +52,7 @@ private int DoSneak(CmdResult r) } else { - var result = handler.SendEntityAction(Protocol.EntityActionType.StartSneaking); + var result = handler.SendEntityActionAsync(Protocol.EntityActionType.StartSneaking).Result; if (result) sneaking = true; if (result) diff --git a/MinecraftClient/Commands/Upgrade.cs b/MinecraftClient/Commands/Upgrade.cs index 35ae378353..cc5fd88a69 100644 --- a/MinecraftClient/Commands/Upgrade.cs +++ b/MinecraftClient/Commands/Upgrade.cs @@ -1,4 +1,5 @@ -using Brigadier.NET; +using System.Threading.Tasks; +using Brigadier.NET; using Brigadier.NET.Builder; using MinecraftClient.CommandHandler; @@ -71,7 +72,7 @@ private static int CancelDownloadUpdate(CmdResult r) private static int CheckUpdate(CmdResult r) { - UpgradeHelper.CheckUpdate(forceUpdate: true); + Task.Run(async () => { await UpgradeHelper.CheckUpdate(forceUpdate: true); }); return r.SetAndReturn(CmdResult.Status.Done, Translations.mcc_update_start); } } diff --git a/MinecraftClient/Commands/UseItem.cs b/MinecraftClient/Commands/UseItem.cs index 4a0fe1f618..5c15b13140 100644 --- a/MinecraftClient/Commands/UseItem.cs +++ b/MinecraftClient/Commands/UseItem.cs @@ -43,7 +43,7 @@ private int DoUseItem(CmdResult r) if (!handler.GetInventoryEnabled()) return r.SetAndReturn(Status.FailNeedInventory); - handler.UseItemOnHand(); + handler.UseItemOnHandAsync().Wait(); return r.SetAndReturn(Status.Done, Translations.cmd_useitem_use); } } diff --git a/MinecraftClient/Commands/Useblock.cs b/MinecraftClient/Commands/Useblock.cs index 994e34ae42..14b60cea84 100644 --- a/MinecraftClient/Commands/Useblock.cs +++ b/MinecraftClient/Commands/Useblock.cs @@ -48,7 +48,7 @@ private int UseBlockAtLocation(CmdResult r, Location block) Location current = handler.GetCurrentLocation(); block = block.ToAbsolute(current).ToFloor(); Location blockCenter = block.ToCenter(); - bool res = handler.PlaceBlock(block, Direction.Down); + bool res = handler.PlaceBlockAsync(block, Direction.Down).Result; return r.SetAndReturn(string.Format(Translations.cmd_useblock_use, blockCenter.X, blockCenter.Y, blockCenter.Z, res ? "succeeded" : "failed"), res); } } diff --git a/MinecraftClient/ConsoleIO.cs b/MinecraftClient/ConsoleIO.cs index f5262f73b5..8eff12ec66 100644 --- a/MinecraftClient/ConsoleIO.cs +++ b/MinecraftClient/ConsoleIO.cs @@ -61,6 +61,10 @@ public static void SetAutoCompleteEngine(IAutoComplete engine) /// public static string LogPrefix = "§8[Log] "; + private static bool SuppressOutput = false; + + private static List> MessageBuffer = new(); + /// /// Read a password from the standard input /// @@ -101,15 +105,42 @@ public static void DebugReadInput() } } + public static void SuppressPrinting(bool enable) + { + SuppressOutput = enable; + if (!enable) + { + lock (MessageBuffer) + { + foreach ((bool format, string message) in MessageBuffer) + { + if (format) + WriteLineFormatted(message, true, false); + else + WriteLine(message); + } + MessageBuffer.Clear(); + } + } + } + /// /// Write a string to the standard output with a trailing newline /// - public static void WriteLine(string line) + public static void WriteLine(string line, bool ignoreSuppress = false) { - if (BasicIO) - Console.WriteLine(line); + if (!ignoreSuppress && SuppressOutput) + { + lock(MessageBuffer) + MessageBuffer.Add(new(true, line)); + } else - ConsoleInteractive.ConsoleWriter.WriteLine(line); + { + if (BasicIO) + Console.WriteLine(line); + else + ConsoleInteractive.ConsoleWriter.WriteLine(line); + } } /// @@ -123,37 +154,42 @@ public static void WriteLine(string line) /// If true, "hh-mm-ss" timestamp will be prepended. /// If unspecified, value is retrieved from EnableTimestamps. /// - public static void WriteLineFormatted(string str, bool acceptnewlines = false, bool? displayTimestamp = null) + public static void WriteLineFormatted(string str, bool acceptnewlines = false, bool? displayTimestamp = null, bool ignoreSuppress = false) { - StringBuilder output = new(); - - if (!String.IsNullOrEmpty(str)) + if (!string.IsNullOrEmpty(str)) { + StringBuilder output = new(); displayTimestamp ??= EnableTimestamps; if (displayTimestamp.Value) { int hour = DateTime.Now.Hour, minute = DateTime.Now.Minute, second = DateTime.Now.Second; output.Append(String.Format("{0}:{1}:{2} ", hour.ToString("00"), minute.ToString("00"), second.ToString("00"))); } + if (!acceptnewlines) - { str = str.Replace('\n', ' '); + + if (!ignoreSuppress && SuppressOutput) + { + lock (MessageBuffer) + MessageBuffer.Add(new(true, output.ToString())); } - if (BasicIO) + else { - if (BasicIO_NoColor) + if (BasicIO) { - output.Append(ChatBot.GetVerbatim(str)); + if (BasicIO_NoColor) + output.Append(ChatBot.GetVerbatim(str)); + else + output.Append(str); + Console.WriteLine(output.ToString()); } else { output.Append(str); + ConsoleInteractive.ConsoleWriter.WriteLineFormatted(output.ToString()); } - Console.WriteLine(output.ToString()); - return; } - output.Append(str); - ConsoleInteractive.ConsoleWriter.WriteLineFormatted(output.ToString()); } } @@ -178,6 +214,17 @@ private static void ClearLineAndBuffer() { if (BasicIO) return; ConsoleInteractive.ConsoleReader.ClearBuffer(); + + _cancellationTokenSource?.Cancel(); + + AutoCompleteDone = false; + AutoCompleteResult = Array.Empty(); + + Commands.Clear(); + CommandsFromAutoComplete = Array.Empty(); + CommandsFromDeclareCommands = Array.Empty(); + + ConsoleInteractive.ConsoleSuggestion.ClearSuggestions(); } @@ -220,9 +267,7 @@ private static void MccAutocompleteHandler(ConsoleInteractive.ConsoleReader.Buff string command = fullCommand[offset..]; if (command.Length == 0) { - List sugList = new(); - - sugList.Add(new("/")); + List sugList = new() { new("/") }; var childs = McClient.dispatcher.GetRoot().Children; if (childs != null) @@ -336,9 +381,9 @@ public static void OnDeclareMinecraftCommand(string[] rootCommands) MergeCommands(); } - public static void InitCommandList(CommandDispatcher dispatcher) + public static async Task InitCommandList(CommandDispatcher dispatcher) { - autocomplete_engine!.AutoComplete("/"); + await autocomplete_engine!.AutoComplete("/"); } } @@ -353,6 +398,6 @@ public interface IAutoComplete /// /// Text behind the cursor, e.g. "my input comm" /// List of auto-complete words, e.g. ["command", "comment"] - int AutoComplete(string BehindCursor); + Task AutoComplete(string BehindCursor); } } diff --git a/MinecraftClient/Crypto/AesCfb8Stream.cs b/MinecraftClient/Crypto/AesCfb8Stream.cs deleted file mode 100644 index d50aaa5432..0000000000 --- a/MinecraftClient/Crypto/AesCfb8Stream.cs +++ /dev/null @@ -1,201 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.IO; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; -using System.Threading.Tasks; - -namespace MinecraftClient.Crypto -{ - public class AesCfb8Stream : Stream - { - public const int blockSize = 16; - - private readonly Aes? Aes = null; - private readonly FastAes? FastAes = null; - - private bool inStreamEnded = false; - - private readonly byte[] ReadStreamIV = new byte[16]; - private readonly byte[] WriteStreamIV = new byte[16]; - - public Stream BaseStream { get; set; } - - public AesCfb8Stream(Stream stream, byte[] key) - { - BaseStream = stream; - - if (FastAes.IsSupported()) - FastAes = new FastAes(key); - else - { - Aes = Aes.Create(); - Aes.BlockSize = 128; - Aes.KeySize = 128; - Aes.Key = key; - Aes.Mode = CipherMode.ECB; - Aes.Padding = PaddingMode.None; - } - - Array.Copy(key, ReadStreamIV, 16); - Array.Copy(key, WriteStreamIV, 16); - } - - public override bool CanRead - { - get { return true; } - } - - public override bool CanSeek - { - get { return false; } - } - - public override bool CanWrite - { - get { return true; } - } - - public override void Flush() - { - BaseStream.Flush(); - } - - public override long Length - { - get { throw new NotSupportedException(); } - } - - public override long Position - { - get - { - throw new NotSupportedException(); - } - set - { - throw new NotSupportedException(); - } - } - - public override int ReadByte() - { - if (inStreamEnded) - return -1; - - int inputBuf = BaseStream.ReadByte(); - if (inputBuf == -1) - { - inStreamEnded = true; - return -1; - } - - Span blockOutput = stackalloc byte[blockSize]; - if (FastAes != null) - FastAes.EncryptEcb(ReadStreamIV, blockOutput); - else - Aes!.EncryptEcb(ReadStreamIV, blockOutput, PaddingMode.None); - - // Shift left - Array.Copy(ReadStreamIV, 1, ReadStreamIV, 0, blockSize - 1); - ReadStreamIV[blockSize - 1] = (byte)inputBuf; - - return (byte)(blockOutput[0] ^ inputBuf); - } - - [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public override int Read(byte[] buffer, int outOffset, int required) - { - if (inStreamEnded) - return 0; - - Span blockOutput = stackalloc byte[blockSize]; - - byte[] inputBuf = new byte[blockSize + required]; - Array.Copy(ReadStreamIV, inputBuf, blockSize); - - for (int readed = 0, curRead; readed < required; readed += curRead) - { - curRead = BaseStream.Read(inputBuf, blockSize + readed, required - readed); - if (curRead == 0) - { - inStreamEnded = true; - return readed; - } - - int processEnd = readed + curRead; - if (FastAes != null) - { - for (int idx = readed; idx < processEnd; idx++) - { - ReadOnlySpan blockInput = new(inputBuf, idx, blockSize); - FastAes.EncryptEcb(blockInput, blockOutput); - buffer[outOffset + idx] = (byte)(blockOutput[0] ^ inputBuf[idx + blockSize]); - } - } - else - { - for (int idx = readed; idx < processEnd; idx++) - { - ReadOnlySpan blockInput = new(inputBuf, idx, blockSize); - Aes!.EncryptEcb(blockInput, blockOutput, PaddingMode.None); - buffer[outOffset + idx] = (byte)(blockOutput[0] ^ inputBuf[idx + blockSize]); - } - } - } - - Array.Copy(inputBuf, required, ReadStreamIV, 0, blockSize); - - return required; - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void WriteByte(byte b) - { - Span blockOutput = stackalloc byte[blockSize]; - - if (FastAes != null) - FastAes.EncryptEcb(WriteStreamIV, blockOutput); - else - Aes!.EncryptEcb(WriteStreamIV, blockOutput, PaddingMode.None); - - byte outputBuf = (byte)(blockOutput[0] ^ b); - - BaseStream.WriteByte(outputBuf); - - // Shift left - Array.Copy(WriteStreamIV, 1, WriteStreamIV, 0, blockSize - 1); - WriteStreamIV[blockSize - 1] = outputBuf; - } - - [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public override void Write(byte[] input, int offset, int required) - { - byte[] outputBuf = new byte[blockSize + required]; - Array.Copy(WriteStreamIV, outputBuf, blockSize); - - Span blockOutput = stackalloc byte[blockSize]; - for (int wirtten = 0; wirtten < required; ++wirtten) - { - ReadOnlySpan blockInput = new(outputBuf, wirtten, blockSize); - if (FastAes != null) - FastAes.EncryptEcb(blockInput, blockOutput); - else - Aes!.EncryptEcb(blockInput, blockOutput, PaddingMode.None); - outputBuf[blockSize + wirtten] = (byte)(blockOutput[0] ^ input[offset + wirtten]); - } - BaseStream.WriteAsync(outputBuf, blockSize, required); - - Array.Copy(outputBuf, required, WriteStreamIV, 0, blockSize); - } - } -} diff --git a/MinecraftClient/Crypto/AesHandler/BasicAes.cs b/MinecraftClient/Crypto/AesHandler/BasicAes.cs new file mode 100644 index 0000000000..6abf26f97b --- /dev/null +++ b/MinecraftClient/Crypto/AesHandler/BasicAes.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace MinecraftClient.Crypto.AesHandler +{ + public class BasicAes : IAesHandler + { + private readonly Aes Aes; + + public BasicAes(byte[] key) + { + Aes = Aes.Create(); + Aes.BlockSize = 128; + Aes.KeySize = 128; + Aes.Key = key; + Aes.Mode = CipherMode.ECB; + Aes.Padding = PaddingMode.None; + } + + public override void EncryptEcb(Span plaintext, Span destination) + { + Aes.EncryptEcb(plaintext, destination, PaddingMode.None); + } + } +} diff --git a/MinecraftClient/Crypto/AesHandler/FasterAesArm.cs b/MinecraftClient/Crypto/AesHandler/FasterAesArm.cs new file mode 100644 index 0000000000..61a6c13972 --- /dev/null +++ b/MinecraftClient/Crypto/AesHandler/FasterAesArm.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Text; +using System.Threading.Tasks; + +namespace MinecraftClient.Crypto.AesHandler +{ + // https://github.com/Metalnem/aes-armv8 + public class FasterAesArm : IAesHandler + { + private const int BlockSize = 16; + private const int Rounds = 10; + + private readonly byte[] enc; + + public FasterAesArm(Span key) + { + enc = new byte[(Rounds + 1) * BlockSize]; + + int[] intKey = GenerateKeyExpansion(key); + for (int i = 0; i < intKey.Length; ++i) + { + enc[i * 4 + 0] = (byte)((intKey[i] >> 0) & 0xff); + enc[i * 4 + 1] = (byte)((intKey[i] >> 8) & 0xff); + enc[i * 4 + 2] = (byte)((intKey[i] >> 16) & 0xff); + enc[i * 4 + 3] = (byte)((intKey[i] >> 24) & 0xff); + } + } + + /// + /// Detects if the required instruction set is supported + /// + /// Is it supported + public static bool IsSupported() + { + return Aes.IsSupported && AdvSimd.IsSupported; + } + + public override void EncryptEcb(Span plaintext, Span destination) + { + int position = 0; + int left = plaintext.Length; + + var key0 = Unsafe.ReadUnaligned>(ref enc[0 * BlockSize]); + var key1 = Unsafe.ReadUnaligned>(ref enc[1 * BlockSize]); + var key2 = Unsafe.ReadUnaligned>(ref enc[2 * BlockSize]); + var key3 = Unsafe.ReadUnaligned>(ref enc[3 * BlockSize]); + var key4 = Unsafe.ReadUnaligned>(ref enc[4 * BlockSize]); + var key5 = Unsafe.ReadUnaligned>(ref enc[5 * BlockSize]); + var key6 = Unsafe.ReadUnaligned>(ref enc[6 * BlockSize]); + var key7 = Unsafe.ReadUnaligned>(ref enc[7 * BlockSize]); + var key8 = Unsafe.ReadUnaligned>(ref enc[8 * BlockSize]); + var key9 = Unsafe.ReadUnaligned>(ref enc[9 * BlockSize]); + var key10 = Unsafe.ReadUnaligned>(ref enc[10 * BlockSize]); + + while (left >= BlockSize) + { + var block = Unsafe.ReadUnaligned>(ref plaintext[position]); + + block = Aes.Encrypt(block, key0); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key1); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key2); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key3); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key4); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key5); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key6); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key7); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key8); + block = Aes.MixColumns(block); + + block = Aes.Encrypt(block, key9); + block = AdvSimd.Xor(block, key10); + + Unsafe.WriteUnaligned(ref destination[position], block); + + position += BlockSize; + left -= BlockSize; + } + } + + private int[] GenerateKeyExpansion(Span rgbKey) + { + var m_encryptKeyExpansion = new int[4 * (Rounds + 1)]; + + int index = 0; + for (int i = 0; i < 4; ++i) + { + int i0 = rgbKey[index++]; + int i1 = rgbKey[index++]; + int i2 = rgbKey[index++]; + int i3 = rgbKey[index++]; + m_encryptKeyExpansion[i] = i3 << 24 | i2 << 16 | i1 << 8 | i0; + } + + for (int i = 4; i < 4 * (Rounds + 1); ++i) + { + int iTemp = m_encryptKeyExpansion[i - 1]; + + if (i % 4 == 0) + { + iTemp = SubWord(Rot3(iTemp)); + iTemp ^= s_Rcon[(i / 4) - 1]; + } + + m_encryptKeyExpansion[i] = m_encryptKeyExpansion[i - 4] ^ iTemp; + } + + return m_encryptKeyExpansion; + } + + private static int SubWord(int a) + { + return s_Sbox[a & 0xFF] | + s_Sbox[a >> 8 & 0xFF] << 8 | + s_Sbox[a >> 16 & 0xFF] << 16 | + s_Sbox[a >> 24 & 0xFF] << 24; + } + + private static int Rot3(int val) + { + return (val << 24 & unchecked((int)0xFF000000)) | (val >> 8 & unchecked((int)0x00FFFFFF)); + } + + private static readonly byte[] s_Sbox = new byte[] { + 99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171, 118, + 202, 130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, 164, 114, 192, + 183, 253, 147, 38, 54, 63, 247, 204, 52, 165, 229, 241, 113, 216, 49, 21, + 4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, 235, 39, 178, 117, + 9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, 41, 227, 47, 132, + 83, 209, 0, 237, 32, 252, 177, 91, 106, 203, 190, 57, 74, 76, 88, 207, + 208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, + 81, 163, 64, 143, 146, 157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, + 205, 12, 19, 236, 95, 151, 68, 23, 196, 167, 126, 61, 100, 93, 25, 115, + 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94, 11, 219, + 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, + 231, 200, 55, 109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, + 186, 120, 37, 46, 28, 166, 180, 198, 232, 221, 116, 31, 75, 189, 139, 138, + 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158, + 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223, + 140, 161, 137, 13, 191, 230, 66, 104, 65, 153, 45, 15, 176, 84, 187, 22 }; + + private static readonly int[] s_Rcon = new int[] { + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, + 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, + 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 }; + } +} diff --git a/MinecraftClient/Crypto/FastAes.cs b/MinecraftClient/Crypto/AesHandler/FasterAesX86.cs similarity index 89% rename from MinecraftClient/Crypto/FastAes.cs rename to MinecraftClient/Crypto/AesHandler/FasterAesX86.cs index 41de7d54b3..1151d3d975 100644 --- a/MinecraftClient/Crypto/FastAes.cs +++ b/MinecraftClient/Crypto/AesHandler/FasterAesX86.cs @@ -4,15 +4,15 @@ using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; -namespace MinecraftClient.Crypto +namespace MinecraftClient.Crypto.AesHandler { // Using the AES-NI instruction set // https://gist.github.com/Thealexbarney/9f75883786a9f3100408ff795fb95d85 - public class FastAes + public class FasterAesX86 : IAesHandler { private Vector128[] RoundKeys { get; } - public FastAes(Span key) + public FasterAesX86(Span key) { RoundKeys = KeyExpansion(key); } @@ -23,11 +23,10 @@ public FastAes(Span key) /// Is it supported public static bool IsSupported() { - return Sse2.IsSupported && Aes.IsSupported; + return Aes.IsSupported && Sse2.IsSupported; } - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public void EncryptEcb(ReadOnlySpan plaintext, Span destination) + public override void EncryptEcb(Span plaintext, Span destination) { Vector128[] keys = RoundKeys; diff --git a/MinecraftClient/Crypto/IAesHandler.cs b/MinecraftClient/Crypto/IAesHandler.cs new file mode 100644 index 0000000000..1b73f643fd --- /dev/null +++ b/MinecraftClient/Crypto/IAesHandler.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MinecraftClient.Crypto +{ + public abstract class IAesHandler + { + public abstract void EncryptEcb(Span plaintext, Span destination); + } +} diff --git a/MinecraftClient/EntityHandler/Effect.cs b/MinecraftClient/EntityHandler/Effect.cs new file mode 100644 index 0000000000..b614810276 --- /dev/null +++ b/MinecraftClient/EntityHandler/Effect.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MinecraftClient.EntityHandler +{ + public record Effect + { + public EffectType Type { init; get; } + + public int EffectLevel { init; get; } + + public ulong StartTick { init; get; } + + public int DurationInTick { init; get; } + + public bool IsFromBeacon { init; get; } + + public bool ShowParticles { init; get; } + + public bool ShowIcon { init; get; } + + public Dictionary? FactorData { init; get; } = null; + + /* Factor Data + Name Type + padding_duration TAG_INT + factor_start TAG_FLOAT + factor_target TAG_FLOAT + factor_current TAG_FLOAT + effect_changed_timestamp TAG_INT + factor_previous_frame TAG_FLOAT + had_effect_last_tick TAG_BOOLEAN + */ + } +} diff --git a/MinecraftClient/Inventory/Effects.cs b/MinecraftClient/EntityHandler/EffectType.cs similarity index 92% rename from MinecraftClient/Inventory/Effects.cs rename to MinecraftClient/EntityHandler/EffectType.cs index 7b5df3bd25..a58ff74932 100644 --- a/MinecraftClient/Inventory/Effects.cs +++ b/MinecraftClient/EntityHandler/EffectType.cs @@ -1,9 +1,9 @@ -namespace MinecraftClient.Inventory +namespace MinecraftClient.EntityHandler { /// /// Represents a Minecraft effects /// - public enum Effects + public enum EffectType { Speed = 1, Slowness = 2, diff --git a/MinecraftClient/Mapping/Entity.cs b/MinecraftClient/EntityHandler/Entity.cs similarity index 98% rename from MinecraftClient/Mapping/Entity.cs rename to MinecraftClient/EntityHandler/Entity.cs index ad9fbf7b21..9ac9b6043f 100644 --- a/MinecraftClient/Mapping/Entity.cs +++ b/MinecraftClient/EntityHandler/Entity.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; using MinecraftClient.Inventory; +using MinecraftClient.Mapping; using MinecraftClient.Protocol.Message; -namespace MinecraftClient.Mapping +namespace MinecraftClient.EntityHandler { /// /// Represents an entity evolving into a Minecraft world diff --git a/MinecraftClient/Mapping/EntityPose.cs b/MinecraftClient/EntityHandler/EntityPose.cs similarity index 82% rename from MinecraftClient/Mapping/EntityPose.cs rename to MinecraftClient/EntityHandler/EntityPose.cs index 1690b089fe..f0666e14ec 100644 --- a/MinecraftClient/Mapping/EntityPose.cs +++ b/MinecraftClient/EntityHandler/EntityPose.cs @@ -1,4 +1,4 @@ -namespace MinecraftClient.Mapping +namespace MinecraftClient.EntityHandler { public enum EntityPose { diff --git a/MinecraftClient/Mapping/EntityType.cs b/MinecraftClient/EntityHandler/EntityType.cs similarity index 98% rename from MinecraftClient/Mapping/EntityType.cs rename to MinecraftClient/EntityHandler/EntityType.cs index ddb8a6b289..8a8d83edc4 100644 --- a/MinecraftClient/Mapping/EntityType.cs +++ b/MinecraftClient/EntityHandler/EntityType.cs @@ -1,4 +1,4 @@ -namespace MinecraftClient.Mapping +namespace MinecraftClient.EntityHandler { /// /// Represents Minecraft Entity Types diff --git a/MinecraftClient/Mapping/EntityTypeExtensions.cs b/MinecraftClient/EntityHandler/EntityTypeExtensions.cs similarity index 99% rename from MinecraftClient/Mapping/EntityTypeExtensions.cs rename to MinecraftClient/EntityHandler/EntityTypeExtensions.cs index e6546d2237..9476b4e32b 100644 --- a/MinecraftClient/Mapping/EntityTypeExtensions.cs +++ b/MinecraftClient/EntityHandler/EntityTypeExtensions.cs @@ -1,4 +1,4 @@ -namespace MinecraftClient.Mapping +namespace MinecraftClient.EntityHandler { public static class EntityTypeExtensions { diff --git a/MinecraftClient/Mapping/InteractType.cs b/MinecraftClient/EntityHandler/InteractType.cs similarity index 72% rename from MinecraftClient/Mapping/InteractType.cs rename to MinecraftClient/EntityHandler/InteractType.cs index f54a1014e5..f772643367 100644 --- a/MinecraftClient/Mapping/InteractType.cs +++ b/MinecraftClient/EntityHandler/InteractType.cs @@ -1,4 +1,4 @@ -namespace MinecraftClient.Mapping +namespace MinecraftClient.EntityHandler { public enum InteractType { diff --git a/MinecraftClient/Inventory/ItemMovingHelper.cs b/MinecraftClient/Inventory/ItemMovingHelper.cs index da9a009769..0d71ad9a96 100644 --- a/MinecraftClient/Inventory/ItemMovingHelper.cs +++ b/MinecraftClient/Inventory/ItemMovingHelper.cs @@ -39,8 +39,8 @@ public bool MoveTo(int source, int dest, Container? destContainer = null) if (ValidateSlots(source, dest, destContainer) && HasItem(source) && ((destContainer != null && !HasItem(dest, destContainer)) || (destContainer == null && !HasItem(dest)))) - return mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick) - && mc.DoWindowAction(destContainer == null ? c.ID : destContainer.ID, dest, WindowActionType.LeftClick); + return mc.DoWindowActionAsync(c.ID, source, WindowActionType.LeftClick).Result + && mc.DoWindowActionAsync(destContainer == null ? c.ID : destContainer.ID, dest, WindowActionType.LeftClick).Result; else return false; } @@ -57,9 +57,9 @@ public bool Swap(int slot1, int slot2, Container? destContainer = null) if (ValidateSlots(slot1, slot2, destContainer) && HasItem(slot1) && (destContainer != null && HasItem(slot2, destContainer) || (destContainer == null && HasItem(slot2)))) - return mc.DoWindowAction(c.ID, slot1, WindowActionType.LeftClick) - && mc.DoWindowAction(destContainer == null ? c.ID : destContainer.ID, slot2, WindowActionType.LeftClick) - && mc.DoWindowAction(c.ID, slot1, WindowActionType.LeftClick); + return mc.DoWindowActionAsync(c.ID, slot1, WindowActionType.LeftClick).Result + && mc.DoWindowActionAsync(destContainer == null ? c.ID : destContainer.ID, slot2, WindowActionType.LeftClick).Result + && mc.DoWindowActionAsync(c.ID, slot1, WindowActionType.LeftClick).Result; else return false; } @@ -104,14 +104,14 @@ public bool DragOverSlots(int source, IEnumerable slots, WindowActionType m break; } } - mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick); // grab item - mc.DoWindowAction(c.ID, -999, startDragging); + mc.DoWindowActionAsync(c.ID, source, WindowActionType.LeftClick).Wait(); // grab item + mc.DoWindowActionAsync(c.ID, -999, startDragging).Wait(); foreach (var slot in availableSlots) { - mc.DoWindowAction(c.ID, slot, addDragging); + mc.DoWindowActionAsync(c.ID, slot, addDragging).Wait(); } - mc.DoWindowAction(c.ID, -999, endDragging); - mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick); // put down item left (if any) + mc.DoWindowActionAsync(c.ID, -999, endDragging).Wait(); + mc.DoWindowActionAsync(c.ID, source, WindowActionType.LeftClick).Wait(); // put down item left (if any) return true; } else return false; diff --git a/MinecraftClient/Logger/LoggerBase.cs b/MinecraftClient/Logger/LoggerBase.cs index 8569bfc954..2a655db44a 100644 --- a/MinecraftClient/Logger/LoggerBase.cs +++ b/MinecraftClient/Logger/LoggerBase.cs @@ -78,17 +78,17 @@ public void Warn(object msg) protected virtual void Log(object msg) { - ConsoleIO.WriteLineFormatted(msg.ToString() ?? string.Empty); + ConsoleIO.WriteLineFormatted(msg.ToString() ?? string.Empty, true); } protected virtual void Log(string msg) { - ConsoleIO.WriteLineFormatted(msg); + ConsoleIO.WriteLineFormatted(msg, true); } protected virtual void Log(string msg, params object[] args) { - ConsoleIO.WriteLineFormatted(string.Format(msg, args)); + ConsoleIO.WriteLineFormatted(string.Format(msg, args), true); } } } diff --git a/MinecraftClient/Mapping/EntityPalettes/EntityPalette.cs b/MinecraftClient/Mapping/EntityPalettes/EntityPalette.cs index d1e30c1627..b08d4bd20a 100644 --- a/MinecraftClient/Mapping/EntityPalettes/EntityPalette.cs +++ b/MinecraftClient/Mapping/EntityPalettes/EntityPalette.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using MinecraftClient.EntityHandler; namespace MinecraftClient.Mapping.EntityPalettes { diff --git a/MinecraftClient/Mapping/EntityPalettes/EntityPalette112.cs b/MinecraftClient/Mapping/EntityPalettes/EntityPalette112.cs index 15d0dcd403..185e0d5540 100644 --- a/MinecraftClient/Mapping/EntityPalettes/EntityPalette112.cs +++ b/MinecraftClient/Mapping/EntityPalettes/EntityPalette112.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using MinecraftClient.EntityHandler; namespace MinecraftClient.Mapping.EntityPalettes { diff --git a/MinecraftClient/Mapping/EntityPalettes/EntityPalette113.cs b/MinecraftClient/Mapping/EntityPalettes/EntityPalette113.cs index d78a724062..37bfae10b4 100644 --- a/MinecraftClient/Mapping/EntityPalettes/EntityPalette113.cs +++ b/MinecraftClient/Mapping/EntityPalettes/EntityPalette113.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using MinecraftClient.EntityHandler; namespace MinecraftClient.Mapping.EntityPalettes { diff --git a/MinecraftClient/Mapping/EntityPalettes/EntityPalette114.cs b/MinecraftClient/Mapping/EntityPalettes/EntityPalette114.cs index 403fcc0080..dec2cc9f03 100644 --- a/MinecraftClient/Mapping/EntityPalettes/EntityPalette114.cs +++ b/MinecraftClient/Mapping/EntityPalettes/EntityPalette114.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using MinecraftClient.EntityHandler; namespace MinecraftClient.Mapping.EntityPalettes { diff --git a/MinecraftClient/Mapping/EntityPalettes/EntityPalette115.cs b/MinecraftClient/Mapping/EntityPalettes/EntityPalette115.cs index bd4887bef4..7467eee777 100644 --- a/MinecraftClient/Mapping/EntityPalettes/EntityPalette115.cs +++ b/MinecraftClient/Mapping/EntityPalettes/EntityPalette115.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using MinecraftClient.EntityHandler; namespace MinecraftClient.Mapping.EntityPalettes { diff --git a/MinecraftClient/Mapping/EntityPalettes/EntityPalette1161.cs b/MinecraftClient/Mapping/EntityPalettes/EntityPalette1161.cs index 4de8bdca56..1ef5f4ab1f 100644 --- a/MinecraftClient/Mapping/EntityPalettes/EntityPalette1161.cs +++ b/MinecraftClient/Mapping/EntityPalettes/EntityPalette1161.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using MinecraftClient.EntityHandler; namespace MinecraftClient.Mapping.EntityPalettes { diff --git a/MinecraftClient/Mapping/EntityPalettes/EntityPalette1162.cs b/MinecraftClient/Mapping/EntityPalettes/EntityPalette1162.cs index 606e2d9410..7ee9036ce9 100644 --- a/MinecraftClient/Mapping/EntityPalettes/EntityPalette1162.cs +++ b/MinecraftClient/Mapping/EntityPalettes/EntityPalette1162.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using MinecraftClient.EntityHandler; namespace MinecraftClient.Mapping.EntityPalettes { diff --git a/MinecraftClient/Mapping/EntityPalettes/EntityPalette117.cs b/MinecraftClient/Mapping/EntityPalettes/EntityPalette117.cs index 1658aa33ba..2d9f9d97a1 100644 --- a/MinecraftClient/Mapping/EntityPalettes/EntityPalette117.cs +++ b/MinecraftClient/Mapping/EntityPalettes/EntityPalette117.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using MinecraftClient.EntityHandler; namespace MinecraftClient.Mapping.EntityPalettes { diff --git a/MinecraftClient/Mapping/EntityPalettes/EntityPalette119.cs b/MinecraftClient/Mapping/EntityPalettes/EntityPalette119.cs index f4e352d5f6..4e11e54030 100644 --- a/MinecraftClient/Mapping/EntityPalettes/EntityPalette119.cs +++ b/MinecraftClient/Mapping/EntityPalettes/EntityPalette119.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using MinecraftClient.EntityHandler; namespace MinecraftClient.Mapping.EntityPalettes { diff --git a/MinecraftClient/Mapping/MapData.cs b/MinecraftClient/Mapping/MapData.cs new file mode 100644 index 0000000000..edcec1b243 --- /dev/null +++ b/MinecraftClient/Mapping/MapData.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MinecraftClient.Mapping +{ + public record MapData + { + public int MapId { init; get; } + + public byte Scale { init; get; } + + public bool TrackingPosition { init; get; } + + public bool Locked { init; get; } + + public List Icons { init; get; } = new(); + + public byte ColumnsUpdated { init; get; } + + public byte RowsUpdated { init; get; } + + public byte MapCoulmnX { init; get; } + + public byte MapRowZ { init; get; } + + public byte[]? Colors { init; get; } + } +} diff --git a/MinecraftClient/Mapping/Movement.cs b/MinecraftClient/Mapping/Movement.cs index 2f74cfa60d..ba50a89e70 100644 --- a/MinecraftClient/Mapping/Movement.cs +++ b/MinecraftClient/Mapping/Movement.cs @@ -140,17 +140,10 @@ public static Queue Move2Steps(Location start, Location goal, ref doub /// How long to wait before stopping computation /// When location is unreachable, computation will reach timeout, then optionally fallback to a close location within maxOffset /// A list of locations, or null if calculation failed - public static Queue? CalculatePath(World world, Location start, Location goal, bool allowUnsafe, int maxOffset, int minOffset, TimeSpan timeout) + public static async Task?> CalculatePath(World world, Location start, Location goal, bool allowUnsafe, int maxOffset, int minOffset, TimeSpan timeout) { - CancellationTokenSource cts = new(); - Task?> pathfindingTask = Task.Factory.StartNew(() => Movement.CalculatePath(world, start, goal, allowUnsafe, maxOffset, minOffset, cts.Token)); - pathfindingTask.Wait(timeout); - if (!pathfindingTask.IsCompleted) - { - cts.Cancel(); - pathfindingTask.Wait(); - } - return pathfindingTask.Result; + CancellationTokenSource cts = new(timeout); + return await Task.Run(() => { return CalculatePath(world, start, goal, allowUnsafe, maxOffset, minOffset, cts.Token); }, cts.Token); } /// diff --git a/MinecraftClient/Mapping/RaycastHelper.cs b/MinecraftClient/Mapping/RaycastHelper.cs index 483b0b86b0..a2e98d9fd4 100644 --- a/MinecraftClient/Mapping/RaycastHelper.cs +++ b/MinecraftClient/Mapping/RaycastHelper.cs @@ -1,4 +1,5 @@ using System; +using MinecraftClient.EntityHandler; namespace MinecraftClient.Mapping { diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 80defaf697..844991e72f 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Net.Sockets; using System.Text; using System.Threading; +using System.Threading.Tasks; using Brigadier.NET; using Brigadier.NET.Exceptions; using MinecraftClient.ChatBots; @@ -11,6 +14,7 @@ using MinecraftClient.CommandHandler.Patch; using MinecraftClient.Inventory; using MinecraftClient.Logger; +using MinecraftClient.EntityHandler; using MinecraftClient.Mapping; using MinecraftClient.Protocol; using MinecraftClient.Protocol.Handlers.Forge; @@ -31,22 +35,17 @@ public class McClient : IMinecraftComHandler public static int ReconnectionAttemptsLeft = 0; public static CommandDispatcher dispatcher = new(); - private readonly Dictionary onlinePlayers = new(); - - private static bool commandsLoaded = false; - private readonly Queue chatQueue = new(); - private static DateTime nextMessageSendTime = DateTime.MinValue; + private readonly Dictionary onlinePlayers = new(); - private readonly Queue threadTasks = new(); - private readonly object threadTasksLock = new(); + private readonly ConcurrentQueue chatQueue = new(); + private DateTime nextMessageSendTime = DateTime.MinValue; - private readonly List bots = new(); - private static readonly List botsOnHold = new(); - private static readonly Dictionary inventories = new(); + private readonly object inventoryLock = new(); + private readonly Dictionary inventories = new(); - private readonly Dictionary> registeredBotPluginChannels = new(); private readonly List registeredServerPluginChannels = new(); + private readonly Dictionary> registeredBotPluginChannels = new(); private bool terrainAndMovementsEnabled; private bool terrainAndMovementsRequested = false; @@ -54,7 +53,7 @@ public class McClient : IMinecraftComHandler private bool inventoryHandlingRequested = false; private bool entityHandlingEnabled; - private readonly object locationLock = new(); + private static SemaphoreSlim locationLock = new(1, 1); private bool locationReceived = false; private readonly World world = new(); private Queue? steps; @@ -70,14 +69,13 @@ public enum MovementType { Sneak, Walk, Sprint } private readonly string host; private readonly int port; - private readonly int protocolversion; - private readonly string username; + private int protocolversion; + private string username; private Guid uuid; private string uuidStr; - private readonly string sessionid; - private readonly PlayerKeyPair? playerKeyPair; + private string sessionId; + private PlayerKeyPair? playerKeyPair; private DateTime lastKeepAlive; - private readonly object lastKeepAliveLock = new(); private int respawnTicks = 0; private int gamemode = 0; private bool isSupportPreviewsChat; @@ -104,15 +102,27 @@ public enum MovementType { Sneak, Walk, Sprint } private readonly List tpsSamples = new(maxSamples); private double sampleSum = 0; - // ChatBot OnNetworkPacket event + // ChatBot + private ChatBot[] chatbots = Array.Empty(); + private static ChatBot[] botsOnHold = Array.Empty(); + private bool OldChatBotUpdateTrigger = false; private bool networkPacketCaptureEnabled = false; + // ChatBot Async Events + private static int EventTypeCount = typeof(McClientEventType).GetFields().Length; + private static SemaphoreSlim EventCallbackWriteLock = new(1, 1); + private static Task[][] ChatbotEventTasks = new Task[EventTypeCount][]; + private static Task[] WaitChatbotExecuteTask = new Task[EventTypeCount]; + private static SemaphoreSlim[] ChatbotEventTaskLocks = new SemaphoreSlim[EventTypeCount]; + private static Func[][] ChatbotEvents = new Func[EventTypeCount][]; + private static Dictionary>>> ChatbotRegisteredEvents = new(); + public int GetServerPort() { return port; } public string GetServerHost() { return host; } public string GetUsername() { return username; } public Guid GetUserUuid() { return uuid; } public string GetUserUuidStr() { return uuidStr; } - public string GetSessionID() { return sessionid; } + public string GetSessionID() { return sessionId; } public Location GetCurrentLocation() { return location; } public float GetYaw() { return playerYaw; } public int GetSequenceId() { return sequenceId; } @@ -130,42 +140,70 @@ public enum MovementType { Sneak, Walk, Sprint } 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; } - readonly TcpClient client; - readonly IMinecraftCom handler; - CancellationTokenSource? cmdprompt = null; - Tuple? timeoutdetector = null; + private TcpClient? tcpClient; + private IMinecraftCom? handler; + private readonly CancellationTokenSource CancelTokenSource; public ILogger Log; + public static void LoadCommandsAndChatbots() + { + for (int i = 0; i < EventTypeCount; ++i) + { + ChatbotEventTaskLocks[i] = new(1, 1); + WaitChatbotExecuteTask[i] = Task.CompletedTask; + } + + /* 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 = GetChatbotsToRegister(); + foreach (ChatBot bot in botsOnHold) + bot.Initialize(); + + InitializeChatbotEventCallbacks(botsOnHold).Wait(); + } + /// /// Starts the main chat client, wich will login to the server using the MinecraftCom class. /// /// A valid session obtained with MinecraftCom.GetLogin() /// Key for message signing - /// The server IP - /// The server port to use + /// The server IP + /// The server port to use /// Minecraft protocol version to use /// ForgeInfo item stating that Forge is enabled - public McClient(SessionToken session, PlayerKeyPair? playerKeyPair, string server_ip, ushort port, int protocolversion, ForgeInfo? forgeInfo) + public McClient(string serverHost, ushort serverPort, CancellationTokenSource cancelTokenSource) { + CancelTokenSource = cancelTokenSource; + CmdResult.currentHandler = this; terrainAndMovementsEnabled = Config.Main.Advanced.TerrainAndMovements; inventoryHandlingEnabled = Config.Main.Advanced.InventoryHandling; entityHandlingEnabled = Config.Main.Advanced.EntityHandling; - sessionid = session.ID; - if (!Guid.TryParse(session.PlayerID, out uuid)) - uuid = Guid.Empty; - uuidStr = session.PlayerID; - username = session.PlayerName; - host = server_ip; - this.port = port; - this.protocolversion = protocolversion; - this.playerKeyPair = playerKeyPair; + host = serverHost; + port = serverPort; - Log = Settings.Config.Logging.LogToFile + uuid = Guid.Empty; + uuidStr = string.Empty; + username = string.Empty; + sessionId = string.Empty; + playerKeyPair = null; + protocolversion = 0; + + Log = Config.Logging.LogToFile ? new FileLogLogger(Config.AppVar.ExpandVars(Settings.Config.Logging.LogFile), Settings.Config.Logging.PrependTimestamp) : new FilteredLogger(); Log.DebugEnabled = Config.Logging.DebugMessages; @@ -174,207 +212,213 @@ public McClient(SessionToken session, PlayerKeyPair? playerKeyPair, string serve Log.WarnEnabled = Config.Logging.WarningMessages; Log.ErrorEnabled = Config.Logging.ErrorMessages; - /* Load commands from Commands namespace */ - LoadCommands(); + ClearInventories(); + + chatbots = botsOnHold; + botsOnHold = Array.Empty(); + foreach (ChatBot bot in chatbots) + bot.SetHandler(this); + } + + public async Task Login(HttpClient httpClient, SessionToken session, PlayerKeyPair? playerKeyPair, int protocolversion, ForgeInfo? forgeInfo) + { + sessionId = session.ID; + if (!Guid.TryParse(session.PlayerID, out uuid)) + uuid = Guid.Empty; + uuidStr = session.PlayerID; + username = session.PlayerName; + this.playerKeyPair = playerKeyPair; - if (botsOnHold.Count == 0) - RegisterBots(); + this.protocolversion = protocolversion; try { - client = ProxyHandler.NewTcpClient(host, port); - client.ReceiveBufferSize = 1024 * 1024; - client.ReceiveTimeout = Config.Main.Advanced.TcpTimeout * 1000; // Default: 30 seconds - handler = Protocol.ProtocolHandler.GetProtocolHandler(client, protocolversion, forgeInfo, this); + tcpClient = ProxyHandler.NewTcpClient(host, port, ProxyHandler.ClientType.Ingame); + tcpClient.ReceiveBufferSize = 1024 * 1024; + tcpClient.ReceiveTimeout = Config.Main.Advanced.TcpTimeout * 1000; // Default: 30 seconds + + handler = ProtocolHandler.GetProtocolHandler(CancelTokenSource.Token, tcpClient, protocolversion, forgeInfo, this); Log.Info(Translations.mcc_version_supported); - timeoutdetector = new(new Thread(new ParameterizedThreadStart(TimeoutDetector)), new CancellationTokenSource()); - timeoutdetector.Item1.Name = "MCC Connection timeout detector"; - timeoutdetector.Item1.Start(timeoutdetector.Item2.Token); + _ = Task.Run(TimeoutDetector, CancelTokenSource.Token); try { - if (handler.Login(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; - } - else + if (await handler.Login(httpClient, this.playerKeyPair, session)) { - Log.Error(Translations.error_login_failed); - goto Retry; + DispatchBotEvent(bot => bot.AfterGameJoined()); + await TriggerEvent(McClientEventType.GameJoin, null); + return; } + + Log.Error(Translations.error_login_failed); } catch (Exception e) { - Log.Error(e.GetType().Name + ": " + e.Message); + Log.Error($"{e.GetType().Name}: {e.Message}"); + if (e.StackTrace != null) + Log.Error(e.StackTrace); Log.Error(Translations.error_join); - goto Retry; } } catch (SocketException e) { Log.Error(e.Message); Log.Error(Translations.error_connect); - goto Retry; } - return; - - Retry: - if (timeoutdetector != null) - { - timeoutdetector.Item2.Cancel(); - timeoutdetector = null; - } if (ReconnectionAttemptsLeft > 0) { 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.HandleFailure(); + Program.SetExit(); } throw new Exception("Initialization failed."); } + 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.MessageReceived -= ConsoleReaderOnMessageReceived; + ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler; + ConsoleInteractive.ConsoleReader.StopReadThread(); + + ConsoleIO.CancelAutocomplete(); + ConsoleIO.WriteLine(string.Empty); + } + /// /// Register bots /// - private void RegisterBots(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()); } - //Add your ChatBot here by uncommenting and adapting - //BotLoad(new ChatBots.YourBot()); + private static ChatBot[] GetChatbotsToRegister(bool reload = false) + { + 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 + // chatbotList.Add(new ChatBots.YourBot()); + chatbotList.Add(new TestBot()); + + return chatbotList.ToArray(); } /// /// Retrieve messages from the queue and send. /// Note: requires external locking. /// - private void TrySendMessageToServer() + private async Task TrySendMessageToServer() { - while (chatQueue.Count > 0 && nextMessageSendTime < DateTime.Now) + if (handler != null) { - string text = chatQueue.Dequeue(); - 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); + } } } /// - /// Called ~10 times per second by the protocol handler + /// Called ~20 times per second by the protocol handler /// - public void OnUpdate() + public async Task OnUpdate() { - foreach (ChatBot bot in bots.ToArray()) + OldChatBotUpdateTrigger = !OldChatBotUpdateTrigger; + foreach (ChatBot bot in chatbots) { - try - { - bot.Update(); - bot.UpdateInternal(); - } - catch (Exception e) + await bot.OnClientTickAsync(); + if (OldChatBotUpdateTrigger) { - if (e is not ThreadAbortException) + try + { + bot.Update(); + bot.UpdateInternal(); + } + catch (Exception e) + { Log.Warn("Update: Got error from " + bot.ToString() + ": " + e.ToString()); - else - throw; //ThreadAbortException should not be caught + } } } - lock (chatQueue) - { - TrySendMessageToServer(); - } + await TrySendMessageToServer(); if (terrainAndMovementsEnabled && locationReceived) { - lock (locationLock) + for (int i = 0; i < Config.Main.Advanced.MovementSpeed / 2; i++) //Needs to run at 20 tps; MCC runs at 10 tps { - for (int i = 0; i < Config.Main.Advanced.MovementSpeed; i++) //Needs to run at 20 tps; MCC runs at 10 tps + await locationLock.WaitAsync(); + if (_yaw == null || _pitch == null) { - if (_yaw == null || _pitch == null) + if (steps != null && steps.Count > 0) { - if (steps != null && steps.Count > 0) - { - location = steps.Dequeue(); - } - else if (path != null && path.Count > 0) - { - Location next = path.Dequeue(); - steps = Movement.Move2Steps(location, next, ref motionY); + location = steps.Dequeue(); + } + else if (path != null && path.Count > 0) + { + Location next = path.Dequeue(); + steps = Movement.Move2Steps(location, next, ref motionY); - if (Config.Main.Advanced.MoveHeadWhileWalking) // Disable head movements to avoid anti-cheat triggers - UpdateLocation(location, next + new Location(0, 1, 0)); // Update yaw and pitch to look at next step - } - else - { - location = Movement.HandleGravity(world, location, ref motionY); - } + if (Config.Main.Advanced.MoveHeadWhileWalking) // Disable head movements to avoid anti-cheat triggers + UpdateLocation(location, next + new Location(0, 1, 0)); // Update yaw and pitch to look at next step + } + else + { + location = Movement.HandleGravity(world, location, ref motionY); } - playerYaw = _yaw == null ? playerYaw : _yaw.Value; - playerPitch = _pitch == null ? playerPitch : _pitch.Value; - handler.SendLocationUpdate(location, Movement.IsOnGround(world, location), _yaw, _pitch); } - // First 2 updates must be player position AND look, and player must not move (to conform with vanilla) - // Once yaw and pitch have been sent, switch back to location-only updates (without yaw and pitch) - _yaw = null; - _pitch = null; + playerYaw = _yaw == null ? playerYaw : _yaw.Value; + playerPitch = _pitch == null ? playerPitch : _pitch.Value; + locationLock.Release(); + await handler!.SendLocationUpdate(location, Movement.IsOnGround(world, location), _yaw, _pitch); } + // First 2 updates must be player position AND look, and player must not move (to conform with vanilla) + // Once yaw and pitch have been sent, switch back to location-only updates (without yaw and pitch) + _yaw = null; + _pitch = null; } if (Config.Main.Advanced.AutoRespawn && respawnTicks > 0) { - respawnTicks--; - if (respawnTicks == 0) - SendRespawnPacket(); + if (--respawnTicks == 0) + await SendRespawnPacketAsync(); } - lock (threadTasksLock) - { - while (threadTasks.Count > 0) - { - Action taskToRun = threadTasks.Dequeue(); - taskToRun(); - } - } + await TriggerEvent(McClientEventType.ClientTick, null); } #region Connection Lost and Disconnect from Server @@ -382,29 +426,27 @@ public void OnUpdate() /// /// Periodically checks for server keepalives and consider that connection has been lost if the last received keepalive is too old. /// - private void TimeoutDetector(object? o) + private async Task TimeoutDetector() { UpdateKeepAlive(); - do + using PeriodicTimer periodicTimer = new(TimeSpan.FromSeconds(Config.Main.Advanced.TcpTimeout)); + try { - Thread.Sleep(TimeSpan.FromSeconds(15)); - - if (((CancellationToken)o!).IsCancellationRequested) - return; - - lock (lastKeepAliveLock) + while (await periodicTimer.WaitForNextTickAsync(CancelTokenSource.Token) && !CancelTokenSource.IsCancellationRequested) { if (lastKeepAlive.AddSeconds(Config.Main.Advanced.TcpTimeout) < DateTime.Now) { - if (((CancellationToken)o!).IsCancellationRequested) - return; - OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, Translations.error_timeout); return; } } } - while (!((CancellationToken)o!).IsCancellationRequested); + catch (AggregateException e) + { + if (e.InnerException is not OperationCanceledException) + throw; + } + catch (OperationCanceledException) { } } /// @@ -412,10 +454,7 @@ private void TimeoutDetector(object? o) /// private void UpdateKeepAlive() { - lock (lastKeepAliveLock) - { - lastKeepAlive = DateTime.Now; - } + lastKeepAlive = DateTime.Now; } /// @@ -423,10 +462,22 @@ private void UpdateKeepAlive() /// public void Disconnect() { - DispatchBotEvent(bot => bot.OnDisconnect(ChatBot.DisconnectReason.UserLogout, "")); + for (int i = 0; i < EventTypeCount; ++i) + { + ChatbotEventTaskLocks[i].Wait(); + WaitChatbotExecuteTask[i].Wait(); + ChatbotEventTaskLocks[i].Release(); + } + + DispatchBotEvent(bot => bot.OnDisconnect(ChatBot.DisconnectReason.UserLogout, string.Empty)); + + TriggerEvent(McClientEventType.ClientDisconnect, + new Tuple(ChatBot.DisconnectReason.UserLogout, string.Empty)).Wait(); - botsOnHold.Clear(); - botsOnHold.AddRange(bots); + WaitChatbotExecuteTask[(int)McClientEventType.ClientDisconnect].Wait(); + + botsOnHold = chatbots; + chatbots = Array.Empty(); if (handler != null) { @@ -434,20 +485,7 @@ public void Disconnect() handler.Dispose(); } - if (cmdprompt != null) - { - cmdprompt.Cancel(); - cmdprompt = null; - } - - if (timeoutdetector != null) - { - timeoutdetector.Item2.Cancel(); - timeoutdetector = null; - } - - if (client != null) - client.Close(); + tcpClient?.Close(); } /// @@ -455,21 +493,6 @@ public void Disconnect() /// public void OnConnectionLost(ChatBot.DisconnectReason reason, string message) { - ConsoleIO.CancelAutocomplete(); - - handler.Dispose(); - - world.Clear(); - - if (timeoutdetector != null) - { - if (timeoutdetector != null && Thread.CurrentThread != timeoutdetector.Item1) - timeoutdetector.Item2.Cancel(); - timeoutdetector = null; - } - - bool will_restart = false; - switch (reason) { case ChatBot.DisconnectReason.ConnectionLost: @@ -491,15 +514,16 @@ public void OnConnectionLost(ChatBot.DisconnectReason reason, string message) throw new InvalidOperationException(Translations.exception_user_logout); } - //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)); + // Process AutoRelog last to make sure other bots can perform their cleanup tasks first (issue #1517) + 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) { try { - will_restart |= bot.OnDisconnect(reason, message); + restartDelay = Math.Max(restartDelay, bot.OnDisconnect(reason, message)); } catch (Exception e) { @@ -507,36 +531,165 @@ public void OnConnectionLost(ChatBot.DisconnectReason reason, string message) { Log.Warn("OnDisconnect: Got error from " + bot.ToString() + ": " + e.ToString()); } - else throw; //ThreadAbortException should not be caught + else throw; // ThreadAbortException should not be caught + } + } + + if (restartDelay < 0) + Program.SetExit(handleFailure: true); + else + Program.SetRestart(restartDelay, true); + + handler!.Dispose(); + + world.Clear(); + } + + #endregion + + #region ChatBot event callback + + private async Task TriggerEvent(McClientEventType eventType, object? parameter) + { + int eventId = (int)eventType; + Func[] eventList = ChatbotEvents[eventId]; + if (eventList.Length > 0) + { + await ChatbotEventTaskLocks[eventId].WaitAsync(); + await WaitChatbotExecuteTask[eventId]; + for (int i = 0; i < eventList.Length; ++i) + ChatbotEventTasks[eventId][i] = eventList[i](parameter); + WaitChatbotExecuteTask[eventId] = WaitTaskAndHandleException(eventType); + ChatbotEventTaskLocks[eventId].Release(); + } + } + + private async Task WaitTaskAndHandleException(McClientEventType eventType) + { + Task[] taskList = ChatbotEventTasks[(int)eventType]; + for (int i = 0; i < taskList.Length; ++i) + { + try + { + await taskList[i]; + } + catch (Exception exception) + { + Log.Error(string.Format(Translations.mcc_chatbot_event_exception, eventType.ToString(), exception.ToString())); + } + } + } + + private static async Task InitializeChatbotEventCallbacks(IEnumerable chatbotList) + { + List>[] tmpCallbackList = new List>[EventTypeCount]; + for (int i = 0; i < EventTypeCount; ++i) + tmpCallbackList[i] = new(); + + foreach (ChatBot bot in chatbotList) + { + Tuple>[]? botEvents = bot.InitializeEventCallbacks(); + if (botEvents != null) + { + ChatbotRegisteredEvents[bot] = new(botEvents); + foreach ((McClientEventType eventType, Func callback) in botEvents) + tmpCallbackList[(int)eventType].Add(callback); + } + else + { + ChatbotRegisteredEvents[bot] = new(); } } - if (!will_restart) + await EventCallbackWriteLock.WaitAsync(); + for (int i = 0; i < EventTypeCount; ++i) { - ConsoleInteractive.ConsoleReader.StopReadThread(); - ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived; - ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler; - Program.HandleFailure(); + ChatbotEvents[i] = tmpCallbackList[i].ToArray(); + await UpdateChatbotEventTasksArray(i); } + EventCallbackWriteLock.Release(); } - #endregion + /// + /// + /// + /// + /// + /// + /// + public static async Task RegisterEventCallback(ChatBot bot, McClientEventType eventType, Func callback) + { + int eventId = (int)eventType; + await EventCallbackWriteLock.WaitAsync(); - #region Command prompt and internal MCC commands + ChatbotEvents[eventId] = new List>(ChatbotEvents[eventId]) { callback }.ToArray(); + if (ChatbotRegisteredEvents.TryGetValue(bot, out var botEvents)) + botEvents.Add(new(eventType, callback)); + else + ChatbotRegisteredEvents[bot] = new() { new(eventType, callback) }; + await UpdateChatbotEventTasksArray(eventId); + EventCallbackWriteLock.Release(); + } - private void ConsoleReaderOnMessageReceived(object? sender, string e) + /// + /// + /// + /// + /// + /// + /// + public static async Task UnregisterEventCallback(ChatBot bot, McClientEventType eventType, Func callback) { + int eventId = (int)eventType; + await EventCallbackWriteLock.WaitAsync(); - if (client.Client == null) - return; + List> newList = new(ChatbotEvents[eventId]); + newList.RemoveAll(c => c == callback); + ChatbotEvents[eventId] = newList.ToArray(); + if (ChatbotRegisteredEvents.TryGetValue(bot, out var botEvents)) + botEvents.RemoveAll(c => c.Item1 == eventType && c.Item2 == callback); + await UpdateChatbotEventTasksArray(eventId); + EventCallbackWriteLock.Release(); + } - if (client.Client.Connected) + public static async Task UnregisterChatbotEventCallback(ChatBot bot) + { + await EventCallbackWriteLock.WaitAsync(); + if (ChatbotRegisteredEvents.TryGetValue(bot, out var botEvents)) { - new Thread(() => + foreach ((McClientEventType eventType, Func callback) in botEvents) { - InvokeOnMainThread(() => HandleCommandPromptText(e)); - }).Start(); + int eventId = (int)eventType; + List> newList = new(ChatbotEvents[eventId]); + newList.RemoveAll(c => c == callback); + ChatbotEvents[eventId] = newList.ToArray(); + await UpdateChatbotEventTasksArray(eventId); + } + ChatbotRegisteredEvents.Remove(bot); } + EventCallbackWriteLock.Release(); + } + + private static async Task UpdateChatbotEventTasksArray(int eventId) + { + await ChatbotEventTaskLocks[eventId].WaitAsync(); + await WaitChatbotExecuteTask[eventId]; + ChatbotEventTasks[eventId] = new Task[ChatbotEvents[eventId].Length]; + ChatbotEventTaskLocks[eventId].Release(); + } + + #endregion + + #region Command prompt and internal MCC commands + + private void ConsoleReaderOnMessageReceived(object? sender, string text) + { + + if (tcpClient!.Client == null) + return; + + if (tcpClient.Client.Connected) + Task.Run(async () => { await HandleCommandPromptText(text); }); else return; } @@ -545,7 +698,7 @@ private void ConsoleReaderOnMessageReceived(object? sender, string e) /// Allows the user to send chat messages, commands, and leave the server. /// Process text from the MCC command prompt on the main thread. /// - private void HandleCommandPromptText(string text) + private async Task HandleCommandPromptText(string text) { if (ConsoleIO.BasicIO && text.Length > 0 && text[0] == (char)0x00) { @@ -554,8 +707,8 @@ private void HandleCommandPromptText(string text) switch (command[0].ToLower()) { case "autocomplete": - int id = handler.AutoComplete(command[1]); - while (!ConsoleIO.AutoCompleteDone) { Thread.Sleep(100); } + int id = await handler!.AutoComplete(command[1]); + while (!ConsoleIO.AutoCompleteDone) { await Task.Delay(100); } if (command.Length > 1) { ConsoleIO.WriteLine((char)0x00 + "autocomplete" + (char)0x00 + ConsoleIO.AutoCompleteResult); } else ConsoleIO.WriteLine((char)0x00 + "autocomplete" + (char)0x00); break; @@ -569,14 +722,14 @@ private void HandleCommandPromptText(string text) && Config.Main.Advanced.InternalCmdChar == MainConfigHealper.MainConfig.AdvancedConfig.InternalCmdCharType.none && text[0] == '/') { - SendText(text); + await SendTextAsync(text); } else if (text.Length > 2 && Config.Main.Advanced.InternalCmdChar != MainConfigHealper.MainConfig.AdvancedConfig.InternalCmdCharType.none && text[0] == Config.Main.Advanced.InternalCmdChar.ToChar() && text[1] == '/') { - SendText(text[1..]); + await SendTextAsync(text[1..]); } else if (text.Length > 0) { @@ -587,7 +740,7 @@ private void HandleCommandPromptText(string text) string command = Config.Main.Advanced.InternalCmdChar.ToChar() == ' ' ? text : text[1..]; if (!PerformInternalCommand(Config.AppVar.ExpandVars(command), ref result, Settings.Config.AppVar.GetVariables()) && Config.Main.Advanced.InternalCmdChar.ToChar() == '/') { - SendText(text); + await SendTextAsync(text); } else if (result.status != CmdResult.Status.NotRun && (result.status != CmdResult.Status.Done || !string.IsNullOrWhiteSpace(result.result))) { @@ -596,7 +749,7 @@ private void HandleCommandPromptText(string text) } else { - SendText(text); + await SendTextAsync(text); } } } @@ -627,11 +780,11 @@ public bool PerformInternalCommand(string command, ref CmdResult result, Diction { 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) { @@ -660,135 +813,55 @@ public bool PerformInternalCommand(string command, ref CmdResult result, Diction } } - 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 /// /// Marks if bots need to be hard reloaded - public void ReloadSettings() + public async Task ReloadSettings() { Program.ReloadSettings(true); - ReloadBots(); + await ReloadBots(); } /// /// Reload loaded bots (Only builtin bots) /// - public void ReloadBots() + public async Task ReloadBots() { - UnloadAllBots(); - RegisterBots(true); + await UnloadAllBots(); - if (client.Client.Connected) - bots.ForEach(bot => bot.AfterGameJoined()); - } + ChatBot[] bots = GetChatbotsToRegister(true); - /// - /// Unload All Bots - /// - public void UnloadAllBots() - { - foreach (ChatBot bot in GetLoadedChatBots()) - BotUnLoad(bot); - } + foreach (ChatBot bot in bots) + { + bot.SetHandler(this); + bot.Initialize(); + } - #endregion + await InitializeChatbotEventCallbacks(bots); - #region Thread-Invoke: Cross-thread method calls + if (handler != null) + foreach (ChatBot bot in bots) + bot.AfterGameJoined(); - /// - /// Invoke a task on the main thread, wait for completion and retrieve return value. - /// - /// Task to run with any type or return value - /// Any result returned from task, result type is inferred from the task - /// bool result = InvokeOnMainThread(methodThatReturnsAbool); - /// bool result = InvokeOnMainThread(() => methodThatReturnsAbool(argument)); - /// int result = InvokeOnMainThread(() => { yourCode(); return 42; }); - /// Type of the return value - public T InvokeOnMainThread(Func task) - { - if (!InvokeRequired) - { - return task(); - } - else - { - TaskWithResult taskWithResult = new(task); - lock (threadTasksLock) - { - threadTasks.Enqueue(taskWithResult.ExecuteSynchronously); - } - return taskWithResult.WaitGetResult(); - } + chatbots = bots; } /// - /// Invoke a task on the main thread and wait for completion + /// Unload All Bots /// - /// Task to run without return value - /// InvokeOnMainThread(methodThatReturnsNothing); - /// InvokeOnMainThread(() => methodThatReturnsNothing(argument)); - /// InvokeOnMainThread(() => { yourCode(); }); - public void InvokeOnMainThread(Action task) + public async Task UnloadAllBots() { - InvokeOnMainThread(() => { task(); return true; }); - } + foreach (ChatBot bot in chatbots) + bot.OnUnload(); + chatbots = Array.Empty(); + registeredBotPluginChannels.Clear(); - /// - /// Clear all tasks - /// - public void ClearTasks() - { - lock (threadTasksLock) - { - threadTasks.Clear(); - } - } + for (int i = 0; i < ChatbotEvents.Length; ++i) + ChatbotEvents[i] = Array.Empty>(); + ChatbotRegisteredEvents.Clear(); - /// - /// Check if running on a different thread and InvokeOnMainThread is required - /// - /// True if calling thread is not the main thread - public bool InvokeRequired - { - get - { - int callingThreadId = Environment.CurrentManagedThreadId; - if (handler != null) - { - return handler.GetNetMainThreadId() != callingThreadId; - } - else - { - // net read thread (main thread) not yet ready - return false; - } - } + await Task.CompletedTask; } #endregion @@ -798,53 +871,41 @@ public bool InvokeRequired /// /// Load a new bot /// - public void BotLoad(ChatBot b, bool init = true) + public async Task BotLoad(ChatBot bot, bool init = true) { - if (InvokeRequired) + bot.SetHandler(this); + chatbots = new List(chatbots) { bot }.ToArray(); + if (init) { - InvokeOnMainThread(() => BotLoad(b, init)); - return; + bot.Initialize(); + await InitializeChatbotEventCallbacks(new ChatBot[] { bot }); } - - b.SetHandler(this); - bots.Add(b); - if (init) - DispatchBotEvent(bot => bot.Initialize(), new ChatBot[] { b }); if (handler != null) - DispatchBotEvent(bot => bot.AfterGameJoined(), new ChatBot[] { b }); + bot.AfterGameJoined(); } /// /// Unload a bot /// - public void BotUnLoad(ChatBot b) + public async Task BotUnLoad(ChatBot bot) { - if (InvokeRequired) - { - InvokeOnMainThread(() => BotUnLoad(b)); - return; - } - - b.OnUnload(); + List botList = new(); + botList.AddRange(from botInList in chatbots + where !ReferenceEquals(botInList, bot) + select botInList); + chatbots = botList.ToArray(); - bots.RemoveAll(item => ReferenceEquals(item, b)); + bot.OnUnload(); + await UnregisterChatbotEventCallback(bot); // 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(); + var botRegistrations = registeredBotPluginChannels.Where(entry => entry.Value.Contains(bot)).ToList(); foreach (var entry in botRegistrations) { - UnregisterPluginChannel(entry.Key, b); + await UnregisterPluginChannelAsync(entry.Key, bot); } } - /// - /// Clear bots - /// - public void BotClear() - { - InvokeOnMainThread(bots.Clear); - } - /// /// Get Terrain and Movements status. /// @@ -879,9 +940,6 @@ public bool GetEntityHandlingEnabled() /// TRUE if the setting was applied immediately, FALSE if delayed. public bool SetTerrainEnabled(bool enabled) { - if (InvokeRequired) - return InvokeOnMainThread(() => SetTerrainEnabled(enabled)); - if (enabled) { if (!terrainAndMovementsEnabled) @@ -908,9 +966,6 @@ public bool SetTerrainEnabled(bool enabled) /// TRUE if the setting was applied immediately, FALSE if delayed. public bool SetInventoryEnabled(bool enabled) { - if (InvokeRequired) - return InvokeOnMainThread(() => SetInventoryEnabled(enabled)); - if (enabled) { if (!inventoryHandlingEnabled) @@ -936,9 +991,6 @@ public bool SetInventoryEnabled(bool enabled) /// TRUE if the setting was applied immediately, FALSE if delayed. public bool SetEntityHandlingEnabled(bool enabled) { - if (InvokeRequired) - return InvokeOnMainThread(() => SetEntityHandlingEnabled(enabled)); - if (!enabled) { if (entityHandlingEnabled) @@ -967,12 +1019,6 @@ public bool SetEntityHandlingEnabled(bool enabled) /// public void SetNetworkPacketCaptureEnabled(bool enabled) { - if (InvokeRequired) - { - InvokeOnMainThread(() => SetNetworkPacketCaptureEnabled(enabled)); - return; - } - networkPacketCaptureEnabled = enabled; } @@ -986,7 +1032,7 @@ public void SetNetworkPacketCaptureEnabled(bool enabled) /// Max length, in characters public int GetMaxChatMessageLength() { - return handler.GetMaxChatMessageLength(); + return handler!.GetMaxChatMessageLength(); } /// @@ -1044,9 +1090,6 @@ public Dictionary GetPlayersLatency() /// Item Dictionary indexed by Slot ID (Check wiki.vg for slot ID) public Container? GetInventory(int inventoryID) { - if (InvokeRequired) - return InvokeOnMainThread(() => GetInventory(inventoryID)); - if (inventories.TryGetValue(inventoryID, out Container? inv)) return inv; else @@ -1104,8 +1147,8 @@ public Dictionary GetOnlinePlayersWithUUID() { lock (onlinePlayers) { - if (onlinePlayers.ContainsKey(uuid)) - return onlinePlayers[uuid]; + if (onlinePlayers.TryGetValue(uuid, out PlayerInfo? playerInfo)) + return playerInfo; else return null; } @@ -1126,23 +1169,25 @@ public Dictionary GetOnlinePlayersWithUUID() /// How long to wait until the path is evaluated (default: 5 seconds) /// When location is unreachable, computation will reach timeout, then optionally fallback to a close location within maxOffset /// True if a path has been found - public bool MoveTo(Location goal, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, TimeSpan? timeout = null) + public async Task MoveToAsync(Location goal, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, TimeSpan? timeout = null) { - lock (locationLock) + if (handler == null) + return false; + + if (allowDirectTeleport) { - if (allowDirectTeleport) - { - // 1-step path to the desired location without checking anything - UpdateLocation(goal, goal); // Update yaw and pitch to look at next step - handler.SendLocationUpdate(goal, Movement.IsOnGround(world, goal), _yaw, _pitch); - return true; - } - else - { - // Calculate path through pathfinding. Path contains a list of 1-block movement that will be divided into steps - path = Movement.CalculatePath(world, location, goal, allowUnsafe, maxOffset, minOffset, timeout ?? TimeSpan.FromSeconds(5)); - return path != null; - } + await locationLock.WaitAsync(); + // 1-step path to the desired location without checking anything + UpdateLocation(goal, goal); // Update yaw and pitch to look at next step + await handler.SendLocationUpdate(goal, Movement.IsOnGround(world, goal), _yaw, _pitch); + locationLock.Release(); + return true; + } + else + { + // Calculate path through pathfinding. Path contains a list of 1-block movement that will be divided into steps + path = await Movement.CalculatePath(world, location, goal, allowUnsafe, maxOffset, minOffset, timeout ?? TimeSpan.FromSeconds(5)); + return path != null; } } @@ -1150,51 +1195,53 @@ public bool MoveTo(Location goal, bool allowUnsafe = false, bool allowDirectTele /// Send a chat message or command to the server /// /// Text to send to the server - public void SendText(string text) + public async Task SendTextAsync(string text) { - if (String.IsNullOrEmpty(text)) + if (handler == null) return; - int maxLength = handler.GetMaxChatMessageLength(); + if (string.IsNullOrEmpty(text)) + return; - lock (chatQueue) + int maxLength = handler!.GetMaxChatMessageLength(); + + if (text.Length > maxLength) //Message is too long? { - if (text.Length > maxLength) //Message is too long? + if (text[0] == '/') { - if (text[0] == '/') + //Send the first 100/256 chars of the command + text = text[..maxLength]; + chatQueue.Enqueue(text); + } + else + { + //Split the message into several messages + while (text.Length > maxLength) { - //Send the first 100/256 chars of the command - text = text[..maxLength]; - chatQueue.Enqueue(text); + chatQueue.Enqueue(text[..maxLength]); + text = text[maxLength..]; } - else - { - //Split the message into several messages - while (text.Length > maxLength) - { - chatQueue.Enqueue(text[..maxLength]); - text = text[maxLength..]; - } + if (!string.IsNullOrEmpty(text)) chatQueue.Enqueue(text); - } } - else - chatQueue.Enqueue(text); - - TrySendMessageToServer(); } + else + { + chatQueue.Enqueue(text); + } + + await TrySendMessageToServer(); } /// /// Allow to respawn after death /// /// True if packet successfully sent - public bool SendRespawnPacket() + public async Task SendRespawnPacketAsync() { - if (InvokeRequired) - return InvokeOnMainThread(SendRespawnPacket); - - return handler.SendRespawnPacket(); + if (handler == null) + return false; + return await handler.SendRespawnPacket(); } /// @@ -1202,26 +1249,17 @@ public bool SendRespawnPacket() /// /// The channel to register. /// The bot to register the channel for. - public void RegisterPluginChannel(string channel, ChatBot bot) + public async Task RegisterPluginChannelAsync(string channel, ChatBot bot) { - if (InvokeRequired) + if (registeredBotPluginChannels.TryGetValue(channel, out List? channelList)) { - InvokeOnMainThread(() => RegisterPluginChannel(channel, bot)); - return; - } - - if (registeredBotPluginChannels.ContainsKey(channel)) - { - registeredBotPluginChannels[channel].Add(bot); + channelList.Add(bot); } else { - List bots = new() - { - bot - }; + List bots = new() { bot }; registeredBotPluginChannels[channel] = bots; - SendPluginChannelMessage("REGISTER", Encoding.UTF8.GetBytes(channel), true); + await SendPluginChannelMessageAsync("REGISTER", Encoding.UTF8.GetBytes(channel), true); } } @@ -1230,22 +1268,16 @@ public void RegisterPluginChannel(string channel, ChatBot bot) /// /// The channel to unregister. /// The bot to unregister the channel for. - public void UnregisterPluginChannel(string channel, ChatBot bot) + public async Task UnregisterPluginChannelAsync(string channel, ChatBot bot) { - if (InvokeRequired) + if (registeredBotPluginChannels.TryGetValue(channel, out List? channelList)) { - InvokeOnMainThread(() => UnregisterPluginChannel(channel, bot)); - return; - } - - if (registeredBotPluginChannels.ContainsKey(channel)) - { - List registeredBots = registeredBotPluginChannels[channel]; - registeredBots.RemoveAll(item => object.ReferenceEquals(item, bot)); + List registeredBots = channelList; + registeredBots.RemoveAll(item => ReferenceEquals(item, bot)); if (registeredBots.Count == 0) { registeredBotPluginChannels.Remove(channel); - SendPluginChannelMessage("UNREGISTER", Encoding.UTF8.GetBytes(channel), true); + await SendPluginChannelMessageAsync("UNREGISTER", Encoding.UTF8.GetBytes(channel), true); } } } @@ -1258,10 +1290,10 @@ public void UnregisterPluginChannel(string channel, ChatBot bot) /// The payload for the packet. /// Whether the packet should be sent even if the server or the client hasn't registered it yet. /// Whether the packet was sent: true if it was sent, false if there was a connection error or it wasn't registered. - public bool SendPluginChannelMessage(string channel, byte[] data, bool sendEvenIfNotRegistered = false) + public async Task SendPluginChannelMessageAsync(string channel, byte[] data, bool sendEvenIfNotRegistered = false) { - if (InvokeRequired) - return InvokeOnMainThread(() => SendPluginChannelMessage(channel, data, sendEvenIfNotRegistered)); + if (handler == null) + return false; if (!sendEvenIfNotRegistered) { @@ -1274,34 +1306,40 @@ public bool SendPluginChannelMessage(string channel, byte[] data, bool sendEvenI return false; } } - return handler.SendPluginChannelPacket(channel, data); + return await handler.SendPluginChannelPacket(channel, data); } /// /// Send the Entity Action packet with the Specified ID /// /// TRUE if the item was successfully used - public bool SendEntityAction(EntityActionType entityAction) + public async Task SendEntityActionAsync(EntityActionType entityAction) { - return InvokeOnMainThread(() => handler.SendEntityAction(playerEntityID, (int)entityAction)); + if (handler == null) + return false; + return await handler.SendEntityAction(playerEntityID, (int)entityAction); } /// /// Use the item currently in the player's hand /// /// TRUE if the item was successfully used - public bool UseItemOnHand() + public async Task UseItemOnHandAsync() { - return InvokeOnMainThread(() => handler.SendUseItem(0, sequenceId)); + if (handler == null) + return false; + return await handler.SendUseItem(0, sequenceId); } /// /// Use the item currently in the player's left hand /// /// TRUE if the item was successfully used - public bool UseItemOnLeftHand() + public async Task UseItemOnOffHandAsync() { - return InvokeOnMainThread(() => handler.SendUseItem(1, sequenceId)); + if (handler == null) + return false; + return await handler.SendUseItem(1, sequenceId); } /// @@ -1365,663 +1403,686 @@ private static void StoreInNewSlot(Container inventory, Item item, int slotId, i /// Click a slot in the specified window /// /// TRUE if the slot was successfully clicked - public bool DoWindowAction(int windowId, int slotId, WindowActionType action) + public async Task DoWindowActionAsync(int windowId, int slotId, WindowActionType action) { - if (InvokeRequired) - return InvokeOnMainThread(() => DoWindowAction(windowId, slotId, action)); + if (handler == null) + return false; Item? item = null; - if (inventories.ContainsKey(windowId) && inventories[windowId].Items.ContainsKey(slotId)) - item = inventories[windowId].Items[slotId]; - List> changedSlots = new(); // List - - // Update our inventory base on action type - Container inventory = GetInventory(windowId)!; - Container playerInventory = GetInventory(0)!; - if (inventory != null) + lock (inventoryLock) { - switch (action) - { - case WindowActionType.LeftClick: - // Check if cursor have item (slot -1) - if (playerInventory.Items.ContainsKey(-1)) - { - // When item on cursor and clicking slot 0, nothing will happen - if (slotId == 0) break; + if (inventories.TryGetValue(windowId, out Container? container)) + container.Items.TryGetValue(slotId, out item); - // Check target slot also have item? - if (inventory.Items.ContainsKey(slotId)) + // Update our inventory base on action type + Container inventory = GetInventory(windowId)!; + Container playerInventory = GetInventory(0)!; + if (inventory != null) + { + switch (action) + { + case WindowActionType.LeftClick: + // Check if cursor have item (slot -1) + if (playerInventory.Items.ContainsKey(-1)) { - // Check if both item are the same? - if (inventory.Items[slotId].Type == playerInventory.Items[-1].Type) + // When item on cursor and clicking slot 0, nothing will happen + if (slotId == 0) break; + + // Check target slot also have item? + if (inventory.Items.ContainsKey(slotId)) { - int maxCount = inventory.Items[slotId].Type.StackCount(); - // Check item stacking - if ((inventory.Items[slotId].Count + playerInventory.Items[-1].Count) <= maxCount) + // Check if both item are the same? + if (inventory.Items[slotId].Type == playerInventory.Items[-1].Type) { - // Put cursor item to target - inventory.Items[slotId].Count += playerInventory.Items[-1].Count; - playerInventory.Items.Remove(-1); + int maxCount = inventory.Items[slotId].Type.StackCount(); + // Check item stacking + if ((inventory.Items[slotId].Count + playerInventory.Items[-1].Count) <= maxCount) + { + // Put cursor item to target + inventory.Items[slotId].Count += playerInventory.Items[-1].Count; + playerInventory.Items.Remove(-1); + } + else + { + // Leave some item on cursor + playerInventory.Items[-1].Count -= (maxCount - inventory.Items[slotId].Count); + inventory.Items[slotId].Count = maxCount; + } } else { - // Leave some item on cursor - playerInventory.Items[-1].Count -= (maxCount - inventory.Items[slotId].Count); - inventory.Items[slotId].Count = maxCount; + // Swap two items + (inventory.Items[slotId], playerInventory.Items[-1]) = (playerInventory.Items[-1], inventory.Items[slotId]); } } else { - // Swap two items - (inventory.Items[slotId], playerInventory.Items[-1]) = (playerInventory.Items[-1], inventory.Items[slotId]); + // Put cursor item to target + inventory.Items[slotId] = playerInventory.Items[-1]; + playerInventory.Items.Remove(-1); } + + if (inventory.Items.TryGetValue(slotId, out Item? item1)) + changedSlots.Add(new Tuple((short)slotId, item1)); + else + changedSlots.Add(new Tuple((short)slotId, null)); } else { - // Put cursor item to target - inventory.Items[slotId] = playerInventory.Items[-1]; - playerInventory.Items.Remove(-1); - } - - if (inventory.Items.ContainsKey(slotId)) - changedSlots.Add(new Tuple((short)slotId, inventory.Items[slotId])); - else - changedSlots.Add(new Tuple((short)slotId, null)); - } - else - { - // Check target slot have item? - if (inventory.Items.ContainsKey(slotId)) - { - // When taking item from slot 0, server will update us - if (slotId == 0) break; + // Check target slot have item? + if (inventory.Items.ContainsKey(slotId)) + { + // When taking item from slot 0, server will update us + if (slotId == 0) break; - // Put target slot item to cursor - playerInventory.Items[-1] = inventory.Items[slotId]; - inventory.Items.Remove(slotId); + // Put target slot item to cursor + playerInventory.Items[-1] = inventory.Items[slotId]; + inventory.Items.Remove(slotId); - changedSlots.Add(new Tuple((short)slotId, null)); + changedSlots.Add(new Tuple((short)slotId, null)); + } } - } - break; - case WindowActionType.RightClick: - // Check if cursor have item (slot -1) - if (playerInventory.Items.ContainsKey(-1)) - { - // When item on cursor and clicking slot 0, nothing will happen - if (slotId == 0) break; - - // Check target slot have item? - if (inventory.Items.ContainsKey(slotId)) + break; + case WindowActionType.RightClick: + // Check if cursor have item (slot -1) + if (playerInventory.Items.TryGetValue(-1, out Item? playerItem)) { - // Check if both item are the same? - if (inventory.Items[slotId].Type == playerInventory.Items[-1].Type) + // When item on cursor and clicking slot 0, nothing will happen + if (slotId == 0) break; + + // Check target slot have item? + if (inventory.Items.TryGetValue(slotId, out Item? invItem)) { - // Check item stacking - if (inventory.Items[slotId].Count < inventory.Items[slotId].Type.StackCount()) + // Check if both item are the same? + if (invItem.Type == playerItem.Type) { - // Drop 1 item count from cursor - playerInventory.Items[-1].Count--; - inventory.Items[slotId].Count++; + // Check item stacking + if (invItem.Count < invItem.Type.StackCount()) + { + // Drop 1 item count from cursor + playerItem.Count--; + invItem.Count++; + } + } + else + { + // Swap two items + (invItem, playerItem) = (playerItem, invItem); } } else { - // Swap two items - (inventory.Items[slotId], playerInventory.Items[-1]) = (playerInventory.Items[-1], inventory.Items[slotId]); + // Drop 1 item count from cursor + inventory.Items[slotId] = new(playerItem.Type, 1, playerItem.NBT); + playerItem.Count--; } } else { - // Drop 1 item count from cursor - Item itemTmp = playerInventory.Items[-1]; - Item itemClone = new(itemTmp.Type, 1, itemTmp.NBT); - inventory.Items[slotId] = itemClone; - playerInventory.Items[-1].Count--; - } - } - else - { - // Check target slot have item? - if (inventory.Items.ContainsKey(slotId)) - { - if (slotId == 0) - { - // no matter how many item in slot 0, only 1 will be taken out - // Also server will update us - break; - } - if (inventory.Items[slotId].Count == 1) - { - // Only 1 item count. Put it to cursor - playerInventory.Items[-1] = inventory.Items[slotId]; - inventory.Items.Remove(slotId); - } - else + // Check target slot have item? + if (inventory.Items.ContainsKey(slotId)) { - // Take half of the item stack to cursor - if (inventory.Items[slotId].Count % 2 == 0) + if (slotId == 0) { - // Can be evenly divided - Item itemTmp = inventory.Items[slotId]; - playerInventory.Items[-1] = new Item(itemTmp.Type, itemTmp.Count / 2, itemTmp.NBT); - inventory.Items[slotId].Count = itemTmp.Count / 2; + // no matter how many item in slot 0, only 1 will be taken out + // Also server will update us + break; + } + if (inventory.Items[slotId].Count == 1) + { + // Only 1 item count. Put it to cursor + playerInventory.Items[-1] = inventory.Items[slotId]; + inventory.Items.Remove(slotId); } else { - // Cannot be evenly divided. item count on cursor is always larger than item on inventory - Item itemTmp = inventory.Items[slotId]; - playerInventory.Items[-1] = new Item(itemTmp.Type, (itemTmp.Count + 1) / 2, itemTmp.NBT); - inventory.Items[slotId].Count = (itemTmp.Count - 1) / 2; + // Take half of the item stack to cursor + if (inventory.Items[slotId].Count % 2 == 0) + { + // Can be evenly divided + Item itemTmp = inventory.Items[slotId]; + playerInventory.Items[-1] = new Item(itemTmp.Type, itemTmp.Count / 2, itemTmp.NBT); + inventory.Items[slotId].Count = itemTmp.Count / 2; + } + else + { + // Cannot be evenly divided. item count on cursor is always larger than item on inventory + Item itemTmp = inventory.Items[slotId]; + playerInventory.Items[-1] = new Item(itemTmp.Type, (itemTmp.Count + 1) / 2, itemTmp.NBT); + inventory.Items[slotId].Count = (itemTmp.Count - 1) / 2; + } } } } - } - if (inventory.Items.ContainsKey(slotId)) - changedSlots.Add(new Tuple((short)slotId, inventory.Items[slotId])); - else - changedSlots.Add(new Tuple((short)slotId, null)); - break; - case WindowActionType.ShiftClick: - if (slotId == 0) break; - if (item != null) - { - /* Target slot have item */ + if (inventory.Items.TryGetValue(slotId, out Item? item2)) + changedSlots.Add(new Tuple((short)slotId, item2)); + else + changedSlots.Add(new Tuple((short)slotId, null)); + break; + case WindowActionType.ShiftClick: + if (slotId == 0) break; + if (item != null) + { + /* Target slot have item */ - bool lower2upper = false, upper2backpack = false, backpack2hotbar = false; // mutual exclusion - bool hotbarFirst = true; // Used when upper2backpack = true - int upperStartSlot = 9; - int upperEndSlot = 35; - int lowerStartSlot = 36; + bool lower2upper = false, upper2backpack = false, backpack2hotbar = false; // mutual exclusion + bool hotbarFirst = true; // Used when upper2backpack = true + int upperStartSlot = 9; + int upperEndSlot = 35; + int lowerStartSlot = 36; - switch (inventory.Type) - { - case ContainerType.PlayerInventory: - if (slotId >= 0 && slotId <= 8 || slotId == 45) - { - if (slotId != 0) - hotbarFirst = false; - upper2backpack = true; - lowerStartSlot = 9; - } - else if (item != null && false /* Check if wearable */) - { - lower2upper = true; - // upperStartSlot = ?; - // upperEndSlot = ?; - // Todo: Distinguish the type of equipment - } - else - { - if (slotId >= 9 && slotId <= 35) + switch (inventory.Type) + { + case ContainerType.PlayerInventory: + if (slotId >= 0 && slotId <= 8 || slotId == 45) + { + if (slotId != 0) + hotbarFirst = false; + upper2backpack = true; + lowerStartSlot = 9; + } + else if (item != null && false /* Check if wearable */) + { + lower2upper = true; + // upperStartSlot = ?; + // upperEndSlot = ?; + // Todo: Distinguish the type of equipment + } + else { - backpack2hotbar = true; + if (slotId >= 9 && slotId <= 35) + { + backpack2hotbar = true; + lowerStartSlot = 36; + } + else + { + lower2upper = true; + upperStartSlot = 9; + upperEndSlot = 35; + } + } + break; + case ContainerType.Generic_9x1: + if (slotId >= 0 && slotId <= 8) + { + upper2backpack = true; + lowerStartSlot = 9; + } + else + { + lower2upper = true; + upperStartSlot = 0; + upperEndSlot = 8; + } + break; + case ContainerType.Generic_9x2: + if (slotId >= 0 && slotId <= 17) + { + upper2backpack = true; + lowerStartSlot = 18; + } + else + { + lower2upper = true; + upperStartSlot = 0; + upperEndSlot = 17; + } + break; + case ContainerType.Generic_9x3: + case ContainerType.ShulkerBox: + if (slotId >= 0 && slotId <= 26) + { + upper2backpack = true; + lowerStartSlot = 27; + } + else + { + lower2upper = true; + upperStartSlot = 0; + upperEndSlot = 26; + } + break; + case ContainerType.Generic_9x4: + if (slotId >= 0 && slotId <= 35) + { + upper2backpack = true; lowerStartSlot = 36; } else { lower2upper = true; - upperStartSlot = 9; + upperStartSlot = 0; upperEndSlot = 35; } - } - break; - case ContainerType.Generic_9x1: - if (slotId >= 0 && slotId <= 8) - { - upper2backpack = true; - lowerStartSlot = 9; - } - else - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 8; - } - break; - case ContainerType.Generic_9x2: - if (slotId >= 0 && slotId <= 17) - { - upper2backpack = true; - lowerStartSlot = 18; - } - else - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 17; - } - break; - case ContainerType.Generic_9x3: - case ContainerType.ShulkerBox: - if (slotId >= 0 && slotId <= 26) - { - upper2backpack = true; - lowerStartSlot = 27; - } - else - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 26; - } - break; - case ContainerType.Generic_9x4: - if (slotId >= 0 && slotId <= 35) - { - upper2backpack = true; - lowerStartSlot = 36; - } - else - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 35; - } - break; - case ContainerType.Generic_9x5: - if (slotId >= 0 && slotId <= 44) - { - upper2backpack = true; - lowerStartSlot = 45; - } - else - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 44; - } - break; - case ContainerType.Generic_9x6: - if (slotId >= 0 && slotId <= 53) - { - upper2backpack = true; - lowerStartSlot = 54; - } - else - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 53; - } - break; - case ContainerType.Generic_3x3: - if (slotId >= 0 && slotId <= 8) - { - upper2backpack = true; - lowerStartSlot = 9; - } - else - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 8; - } - break; - case ContainerType.Anvil: - if (slotId >= 0 && slotId <= 2) - { - if (slotId >= 0 && slotId <= 1) + break; + case ContainerType.Generic_9x5: + if (slotId >= 0 && slotId <= 44) + { + upper2backpack = true; + lowerStartSlot = 45; + } + else + { + lower2upper = true; + upperStartSlot = 0; + upperEndSlot = 44; + } + break; + case ContainerType.Generic_9x6: + if (slotId >= 0 && slotId <= 53) + { + upper2backpack = true; + lowerStartSlot = 54; + } + else + { + lower2upper = true; + upperStartSlot = 0; + upperEndSlot = 53; + } + break; + case ContainerType.Generic_3x3: + if (slotId >= 0 && slotId <= 8) + { + upper2backpack = true; + lowerStartSlot = 9; + } + else + { + lower2upper = true; + upperStartSlot = 0; + upperEndSlot = 8; + } + break; + case ContainerType.Anvil: + if (slotId >= 0 && slotId <= 2) + { + if (slotId >= 0 && slotId <= 1) + hotbarFirst = false; + upper2backpack = true; + lowerStartSlot = 3; + } + else + { + lower2upper = true; + upperStartSlot = 0; + upperEndSlot = 1; + } + break; + case ContainerType.Beacon: + if (slotId == 0) + { hotbarFirst = false; - upper2backpack = true; - lowerStartSlot = 3; - } - else - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 1; - } - break; - case ContainerType.Beacon: - if (slotId == 0) - { - hotbarFirst = false; - upper2backpack = true; - lowerStartSlot = 1; - } - else if (item != null && item.Count == 1 && (item.Type == ItemType.NetheriteIngot || - item.Type == ItemType.Emerald || item.Type == ItemType.Diamond || item.Type == ItemType.GoldIngot || - item.Type == ItemType.IronIngot) && !inventory.Items.ContainsKey(0)) - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 0; - } - else - { - if (slotId >= 1 && slotId <= 27) + upper2backpack = true; + lowerStartSlot = 1; + } + else if (item != null && item.Count == 1 && (item.Type == ItemType.NetheriteIngot || + item.Type == ItemType.Emerald || item.Type == ItemType.Diamond || item.Type == ItemType.GoldIngot || + item.Type == ItemType.IronIngot) && !inventory.Items.ContainsKey(0)) + { + lower2upper = true; + upperStartSlot = 0; + upperEndSlot = 0; + } + else + { + if (slotId >= 1 && slotId <= 27) + { + backpack2hotbar = true; + lowerStartSlot = 28; + } + else + { + lower2upper = true; + upperStartSlot = 1; + upperEndSlot = 27; + } + } + break; + case ContainerType.BlastFurnace: + case ContainerType.Furnace: + case ContainerType.Smoker: + if (slotId >= 0 && slotId <= 2) { - backpack2hotbar = true; - lowerStartSlot = 28; + if (slotId >= 0 && slotId <= 1) + hotbarFirst = false; + upper2backpack = true; + lowerStartSlot = 3; + } + else if (item != null && false /* Check if it can be burned */) + { + lower2upper = true; + upperStartSlot = 0; + upperEndSlot = 0; + } + else + { + if (slotId >= 3 && slotId <= 29) + { + backpack2hotbar = true; + lowerStartSlot = 30; + } + else + { + lower2upper = true; + upperStartSlot = 3; + upperEndSlot = 29; + } + } + break; + case ContainerType.BrewingStand: + if (slotId >= 0 && slotId <= 3) + { + upper2backpack = true; + lowerStartSlot = 5; + } + else if (item != null && item.Type == ItemType.BlazePowder) + { + lower2upper = true; + if (!inventory.Items.ContainsKey(4) || inventory.Items[4].Count < 64) + upperStartSlot = upperEndSlot = 4; + else + upperStartSlot = upperEndSlot = 3; + } + else if (item != null && false /* Check if it can be used for alchemy */) + { + lower2upper = true; + upperStartSlot = upperEndSlot = 3; + } + else if (item != null && (item.Type == ItemType.Potion || item.Type == ItemType.GlassBottle)) + { + lower2upper = true; + upperStartSlot = 0; + upperEndSlot = 2; + } + else + { + if (slotId >= 5 && slotId <= 31) + { + backpack2hotbar = true; + lowerStartSlot = 32; + } + else + { + lower2upper = true; + upperStartSlot = 5; + upperEndSlot = 31; + } + } + break; + case ContainerType.Crafting: + if (slotId >= 0 && slotId <= 9) + { + if (slotId >= 1 && slotId <= 9) + hotbarFirst = false; + upper2backpack = true; + lowerStartSlot = 10; + } + else + { + lower2upper = true; + upperStartSlot = 1; + upperEndSlot = 9; + } + break; + case ContainerType.Enchantment: + if (slotId >= 0 && slotId <= 1) + { + upper2backpack = true; + lowerStartSlot = 5; + } + else if (item != null && item.Type == ItemType.LapisLazuli) + { + lower2upper = true; + upperStartSlot = upperEndSlot = 1; } else { lower2upper = true; - upperStartSlot = 1; - upperEndSlot = 27; + upperStartSlot = 0; + upperEndSlot = 0; } - } - break; - case ContainerType.BlastFurnace: - case ContainerType.Furnace: - case ContainerType.Smoker: - if (slotId >= 0 && slotId <= 2) - { - if (slotId >= 0 && slotId <= 1) - hotbarFirst = false; - upper2backpack = true; - lowerStartSlot = 3; - } - else if (item != null && false /* Check if it can be burned */) - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 0; - } - else - { - if (slotId >= 3 && slotId <= 29) + break; + case ContainerType.Grindstone: + if (slotId >= 0 && slotId <= 2) { - backpack2hotbar = true; - lowerStartSlot = 30; + if (slotId >= 0 && slotId <= 1) + hotbarFirst = false; + upper2backpack = true; + lowerStartSlot = 3; } - else + else if (item != null && false /* Check */) { lower2upper = true; - upperStartSlot = 3; - upperEndSlot = 29; + upperStartSlot = 0; + upperEndSlot = 1; } - } - break; - case ContainerType.BrewingStand: - if (slotId >= 0 && slotId <= 3) - { - upper2backpack = true; - lowerStartSlot = 5; - } - else if (item != null && item.Type == ItemType.BlazePowder) - { - lower2upper = true; - if (!inventory.Items.ContainsKey(4) || inventory.Items[4].Count < 64) - upperStartSlot = upperEndSlot = 4; else - upperStartSlot = upperEndSlot = 3; - } - else if (item != null && false /* Check if it can be used for alchemy */) - { - lower2upper = true; - upperStartSlot = upperEndSlot = 3; - } - else if (item != null && (item.Type == ItemType.Potion || item.Type == ItemType.GlassBottle)) - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 2; - } - else - { - if (slotId >= 5 && slotId <= 31) { - backpack2hotbar = true; - lowerStartSlot = 32; + lower2upper = true; + upperStartSlot = 0; + upperEndSlot = 1; + } + break; + case ContainerType.Hopper: + if (slotId >= 0 && slotId <= 4) + { + upper2backpack = true; + lowerStartSlot = 5; } else { lower2upper = true; - upperStartSlot = 5; - upperEndSlot = 31; + upperStartSlot = 0; + upperEndSlot = 4; } - } - break; - case ContainerType.Crafting: - if (slotId >= 0 && slotId <= 9) - { - if (slotId >= 1 && slotId <= 9) - hotbarFirst = false; - upper2backpack = true; - lowerStartSlot = 10; - } - else - { - lower2upper = true; - upperStartSlot = 1; - upperEndSlot = 9; - } - break; - case ContainerType.Enchantment: - if (slotId >= 0 && slotId <= 1) - { - upper2backpack = true; - lowerStartSlot = 5; - } - else if (item != null && item.Type == ItemType.LapisLazuli) - { - lower2upper = true; - upperStartSlot = upperEndSlot = 1; - } - else - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 0; - } - break; - case ContainerType.Grindstone: - if (slotId >= 0 && slotId <= 2) - { - if (slotId >= 0 && slotId <= 1) - hotbarFirst = false; - upper2backpack = true; - lowerStartSlot = 3; - } - else if (item != null && false /* Check */) - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 1; - } - else - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 1; - } - break; - case ContainerType.Hopper: - if (slotId >= 0 && slotId <= 4) - { - upper2backpack = true; - lowerStartSlot = 5; - } - else - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 4; - } - break; - case ContainerType.Lectern: - return false; - // break; - case ContainerType.Loom: - if (slotId >= 0 && slotId <= 3) - { - if (slotId >= 0 && slotId <= 5) - hotbarFirst = false; - upper2backpack = true; - lowerStartSlot = 4; - } - else if (item != null && false /* Check for availability for staining */) - { - lower2upper = true; - // upperStartSlot = ?; - // upperEndSlot = ?; - } - else - { - if (slotId >= 4 && slotId <= 30) + break; + case ContainerType.Lectern: + return false; + // break; + case ContainerType.Loom: + if (slotId >= 0 && slotId <= 3) { - backpack2hotbar = true; - lowerStartSlot = 31; + if (slotId >= 0 && slotId <= 5) + hotbarFirst = false; + upper2backpack = true; + lowerStartSlot = 4; } - else + else if (item != null && false /* Check for availability for staining */) { lower2upper = true; - upperStartSlot = 4; - upperEndSlot = 30; + // upperStartSlot = ?; + // upperEndSlot = ?; } - } - break; - case ContainerType.Merchant: - if (slotId >= 0 && slotId <= 2) - { - if (slotId >= 0 && slotId <= 1) - hotbarFirst = false; - upper2backpack = true; - lowerStartSlot = 3; - } - else if (item != null && false /* Check if it is available for trading */) - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 1; - } - else - { - if (slotId >= 3 && slotId <= 29) + else { - backpack2hotbar = true; - lowerStartSlot = 30; + if (slotId >= 4 && slotId <= 30) + { + backpack2hotbar = true; + lowerStartSlot = 31; + } + else + { + lower2upper = true; + upperStartSlot = 4; + upperEndSlot = 30; + } } - else + break; + case ContainerType.Merchant: + if (slotId >= 0 && slotId <= 2) { - lower2upper = true; - upperStartSlot = 3; - upperEndSlot = 29; + if (slotId >= 0 && slotId <= 1) + hotbarFirst = false; + upper2backpack = true; + lowerStartSlot = 3; } - } - break; - case ContainerType.Cartography: - if (slotId >= 0 && slotId <= 2) - { - if (slotId >= 0 && slotId <= 1) - hotbarFirst = false; - upper2backpack = true; - lowerStartSlot = 3; - } - else if (item != null && item.Type == ItemType.FilledMap) - { - lower2upper = true; - upperStartSlot = upperEndSlot = 0; - } - else if (item != null && item.Type == ItemType.Map) - { - lower2upper = true; - upperStartSlot = upperEndSlot = 1; - } - else - { - if (slotId >= 3 && slotId <= 29) + else if (item != null && false /* Check if it is available for trading */) { - backpack2hotbar = true; - lowerStartSlot = 30; + lower2upper = true; + upperStartSlot = 0; + upperEndSlot = 1; } else + { + if (slotId >= 3 && slotId <= 29) + { + backpack2hotbar = true; + lowerStartSlot = 30; + } + else + { + lower2upper = true; + upperStartSlot = 3; + upperEndSlot = 29; + } + } + break; + case ContainerType.Cartography: + if (slotId >= 0 && slotId <= 2) + { + if (slotId >= 0 && slotId <= 1) + hotbarFirst = false; + upper2backpack = true; + lowerStartSlot = 3; + } + else if (item != null && item.Type == ItemType.FilledMap) { lower2upper = true; - upperStartSlot = 3; - upperEndSlot = 29; + upperStartSlot = upperEndSlot = 0; } - } - break; - case ContainerType.Stonecutter: - if (slotId >= 0 && slotId <= 1) - { - if (slotId == 0) - hotbarFirst = false; - upper2backpack = true; - lowerStartSlot = 2; - } - else if (item != null && false /* Check if it is available for stone cutteing */) - { - lower2upper = true; - upperStartSlot = 0; - upperEndSlot = 0; - } - else - { - if (slotId >= 2 && slotId <= 28) + else if (item != null && item.Type == ItemType.Map) { - backpack2hotbar = true; - lowerStartSlot = 29; + lower2upper = true; + upperStartSlot = upperEndSlot = 1; } else + { + if (slotId >= 3 && slotId <= 29) + { + backpack2hotbar = true; + lowerStartSlot = 30; + } + else + { + lower2upper = true; + upperStartSlot = 3; + upperEndSlot = 29; + } + } + break; + case ContainerType.Stonecutter: + if (slotId >= 0 && slotId <= 1) + { + if (slotId == 0) + hotbarFirst = false; + upper2backpack = true; + lowerStartSlot = 2; + } + else if (item != null && false /* Check if it is available for stone cutteing */) { lower2upper = true; - upperStartSlot = 2; - upperEndSlot = 28; + upperStartSlot = 0; + upperEndSlot = 0; } - } - break; - // TODO: Define more container type here - default: - return false; - } - - // Cursor have item or not doesn't matter - // If hotbar already have same item, will put on it first until every stack are full - // If no more same item , will put on the first empty slot (smaller slot id) - // If inventory full, item will not move - int itemCount = inventory.Items[slotId].Count; - if (lower2upper) - { - int firstEmptySlot = -1; - for (int i = upperStartSlot; i <= upperEndSlot; ++i) - { - if (inventory.Items.TryGetValue(i, out Item? curItem)) - { - if (TryMergeSlot(inventory, item!, slotId, curItem, i, changedSlots)) - break; - } - else if (firstEmptySlot == -1) - firstEmptySlot = i; - } - if (item!.Count > 0) - { - if (firstEmptySlot != -1) - StoreInNewSlot(inventory, item, slotId, firstEmptySlot, changedSlots); - else if (item.Count != itemCount) - changedSlots.Add(new Tuple((short)slotId, inventory.Items[slotId])); + else + { + if (slotId >= 2 && slotId <= 28) + { + backpack2hotbar = true; + lowerStartSlot = 29; + } + else + { + lower2upper = true; + upperStartSlot = 2; + upperEndSlot = 28; + } + } + break; + // TODO: Define more container type here + default: + return false; } - } - else if (upper2backpack) - { - int hotbarEnd = lowerStartSlot + 4 * 9 - 1; - if (hotbarFirst) + + // Cursor have item or not doesn't matter + // If hotbar already have same item, will put on it first until every stack are full + // If no more same item , will put on the first empty slot (smaller slot id) + // If inventory full, item will not move + int itemCount = inventory.Items[slotId].Count; + if (lower2upper) { - int lastEmptySlot = -1; - for (int i = hotbarEnd; i >= lowerStartSlot; --i) + int firstEmptySlot = -1; + for (int i = upperStartSlot; i <= upperEndSlot; ++i) { if (inventory.Items.TryGetValue(i, out Item? curItem)) { if (TryMergeSlot(inventory, item!, slotId, curItem, i, changedSlots)) break; } - else if (lastEmptySlot == -1) - lastEmptySlot = i; + else if (firstEmptySlot == -1) + firstEmptySlot = i; } if (item!.Count > 0) { - if (lastEmptySlot != -1) - StoreInNewSlot(inventory, item, slotId, lastEmptySlot, changedSlots); + if (firstEmptySlot != -1) + StoreInNewSlot(inventory, item, slotId, firstEmptySlot, changedSlots); else if (item.Count != itemCount) changedSlots.Add(new Tuple((short)slotId, inventory.Items[slotId])); } } - else + else if (upper2backpack) + { + int hotbarEnd = lowerStartSlot + 4 * 9 - 1; + if (hotbarFirst) + { + int lastEmptySlot = -1; + for (int i = hotbarEnd; i >= lowerStartSlot; --i) + { + if (inventory.Items.TryGetValue(i, out Item? curItem)) + { + if (TryMergeSlot(inventory, item!, slotId, curItem, i, changedSlots)) + break; + } + else if (lastEmptySlot == -1) + lastEmptySlot = i; + } + if (item!.Count > 0) + { + if (lastEmptySlot != -1) + StoreInNewSlot(inventory, item, slotId, lastEmptySlot, changedSlots); + else if (item.Count != itemCount) + changedSlots.Add(new Tuple((short)slotId, inventory.Items[slotId])); + } + } + else + { + int firstEmptySlot = -1; + for (int i = lowerStartSlot; i <= hotbarEnd; ++i) + { + if (inventory.Items.TryGetValue(i, out Item? curItem)) + { + if (TryMergeSlot(inventory, item!, slotId, curItem, i, changedSlots)) + break; + } + else if (firstEmptySlot == -1) + firstEmptySlot = i; + } + if (item!.Count > 0) + { + if (firstEmptySlot != -1) + StoreInNewSlot(inventory, item, slotId, firstEmptySlot, changedSlots); + else if (item.Count != itemCount) + changedSlots.Add(new Tuple((short)slotId, inventory.Items[slotId])); + } + } + } + else if (backpack2hotbar) { + int hotbarEnd = lowerStartSlot + 1 * 9 - 1; + int firstEmptySlot = -1; for (int i = lowerStartSlot; i <= hotbarEnd; ++i) { @@ -2042,53 +2103,29 @@ public bool DoWindowAction(int windowId, int slotId, WindowActionType action) } } } - else if (backpack2hotbar) + break; + case WindowActionType.DropItem: + if (inventory.Items.TryGetValue(slotId, out Item? item3)) { - int hotbarEnd = lowerStartSlot + 1 * 9 - 1; + item3.Count--; + changedSlots.Add(new Tuple((short)slotId, inventory.Items[slotId])); + } - int firstEmptySlot = -1; - for (int i = lowerStartSlot; i <= hotbarEnd; ++i) - { - if (inventory.Items.TryGetValue(i, out Item? curItem)) - { - if (TryMergeSlot(inventory, item!, slotId, curItem, i, changedSlots)) - break; - } - else if (firstEmptySlot == -1) - firstEmptySlot = i; - } - if (item!.Count > 0) - { - if (firstEmptySlot != -1) - StoreInNewSlot(inventory, item, slotId, firstEmptySlot, changedSlots); - else if (item.Count != itemCount) - changedSlots.Add(new Tuple((short)slotId, inventory.Items[slotId])); - } + if (inventory.Items[slotId].Count <= 0) + { + inventory.Items.Remove(slotId); + changedSlots.Add(new Tuple((short)slotId, null)); } - } - break; - case WindowActionType.DropItem: - if (inventory.Items.ContainsKey(slotId)) - { - inventory.Items[slotId].Count--; - changedSlots.Add(new Tuple((short)slotId, inventory.Items[slotId])); - } - if (inventory.Items[slotId].Count <= 0) - { + break; + case WindowActionType.DropItemStack: inventory.Items.Remove(slotId); changedSlots.Add(new Tuple((short)slotId, null)); - } - - break; - case WindowActionType.DropItemStack: - inventory.Items.Remove(slotId); - changedSlots.Add(new Tuple((short)slotId, null)); - break; + break; + } } } - - return handler.SendWindowAction(windowId, slotId, action, item, changedSlots, inventories[windowId].StateID); + return await handler!.SendWindowAction(windowId, slotId, action, item, changedSlots, inventories[windowId].StateID); } /// @@ -2100,9 +2137,11 @@ public bool DoWindowAction(int windowId, int slotId, WindowActionType action) /// Item count /// Item NBT /// TRUE if item given successfully - public bool DoCreativeGive(int slot, ItemType itemType, int count, Dictionary? nbt = null) + public async Task DoCreativeGiveAsync(int slot, ItemType itemType, int count, Dictionary? nbt = null) { - return InvokeOnMainThread(() => handler.SendCreativeInventoryAction(slot, itemType, count, nbt)); + if (handler == null) + return false; + return await handler.SendCreativeInventoryAction(slot, itemType, count, nbt); } /// @@ -2110,9 +2149,11 @@ public bool DoCreativeGive(int slot, ItemType itemType, int count, Dictionary /// 0 for left arm, 1 for right arm /// TRUE if animation successfully done - public bool DoAnimation(int animation) + public async Task DoAnimationAsync(int animation) { - return InvokeOnMainThread(() => handler.SendAnimation(animation, playerEntityID)); + if (handler == null) + return false; + return await handler.SendAnimation(animation, playerEntityID); } /// @@ -2121,18 +2162,25 @@ public bool DoAnimation(int animation) /// Window ID /// TRUE if the window was successfully closed /// Sending close window for inventory 0 can cause server to update our inventory if there are any item in the crafting area - public bool CloseInventory(int windowId) + public async Task CloseInventoryAsync(int windowId) { - if (InvokeRequired) - return InvokeOnMainThread(() => CloseInventory(windowId)); + if (handler == null) + return false; - if (inventories.ContainsKey(windowId)) + bool needCloseWindow = false; + lock (inventoryLock) { - if (windowId != 0) - inventories.Remove(windowId); - return handler.SendCloseWindow(windowId); + if (inventories.ContainsKey(windowId)) + { + if (windowId != 0) + inventories.Remove(windowId); + needCloseWindow = true; + } } - return false; + if (needCloseWindow) + return await handler.SendCloseWindow(windowId); + else + return false; } /// @@ -2144,11 +2192,11 @@ public bool ClearInventories() if (!inventoryHandlingEnabled) return false; - if (InvokeRequired) - return InvokeOnMainThread(ClearInventories); - - inventories.Clear(); - inventories[0] = new Container(0, ContainerType.PlayerInventory, "Player Inventory"); + lock (inventoryLock) + { + inventories.Clear(); + inventories[0] = new Container(0, ContainerType.PlayerInventory, Translations.cmd_inventory_player_inventory); + } return true; } @@ -2159,20 +2207,20 @@ public bool ClearInventories() /// Type of interaction (interact, attack...) /// Hand.MainHand or Hand.OffHand /// TRUE if interaction succeeded - public bool InteractEntity(int entityID, InteractType type, Hand hand = Hand.MainHand) + public async Task InteractEntityAsync(int entityID, InteractType type, Hand hand = Hand.MainHand) { - if (InvokeRequired) - return InvokeOnMainThread(() => InteractEntity(entityID, type, hand)); + if (handler == null) + return false; if (entities.ContainsKey(entityID)) { if (type == InteractType.Interact) { - return handler.SendInteractEntity(entityID, (int)type, (int)hand); + return await handler.SendInteractEntity(entityID, (int)type, (int)hand); } else { - return handler.SendInteractEntity(entityID, (int)type); + return await handler.SendInteractEntity(entityID, (int)type); } } else return false; @@ -2184,9 +2232,11 @@ public bool InteractEntity(int entityID, InteractType type, Hand hand = Hand.Mai /// Location to place block to /// Block face (e.g. Direction.Down when clicking on the block below to place this block) /// TRUE if successfully placed - public bool PlaceBlock(Location location, Direction blockFace, Hand hand = Hand.MainHand) + public async Task PlaceBlockAsync(Location location, Direction blockFace, Hand hand = Hand.MainHand) { - return InvokeOnMainThread(() => handler.SendPlayerBlockPlacement((int)hand, location, blockFace, sequenceId)); + if (handler == null) + return false; + return await handler.SendPlayerBlockPlacement((int)hand, location, blockFace, sequenceId); } /// @@ -2195,13 +2245,13 @@ public bool PlaceBlock(Location location, Direction blockFace, Hand hand = Hand. /// Location of block to dig /// Also perform the "arm swing" animation /// Also look at the block before digging - public bool DigBlock(Location location, bool swingArms = true, bool lookAtBlock = true) + public async Task DigBlockAsync(Location location, bool swingArms = true, bool lookAtBlock = true) { - if (!GetTerrainEnabled()) + if (handler == null) return false; - if (InvokeRequired) - return InvokeOnMainThread(() => DigBlock(location, swingArms, lookAtBlock)); + if (!GetTerrainEnabled()) + return false; // TODO select best face from current player location Direction blockFace = Direction.Down; @@ -2212,9 +2262,9 @@ public bool DigBlock(Location location, bool swingArms = true, bool lookAtBlock // Send dig start and dig end, will need to wait for server response to know dig result // See https://wiki.vg/How_to_Write_a_Client#Digging for more details - return handler.SendPlayerDigging(0, location, blockFace, sequenceId) - && (!swingArms || DoAnimation((int)Hand.MainHand)) - && handler.SendPlayerDigging(2, location, blockFace, sequenceId); + return (await handler.SendPlayerDigging(0, location, blockFace, sequenceId)) + && (!swingArms || await DoAnimationAsync((int)Hand.MainHand)) + && await handler.SendPlayerDigging(2, location, blockFace, sequenceId); } /// @@ -2222,16 +2272,16 @@ public bool DigBlock(Location location, bool swingArms = true, bool lookAtBlock /// /// Slot to activate (0 to 8) /// TRUE if the slot was changed - public bool ChangeSlot(short slot) + public async Task ChangeSlotAsync(short slot) { - if (slot < 0 || slot > 8) + if (handler == null) return false; - if (InvokeRequired) - return InvokeOnMainThread(() => ChangeSlot(slot)); + if (slot < 0 || slot > 8) + return false; CurrentSlot = Convert.ToByte(slot); - return handler.SendHeldItemChange(slot); + return await handler.SendHeldItemChange(slot); } /// @@ -2242,19 +2292,23 @@ public bool ChangeSlot(short slot) /// text two /// text three /// text1 four - public bool UpdateSign(Location location, string line1, string line2, string line3, string line4) + public async Task UpdateSignAsync(Location location, string line1, string line2, string line3, string line4) { + if (handler == null) + return false; // TODO Open sign editor first https://wiki.vg/Protocol#Open_Sign_Editor - return InvokeOnMainThread(() => handler.SendUpdateSign(location, line1, line2, line3, line4)); + return await handler.SendUpdateSign(location, line1, line2, line3, line4); } /// /// Select villager trade /// /// The slot of the trade, starts at 0. - public bool SelectTrade(int selectedSlot) + public async Task SelectTradeAsync(int selectedSlot) { - return InvokeOnMainThread(() => handler.SelectTrade(selectedSlot)); + if (handler == null) + return false; + return await handler.SelectTrade(selectedSlot); } /// @@ -2264,9 +2318,11 @@ public bool SelectTrade(int selectedSlot) /// command /// command block mode /// command block flags - public bool UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags) + public async Task UpdateCommandBlockAsync(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags) { - return InvokeOnMainThread(() => handler.UpdateCommandBlock(location, command, mode, flags)); + if (handler == null) + return false; + return await handler.UpdateCommandBlock(location, command, mode, flags); } /// @@ -2274,11 +2330,11 @@ public bool UpdateCommandBlock(Location location, string command, CommandBlockMo /// /// Player to teleport to /// Teleporting to other entityies is NOT implemented yet - public bool Spectate(Entity entity) + public async Task SpectateAsync(Entity entity) { if (entity.Type == EntityType.Player) { - return SpectateByUUID(entity.UUID); + return await SpectateByUuidAsync(entity.UUID); } else { @@ -2290,13 +2346,14 @@ public bool Spectate(Entity entity) /// Teleport to player/entity in spectator mode /// /// UUID of player/entity to teleport to - public bool SpectateByUUID(Guid UUID) + public async Task SpectateByUuidAsync(Guid UUID) { + if (handler == null) + return false; + if (GetGamemode() == 3) { - if (InvokeRequired) - return InvokeOnMainThread(() => SpectateByUUID(UUID)); - return handler.SendSpectate(UUID); + return await handler.SendSpectate(UUID); } else { @@ -2318,18 +2375,8 @@ public bool SpectateByUUID(Guid UUID) /// 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 { @@ -2339,15 +2386,15 @@ private void DispatchBotEvent(Action action, IEnumerable? botL { if (e is not ThreadAbortException) { - //Retrieve parent method name to determine which event caused the exception + // Retrieve parent method name to determine which event caused the exception System.Diagnostics.StackFrame frame = new(1); System.Reflection.MethodBase method = frame.GetMethod()!; string parentMethodName = method.Name; - //Display a meaningful error message to help debugging the ChatBot + // Display a meaningful error message to help debugging the ChatBot Log.Error(parentMethodName + ": Got error from " + bot.ToString() + ": " + e.ToString()); } - else throw; //ThreadAbortException should not be caught here as in can happen when disconnecting from server + else throw; // ThreadAbortException should not be caught here as in can happen when disconnecting from server } } } @@ -2362,22 +2409,30 @@ private void DispatchBotEvent(Action action, IEnumerable? botL /// A copy of Packet Data /// The packet is login phase or playing phase /// The packet is received from server or sent by client - public void OnNetworkPacket(int packetID, List packetData, bool isLogin, bool isInbound) + public async Task OnNetworkPacketAsync(int packetID, byte[] packetData, bool isLogin, bool isInbound) { - DispatchBotEvent(bot => bot.OnNetworkPacket(packetID, packetData, isLogin, isInbound)); + if (networkPacketCaptureEnabled) + { + await TriggerEvent(McClientEventType.NetworkPacket, + new Tuple(packetID, packetData, isLogin, isInbound)); + DispatchBotEvent(bot => bot.OnNetworkPacket(packetID, new(packetData), isLogin, isInbound)); + } } /// /// Called when a server was successfully joined /// - public void OnGameJoined() + public async Task OnGameJoinedAsync() { + if (handler == null) + return; + string? bandString = Config.Main.Advanced.BrandInfo.ToBrandString(); - if (!String.IsNullOrWhiteSpace(bandString)) - handler.SendBrandInfo(bandString.Trim()); + if (!string.IsNullOrWhiteSpace(bandString)) + await handler.SendBrandInfo(bandString.Trim()); if (Config.MCSettings.Enabled) - handler.SendClientSettings( + await handler.SendClientSettings( Config.MCSettings.Locale, Config.MCSettings.RenderDistance, (byte)Config.MCSettings.Difficulty, @@ -2394,20 +2449,17 @@ public void OnGameJoined() Log.Info(Translations.extra_inventory_enabled); } - ClearInventories(); - + await TriggerEvent(McClientEventType.GameJoin, null); DispatchBotEvent(bot => bot.AfterGameJoined()); - ConsoleIO.InitCommandList(dispatcher); + await ConsoleIO.InitCommandList(dispatcher); } /// /// Called when the player respawns, which happens on login, respawn and world change. /// - public void OnRespawn() + public async Task OnRespawnAsync() { - ClearTasks(); - if (terrainAndMovementsRequested) { terrainAndMovementsEnabled = true; @@ -2422,6 +2474,8 @@ public void OnRespawn() entities.Clear(); ClearInventories(); + + await TriggerEvent(McClientEventType.Respawn, null); DispatchBotEvent(bot => bot.OnRespawn()); } @@ -2440,7 +2494,7 @@ public bool ClientIsMoving() /// Current goal of movement. Location.Zero if not set. public Location GetCurrentMovementGoal() { - return (ClientIsMoving() || path == null) ? Location.Zero : path.Last(); + return (!ClientIsMoving() || path == null) ? Location.Zero : path.Last(); } /// @@ -2483,17 +2537,10 @@ public void SetMovementSpeed(MovementType newSpeed) /// /// The new location /// If true, the location is relative to the current location - public void UpdateLocation(Location location, bool relative) + public void UpdateLocation(Location location) { - lock (locationLock) - { - if (relative) - { - this.location += location; - } - else this.location = location; - locationReceived = true; - } + this.location = location; + locationReceived = true; } /// @@ -2507,7 +2554,7 @@ public void UpdateLocation(Location location, float yaw, float pitch) { _yaw = yaw; _pitch = pitch; - UpdateLocation(location, false); + UpdateLocation(location); } /// @@ -2572,7 +2619,7 @@ public void UpdateLocation(Location location, Direction direction) /// Received chat/system message from the server /// /// Message received - public void OnTextReceived(ChatMessage message) + public async Task OnTextReceivedAsync(ChatMessage message) { UpdateKeepAlive(); @@ -2599,6 +2646,8 @@ public void OnTextReceived(ChatMessage message) foreach (string link in links) Log.Chat(string.Format(Translations.mcc_link, link)); + await TriggerEvent(McClientEventType.TextReceive, + new Tuple(messageText, message.content)); DispatchBotEvent(bot => bot.GetText(messageText)); DispatchBotEvent(bot => bot.GetText(messageText, message.content)); } @@ -2616,7 +2665,7 @@ public void OnServerKeepAlive() /// /// The inventory /// Inventory ID - public void OnInventoryOpen(int inventoryID, Container inventory) + public async Task OnInventoryOpenAsync(int inventoryID, Container inventory) { inventories[inventoryID] = inventory; @@ -2624,6 +2673,8 @@ public void OnInventoryOpen(int inventoryID, Container inventory) { Log.Info(string.Format(Translations.extra_inventory_open, inventoryID, inventory.Title)); Log.Info(Translations.extra_inventory_interact); + + await TriggerEvent(McClientEventType.InventoryOpen, inventoryID); DispatchBotEvent(bot => bot.OnInventoryOpen(inventoryID)); } } @@ -2632,9 +2683,9 @@ public void OnInventoryOpen(int inventoryID, Container inventory) /// When an inventory is close /// /// Inventory ID - public void OnInventoryClose(int inventoryID) + public async Task OnInventoryCloseAsync(int inventoryID) { - if (inventories.ContainsKey(inventoryID)) + lock (inventoryLock) { if (inventoryID == 0) inventories[0].Items.Clear(); // Don't delete player inventory @@ -2645,6 +2696,8 @@ public void OnInventoryClose(int inventoryID) if (inventoryID != 0) { Log.Info(string.Format(Translations.extra_inventory_close, inventoryID)); + + await TriggerEvent(McClientEventType.InventoryClose, inventoryID); DispatchBotEvent(bot => bot.OnInventoryClose(inventoryID)); } } @@ -2657,18 +2710,17 @@ public void OnInventoryClose(int inventoryID) /// Inventory ID /// Property ID /// Property Value - public void OnWindowProperties(byte inventoryID, short propertyId, short propertyValue) + public async Task OnWindowPropertiesAsync(byte inventoryID, short propertyId, short propertyValue) { - if (!inventories.ContainsKey(inventoryID)) + if (!inventories.TryGetValue(inventoryID, out Container? inventory)) return; - Container inventory = inventories[inventoryID]; - - if (inventory.Properties.ContainsKey(propertyId)) - inventory.Properties.Remove(propertyId); + inventory.Properties.Remove(propertyId); inventory.Properties.Add(propertyId, propertyValue); + await TriggerEvent(McClientEventType.InventoryProperties, + new Tuple(inventoryID, propertyId, propertyValue)); DispatchBotEvent(bot => bot.OnInventoryProperties(inventoryID, propertyId, propertyValue)); if (inventory.Type == ContainerType.Enchantment) @@ -2717,21 +2769,25 @@ public void OnWindowProperties(byte inventoryID, short propertyId, short propert Log.Info(sb.ToString()); - lastEnchantment = new(); - lastEnchantment.TopEnchantment = topEnchantment; - lastEnchantment.MiddleEnchantment = middleEnchantment; - lastEnchantment.BottomEnchantment = bottomEnchantment; + lastEnchantment = new() + { + TopEnchantment = topEnchantment, + MiddleEnchantment = middleEnchantment, + BottomEnchantment = bottomEnchantment, + + Seed = inventory.Properties[3], - lastEnchantment.Seed = inventory.Properties[3]; + TopEnchantmentLevel = topEnchantmentLevel, + MiddleEnchantmentLevel = middleEnchantmentLevel, + BottomEnchantmentLevel = bottomEnchantmentLevel, - lastEnchantment.TopEnchantmentLevel = topEnchantmentLevel; - lastEnchantment.MiddleEnchantmentLevel = middleEnchantmentLevel; - lastEnchantment.BottomEnchantmentLevel = bottomEnchantmentLevel; + TopEnchantmentLevelRequirement = topEnchantmentLevelRequirement, + MiddleEnchantmentLevelRequirement = middleEnchantmentLevelRequirement, + BottomEnchantmentLevelRequirement = bottomEnchantmentLevelRequirement + }; - lastEnchantment.TopEnchantmentLevelRequirement = topEnchantmentLevelRequirement; - lastEnchantment.MiddleEnchantmentLevelRequirement = middleEnchantmentLevelRequirement; - lastEnchantment.BottomEnchantmentLevelRequirement = bottomEnchantmentLevelRequirement; + await TriggerEvent(McClientEventType.Enchantments, lastEnchantment); DispatchBotEvent(bot => bot.OnEnchantments( // Enchantments @@ -2759,12 +2815,13 @@ public void OnWindowProperties(byte inventoryID, short propertyId, short propert /// /// Inventory ID /// Item list, key = slot ID, value = Item information - public void OnWindowItems(byte inventoryID, Dictionary itemList, int stateId) + public async Task OnWindowItemsAsync(byte inventoryID, Dictionary itemList, int stateId) { - if (inventories.ContainsKey(inventoryID)) + if (inventories.TryGetValue(inventoryID, out Container? container)) { - inventories[inventoryID].Items = itemList; - inventories[inventoryID].StateID = stateId; + container.Items = itemList; + container.StateID = stateId; + await TriggerEvent(McClientEventType.InventoryUpdate, inventoryID); DispatchBotEvent(bot => bot.OnInventoryUpdate(inventoryID)); } } @@ -2775,38 +2832,40 @@ public void OnWindowItems(byte inventoryID, Dictionary item /// Window ID /// Slot ID /// Item (may be null for empty slot) - public void OnSetSlot(byte inventoryID, short slotID, Item? item, int stateId) + public async Task OnSetSlotAsync(byte inventoryID, short slotID, Item? item, int stateId) { - if (inventories.ContainsKey(inventoryID)) - inventories[inventoryID].StateID = stateId; - - // Handle inventoryID -2 - Add item to player inventory without animation - if (inventoryID == 254) - inventoryID = 0; - // Handle cursor item - if (inventoryID == 255 && slotID == -1) + lock (inventoryLock) { - inventoryID = 0; // Prevent key not found for some bots relied to this event - if (inventories.ContainsKey(0)) + if (inventories.TryGetValue(inventoryID, out Container? container)) + container.StateID = stateId; + + // Handle inventoryID -2 - Add item to player inventory without animation + if (inventoryID == 254) + inventoryID = 0; + // Handle cursor item + if (inventoryID == 255 && slotID == -1) { - if (item != null) - inventories[0].Items[-1] = item; - else - inventories[0].Items.Remove(-1); + inventoryID = 0; // Prevent key not found for some bots relied to this event + if (inventories.ContainsKey(0)) + { + if (item != null) + inventories[0].Items[-1] = item; + else + inventories[0].Items.Remove(-1); + } } - } - else - { - if (inventories.ContainsKey(inventoryID)) + else { - if (item == null || item.IsEmpty) + if (inventories.ContainsKey(inventoryID)) { - if (inventories[inventoryID].Items.ContainsKey(slotID)) + if (item == null || item.IsEmpty) inventories[inventoryID].Items.Remove(slotID); + else + inventories[inventoryID].Items[slotID] = item; } - else inventories[inventoryID].Items[slotID] = item; } } + await TriggerEvent(McClientEventType.InventoryUpdate, inventoryID); DispatchBotEvent(bot => bot.OnInventoryUpdate(inventoryID)); } @@ -2823,7 +2882,7 @@ public void OnReceivePlayerEntityID(int EntityID) /// Triggered when a new player joins the game /// /// player info - public void OnPlayerJoin(PlayerInfo player) + public async Task OnPlayerJoinAsync(PlayerInfo player) { //Ignore placeholders eg 0000tab# from TabListPlus if (!ChatBot.IsValidName(player.Name)) @@ -2841,6 +2900,7 @@ public void OnPlayerJoin(PlayerInfo player) onlinePlayers[player.Uuid] = player; } + await TriggerEvent(McClientEventType.PlayerJoin, player); DispatchBotEvent(bot => bot.OnPlayerJoin(player.Uuid, player.Name)); } @@ -2848,19 +2908,18 @@ public void OnPlayerJoin(PlayerInfo player) /// Triggered when a player has left the game /// /// UUID of the player - public void OnPlayerLeave(Guid uuid) + public async Task OnPlayerLeaveAsync(Guid uuid) { - string? username = null; + PlayerInfo? playerInfo = null; lock (onlinePlayers) { - if (onlinePlayers.ContainsKey(uuid)) - { - username = onlinePlayers[uuid].Name; + if (onlinePlayers.TryGetValue(uuid, out playerInfo)) onlinePlayers.Remove(uuid); - } } + await TriggerEvent(McClientEventType.PlayerLeave, + new Tuple(uuid, playerInfo)); DispatchBotEvent(bot => bot.OnPlayerLeave(uuid, username)); } @@ -2869,12 +2928,14 @@ public void OnPlayerLeave(Guid uuid) /// /// Victim's entity /// Killer's entity - public void OnPlayerKilled(int killerEntityId, string chatMessage) + public async Task OnPlayerKilledAsync(int killerEntityId, string chatMessage) { - if (!entities.ContainsKey(killerEntityId)) + if (!entities.TryGetValue(killerEntityId, out Entity? killer)) return; - DispatchBotEvent(bot => bot.OnKilled(entities[killerEntityId], chatMessage)); + await TriggerEvent(McClientEventType.PlayerKilled, + new Tuple(killer, chatMessage)); + DispatchBotEvent(bot => bot.OnKilled(killer, chatMessage)); } /// @@ -2904,38 +2965,46 @@ public void OnPluginChannelMessage(string channel, byte[] data) } } - if (registeredBotPluginChannels.ContainsKey(channel)) + if (registeredBotPluginChannels.TryGetValue(channel, out List? channelList)) { - DispatchBotEvent(bot => bot.OnPluginMessage(channel, data), registeredBotPluginChannels[channel]); + DispatchBotEvent(bot => bot.OnPluginMessage(channel, data), channelList); } } /// /// Called when an entity spawned /// - public void OnSpawnEntity(Entity entity) + public async Task OnSpawnEntity(Entity entity) { // The entity should not already exist, but if it does, let's consider the previous one is being destroyed if (entities.ContainsKey(entity.ID)) - OnDestroyEntities(new[] { entity.ID }); + await OnDestroyEntities(new[] { entity.ID }); entities.Add(entity.ID, entity); + + await TriggerEvent(McClientEventType.EntitySpawn, entity); DispatchBotEvent(bot => bot.OnEntitySpawn(entity)); } /// /// Called when an entity effects /// - public void OnEntityEffect(int entityid, Effects effect, int amplifier, int duration, byte flags, bool hasFactorData, Dictionary? factorCodec) + public async Task OnEntityEffect(int entityid, Effect effect) { - if (entities.ContainsKey(entityid)) - DispatchBotEvent(bot => bot.OnEntityEffect(entities[entityid], effect, amplifier, duration, flags)); - } + if (!entities.TryGetValue(entityid, out Entity? entity)) + return; + await TriggerEvent(McClientEventType.EntityEffect, + new Tuple(entity, effect)); + + byte flag = (byte)((effect.IsFromBeacon ? 1 : 0) | (effect.ShowParticles ? 2 : 0) | (effect.ShowIcon ? 2 : 0)); + DispatchBotEvent(bot => bot.OnEntityEffect(entity, effect.Type, effect.EffectLevel - 1, effect.DurationInTick, flag)); + } + /// /// Called when a player spawns or enters the client's render distance /// - public void OnSpawnPlayer(int entityID, Guid uuid, Location location, byte yaw, byte pitch) + public async Task OnSpawnPlayer(int entityID, Guid uuid, Location location, byte yaw, byte pitch) { Entity playerEntity; if (onlinePlayers.TryGetValue(uuid, out PlayerInfo? player)) @@ -2945,7 +3014,7 @@ public void OnSpawnPlayer(int entityID, Guid uuid, Location location, byte yaw, } else playerEntity = new(entityID, EntityType.Player, location, uuid, null, yaw, pitch); - OnSpawnEntity(playerEntity); + await OnSpawnEntity(playerEntity); } /// @@ -2954,16 +3023,17 @@ public void OnSpawnPlayer(int entityID, Guid uuid, Location location, byte yaw, /// Entity ID /// Equipment slot. 0: main hand, 1: off hand, 2-5: armor slot (2: boots, 3: leggings, 4: chestplate, 5: helmet) /// Item) - public void OnEntityEquipment(int entityid, int slot, Item? item) + public async Task OnEntityEquipment(int entityid, int slot, Item? item) { - if (entities.ContainsKey(entityid)) + if (entities.TryGetValue(entityid, out Entity? entity)) { - Entity entity = entities[entityid]; - if (entity.Equipment.ContainsKey(slot)) - entity.Equipment.Remove(slot); + entity.Equipment.Remove(slot); if (item != null) entity.Equipment[slot] = item; - DispatchBotEvent(bot => bot.OnEntityEquipment(entities[entityid], slot, item)); + + await TriggerEvent(McClientEventType.EntityEquipment, + new Tuple(entity, slot, item)); + DispatchBotEvent(bot => bot.OnEntityEquipment(entity, slot, item)); } } @@ -2973,31 +3043,34 @@ public void OnEntityEquipment(int entityid, int slot, Item? item) /// Player Name /// Player UUID (Empty for initial gamemode on login) /// New Game Mode (0: Survival, 1: Creative, 2: Adventure, 3: Spectator). - public void OnGamemodeUpdate(Guid uuid, int gamemode) + public async Task OnGamemodeUpdate(Guid uuid, int gamemode) { // Initial gamemode on login if (uuid == Guid.Empty) this.gamemode = gamemode; // Further regular gamemode change events - if (onlinePlayers.ContainsKey(uuid)) + if (onlinePlayers.TryGetValue(uuid, out PlayerInfo? playerInfo)) { - string playerName = onlinePlayers[uuid].Name; - if (playerName == username) + if (playerInfo.Name == username) this.gamemode = gamemode; - DispatchBotEvent(bot => bot.OnGamemodeUpdate(playerName, uuid, gamemode)); + + await TriggerEvent(McClientEventType.GamemodeUpdate, + new Tuple(playerInfo, gamemode)); + DispatchBotEvent(bot => bot.OnGamemodeUpdate(playerInfo.Name, uuid, gamemode)); } } /// /// Called when entities dead/despawn. /// - public void OnDestroyEntities(int[] Entities) + public async Task OnDestroyEntities(int[] Entities) { foreach (int a in Entities) { if (entities.TryGetValue(a, out Entity? entity)) { + await TriggerEvent(McClientEventType.EntityDespawn, entity); DispatchBotEvent(bot => bot.OnEntityDespawn(entity)); entities.Remove(a); } @@ -3012,18 +3085,17 @@ public void OnDestroyEntities(int[] Entities) /// /// /// - public void OnEntityPosition(int EntityID, Double Dx, Double Dy, Double Dz, bool onGround) + public async Task OnEntityPosition(int EntityID, Double Dx, Double Dy, Double Dz, bool onGround) { - if (entities.ContainsKey(EntityID)) + if (entities.TryGetValue(EntityID, out Entity? entity)) { - Location L = entities[EntityID].Location; - L.X += Dx; - L.Y += Dy; - L.Z += Dz; - entities[EntityID].Location = L; - DispatchBotEvent(bot => bot.OnEntityMove(entities[EntityID])); - } + entity.Location.X += Dx; + entity.Location.Y += Dy; + entity.Location.Z += Dz; + await TriggerEvent(McClientEventType.EntityMove, entity); + DispatchBotEvent(bot => bot.OnEntityMove(entity)); + } } /// @@ -3034,13 +3106,14 @@ public void OnEntityPosition(int EntityID, Double Dx, Double Dy, Double Dz, bool /// /// /// - public void OnEntityTeleport(int EntityID, Double X, Double Y, Double Z, bool onGround) + public async Task OnEntityTeleport(int EntityID, Double X, Double Y, Double Z, bool onGround) { - if (entities.ContainsKey(EntityID)) + if (entities.TryGetValue(EntityID, out Entity? entity)) { - Location location = new(X, Y, Z); - entities[EntityID].Location = location; - DispatchBotEvent(bot => bot.OnEntityMove(entities[EntityID])); + entity.Location = new Location(X, Y, Z); + + await TriggerEvent(McClientEventType.EntityMove, entity); + DispatchBotEvent(bot => bot.OnEntityMove(entity)); } } @@ -3049,10 +3122,11 @@ public void OnEntityTeleport(int EntityID, Double X, Double Y, Double Z, bool on /// /// /// - public void OnEntityProperties(int EntityID, Dictionary prop) + public async Task OnEntityProperties(int EntityID, Dictionary prop) { if (EntityID == playerEntityID) { + await TriggerEvent(McClientEventType.PlayerPropertyReceive, prop); DispatchBotEvent(bot => bot.OnPlayerProperty(prop)); } } @@ -3062,10 +3136,11 @@ public void OnEntityProperties(int EntityID, Dictionary prop) /// /// Entity ID /// Status ID - public void OnEntityStatus(int entityID, byte status) + public async Task OnEntityStatus(int entityID, byte status) { if (entityID == playerEntityID) { + await TriggerEvent(McClientEventType.PlayerStatusUpdate, status); DispatchBotEvent(bot => bot.OnPlayerStatus(status)); } } @@ -3075,7 +3150,7 @@ public void OnEntityStatus(int entityID, byte status) /// /// /// - public void OnTimeUpdate(long WorldAge, long TimeOfDay) + public async Task OnTimeUpdate(long WorldAge, long TimeOfDay) { // TimeUpdate sent every server tick hence used as timeout detect UpdateKeepAlive(); @@ -3084,7 +3159,7 @@ public void OnTimeUpdate(long WorldAge, long TimeOfDay) { DateTime currentTime = DateTime.Now; long tickDiff = WorldAge - lastAge; - Double tps = tickDiff / (currentTime - lastTime).TotalSeconds; + double tps = tickDiff / (currentTime - lastTime).TotalSeconds; lastAge = WorldAge; lastTime = currentTime; if (tps <= 20 && tps > 0) @@ -3100,6 +3175,7 @@ public void OnTimeUpdate(long WorldAge, long TimeOfDay) sampleSum += tps; averageTPS = sampleSum / tpsSamples.Count; serverTPS = tps; + await TriggerEvent(McClientEventType.ServerTpsUpdate, tps); DispatchBotEvent(bot => bot.OnServerTpsUpdate(tps)); } } @@ -3108,6 +3184,9 @@ public void OnTimeUpdate(long WorldAge, long TimeOfDay) lastAge = WorldAge; lastTime = DateTime.Now; } + + await TriggerEvent(McClientEventType.TimeUpdate, + new Tuple(WorldAge, TimeOfDay)); DispatchBotEvent(bot => bot.OnTimeUpdate(WorldAge, TimeOfDay)); } @@ -3115,26 +3194,30 @@ public void OnTimeUpdate(long WorldAge, long TimeOfDay) /// Called when client player's health changed, e.g. getting attack /// /// Player current health - public void OnUpdateHealth(float health, int food) + public async Task OnUpdateHealth(float health, int food) { playerHealth = health; playerFoodSaturation = food; + await TriggerEvent(McClientEventType.HealthUpdate, + new Tuple(health, food)); + DispatchBotEvent(bot => bot.OnHealthUpdate(health, food)); + if (health <= 0) { if (Config.Main.Advanced.AutoRespawn) { Log.Info(Translations.mcc_player_dead_respawn); - respawnTicks = 10; + respawnTicks = 20; } else { Log.Info(string.Format(Translations.mcc_player_dead, Config.Main.Advanced.InternalCmdChar.ToLogString())); } + + await TriggerEvent(McClientEventType.Death, null); DispatchBotEvent(bot => bot.OnDeath()); } - - DispatchBotEvent(bot => bot.OnHealthUpdate(health, food)); } /// @@ -3143,10 +3226,13 @@ public void OnUpdateHealth(float health, int food) /// Between 0 and 1 /// Level /// Total Experience - public void OnSetExperience(float Experiencebar, int Level, int TotalExperience) + public async Task OnSetExperience(float Experiencebar, int Level, int TotalExperience) { playerLevel = Level; playerTotalExperience = TotalExperience; + + await TriggerEvent(McClientEventType.ExperienceChange, + new Tuple(Experiencebar, Level, TotalExperience)); DispatchBotEvent(bot => bot.OnSetExperience(Experiencebar, Level, TotalExperience)); } @@ -3156,8 +3242,10 @@ public void OnSetExperience(float Experiencebar, int Level, int TotalExperience) /// Explosion location /// Explosion strength /// Amount of affected blocks - public void OnExplosion(Location location, float strength, int affectedBlocks) + public async Task OnExplosion(Location location, float strength, int affectedBlocks) { + await TriggerEvent(McClientEventType.Explosion, + new Tuple(location, strength, affectedBlocks)); DispatchBotEvent(bot => bot.OnExplosion(location, strength, affectedBlocks)); } @@ -3166,23 +3254,19 @@ public void OnExplosion(Location location, float strength, int affectedBlocks) /// /// player uuid /// Latency - public void OnLatencyUpdate(Guid uuid, int latency) + public async Task OnLatencyUpdate(Guid uuid, int latency) { - if (onlinePlayers.ContainsKey(uuid)) + if (onlinePlayers.TryGetValue(uuid, out PlayerInfo? player)) { - PlayerInfo player = onlinePlayers[uuid]; player.Ping = latency; + + await TriggerEvent(McClientEventType.PlayerLatencyUpdate, + new Tuple(player, latency)); + string playerName = player.Name; - foreach (KeyValuePair ent in entities) - { - if (ent.Value.UUID == uuid && ent.Value.Name == playerName) - { - ent.Value.Latency = latency; - DispatchBotEvent(bot => bot.OnLatencyUpdate(ent.Value, playerName, uuid, latency)); - break; - } - } DispatchBotEvent(bot => bot.OnLatencyUpdate(playerName, uuid, latency)); + if (player.entity != null) + DispatchBotEvent(bot => bot.OnLatencyUpdate(player.entity, playerName, uuid, latency)); } } @@ -3190,9 +3274,11 @@ public void OnLatencyUpdate(Guid uuid, int latency) /// Called when held item change /// /// item slot - public void OnHeldItemChange(byte slot) + public async Task OnHeldItemChange(byte slot) { CurrentSlot = slot; + + await TriggerEvent(McClientEventType.HeldItemChange, slot); DispatchBotEvent(bot => bot.OnHeldItemChange(slot)); } @@ -3210,9 +3296,19 @@ public void OnHeldItemChange(byte slot) /// x offset of the westernmost column /// z offset of the northernmost row /// a byte array of colors on the map - public void OnMapData(int mapid, byte scale, bool trackingPosition, bool locked, List icons, byte columnsUpdated, byte rowsUpdated, byte mapCoulmnX, byte mapCoulmnZ, byte[]? colors) + public async Task OnMapData(MapData mapData) { - DispatchBotEvent(bot => bot.OnMapData(mapid, scale, trackingPosition, locked, icons, columnsUpdated, rowsUpdated, mapCoulmnX, mapCoulmnZ, colors)); + await TriggerEvent(McClientEventType.MapDataReceive, mapData); + DispatchBotEvent(bot => bot.OnMapData(mapData.MapId, + mapData.Scale, + mapData.TrackingPosition, + mapData.Locked, + mapData.Icons, + mapData.ColumnsUpdated, + mapData.RowsUpdated, + mapData.MapCoulmnX, + mapData.MapRowZ, + mapData.Colors)); } /// @@ -3225,9 +3321,17 @@ public void OnMapData(int mapid, byte scale, bool trackingPosition, bool locked, /// Stay /// Fade Out /// json text - public void OnTitle(int action, string titletext, string subtitletext, string actionbartext, int fadein, int stay, int fadeout, string json) + public async Task OnTitle(TitlePacket title) { - DispatchBotEvent(bot => bot.OnTitle(action, titletext, subtitletext, actionbartext, fadein, stay, fadeout, json)); + await TriggerEvent(McClientEventType.TitleReceive, title); + DispatchBotEvent(bot => bot.OnTitle(title.Action, + title.TitleText, + title.SubtitleText, + title.ActionbarText, + title.FadeIn, + title.Stay, + title.FadeOut, + title.JsonText)); } /// @@ -3263,10 +3367,10 @@ public void OnUpdateScore(string entityname, int action, string objectivename, i /// The health of the entity public void OnEntityHealth(int entityID, float health) { - if (entities.ContainsKey(entityID)) + if (entities.TryGetValue(entityID, out Entity? entity)) { - entities[entityID].Health = health; - DispatchBotEvent(bot => bot.OnEntityHealth(entities[entityID], health)); + entity.Health = health; + DispatchBotEvent(bot => bot.OnEntityHealth(entity, health)); } } @@ -3277,9 +3381,8 @@ public void OnEntityHealth(int entityID, float health) /// The metadata of the entity public void OnEntityMetadata(int entityID, Dictionary metadata) { - if (entities.ContainsKey(entityID)) + if (entities.TryGetValue(entityID, out Entity? entity)) { - Entity entity = entities[entityID]; entity.Metadata = metadata; if (entity.Type.ContainsItem() && metadata.TryGetValue(7, out object? itemObj) && itemObj != null && itemObj.GetType() == typeof(Item)) { @@ -3325,9 +3428,8 @@ public void OnTradeList(int windowID, List trades, VillagerInfo v /// Destroy stage, maximum 255 public void OnBlockBreakAnimation(int entityId, Location location, byte stage) { - if (entities.ContainsKey(entityId)) + if (entities.TryGetValue(entityId, out Entity? entity)) { - Entity entity = entities[entityId]; DispatchBotEvent(bot => bot.OnBlockBreakAnimation(entity, location, stage)); } } @@ -3339,9 +3441,8 @@ public void OnBlockBreakAnimation(int entityId, Location location, byte stage) /// 0 = LMB, 1 = RMB (RMB Corrent not work) public void OnEntityAnimation(int entityID, byte animation) { - if (entities.ContainsKey(entityID)) + if (entities.TryGetValue(entityID, out Entity? entity)) { - Entity entity = entities[entityID]; DispatchBotEvent(bot => bot.OnEntityAnimation(entity, animation)); } } @@ -3439,9 +3540,9 @@ public void OnAutoCompleteDone(int transactionId, string[] result) /// Id of the clicked button /// True if packet was successfully sent - public bool ClickContainerButton(int windowId, int buttonId) + public async Task ClickContainerButton(int windowId, int buttonId) { - return handler.ClickContainerButton(windowId, buttonId); + return await handler!.ClickContainerButton(windowId, buttonId); } #endregion diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index b0faada9d7..9b9b650784 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -29,7 +29,6 @@ - diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 503f4f4039..1eec5d6f82 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -3,10 +3,13 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; +using System.Net.Sockets; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; +using MinecraftClient.Commands; using MinecraftClient.Inventory.ItemPalettes; using MinecraftClient.Mapping.BlockPalettes; using MinecraftClient.Mapping.EntityPalettes; @@ -14,6 +17,7 @@ using MinecraftClient.Protocol.Handlers.Forge; using MinecraftClient.Protocol.ProfileKey; using MinecraftClient.Protocol.Session; +using MinecraftClient.Proxy; using MinecraftClient.Scripting; using MinecraftClient.WinAPI; using Tomlet; @@ -40,7 +44,7 @@ namespace MinecraftClient /// static class Program { - private static McClient? client; + private static McClient? McClient; public static string[]? startupargs; public static CultureInfo ActualCulture = CultureInfo.CurrentCulture; @@ -49,38 +53,25 @@ static class Program public const string MCHighestVersion = "1.19.2"; public static readonly string? BuildInfo = null; - private static Tuple? offlinePrompt = null; private static bool useMcVersionOnce = false; private static string settingsIniPath = "MinecraftClient.ini"; + private static bool RestartKeepSettings = false; + private static int RestartAfter = -1, Exitcode = 0; + + private static Task McClientInit = Task.CompletedTask; + private static CancellationTokenSource McClientCancelTokenSource = new(); + /// /// The main entry point of Minecraft Console Client /// - static void Main(string[] args) + static async Task Main(string[] args) { - 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; - } + // Take advantage of Windows 10 / Mac / Linux UTF-8 console + if (!OperatingSystem.IsWindows() || OperatingSystem.IsWindowsVersionAtLeast(10)) + Console.OutputEncoding = Console.InputEncoding = Encoding.UTF8; - // Fix issue #2119 - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - }); - - //Setup ConsoleIO + // Setup ConsoleIO ConsoleIO.LogPrefix = "§8[MCC] "; if (args.Length >= 1 && args[^1] == "BasicIO" || args.Length >= 1 && args[^1] == "BasicIO-NoColor") { @@ -89,7 +80,7 @@ static void Main(string[] args) ConsoleIO.BasicIO_NoColor = true; } ConsoleIO.BasicIO = true; - args = args.Where(o => !Object.ReferenceEquals(o, args[^1])).ToArray(); + args = args.Where(o => !ReferenceEquals(o, args[^1])).ToArray(); } if (!ConsoleIO.BasicIO) @@ -97,94 +88,197 @@ static void Main(string[] args) ConsoleIO.WriteLine($"Minecraft Console Client v{Version} - for MC {MCLowestVersion} to {MCHighestVersion} - Github.com/MCCTeam"); - //Build information to facilitate processing of bug reports + // Build information to facilitate processing of bug reports if (BuildInfo != null) ConsoleIO.WriteLineFormatted("§8" + BuildInfo); - //Debug input ? - if (args.Length == 1 && args[0] == "--keyboard-debug") + ConsoleIO.WriteLine("\u001b[33m\u001b[46m▀\u001b[0m"); + + string? specifiedSettingFile = null; + if (args.Length >= 1 && File.Exists(args[0]) && Settings.ToLowerIfNeed(Path.GetExtension(args[0])) == ".ini") { - ConsoleIO.WriteLine("Keyboard debug mode: Press any key to display info"); - ConsoleIO.DebugReadInput(); + specifiedSettingFile = args[0]; + // remove ini configuration file from arguments array + string[] args_tmp = new string[args.Length - 1]; + Array.Copy(args, 1, args_tmp, 0, args.Length - 1); + args = args_tmp; } - //Process ini configuration file + if (HandleOtherArguments(ref args)) + return; + + // Load cached sessions from disk if necessary + AsyncTaskHandler.CacheSessionReader = Task.Run(SessionCache.ReadCacheSessionAsync); + + // Fix issue #2119 + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + if (!ProcessConfigurationFile(specifiedSettingFile)) + return; + + // Load command-line setting arguments + if (args.Length >= 1) { - bool loadSucceed, needWriteDefaultSetting, newlyGenerated = false; - if (args.Length >= 1 && File.Exists(args[0]) && Settings.ToLowerIfNeed(Path.GetExtension(args[0])) == ".ini") + try { - (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(); + Settings.LoadArguments(args); } - else if (File.Exists("MinecraftClient.ini")) + catch (ArgumentException e) { - (loadSucceed, needWriteDefaultSetting) = Settings.LoadFromFile("MinecraftClient.ini"); + ConsoleIO.WriteLine(string.Format(Translations.mcc_load_from_args_fail, e.Message)); } - else - { - loadSucceed = true; - needWriteDefaultSetting = true; - newlyGenerated = true; + } + + // Setup exit cleaning code + ExitCleanUp.Add(() => { DoExit(); }); + + McClientInit = Task.Run(McClient.LoadCommandsAndChatbots); + + if (!string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle)) + { + InternalConfig.Username = "New Window"; + Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle); + } + + //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.ToString())); + } + { // 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.ToString())); } + } + + 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; + while (string.IsNullOrWhiteSpace(InternalConfig.Account.Login) && !useBrowser) + { + ConsoleIO.WriteLine(ConsoleIO.BasicIO ? Translations.mcc_login_basic_io : Translations.mcc_login, ignoreSuppress: true); + InternalConfig.Account.Login = ConsoleIO.ReadLine().Trim(); + if (!string.IsNullOrWhiteSpace(InternalConfig.Account.Login)) + break; + } + InternalConfig.Username = InternalConfig.Account.Login; + + if (string.IsNullOrWhiteSpace(InternalConfig.Account.Password) && !useBrowser + && (Config.Main.Advanced.SessionCache == CacheType.none || + (AsyncTaskHandler.CacheSessionReader != null && SessionCache.ContainsSession(InternalConfig.Account.Login)))) + RequestPassword(); + ConsoleIO.SuppressPrinting(false); - if (needWriteDefaultSetting) + startupargs = args; + + // Check for updates + AsyncTaskHandler.CheckUpdate = Task.Run(async () => + { + bool needPromptUpdate = true; + if (UpgradeHelper.CompareVersionInfo(Settings.Config.Head.CurrentVersion, Settings.Config.Head.LatestVersion)) { - 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); + needPromptUpdate = false; + ConsoleIO.WriteLineFormatted("§e" + string.Format(Translations.mcc_has_update, UpgradeHelper.GithubReleaseUrl), true); } - else if (!loadSucceed) + await Task.Delay(20 * 1000); + await UpgradeHelper.DoCheckUpdate(CancellationToken.None); + if (needPromptUpdate) { - ConsoleInteractive.ConsoleReader.StopReadThread(); - string command = " "; - while (command.Length > 0) + if (UpgradeHelper.CompareVersionInfo(Settings.Config.Head.CurrentVersion, Settings.Config.Head.LatestVersion)) { - ConsoleIO.WriteLine(string.Empty); - ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_invaild_config, Config.Main.Advanced.InternalCmdChar.ToLogString())); - ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true); - command = ConsoleInteractive.ConsoleReader.RequestImmediateInput().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; - } + ConsoleIO.WriteLineFormatted("§e" + string.Format(Translations.mcc_has_update, UpgradeHelper.GithubReleaseUrl), true); } - return; } - else + }); + + HttpClient loginHttpClient = new(); + + while (true) + { + try { await InitializeClient(loginHttpClient); } + catch (Exception e) + { + ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_unhandled_exception); + ConsoleIO.WriteLine($"{e.GetType().Name}:{e.GetFullMessage()}"); + string? stackTrace = e.StackTrace; + if (stackTrace != null) + ConsoleIO.WriteLine(stackTrace); + } + + if (McClient != null) + { + McClient.Disconnect(); + ConsoleIO.Reset(); + McClient = null; + } + + if (RestartAfter < 0 && FailureInfo.hasFailure) + RestartAfter = HandleFailure(); + + if (RestartAfter < 0) + break; + + if (RestartAfter > 0) { - //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 + ConsoleIO.WriteLine(string.Format(Translations.mcc_restart_delay, (double)RestartAfter / 1000.0)); + Thread.Sleep(RestartAfter); } + + ConsoleIO.WriteLine(Translations.mcc_restart); + + ReloadSettings(RestartKeepSettings); + + Exitcode = 0; + RestartAfter = -1; + RestartKeepSettings = false; + FailureInfo.hasFailure = false; + McClientCancelTokenSource = new CancellationTokenSource(); + } + + DoExit(); + } + + /// + /// Handles command line arguments other than settings. + /// + /// Arguments + /// Whether the program needs to be exited. + /// + private static bool HandleOtherArguments(ref string[] args) + { + // Debug input ? + if (args.Length == 1 && args[0] == "--keyboard-debug") + { + ConsoleIO.WriteLine("Keyboard debug mode: Press any key to display info"); + ConsoleIO.DebugReadInput(); + return true; } - //Other command-line arguments + // Other command-line arguments if (args.Length >= 1) { if (args.Contains("--help")) @@ -195,19 +289,19 @@ static void Main(string[] args) Console.WriteLine("MinecraftClient.exe --setting=value [--other settings]"); Console.WriteLine("MinecraftClient.exe --section.setting=value [--other settings]"); Console.WriteLine("MinecraftClient.exe [--other settings]"); - return; + return true; } if (args.Contains("--upgrade")) { UpgradeHelper.HandleBlockingUpdate(forceUpgrade: false); - return; + return true; } if (args.Contains("--force-upgrade")) { UpgradeHelper.HandleBlockingUpdate(forceUpgrade: true); - return; + return true; } if (args.Contains("--generate")) @@ -243,26 +337,26 @@ static void Main(string[] args) { Console.WriteLine(Translations.error_generator_invalid); Console.WriteLine(Translations.error_usage + " MinecraftClient.exe --data-generator= --data-path=\"\""); - return; + return true; } if (string.IsNullOrEmpty(dataPath)) { Console.WriteLine(string.Format(Translations.error_missing_argument, "--data-path")); Console.WriteLine(Translations.error_usage + " MinecraftClient.exe --data-generator= --data-path=\"\""); - return; + return true; } if (!File.Exists(dataPath)) { Console.WriteLine(string.Format(Translations.error_generator_path, dataPath)); - return; + return true; } if (!dataPath.EndsWith(".json")) { Console.WriteLine(string.Format(Translations.error_generator_json, dataPath)); - return; + return true; } Console.WriteLine(string.Format(Translations.mcc_generator_generating, dataGenerator, dataPath)); @@ -283,101 +377,86 @@ static void Main(string[] args) } Console.WriteLine(string.Format(Translations.mcc_generator_done, dataGenerator, dataPath)); - return; + return true; } } - if (!string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle)) + return false; + } + + /// + /// + /// + /// Arguments + /// Whether the program needs to be exited. + private static bool ProcessConfigurationFile(string? specifiedSettingFile) + { + bool loadSucceed, needWriteDefaultSetting, newlyGenerated = false; + if (!string.IsNullOrEmpty(specifiedSettingFile)) { - InternalConfig.Username = "New Window"; - Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle); + (loadSucceed, needWriteDefaultSetting) = Settings.LoadFromFile(specifiedSettingFile); + settingsIniPath = specifiedSettingFile; } - - // Check for updates - UpgradeHelper.CheckUpdate(); - - // Load command-line arguments - if (args.Length >= 1) + else if (File.Exists("MinecraftClient.ini")) { - try - { - Settings.LoadArguments(args); - } - catch (ArgumentException e) - { - InternalConfig.InteractiveMode = false; - HandleFailure(e.Message); - return; - } + (loadSucceed, needWriteDefaultSetting) = Settings.LoadFromFile("MinecraftClient.ini"); } - - //Test line to troubleshoot invisible colors - if (Config.Logging.DebugMessages) + else { - 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.ToString())); - } - { // 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.ToString())); - } + loadSucceed = true; + needWriteDefaultSetting = true; + newlyGenerated = true; } - //Load cached sessions from disk if necessary - if (Config.Main.Advanced.SessionCache == CacheType.disk) + if (needWriteDefaultSetting) { - bool cacheLoaded = SessionCache.InitializeDiskCache(); - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted("§8" + (cacheLoaded ? Translations.debug_session_cache_ok : Translations.debug_session_cache_fail), acceptnewlines: true); + Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage(); + _ = WriteBackSettings(false); + if (newlyGenerated) + ConsoleIO.WriteLineFormatted("§c" + string.Format(Translations.mcc_settings_generated, Path.GetFullPath(settingsIniPath))); + ConsoleIO.WriteLine(Translations.mcc_run_with_default_settings); } - - // Setup exit cleaning code - ExitCleanUp.Add(() => { DoExit(0); }); - - //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; - if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login) && !useBrowser) + else if (!loadSucceed) { - ConsoleIO.WriteLine(ConsoleIO.BasicIO ? Translations.mcc_login_basic_io : Translations.mcc_login); - InternalConfig.Account.Login = ConsoleIO.ReadLine().Trim(); - if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login)) + while (true) { - HandleFailure(Translations.error_login_blocked, false, ChatBot.DisconnectReason.LoginRejected); - return; + ConsoleIO.WriteLine(string.Empty); + ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_invaild_config, Config.Main.Advanced.InternalCmdChar.ToLogString())); + ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true); + string command = ConsoleInteractive.ConsoleReader.RequestImmediateInput().Trim(); + if (command.Length > 0) + { + if (Config.Main.Advanced.InternalCmdChar != InternalCmdCharType.none + && command[0] == Config.Main.Advanced.InternalCmdChar.ToChar()) + command = command[1..]; + + if (command.StartsWith("exit") || command.StartsWith("quit")) + { + return false; + } + + if (command.StartsWith("new")) + { + Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage(); + _ = WriteBackSettings(true); + ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_gen_new_config, settingsIniPath)); + return true; + } + } + else + { + return false; + } } } - InternalConfig.Username = InternalConfig.Account.Login; - if (string.IsNullOrWhiteSpace(InternalConfig.Account.Password) && !useBrowser && - (Config.Main.Advanced.SessionCache == CacheType.none || !SessionCache.Contains(ToLowerIfNeed(InternalConfig.Account.Login)))) + else { - RequestPassword(); + //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 } - - startupargs = args; - InitializeClient(); + return true; } /// @@ -385,7 +464,7 @@ static void Main(string[] args) /// private static void RequestPassword() { - ConsoleIO.WriteLine(ConsoleIO.BasicIO ? string.Format(Translations.mcc_password_basic_io, InternalConfig.Account.Login) + "\n" : Translations.mcc_password_hidden); + ConsoleIO.WriteLine(ConsoleIO.BasicIO ? string.Format(Translations.mcc_password_basic_io, InternalConfig.Account.Login) + "\n" : Translations.mcc_password_hidden, ignoreSuppress: true); string? password = ConsoleIO.BasicIO ? Console.ReadLine() : ConsoleIO.ReadPassword(); if (string.IsNullOrWhiteSpace(password)) InternalConfig.Account.Password = "-"; @@ -393,33 +472,32 @@ private static void RequestPassword() InternalConfig.Account.Password = password; } - /// - /// Start a new Client - /// - private static void InitializeClient() + private static async Task> LoginAsync(HttpClient httpClient) { - InternalConfig.MinecraftVersion = Config.Main.Advanced.MinecraftVersion; - - SessionToken session = new(); - PlayerKeyPair? playerKeyPair = null; - + SessionToken? session; + PlayerKeyPair? playerKeyPair; 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; + session = new() + { + PlayerID = "0", + PlayerName = InternalConfig.Username + }; + playerKeyPair = null; } else { + await AsyncTaskHandler.CacheSessionReader; + (session, playerKeyPair) = SessionCache.GetSession(InternalConfig.Account.Login); + // Validate cached session or login new session. - if (Config.Main.Advanced.SessionCache != CacheType.none && SessionCache.Contains(loginLower)) + if (Config.Main.Advanced.SessionCache != CacheType.none && session != null) { - session = SessionCache.Get(loginLower); - result = ProtocolHandler.GetTokenValidation(session); + result = await ProtocolHandler.GetTokenValidation(session); if (result != ProtocolHandler.LoginResult.Success) { ConsoleIO.WriteLineFormatted("§8" + Translations.mcc_session_invalid, acceptnewlines: true); @@ -428,7 +506,7 @@ private static void InitializeClient() { try { - result = ProtocolHandler.MicrosoftLoginRefresh(session.RefreshToken, out session); + (result, session) = await ProtocolHandler.MicrosoftLoginRefreshAsync(httpClient, session.RefreshToken); } catch (Exception ex) { @@ -440,62 +518,99 @@ private static void InitializeClient() if (result != ProtocolHandler.LoginResult.Success && string.IsNullOrWhiteSpace(InternalConfig.Account.Password) && !(Config.Main.General.AccountType == LoginType.microsoft && Config.Main.General.Method == LoginMethod.browser)) + { + ConsoleIO.SuppressPrinting(true); RequestPassword(); + ConsoleIO.SuppressPrinting(false); + } + } + else + { + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_session_valid, session.PlayerName)); } - 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" : "Microsoft")); - result = ProtocolHandler.GetLogin(InternalConfig.Account.Login, InternalConfig.Account.Password, Config.Main.General.AccountType, out session); + (result, session) = await ProtocolHandler.GetLoginAsync(httpClient, InternalConfig.Account.Login, InternalConfig.Account.Password, Config.Main.General.AccountType); } - 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()); + if (result == ProtocolHandler.LoginResult.Success && session != null) + { + var serverInfo = SessionCache.GetServerInfo($"{InternalConfig.ServerIP}:{InternalConfig.ServerPort}"); + if (serverInfo != null && serverInfo.ServerPublicKey != null) + { + try + { + byte[] key = Crypto.CryptoHandler.ClientAESPrivateKey = Crypto.CryptoHandler.GenerateAESPrivateKey(); + session.ServerInfoHash = Crypto.CryptoHandler.GetServerHash(serverInfo.ServerIDhash!, serverInfo.ServerPublicKey!, key); + session.SessionPreCheckTask = ProtocolHandler.SessionCheckAsync(httpClient, session.PlayerID, session.ID, session.ServerInfoHash); + } + catch (ArgumentException) { } + } + } } - if (result == ProtocolHandler.LoginResult.Success) - { - InternalConfig.Username = session.PlayerName; - bool isRealms = false; + return new(result, session, playerKeyPair); + } - if (Config.Main.Advanced.ConsoleTitle != "") - Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle); + private static async Task RefreshPlayerKeyPair(HttpClient httpClient, Task> loginTask) + { + (ProtocolHandler.LoginResult loginResult, SessionToken? session, PlayerKeyPair? playerKeyPair) = await loginTask; - if (Config.Main.Advanced.PlayerHeadAsIcon && OperatingSystem.IsWindows()) - ConsoleIcon.SetPlayerIconAsync(InternalConfig.Username); + if (loginResult != ProtocolHandler.LoginResult.Success || session == null || string.IsNullOrEmpty(session.ID)) + return null; - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLine(string.Format(Translations.debug_session_id, session.ID)); + bool needRefresh = true; + if (Config.Main.Advanced.ProfileKeyCache != CacheType.none && playerKeyPair != null) + { + needRefresh = playerKeyPair.NeedRefresh(); + if (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 == null || needRefresh) + { + ConsoleIO.WriteLineFormatted(Translations.mcc_fetching_key, acceptnewlines: true); + playerKeyPair = await Protocol.Microsoft.RequestProfileKeyAsync(httpClient, session.ID); + } + + return playerKeyPair; + } - List availableWorlds = new(); - if (Config.Main.Advanced.MinecraftRealms && !String.IsNullOrEmpty(session.ID)) - availableWorlds = ProtocolHandler.RealmsListWorlds(InternalConfig.Username, session.PlayerID, session.ID); + private static async Task> GetServerInfoAsync(HttpClient httpClient, Task> loginTask) + { + bool isRealms = false; + if (string.IsNullOrWhiteSpace(InternalConfig.ServerIP)) + { + ConsoleIO.SuppressPrinting(true); + ConsoleIO.WriteLine(Translations.mcc_ip, ignoreSuppress: true); + string addressInput = ConsoleIO.ReadLine(); + ConsoleIO.SuppressPrinting(false); - if (InternalConfig.ServerIP == string.Empty) + if (addressInput.StartsWith("realms:")) { - ConsoleIO.WriteLine(Translations.mcc_ip); - string addressInput = ConsoleIO.ReadLine(); - if (addressInput.StartsWith("realms:")) + if (Config.Main.Advanced.MinecraftRealms) { - if (Config.Main.Advanced.MinecraftRealms) + (ProtocolHandler.LoginResult loginResult, SessionToken? session, _) = await loginTask; + if (loginResult == ProtocolHandler.LoginResult.Success && session != null && !string.IsNullOrEmpty(session.ID)) { + List availableWorlds = await ProtocolHandler.RealmsListWorldsAsync(httpClient, InternalConfig.Username, session.PlayerID, session.ID); if (availableWorlds.Count == 0) { - HandleFailure(Translations.error_realms_access_denied, false, ChatBot.DisconnectReason.LoginRejected); - return; + FailureInfo.Record(Translations.error_realms_access_denied, false, ChatBot.DisconnectReason.LoginRejected); + return new(false, 0, null); } 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 != "") + string RealmsAddress = await ProtocolHandler.GetRealmsWorldServerAddress(httpClient, worldId, InternalConfig.Username, session.PlayerID, session.ID); + if (!string.IsNullOrEmpty(RealmsAddress)) { addressInput = RealmsAddress; isRealms = true; @@ -503,133 +618,177 @@ private static void InitializeClient() } else { - HandleFailure(Translations.error_realms_server_unavailable, false, ChatBot.DisconnectReason.LoginRejected); - return; + FailureInfo.Record(Translations.error_realms_server_unavailable, false, ChatBot.DisconnectReason.LoginRejected); + return new(false, 0, null); } } else { - HandleFailure(Translations.error_realms_server_id, false, ChatBot.DisconnectReason.LoginRejected); - return; + FailureInfo.Record(Translations.error_realms_server_id, false, ChatBot.DisconnectReason.LoginRejected); + return new(false, 0, null); } } else { - HandleFailure(Translations.error_realms_disabled, false, null); - return; + FailureInfo.Record(Translations.error_realms_disabled, false, null); + return new(false, 0, null); } } - Config.Main.SetServerIP(new MainConfigHealper.MainConfig.ServerInfoConfig(addressInput), true); + else + { + FailureInfo.Record(Translations.error_realms_disabled, false, null); + return new(false, 0, null); + } } + Config.Main.SetServerIP(new MainConfigHealper.MainConfig.ServerInfoConfig(addressInput), true); + } - //Get server version - int protocolversion = 0; - ForgeInfo? forgeInfo = null; + // Get server version + int protocolversion = 0; + ForgeInfo? forgeInfo = null; - if (InternalConfig.MinecraftVersion != "" && Settings.ToLowerIfNeed(InternalConfig.MinecraftVersion) != "auto") - { - protocolversion = Protocol.ProtocolHandler.MCVer2ProtocolVersion(InternalConfig.MinecraftVersion); + if (!string.IsNullOrEmpty(InternalConfig.MinecraftVersion) && !string.Equals(InternalConfig.MinecraftVersion, "auto", StringComparison.InvariantCultureIgnoreCase)) + { + 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 (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), acceptnewlines: true); - if (useMcVersionOnce) - { - useMcVersionOnce = false; - InternalConfig.MinecraftVersion = ""; - } + if (useMcVersionOnce) + { + useMcVersionOnce = false; + InternalConfig.MinecraftVersion = string.Empty; } + } - //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)))) + // 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)))) + { + ConsoleIO.WriteLine(protocolversion == 0 ? Translations.mcc_retrieve : Translations.mcc_forge); + (bool status, protocolversion, forgeInfo) = await ProtocolHandler.GetServerInfoAsync(InternalConfig.ServerIP, InternalConfig.ServerPort, protocolversion); + if (!status) { - 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, ChatBots.AutoRelog.DisconnectReason.ConnectionLost); - return; - } + FailureInfo.Record(Translations.error_ping, true, ChatBot.DisconnectReason.ConnectionLost); + return new(false, 0, null); } + } - if (Config.Main.General.AccountType == LoginType.microsoft - && (InternalConfig.Account.Password != "-" || Config.Main.General.Method == LoginMethod.browser) - && Config.Signature.LoginWithSecureProfile - && protocolversion >= 759 /* 1.19 and above */) + // Force-enable Forge support? + if (!isRealms && (Config.Main.Advanced.EnableForge == ForgeConfigType.force) && forgeInfo == null) + { + if (ProtocolHandler.ProtocolMayForceForge(protocolversion)) { - // 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); - } + ConsoleIO.WriteLine(Translations.mcc_forgeforce); + forgeInfo = ProtocolHandler.ProtocolForceForge(protocolversion); + } + else + { + FailureInfo.Record(Translations.error_forgeforce, true, ChatBot.DisconnectReason.ConnectionLost); + return new(false, 0, null); + } + } - 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)); - } + return new(true, protocolversion, forgeInfo); + } - if (playerKeyPair == null || playerKeyPair.NeedRefresh()) - { - ConsoleIO.WriteLineFormatted(Translations.mcc_fetching_key, acceptnewlines: true); - playerKeyPair = KeyUtils.GetNewProfileKeys(session.ID); - if (Config.Main.Advanced.ProfileKeyCache != CacheType.none && playerKeyPair != null) - { - KeysCache.Store(loginLower, playerKeyPair); - } - } + private static async Task SaveSession(Task refreshPlayerKeyTask, Task> loginTask) + { + if (Config.Main.Advanced.SessionCache != CacheType.none) + { + (ProtocolHandler.LoginResult loginResult, SessionToken? session, _) = await loginTask; + if (loginResult == ProtocolHandler.LoginResult.Success && session != null && !string.IsNullOrEmpty(session.ID)) + { + PlayerKeyPair? playerKeyPair = await refreshPlayerKeyTask; + await SessionCache.StoreSessionAsync(InternalConfig.Account.Login, session, playerKeyPair); } + } + } + + /// + /// Start a new Client + /// + private async static Task InitializeClient(HttpClient loginHttpClient) + { + InternalConfig.MinecraftVersion = Config.Main.Advanced.MinecraftVersion; + + SessionToken? session; + PlayerKeyPair? playerKeyPair; + ProtocolHandler.LoginResult result; + + var loginTask = LoginAsync(loginHttpClient); + var getServerInfoTask = GetServerInfoAsync(loginHttpClient, loginTask); + var refreshPlayerKeyTask = RefreshPlayerKeyPair(loginHttpClient, loginTask); + + // Todo: Detailed status display of login steps. + (result, session, playerKeyPair) = await loginTask; + if (result == ProtocolHandler.LoginResult.Success && session != null) + { + InternalConfig.Username = session.PlayerName; + + if (!string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle)) + 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); }); + + if (Config.Logging.DebugMessages) + ConsoleIO.WriteLine(string.Format(Translations.debug_session_id, session.ID)); + + (bool status, int protocolversion, ForgeInfo? forgeInfo) = await getServerInfoTask; + if (!status) + return; - //Force-enable Forge support? - if (!isRealms && (Config.Main.Advanced.EnableForge == ForgeConfigType.force) && forgeInfo == null) + if (Config.Main.General.AccountType == LoginType.microsoft + && (InternalConfig.Account.Password != "-" || Config.Main.General.Method == LoginMethod.browser) + && Config.Signature.LoginWithSecureProfile + && protocolversion >= 759 /* 1.19 and above */) { - if (ProtocolHandler.ProtocolMayForceForge(protocolversion)) - { - ConsoleIO.WriteLine(Translations.mcc_forgeforce); - forgeInfo = ProtocolHandler.ProtocolForceForge(protocolversion); - } - else - { - HandleFailure(Translations.error_forgeforce, true, ChatBots.AutoRelog.DisconnectReason.ConnectionLost); - return; - } + playerKeyPair = await refreshPlayerKeyTask; } - //Proceed to server login + // Proceed to server login if (protocolversion != 0) { try { - //Start the main TCP client - client = new McClient(session, playerKeyPair, InternalConfig.ServerIP, InternalConfig.ServerPort, protocolversion, forgeInfo); + await McClientInit; + McClient = new McClient(InternalConfig.ServerIP, InternalConfig.ServerPort, McClientCancelTokenSource); - //Update console title - if (Config.Main.Advanced.ConsoleTitle != "") - Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle); + // Start the main TCP client + await McClient.Login(loginHttpClient, session, playerKeyPair, protocolversion, forgeInfo); } catch (NotSupportedException) { - HandleFailure(Translations.error_unsupported, true); + FailureInfo.Record(Translations.error_unsupported, true); + return; + } + catch (Exception e) + { + FailureInfo.Record(e.Message, false, ChatBot.DisconnectReason.ConnectionLost); + return; } - catch (Exception) { } + + await AsyncTaskHandler.SaveSessionToDisk; + AsyncTaskHandler.SaveSessionToDisk = Task.Run(async () => { await SaveSession(refreshPlayerKeyTask, loginTask); }); + + // Update console title + if (!string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle)) + Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle); + + await McClient.StartUpdating(); + } + else + { + FailureInfo.Record(Translations.error_determine, true); + return; } - else HandleFailure(Translations.error_determine, true); } else { - string failureMessage = Translations.error_login; - string failureReason = string.Empty; - failureReason = result switch + string failureMessage = Translations.error_login + result switch { #pragma warning disable format // @formatter:off ProtocolHandler.LoginResult.AccountMigrated => Translations.error_login_migrated, @@ -643,8 +802,8 @@ private static void InitializeClient() _ => Translations.error_login_unknown, #pragma warning restore format // @formatter:on }; - failureMessage += failureReason; - HandleFailure(failureMessage, false, ChatBot.DisconnectReason.LoginRejected); + FailureInfo.Record(failureMessage, false, ChatBot.DisconnectReason.LoginRejected); + return; } } @@ -660,51 +819,49 @@ public static void ReloadSettings(bool keepAccountAndServerSettings = false) /// /// Write-back settings /// - public static void WriteBackSettings(bool enableBackup = true) + public static Task WriteBackSettings(bool enableBackup = true) { - Settings.WriteToFile(settingsIniPath, enableBackup); + return Task.Run(async () => + { + await AsyncTaskHandler.WritebackSettingFile; + AsyncTaskHandler.WritebackSettingFile = Settings.WriteToFileAsync(settingsIniPath, enableBackup); + }); } /// /// Disconnect the current client from the server and restart it /// /// Optional delay, in seconds, before restarting - public static void Restart(int delaySeconds = 0, bool keepAccountAndServerSettings = false) + public static void SetRestart(int delayMilliseconds = 0, bool keepAccountAndServerSettings = false) { - ConsoleInteractive.ConsoleReader.StopReadThread(); - new Thread(new ThreadStart(delegate - { - if (client != null) { client.Disconnect(); ConsoleIO.Reset(); } - if (offlinePrompt != null) { 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(); + RestartAfter = Math.Max(0, delayMilliseconds); + RestartKeepSettings = keepAccountAndServerSettings; + McClientCancelTokenSource.Cancel(); + } + + /// + /// Disconnect the current client from the server and exit the app + /// + public static void SetExit(int exitcode = 0, bool handleFailure = false) + { + RestartAfter = -1; + Exitcode = exitcode; + if (handleFailure) + FailureInfo.Record(); + McClientCancelTokenSource.Cancel(); } - public static void DoExit(int exitcode = 0) + public static void DoExit() { - WriteBackSettings(true); ConsoleInteractive.ConsoleSuggestion.ClearSuggestions(); + WriteBackSettings(true).Wait(); ConsoleIO.WriteLineFormatted("§a" + string.Format(Translations.config_saving, settingsIniPath)); - if (client != null) { client.Disconnect(); ConsoleIO.Reset(); } - if (offlinePrompt != null) { offlinePrompt.Item2.Cancel(); offlinePrompt.Item1.Join(); offlinePrompt = null; ConsoleIO.Reset(); } + if (McClient != null) { McClient.Disconnect(); ConsoleIO.Reset(); } if (Config.Main.Advanced.PlayerHeadAsIcon) { ConsoleIcon.RevertToMCCIcon(); } - Environment.Exit(exitcode); - } - /// - /// Disconnect the current client from the server and exit the app - /// - public static void Exit(int exitcode = 0) - { - new Thread(new ThreadStart(() => { DoExit(exitcode); })).Start(); + AsyncTaskHandler.ExitCleanUp(); + Environment.Exit(Exitcode); } /// @@ -714,130 +871,104 @@ public static void Exit(int exitcode = 0) /// Error message to display and optionally pass to AutoRelog bot /// Specify if the error is related to an incompatible or unkown server version /// If set, the error message will be processed by the AutoRelog bot - public static void HandleFailure(string? errorMessage = null, bool versionError = false, ChatBots.AutoRelog.DisconnectReason? disconnectReason = null) + public static int HandleFailure() { - if (!String.IsNullOrEmpty(errorMessage)) + if (!string.IsNullOrEmpty(FailureInfo.errorMessage)) { ConsoleIO.Reset(); while (Console.KeyAvailable) Console.ReadKey(true); - ConsoleIO.WriteLine(errorMessage); + ConsoleIO.WriteLine(FailureInfo.errorMessage); - if (disconnectReason.HasValue) + if (FailureInfo.disconnectReason.HasValue) { - if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage)) - return; //AutoRelog is triggering a restart of the client + int autoRelogResult = ChatBots.AutoRelog.OnDisconnectStatic(FailureInfo.disconnectReason.Value, FailureInfo.errorMessage); + if (autoRelogResult >= 0) + return autoRelogResult; //AutoRelog is triggering a restart of the client } } if (InternalConfig.InteractiveMode) { - if (versionError) + if (FailureInfo.versionError) { ConsoleIO.WriteLine(Translations.mcc_server_version); InternalConfig.MinecraftVersion = ConsoleInteractive.ConsoleReader.RequestImmediateInput(); - if (InternalConfig.MinecraftVersion != "") + if (!string.IsNullOrEmpty(InternalConfig.MinecraftVersion)) { useMcVersionOnce = true; - Restart(); - return; + return 0; } } - if (offlinePrompt == null) - { - ConsoleInteractive.ConsoleReader.StopReadThread(); + ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_disconnected, Config.Main.Advanced.InternalCmdChar.ToLogString())); + ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true); - var cancellationTokenSource = new CancellationTokenSource(); - offlinePrompt = new(new Thread(new ThreadStart(delegate + while (true) + { + string command = ConsoleInteractive.ConsoleReader.RequestImmediateInput().Trim(); + if (string.IsNullOrEmpty(command)) + { + return -1; + } + else { - bool exitThread = false; - string command = " "; - ConsoleIO.WriteLine(string.Empty); - ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_disconnected, Config.Main.Advanced.InternalCmdChar.ToLogString())); - ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true); + if (Config.Main.Advanced.InternalCmdChar != InternalCmdCharType.none + && command[0] == Config.Main.Advanced.InternalCmdChar.ToChar()) + command = command[1..]; - while (!cancellationTokenSource.IsCancellationRequested) + if (command.StartsWith("reco")) { - if (exitThread) - return; - - while (command.Length > 0) + string message = Commands.Reco.DoReconnect(Config.AppVar.ExpandVars(command)); + if (string.IsNullOrEmpty(message)) { - if (cancellationTokenSource.IsCancellationRequested) - return; - - command = ConsoleInteractive.ConsoleReader.RequestImmediateInput().Trim(); - if (command.Length > 0) - { - 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; - break; - } - } - else if (command.StartsWith("connect")) - { - message = Commands.Connect.DoConnect(Config.AppVar.ExpandVars(command)); - if (message == "") - { - exitThread = true; - break; - } - } - 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); - } - else - { - Commands.Exit.DoExit(Config.AppVar.ExpandVars(command)); - } + ConsoleIO.WriteLine(string.Empty); + RestartKeepSettings = true; + return 0; } - - if (exitThread) - return; + else + ConsoleIO.WriteLineFormatted("§8MCC: " + message); + } + else if (command.StartsWith("connect")) + { + string message = Commands.Connect.DoConnect(Config.AppVar.ExpandVars(command)); + if (string.IsNullOrEmpty(message)) + { + ConsoleIO.WriteLine(string.Empty); + RestartKeepSettings = true; + return 0; + } + else + ConsoleIO.WriteLineFormatted("§8MCC: " + message); + } + else if (command.StartsWith("exit") || command.StartsWith("quit")) + { + return -1; + } + else if (command.StartsWith("help")) + { + ConsoleIO.WriteLineFormatted("§8MCC: " + new Commands.Reco().GetCmdDescTranslated(ListAllUsage: false)); + ConsoleIO.WriteLineFormatted("§8MCC: " + new Commands.Connect().GetCmdDescTranslated(ListAllUsage: false)); } - })), cancellationTokenSource); - offlinePrompt.Item1.Start(); + else + { + ConsoleIO.WriteLineFormatted("§8MCC: " + string.Format(Translations.icmd_unknown, command.Split(' ')[0])); + } + } } } else { // Not in interactive mode, just exit and let the calling script handle the failure - if (disconnectReason.HasValue) + if (FailureInfo.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); + if (FailureInfo.disconnectReason.Value == ChatBot.DisconnectReason.UserLogout) Exitcode = 1; + if (FailureInfo.disconnectReason.Value == ChatBot.DisconnectReason.InGameKick) Exitcode = 2; + if (FailureInfo.disconnectReason.Value == ChatBot.DisconnectReason.ConnectionLost) Exitcode = 3; + if (FailureInfo.disconnectReason.Value == ChatBot.DisconnectReason.LoginRejected) Exitcode = 4; } - Exit(); + return -1; } } @@ -851,7 +982,7 @@ public static void HandleFailure(string? errorMessage = null, bool versionError public static Type[] GetTypesInNamespace(string nameSpace, Assembly? assembly = null) { if (assembly == null) { assembly = Assembly.GetExecutingAssembly(); } - return assembly.GetTypes().Where(t => String.Equals(t.Namespace, nameSpace, StringComparison.Ordinal)).ToArray(); + return assembly.GetTypes().Where(t => string.Equals(t.Namespace, nameSpace, StringComparison.Ordinal)).ToArray(); } /// @@ -861,9 +992,26 @@ static Program() { if (typeof(Program) .Assembly - .GetCustomAttributes(typeof(System.Reflection.AssemblyConfigurationAttribute), false) + .GetCustomAttributes(typeof(AssemblyConfigurationAttribute), false) .FirstOrDefault() is AssemblyConfigurationAttribute attribute) BuildInfo = attribute.Configuration; } + + private static class FailureInfo + { + public static bool hasFailure = false; + + public static string? errorMessage = null; + public static bool versionError = false; + public static ChatBot.DisconnectReason? disconnectReason = null; + + public static void Record(string? errorMessage = null, bool versionError = false, ChatBot.DisconnectReason? disconnectReason = null) + { + FailureInfo.hasFailure = true; + FailureInfo.errorMessage = errorMessage; + FailureInfo.versionError = versionError; + FailureInfo.disconnectReason = disconnectReason; + } + } } } diff --git a/MinecraftClient/Protocol/Handlers/DataTypes.cs b/MinecraftClient/Protocol/Handlers/DataTypes.cs index ecc86bce82..0cdab85d95 100644 --- a/MinecraftClient/Protocol/Handlers/DataTypes.cs +++ b/MinecraftClient/Protocol/Handlers/DataTypes.cs @@ -1,11 +1,17 @@ using System; +using System.Buffers.Binary; using System.Collections.Generic; +using System.Data.SqlTypes; +using System.IO; using System.Runtime.CompilerServices; using System.Text; +using System.Threading.Tasks; +using MinecraftClient.EntityHandler; using MinecraftClient.Inventory; using MinecraftClient.Inventory.ItemPalettes; using MinecraftClient.Mapping; using MinecraftClient.Mapping.EntityPalettes; +using MinecraftClient.Protocol.PacketPipeline; namespace MinecraftClient.Protocol.Handlers { @@ -19,6 +25,9 @@ class DataTypes /// private readonly int protocolversion; + private const int BufferLength = 32; + private static readonly Memory Buffer = new byte[BufferLength]; + /// /// Initialize a new DataTypes instance /// @@ -31,14 +40,13 @@ public DataTypes(int protocol) /// /// Read some data from a cache of bytes and remove it from the cache /// - /// Amount of bytes to read + /// Amount of bytes to read /// Cache of bytes to read from /// The data read from the cache as an array - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public byte[] ReadData(int offset, Queue cache) + public byte[] ReadData(int length, Queue cache) { - byte[] result = new byte[offset]; - for (int i = 0; i < offset; i++) + byte[] result = new byte[length]; + for (int i = 0; i < length; i++) result[i] = cache.Dequeue(); return result; } @@ -46,25 +54,24 @@ public byte[] ReadData(int offset, Queue cache) /// /// Read some data from a cache of bytes and remove it from the cache /// - /// Cache of bytes to read from - /// Storage results - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public void ReadDataReverse(Queue cache, Span dest) + /// Amount of bytes to read + /// Cache of bytes to read from + /// The data read from the cache as an array + public async Task ReadDataAsync(int length, PacketStream stream) { - for (int i = (dest.Length - 1); i >= 0; --i) - dest[i] = cache.Dequeue(); + byte[] result = new byte[length]; + await stream.ReadExactlyAsync(result); + return result; } /// /// Remove some data from the cache /// - /// Amount of bytes to drop - /// Cache of bytes to drop - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public void DropData(int offset, Queue cache) + /// Amount of bytes to drop + /// Cache of bytes to drop + public async Task DropDataAsync(int length, PacketStream stream) { - while (offset-- > 0) - cache.Dequeue(); + await stream.Skip(length); } /// @@ -72,7 +79,6 @@ public void DropData(int offset, Queue cache) /// /// Cache of bytes to read from /// The string - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public string ReadNextString(Queue cache) { int length = ReadNextVarInt(cache); @@ -83,98 +89,243 @@ public string ReadNextString(Queue cache) else return ""; } + /// + /// Read a string from a cache of bytes and remove it from the cache + /// + /// Cache of bytes to read from + /// The string + public async Task ReadNextStringAsync(PacketStream stream) + { + return await ReadNextUtf8StringAsync(stream, ReadNextVarInt(stream)); + } + + /// + /// Read a string from a cache of bytes and remove it from the cache + /// + /// Cache of bytes to read from + /// The string + public async Task ReadNextUtf8StringAsync(PacketStream stream, int length) + { + Memory strByte = length > BufferLength ? new byte[length] : Buffer[..length]; + await stream.ReadExactlyAsync(strByte); + return Encoding.UTF8.GetString(strByte.Span); + } + + /// + /// Read a string from a cache of bytes and remove it from the cache + /// + /// Cache of bytes to read from + /// The string + public async Task ReadNextAsciiStringAsync(PacketStream stream, int length) + { + Memory strByte = length > BufferLength ? new byte[length] : Buffer[..length]; + await stream.ReadExactlyAsync(strByte); + return Encoding.ASCII.GetString(strByte.Span); + } + /// /// Skip a string from a cache of bytes and remove it from the cache /// - /// Cache of bytes to read from - public void SkipNextString(Queue cache) + /// Cache of bytes to read from + public async Task SkipNextStringAsync(PacketStream stream) { - int length = ReadNextVarInt(cache); - DropData(length, cache); + int length = ReadNextVarInt(stream); + await DropDataAsync(length, stream); } /// /// Read a boolean from a cache of bytes and remove it from the cache /// /// The boolean value - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public bool ReadNextBool(Queue cache) { return ReadNextByte(cache) != 0x00; } + /// + /// Read a boolean from a cache of bytes and remove it from the cache + /// + /// The boolean value + public bool ReadNextBool(PacketStream stream) + { + return ReadNextByte(stream) != 0x00; + } + + /// + /// Read a boolean from a cache of bytes and remove it from the cache + /// + /// The boolean value + public async Task ReadNextBoolAsync(PacketStream stream) + { + return (await ReadNextByteAsync(stream)) != 0x00; + } + /// /// Read a short integer from a cache of bytes and remove it from the cache /// /// The short integer value - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public short ReadNextShort(Queue cache) + public short ReadNextShort(PacketStream stream) { - Span rawValue = stackalloc byte[2]; - for (int i = (2 - 1); i >= 0; --i) //Endianness - rawValue[i] = cache.Dequeue(); - return BitConverter.ToInt16(rawValue); + Span buf = Buffer[..2].Span; + buf[1] = ReadNextByte(stream); + buf[0] = ReadNextByte(stream); + return BinaryPrimitives.ReadInt16LittleEndian(buf); + } + + /// + /// Read a short integer from a cache of bytes and remove it from the cache + /// + /// The short integer value + public async Task ReadNextShortAsync(PacketStream stream) + { + Memory buf = Buffer[..2]; + await stream.ReadExactlyAsync(buf); + return BinaryPrimitives.ReadInt16BigEndian(buf.Span); + } + + /// + /// Read a short integer from a cache of bytes and remove it from the cache + /// + /// The short integer value + public async Task SkipNextShortAsync(PacketStream stream) + { + await DropDataAsync(2, stream); } /// /// Read an integer from a cache of bytes and remove it from the cache /// /// The integer value - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public int ReadNextInt(Queue cache) { Span rawValue = stackalloc byte[4]; - for (int i = (4 - 1); i >= 0; --i) //Endianness + for (int i = (4 - 1); i >= 0; --i) // Endianness rawValue[i] = cache.Dequeue(); return BitConverter.ToInt32(rawValue); } + /// + /// Read an integer from a cache of bytes and remove it from the cache + /// + /// The integer value + public int ReadNextInt(PacketStream stream) + { + Span buf = Buffer[..4].Span; + buf[3] = ReadNextByte(stream); + buf[2] = ReadNextByte(stream); + buf[1] = ReadNextByte(stream); + buf[0] = ReadNextByte(stream); + return BinaryPrimitives.ReadInt32LittleEndian(buf); + } + + /// + /// Read an integer from a cache of bytes and remove it from the cache + /// + /// The integer value + public async Task ReadNextIntAsync(PacketStream stream) + { + Memory buf = Buffer[..4]; + await stream.ReadExactlyAsync(buf); + return BinaryPrimitives.ReadInt32BigEndian(buf.Span); + } + /// /// Read a long integer from a cache of bytes and remove it from the cache /// /// The unsigned long integer value - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public long ReadNextLong(Queue cache) + public long ReadNextLong(PacketStream stream) { - Span rawValue = stackalloc byte[8]; - for (int i = (8 - 1); i >= 0; --i) //Endianness - rawValue[i] = cache.Dequeue(); - return BitConverter.ToInt64(rawValue); + Span buf = Buffer[..8].Span; + buf[7] = ReadNextByte(stream); + buf[6] = ReadNextByte(stream); + buf[5] = ReadNextByte(stream); + buf[4] = ReadNextByte(stream); + buf[3] = ReadNextByte(stream); + buf[2] = ReadNextByte(stream); + buf[1] = ReadNextByte(stream); + buf[0] = ReadNextByte(stream); + return BinaryPrimitives.ReadInt64LittleEndian(buf); + } + + /// + /// Read a long integer from a cache of bytes and remove it from the cache + /// + /// The unsigned long integer value + public async Task ReadNextLongAsync(PacketStream stream) + { + Memory buf = Buffer[..8]; + await stream.ReadExactlyAsync(buf); + return BinaryPrimitives.ReadInt64BigEndian(buf.Span); + } + + /// + /// Read a long integer from a cache of bytes and remove it from the cache + /// + /// The unsigned long integer value + public async Task SkipNextLongAsync(PacketStream stream) + { + await DropDataAsync(8, stream); } /// /// Read an unsigned short integer from a cache of bytes and remove it from the cache /// /// The unsigned short integer value - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public ushort ReadNextUShort(Queue cache) + public ushort ReadNextUShort(PacketStream stream) { - Span rawValue = stackalloc byte[2]; - for (int i = (2 - 1); i >= 0; --i) //Endianness - rawValue[i] = cache.Dequeue(); - return BitConverter.ToUInt16(rawValue); + Span buf = Buffer[..2].Span; + buf[1] = ReadNextByte(stream); + buf[0] = ReadNextByte(stream); + return BinaryPrimitives.ReadUInt16LittleEndian(buf); + } + + /// + /// Read an unsigned short integer from a cache of bytes and remove it from the cache + /// + /// The unsigned short integer value + public async Task ReadNextUShortAsync(PacketStream stream) + { + Memory buf = Buffer[..2]; + await stream.ReadExactlyAsync(buf); + return BinaryPrimitives.ReadUInt16BigEndian(buf.Span); } /// /// Read an unsigned long integer from a cache of bytes and remove it from the cache /// /// The unsigned long integer value - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public ulong ReadNextULong(Queue cache) + public ulong ReadNextULong(PacketStream stream) { - Span rawValue = stackalloc byte[8]; - for (int i = (8 - 1); i >= 0; --i) //Endianness - rawValue[i] = cache.Dequeue(); - return BitConverter.ToUInt64(rawValue); + Span buf = Buffer[..8].Span; + buf[7] = ReadNextByte(stream); + buf[6] = ReadNextByte(stream); + buf[5] = ReadNextByte(stream); + buf[4] = ReadNextByte(stream); + buf[3] = ReadNextByte(stream); + buf[2] = ReadNextByte(stream); + buf[1] = ReadNextByte(stream); + buf[0] = ReadNextByte(stream); + return BinaryPrimitives.ReadUInt64LittleEndian(buf); + } + + /// + /// Read a long integer from a cache of bytes and remove it from the cache + /// + /// The unsigned long integer value + public async Task ReadNextULongAsync(PacketStream stream) + { + Memory buf = Buffer[..8]; + await stream.ReadExactlyAsync(buf); + return BinaryPrimitives.ReadUInt64BigEndian(buf.Span); } /// /// Read a Location encoded as an ulong field and remove it from the cache /// /// The Location value - public Location ReadNextLocation(Queue cache) + public Location ReadNextLocation(PacketStream stream) { - ulong locEncoded = ReadNextULong(cache); + ulong locEncoded = ReadNextULong(stream); int x, y, z; if (protocolversion >= Protocol18Handler.MC_1_14_Version) { @@ -198,16 +349,41 @@ public Location ReadNextLocation(Queue cache) } /// - /// Read several little endian unsigned short integers at once from a cache of bytes and remove them from the cache + /// Read a Location encoded as an ulong field and remove it from the cache /// - /// The unsigned short integer value - public ushort[] ReadNextUShortsLittleEndian(int amount, Queue cache) + /// The Location value + public async Task ReadNextLocationAsync(PacketStream stream) { - byte[] rawValues = ReadData(2 * amount, cache); - ushort[] result = new ushort[amount]; - for (int i = 0; i < amount; i++) - result[i] = BitConverter.ToUInt16(rawValues, i * 2); - return result; + ulong locEncoded = await ReadNextULongAsync(stream); + int x, y, z; + if (protocolversion >= Protocol18Handler.MC_1_14_Version) + { + x = (int)(locEncoded >> 38); + y = (int)(locEncoded & 0xFFF); + z = (int)(locEncoded << 26 >> 38); + } + else + { + x = (int)(locEncoded >> 38); + y = (int)((locEncoded >> 26) & 0xFFF); + z = (int)(locEncoded << 38 >> 38); + } + if (x >= 0x02000000) // 33,554,432 + x -= 0x04000000; // 67,108,864 + if (y >= 0x00000800) // 2,048 + y -= 0x00001000; // 4,096 + if (z >= 0x02000000) // 33,554,432 + z -= 0x04000000; // 67,108,864 + return new Location(x, y, z); + } + + /// + /// Read a Location encoded as an ulong field and remove it from the cache + /// + /// The Location value + public async Task SkipNextLocationAsync(PacketStream stream) + { + await SkipNextLongAsync(stream); } /// @@ -226,39 +402,81 @@ public Guid ReadNextUUID(Queue cache) return guid; } + /// + /// Read a uuid from a cache of bytes and remove it from the cache + /// + /// Cache of bytes to read from + /// The uuid + public async Task ReadNextUUIDAsync(PacketStream stream) + { + Memory buf = Buffer[..16]; + await stream.ReadExactlyAsync(buf); + return new Guid(buf.Span).ToLittleEndian(); + } + + /// + /// Read a uuid from a cache of bytes and remove it from the cache + /// + /// Cache of bytes to read from + /// The uuid + public async Task SkipNextUUIDAsync(PacketStream stream) + { + await DropDataAsync(16, stream); + } + /// /// Read a byte array from a cache of bytes and remove it from the cache /// - /// Cache of bytes to read from + /// Cache of bytes to read from /// The byte array - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public byte[] ReadNextByteArray(Queue cache) + public async Task ReadNextByteArrayAsync(PacketStream stream) { int len = protocolversion >= Protocol18Handler.MC_1_8_Version - ? ReadNextVarInt(cache) - : ReadNextShort(cache); - return ReadData(len, cache); + ? await ReadNextVarIntAsync(stream) + : await ReadNextShortAsync(stream); + return await ReadDataAsync(len, stream); } /// /// Reads a length-prefixed array of unsigned long integers and removes it from the cache /// /// The unsigned long integer values - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public ulong[] ReadNextULongArray(Queue cache) + public ulong[] ReadNextULongArray(PacketStream stream) { - int len = ReadNextVarInt(cache); + int len = ReadNextVarInt(stream); ulong[] result = new ulong[len]; for (int i = 0; i < len; i++) - result[i] = ReadNextULong(cache); + result[i] = ReadNextULong(stream); return result; } + /// + /// Reads a length-prefixed array of unsigned long integers and removes it from the cache + /// + /// The unsigned long integer values + public async Task ReadNextULongArrayAsync(PacketStream stream) + { + int len = await ReadNextVarIntAsync(stream); + ulong[] result = new ulong[len]; + for (int i = 0; i < len; i++) + result[i] = await ReadNextULongAsync(stream); + return result; + } + + /// + /// Reads a length-prefixed array of unsigned long integers and removes it from the cache + /// + /// The unsigned long integer values + public async Task SkipNextULongArray(PacketStream stream) + { + int len = await ReadNextVarIntAsync(stream); + await DropDataAsync(len * 8, stream); + } + /// /// Read a double from a cache of bytes and remove it from the cache /// /// The double value - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public double ReadNextDouble(Queue cache) { Span rawValue = stackalloc byte[8]; @@ -267,11 +485,39 @@ public double ReadNextDouble(Queue cache) return BitConverter.ToDouble(rawValue); } + /// + /// Read a double from a cache of bytes and remove it from the cache + /// + /// The double value + public double ReadNextDouble(PacketStream stream) + { + Span buf = Buffer[..8].Span; + buf[7] = ReadNextByte(stream); + buf[6] = ReadNextByte(stream); + buf[5] = ReadNextByte(stream); + buf[4] = ReadNextByte(stream); + buf[3] = ReadNextByte(stream); + buf[2] = ReadNextByte(stream); + buf[1] = ReadNextByte(stream); + buf[0] = ReadNextByte(stream); + return BinaryPrimitives.ReadDoubleLittleEndian(buf); + } + + /// + /// Read a double from a cache of bytes and remove it from the cache + /// + /// The double value + public async Task ReadNextDoubleAsync(PacketStream stream) + { + Memory buf = Buffer[..8]; + await stream.ReadExactlyAsync(buf); + return BinaryPrimitives.ReadDoubleBigEndian(buf.Span); + } + /// /// Read a float from a cache of bytes and remove it from the cache /// /// The float value - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public float ReadNextFloat(Queue cache) { Span rawValue = stackalloc byte[4]; @@ -281,22 +527,74 @@ public float ReadNextFloat(Queue cache) } /// - /// Read an integer from the network + /// Read a float from a cache of bytes and remove it from the cache + /// + /// The float value + public float ReadNextFloat(PacketStream stream) + { + Span buf = Buffer[..4].Span; + buf[3] = ReadNextByte(stream); + buf[2] = ReadNextByte(stream); + buf[1] = ReadNextByte(stream); + buf[0] = ReadNextByte(stream); + return BinaryPrimitives.ReadSingleLittleEndian(buf); + } + + /// + /// Read a float from a cache of bytes and remove it from the cache /// + /// The float value + public async Task ReadNextFloatAsync(PacketStream stream) + { + Memory buf = Buffer[..4]; + await stream.ReadExactlyAsync(buf); + return BinaryPrimitives.ReadSingleBigEndian(buf.Span); + } + + /// + /// Read a float from a cache of bytes and remove it from the cache + /// + /// The float value + public async Task SkipNextFloatAsync(PacketStream stream) + { + await DropDataAsync(4, stream); + } + + /// + /// Read an integer from a cache of bytes and remove it from the cache + /// + /// Cache of bytes to read from /// The integer - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int ReadNextVarIntRAW(SocketWrapper socket) + public int ReadNextVarInt(PacketStream stream) { int i = 0; int j = 0; byte b; - while (true) + do { - b = socket.ReadDataRAW(1)[0]; + b = ReadNextByte(stream); i |= (b & 0x7F) << j++ * 7; if (j > 5) throw new OverflowException("VarInt too big"); - if ((b & 0x80) != 128) break; - } + } while ((b & 0x80) == 128); + return i; + } + + /// + /// Read an integer from a cache of bytes and remove it from the cache + /// + /// Cache of bytes to read from + /// The integer + public async Task ReadNextVarIntAsync(PacketStream stream) + { + int i = 0; + int j = 0; + byte b; + do + { + b = await ReadNextByteAsync(stream); + i |= (b & 0x7F) << j++ * 7; + if (j > 5) throw new OverflowException("VarInt too big"); + } while ((b & 0x80) == 128); return i; } @@ -305,7 +603,6 @@ public int ReadNextVarIntRAW(SocketWrapper socket) /// /// Cache of bytes to read from /// The integer - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public int ReadNextVarInt(Queue cache) { int i = 0; @@ -323,12 +620,22 @@ public int ReadNextVarInt(Queue cache) /// /// Skip a VarInt from a cache of bytes with better performance /// - /// Cache of bytes to read from - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public void SkipNextVarInt(Queue cache) + /// Cache of bytes to read from + public void SkipNextVarInt(PacketStream stream) + { + while (true) + if ((((byte)ReadNextByte(stream)) & 0x80) != 128) + break; + } + + /// + /// Skip a VarInt from a cache of bytes with better performance + /// + /// Cache of bytes to read from + public async Task SkipNextVarIntAsync(PacketStream stream) { while (true) - if ((ReadNextByte(cache) & 0x80) != 128) + if (((await ReadNextByteAsync(stream)) & 0x80) != 128) break; } @@ -337,16 +644,35 @@ public void SkipNextVarInt(Queue cache) /// This is only done with forge. It looks like it's a normal short, except that if the high /// bit is set, it has an extra byte. /// - /// Cache of bytes to read from + /// Cache of bytes to read from /// The int - public int ReadNextVarShort(Queue cache) + public int ReadNextVarShort(PacketStream stream) { - ushort low = ReadNextUShort(cache); + ushort low = ReadNextUShort(stream); byte high = 0; if ((low & 0x8000) != 0) { low &= 0x7FFF; - high = ReadNextByte(cache); + high = ReadNextByte(stream); + } + return ((high & 0xFF) << 15) | low; + } + + /// + /// Read an "extended short", which is actually an int of some kind, from the cache of bytes. + /// This is only done with forge. It looks like it's a normal short, except that if the high + /// bit is set, it has an extra byte. + /// + /// Cache of bytes to read from + /// The int + public async Task ReadNextVarShortAsync(PacketStream stream) + { + ushort low = await ReadNextUShortAsync(stream); + byte high = 0; + if ((low & 0x8000) != 0) + { + low &= 0x7FFF; + high = await ReadNextByteAsync(stream); } return ((high & 0xFF) << 15) | low; } @@ -354,24 +680,45 @@ public int ReadNextVarShort(Queue cache) /// /// Read a long from a cache of bytes and remove it from the cache /// - /// Cache of bytes to read from + /// Cache of bytes to read from /// The long value - public long ReadNextVarLong(Queue cache) + public long ReadNextVarLong(PacketStream stream) { int numRead = 0; long result = 0; byte read; do { - read = ReadNextByte(cache); + read = ReadNextByte(stream); + long value = (read & 0x7F); + result |= (value << (7 * numRead)); + + numRead++; + if (numRead > 10) + throw new OverflowException("VarLong is too big"); + } while ((read & 0x80) != 0); + return result; + } + + /// + /// Read a long from a cache of bytes and remove it from the cache + /// + /// Cache of bytes to read from + /// The long value + public async Task ReadNextVarLongAsync(PacketStream stream) + { + int numRead = 0; + long result = 0; + byte read; + do + { + read = await ReadNextByteAsync(stream); long value = (read & 0x7F); result |= (value << (7 * numRead)); numRead++; if (numRead > 10) - { throw new OverflowException("VarLong is too big"); - } } while ((read & 0x80) != 0); return result; } @@ -380,36 +727,53 @@ public long ReadNextVarLong(Queue cache) /// Read a single byte from a cache of bytes and remove it from the cache /// /// The byte that was read - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public byte ReadNextByte(Queue cache) { byte result = cache.Dequeue(); return result; } + /// + /// Read a single byte from a cache of bytes and remove it from the cache + /// + /// The byte that was read + public byte ReadNextByte(PacketStream stream) + { + return stream.ReadByte(); + } + + /// + /// Read a single byte from a cache of bytes and remove it from the cache + /// + /// The byte that was read + public async Task ReadNextByteAsync(PacketStream stream) + { + return await stream.ReadByteAsync(); + } + /// /// Read an uncompressed Named Binary Tag blob and remove it from the cache /// - public Dictionary ReadNextNbt(Queue cache) + public async Task> ReadNextNbtAsync(PacketStream stream) { - return ReadNextNbt(cache, true); + return await ReadNextNbtAsync(stream, true); } /// /// Read a single item slot from a cache of bytes and remove it from the cache /// /// The item that was read or NULL for an empty slot - public Item? ReadNextItemSlot(Queue cache, ItemPalette itemPalette) + public async Task ReadNextItemSlotAsync(PacketStream stream, ItemPalette itemPalette) { if (protocolversion > Protocol18Handler.MC_1_13_Version) { // MC 1.13 and greater - bool itemPresent = ReadNextBool(cache); + bool itemPresent = await ReadNextBoolAsync(stream); if (itemPresent) { - ItemType type = itemPalette.FromId(ReadNextVarInt(cache)); - byte itemCount = ReadNextByte(cache); - Dictionary nbt = ReadNextNbt(cache); + ItemType type = itemPalette.FromId(await ReadNextVarIntAsync(stream)); + byte itemCount = await ReadNextByteAsync(stream); + Dictionary nbt = await ReadNextNbtAsync(stream); return new Item(type, itemCount, nbt); } else return null; @@ -417,12 +781,12 @@ public Dictionary ReadNextNbt(Queue cache) else { // MC 1.12.2 and lower - short itemID = ReadNextShort(cache); + short itemID = await ReadNextShortAsync(stream); if (itemID == -1) return null; - byte itemCount = ReadNextByte(cache); - short itemDamage = ReadNextShort(cache); - Dictionary nbt = ReadNextNbt(cache); + byte itemCount = await ReadNextByteAsync(stream); + short itemDamage = await ReadNextShortAsync(stream); + Dictionary nbt = await ReadNextNbtAsync(stream); return new Item(itemPalette.FromId(itemID), itemCount, nbt); } } @@ -433,47 +797,47 @@ public Dictionary ReadNextNbt(Queue cache) /// Mappings for converting entity type Ids to EntityType /// TRUE for living entities (layout differs) /// Entity information - public Entity ReadNextEntity(Queue cache, EntityPalette entityPalette, bool living) + public async Task ReadNextEntity(PacketStream stream, EntityPalette entityPalette, bool living) { - int entityID = ReadNextVarInt(cache); + int entityID = await ReadNextVarIntAsync(stream); if (protocolversion > Protocol18Handler.MC_1_8_Version) - ReadNextUUID(cache); + await SkipNextUUIDAsync(stream); EntityType entityType; // Entity type data type change from byte to varint after 1.14 if (protocolversion > Protocol18Handler.MC_1_13_Version) - entityType = entityPalette.FromId(ReadNextVarInt(cache), living); + entityType = entityPalette.FromId(await ReadNextVarIntAsync(stream), living); else - entityType = entityPalette.FromId(ReadNextByte(cache), living); + entityType = entityPalette.FromId(await ReadNextByteAsync(stream), living); - double entityX = ReadNextDouble(cache); - double entityY = ReadNextDouble(cache); - double entityZ = ReadNextDouble(cache); - byte entityPitch = ReadNextByte(cache); - byte entityYaw = ReadNextByte(cache); + double entityX = await ReadNextDoubleAsync(stream); + double entityY = await ReadNextDoubleAsync(stream); + double entityZ = await ReadNextDoubleAsync(stream); + byte entityPitch = await ReadNextByteAsync(stream); + byte entityYaw = await ReadNextByteAsync(stream); int metadata = -1; if (living) { if (protocolversion == Protocol18Handler.MC_1_18_2_Version) - entityYaw = ReadNextByte(cache); + entityYaw = await ReadNextByteAsync(stream); else - entityPitch = ReadNextByte(cache); + entityPitch = await ReadNextByteAsync(stream); } else { if (protocolversion >= Protocol18Handler.MC_1_19_Version) { - entityYaw = ReadNextByte(cache); - metadata = ReadNextVarInt(cache); + entityYaw = await ReadNextByteAsync(stream); + metadata = await ReadNextVarIntAsync(stream); } else - metadata = ReadNextInt(cache); + metadata = await ReadNextIntAsync(stream); } - short velocityX = ReadNextShort(cache); - short velocityY = ReadNextShort(cache); - short velocityZ = ReadNextShort(cache); + short velocityX = await ReadNextShortAsync(stream); + short velocityY = await ReadNextShortAsync(stream); + short velocityZ = await ReadNextShortAsync(stream); return new Entity(entityID, entityType, new Location(entityX, entityY, entityZ), entityYaw, entityPitch, metadata); } @@ -481,38 +845,36 @@ public Entity ReadNextEntity(Queue cache, EntityPalette entityPalette, boo /// /// Read an uncompressed Named Binary Tag blob and remove it from the cache (internal) /// - private Dictionary ReadNextNbt(Queue cache, bool root) + private async Task> ReadNextNbtAsync(PacketStream stream, bool root) { Dictionary nbtData = new(); if (root) { - if (cache.Peek() == 0) // TAG_End - { - cache.Dequeue(); + byte head = await ReadNextByteAsync(stream); // Tag type (TAG_Compound) + + if (head == 0) // TAG_End return nbtData; - } - if (cache.Peek() != 10) // TAG_Compound - throw new System.IO.InvalidDataException("Failed to decode NBT: Does not start with TAG_Compound"); - ReadNextByte(cache); // Tag type (TAG_Compound) + + if (head != 10) // TAG_Compound + throw new InvalidDataException("Failed to decode NBT: Does not start with TAG_Compound"); // NBT root name - string rootName = Encoding.ASCII.GetString(ReadData(ReadNextUShort(cache), cache)); + string rootName = await ReadNextAsciiStringAsync(stream, await ReadNextUShortAsync(stream)); - if (!String.IsNullOrEmpty(rootName)) - nbtData[""] = rootName; + if (!string.IsNullOrEmpty(rootName)) + nbtData[string.Empty] = rootName; } while (true) { - int fieldType = ReadNextByte(cache); + byte fieldType = await ReadNextByteAsync(stream); - if (fieldType == 0) // TAG_End + if (fieldType == 0x00) // TAG_End return nbtData; - int fieldNameLength = ReadNextUShort(cache); - string fieldName = Encoding.ASCII.GetString(ReadData(fieldNameLength, cache)); - object fieldValue = ReadNbtField(cache, fieldType); + string fieldName = await ReadNextAsciiStringAsync(stream, await ReadNextUShortAsync(stream)); + object fieldValue = await ReadNbtFieldAsync(stream, fieldType); // This will override previous tags with the same name nbtData[fieldName] = fieldValue; @@ -522,62 +884,62 @@ private Dictionary ReadNextNbt(Queue cache, bool root) /// /// Read a single Named Binary Tag field of the specified type and remove it from the cache /// - private object ReadNbtField(Queue cache, int fieldType) + private async Task ReadNbtFieldAsync(PacketStream stream, byte fieldType) { switch (fieldType) { case 1: // TAG_Byte - return ReadNextByte(cache); + return await ReadNextByteAsync(stream); case 2: // TAG_Short - return ReadNextShort(cache); + return await ReadNextShortAsync(stream); case 3: // TAG_Int - return ReadNextInt(cache); + return await ReadNextIntAsync(stream); case 4: // TAG_Long - return ReadNextLong(cache); + return await ReadNextLongAsync(stream); case 5: // TAG_Float - return ReadNextFloat(cache); + return await ReadNextFloatAsync(stream); case 6: // TAG_Double - return ReadNextDouble(cache); + return await ReadNextDoubleAsync(stream); case 7: // TAG_Byte_Array - return ReadData(ReadNextInt(cache), cache); + return await ReadDataAsync(await ReadNextIntAsync(stream), stream); case 8: // TAG_String - return Encoding.UTF8.GetString(ReadData(ReadNextUShort(cache), cache)); + return await ReadNextUtf8StringAsync(stream, await ReadNextUShortAsync(stream)); case 9: // TAG_List - int listType = ReadNextByte(cache); - int listLength = ReadNextInt(cache); + byte listType = await ReadNextByteAsync(stream); + int listLength = await ReadNextIntAsync(stream); object[] listItems = new object[listLength]; for (int i = 0; i < listLength; i++) - listItems[i] = ReadNbtField(cache, listType); + listItems[i] = await ReadNbtFieldAsync(stream, listType); return listItems; case 10: // TAG_Compound - return ReadNextNbt(cache, false); + return await ReadNextNbtAsync(stream, false); case 11: // TAG_Int_Array listType = 3; - listLength = ReadNextInt(cache); + listLength = await ReadNextIntAsync(stream); listItems = new object[listLength]; for (int i = 0; i < listLength; i++) - listItems[i] = ReadNbtField(cache, listType); + listItems[i] = await ReadNbtFieldAsync(stream, listType); return listItems; case 12: // TAG_Long_Array listType = 4; - listLength = ReadNextInt(cache); + listLength = await ReadNextIntAsync(stream); listItems = new object[listLength]; for (int i = 0; i < listLength; i++) - listItems[i] = ReadNbtField(cache, listType); + listItems[i] = await ReadNbtFieldAsync(stream, listType); return listItems; default: - throw new System.IO.InvalidDataException("Failed to decode NBT: Unknown field type " + fieldType); + throw new InvalidDataException("Failed to decode NBT: Unknown field type " + fieldType); } } - public Dictionary ReadNextMetadata(Queue cache, ItemPalette itemPalette) + public async Task> ReadNextMetadataAsync(PacketStream stream, ItemPalette itemPalette) { Dictionary data = new(); - byte key = ReadNextByte(cache); + byte key = await ReadNextByteAsync(stream); while (key != 0xff) { - int type = ReadNextVarInt(cache); + int type = await ReadNextVarIntAsync(stream); // starting from 1.13, Optional Chat is inserted as number 5 in 1.13 and IDs after 5 got shifted. // Increase type ID by 1 if @@ -601,121 +963,117 @@ private object ReadNbtField(Queue cache, int fieldType) switch (type) { case 0: // byte - value = ReadNextByte(cache); + value = await ReadNextByteAsync(stream); break; case 1: // VarInt - value = ReadNextVarInt(cache); + value = await ReadNextVarIntAsync(stream); break; case 2: // Float - value = ReadNextFloat(cache); + value = await ReadNextFloatAsync(stream); break; case 3: // String - value = ReadNextString(cache); + value = await ReadNextStringAsync(stream); break; case 4: // Chat - value = ReadNextString(cache); + value = await ReadNextStringAsync(stream); break; case 5: // Optional Chat - if (ReadNextBool(cache)) - value = ReadNextString(cache); + if (await ReadNextBoolAsync(stream)) + value = await ReadNextStringAsync(stream); break; case 6: // Slot - value = ReadNextItemSlot(cache, itemPalette); + value = await ReadNextItemSlotAsync(stream, itemPalette); break; case 7: // Boolean - value = ReadNextBool(cache); + value = await ReadNextBoolAsync(stream); break; case 8: // Rotation (3x floats) value = new List { - ReadNextFloat(cache), - ReadNextFloat(cache), - ReadNextFloat(cache) + await ReadNextFloatAsync(stream), + await ReadNextFloatAsync(stream), + await ReadNextFloatAsync(stream) }; break; case 9: // Position - value = ReadNextLocation(cache); + value = await ReadNextLocationAsync(stream); break; case 10: // Optional Position - if (ReadNextBool(cache)) - { - value = ReadNextLocation(cache); - } + if (await ReadNextBoolAsync(stream)) + value = await ReadNextLocationAsync(stream); break; case 11: // Direction (VarInt) - value = ReadNextVarInt(cache); + value = await ReadNextVarIntAsync(stream); break; case 12: // Optional UUID - if (ReadNextBool(cache)) - { - value = ReadNextUUID(cache); - } + if (await ReadNextBoolAsync(stream)) + value = await ReadNextUUIDAsync(stream); break; case 13: // Optional BlockID (VarInt) - value = ReadNextVarInt(cache); + value = await ReadNextVarIntAsync(stream); break; case 14: // NBT - value = ReadNextNbt(cache); + value = await ReadNextNbtAsync(stream); break; case 15: // Particle // Currecutly not handled. Reading data only - int ParticleID = ReadNextVarInt(cache); + int ParticleID = await ReadNextVarIntAsync(stream); switch (ParticleID) { case 3: - ReadNextVarInt(cache); + await SkipNextVarIntAsync(stream); break; case 14: - ReadNextFloat(cache); - ReadNextFloat(cache); - ReadNextFloat(cache); - ReadNextFloat(cache); + await SkipNextFloatAsync(stream); + await SkipNextFloatAsync(stream); + await SkipNextFloatAsync(stream); + await SkipNextFloatAsync(stream); break; case 23: - ReadNextVarInt(cache); + await SkipNextVarIntAsync(stream); break; case 32: - ReadNextItemSlot(cache, itemPalette); + await ReadNextItemSlotAsync(stream, itemPalette); break; } break; case 16: // Villager Data (3x VarInt) value = new List { - ReadNextVarInt(cache), - ReadNextVarInt(cache), - ReadNextVarInt(cache) + await ReadNextVarIntAsync(stream), + await ReadNextVarIntAsync(stream), + await ReadNextVarIntAsync(stream) }; break; case 17: // Optional VarInt - if (ReadNextBool(cache)) + if (await ReadNextBoolAsync(stream)) { - value = ReadNextVarInt(cache); + value = await ReadNextVarIntAsync(stream); } break; case 18: // Pose - value = ReadNextVarInt(cache); + value = await ReadNextVarIntAsync(stream); break; case 19: // Cat Variant - value = ReadNextVarInt(cache); + value = await ReadNextVarIntAsync(stream); break; case 20: // Frog Varint - value = ReadNextVarInt(cache); + value = await ReadNextVarIntAsync(stream); break; case 21: // GlobalPos at 1.19.2+; Painting Variant at 1.19- if (protocolversion <= Protocol18Handler.MC_1_19_Version) - value = ReadNextVarInt(cache); + value = await ReadNextVarIntAsync(stream); else value = null; // Dimension and blockPos, currently not in use break; case 22: // Painting Variant - value = ReadNextVarInt(cache); + value = await ReadNextVarIntAsync(stream); break; default: - throw new System.IO.InvalidDataException("Unknown Metadata Type ID " + type + ". Is this up to date for new MC Version?"); + throw new InvalidDataException("Unknown Metadata Type ID " + type + ". Is this up to date for new MC Version?"); } data[key] = value; - key = ReadNextByte(cache); + key = await ReadNextByteAsync(stream); } return data; } @@ -724,22 +1082,20 @@ private object ReadNbtField(Queue cache, int fieldType) /// Read a single villager trade from a cache of bytes and remove it from the cache /// /// The item that was read or NULL for an empty slot - public VillagerTrade ReadNextTrade(Queue cache, ItemPalette itemPalette) + public async Task ReadNextTradeAsync(PacketStream stream, ItemPalette itemPalette) { - Item inputItem1 = ReadNextItemSlot(cache, itemPalette)!; - Item outputItem = ReadNextItemSlot(cache, itemPalette)!; + Item inputItem1 = (await ReadNextItemSlotAsync(stream, itemPalette))!; + Item outputItem = (await ReadNextItemSlotAsync(stream, itemPalette))!; Item? inputItem2 = null; - if (ReadNextBool(cache)) //check if villager has second item - { - inputItem2 = ReadNextItemSlot(cache, itemPalette); - } - bool tradeDisabled = ReadNextBool(cache); - int numberOfTradeUses = ReadNextInt(cache); - int maximumNumberOfTradeUses = ReadNextInt(cache); - int xp = ReadNextInt(cache); - int specialPrice = ReadNextInt(cache); - float priceMultiplier = ReadNextFloat(cache); - int demand = ReadNextInt(cache); + if (await ReadNextBoolAsync(stream)) //check if villager has second item + inputItem2 = await ReadNextItemSlotAsync(stream, itemPalette); + bool tradeDisabled = await ReadNextBoolAsync(stream); + int numberOfTradeUses = await ReadNextIntAsync(stream); + int maximumNumberOfTradeUses = await ReadNextIntAsync(stream); + int xp = await ReadNextIntAsync(stream); + int specialPrice = await ReadNextIntAsync(stream); + float priceMultiplier = await ReadNextFloatAsync(stream); + int demand = await ReadNextIntAsync(stream); return new VillagerTrade(inputItem1, outputItem, inputItem2, tradeDisabled, numberOfTradeUses, maximumNumberOfTradeUses, xp, specialPrice, priceMultiplier, demand); } @@ -773,8 +1129,8 @@ private byte[] GetNbt(Dictionary? nbt, bool root) // NBT root name string? rootName = null; - if (nbt.ContainsKey("")) - rootName = nbt[""] as string; + if (nbt.TryGetValue("", out object? rootNameObj)) + rootName = rootNameObj as string; rootName ??= ""; @@ -809,106 +1165,91 @@ private byte[] GetNbt(Dictionary? nbt, bool root) /// Binary data for the passed object private byte[] GetNbtField(object obj, out byte fieldType) { - if (obj is byte) - { - fieldType = 1; // TAG_Byte - return new[] { (byte)obj }; - } - else if (obj is short) - { - fieldType = 2; // TAG_Short - return GetShort((short)obj); - } - else if (obj is int) - { - fieldType = 3; // TAG_Int - return GetInt((int)obj); - } - else if (obj is long) - { - fieldType = 4; // TAG_Long - return GetLong((long)obj); - } - else if (obj is float) + switch (obj) { - fieldType = 5; // TAG_Float - return GetFloat((float)obj); - } - else if (obj is double) - { - fieldType = 6; // TAG_Double - return GetDouble((double)obj); - } - else if (obj is byte[]) - { - fieldType = 7; // TAG_Byte_Array - return (byte[])obj; - } - else if (obj is string) - { - fieldType = 8; // TAG_String - byte[] stringBytes = Encoding.UTF8.GetBytes((string)obj); - return ConcatBytes(GetUShort((ushort)stringBytes.Length), stringBytes); - } - else if (obj is object[]) - { - fieldType = 9; // TAG_List + case byte: + fieldType = 1; // TAG_Byte + return new[] { (byte)obj }; + case short: + fieldType = 2; // TAG_Short + return GetShort((short)obj); + case int: + fieldType = 3; // TAG_Int + return GetInt((int)obj); + case long: + fieldType = 4; // TAG_Long + return GetLong((long)obj); + case float: + fieldType = 5; // TAG_Float + return GetFloat((float)obj); + case double: + fieldType = 6; // TAG_Double + return GetDouble((double)obj); + case byte[]: + fieldType = 7; // TAG_Byte_Array + return (byte[])obj; + case string: + { + fieldType = 8; // TAG_String + byte[] stringBytes = Encoding.UTF8.GetBytes((string)obj); + return ConcatBytes(GetUShort((ushort)stringBytes.Length), stringBytes); + } + case object[]: + { + fieldType = 9; // TAG_List - List list = new((object[])obj); - int arrayLengthTotal = list.Count; + List list = new((object[])obj); + int arrayLengthTotal = list.Count; - // Treat empty list as TAG_Byte, length 0 - if (arrayLengthTotal == 0) - return ConcatBytes(new[] { (byte)1 }, GetInt(0)); + // Treat empty list as TAG_Byte, length 0 + if (arrayLengthTotal == 0) + return ConcatBytes(new[] { (byte)1 }, GetInt(0)); - // Encode first list item, retain its type - string firstItemTypeString = list[0].GetType().Name; - byte[] firstItemBytes = GetNbtField(list[0], out byte firstItemType); - list.RemoveAt(0); + // Encode first list item, retain its type + string firstItemTypeString = list[0].GetType().Name; + byte[] firstItemBytes = GetNbtField(list[0], out byte firstItemType); + list.RemoveAt(0); - // Encode further list items, check they have the same type - List subsequentItemsBytes = new(); - foreach (object item in list) - { - subsequentItemsBytes.AddRange(GetNbtField(item, out byte subsequentItemType)); - if (subsequentItemType != firstItemType) - throw new System.IO.InvalidDataException( - "GetNbt: Cannot encode object[] list with mixed types: " + firstItemTypeString + ", " + item.GetType().Name + " into NBT!"); - } + // Encode further list items, check they have the same type + List subsequentItemsBytes = new(); + foreach (object item in list) + { + subsequentItemsBytes.AddRange(GetNbtField(item, out byte subsequentItemType)); + if (subsequentItemType != firstItemType) + throw new InvalidDataException( + "GetNbt: Cannot encode object[] list with mixed types: " + firstItemTypeString + ", " + item.GetType().Name + " into NBT!"); + } - // Build NBT list: type, length, item array - return ConcatBytes(new[] { firstItemType }, GetInt(arrayLengthTotal), firstItemBytes, subsequentItemsBytes.ToArray()); - } - else if (obj is Dictionary) - { - fieldType = 10; // TAG_Compound - return GetNbt((Dictionary)obj, false); - } - else if (obj is int[]) - { - fieldType = 11; // TAG_Int_Array - - int[] srcIntList = (int[])obj; - List encIntList = new(); - encIntList.AddRange(GetInt(srcIntList.Length)); - foreach (int item in srcIntList) - encIntList.AddRange(GetInt(item)); - return encIntList.ToArray(); - } - else if (obj is long[]) - { - fieldType = 12; // TAG_Long_Array - - long[] srcLongList = (long[])obj; - List encLongList = new(); - encLongList.AddRange(GetInt(srcLongList.Length)); - foreach (long item in srcLongList) - encLongList.AddRange(GetLong(item)); - return encLongList.ToArray(); - } - else - { - throw new System.IO.InvalidDataException("GetNbt: Cannot encode data type " + obj.GetType().Name + " into NBT!"); + // Build NBT list: type, length, item array + return ConcatBytes(new[] { firstItemType }, GetInt(arrayLengthTotal), firstItemBytes, subsequentItemsBytes.ToArray()); + } + case Dictionary: + fieldType = 10; // TAG_Compound + return GetNbt((Dictionary)obj, false); + case int[]: + { + fieldType = 11; // TAG_Int_Array + + int[] srcIntList = (int[])obj; + List encIntList = new(); + encIntList.AddRange(GetInt(srcIntList.Length)); + foreach (int item in srcIntList) + encIntList.AddRange(GetInt(item)); + return encIntList.ToArray(); + } + case long[]: + { + fieldType = 12; // TAG_Long_Array + + long[] srcLongList = (long[])obj; + List encLongList = new(); + encLongList.AddRange(GetInt(srcLongList.Length)); + foreach (long item in srcLongList) + encLongList.AddRange(GetLong(item)); + return encLongList.ToArray(); + } + default: + throw new InvalidDataException("GetNbt: Cannot encode data type " + obj.GetType().Name + " into NBT!"); } } diff --git a/MinecraftClient/Protocol/Handlers/Forge/ForgeInfo.cs b/MinecraftClient/Protocol/Handlers/Forge/ForgeInfo.cs index 781cfd5a99..51e58e0a5d 100755 --- a/MinecraftClient/Protocol/Handlers/Forge/ForgeInfo.cs +++ b/MinecraftClient/Protocol/Handlers/Forge/ForgeInfo.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; namespace MinecraftClient.Protocol.Handlers.Forge { @@ -11,16 +12,22 @@ public class ForgeInfo /// /// Represents an individual forge mod. /// - public class ForgeMod + public record ForgeMod { - public ForgeMod(String ModID, String Version) + public ForgeMod(string? modID, string? version) { - this.ModID = ModID; - this.Version = Version; + ModID = modID; + Version = ModMarker = version; } - public readonly String ModID; - public readonly String Version; + [JsonPropertyName("modId")] + public string? ModID { init; get; } + + [JsonPropertyName("version")] + public string? Version { init; get; } + + [JsonPropertyName("modmarker")] + public string? ModMarker { init; get; } public override string ToString() { @@ -138,5 +145,16 @@ internal ForgeInfo(Json.JSONData data, FMLVersion fmlVersion) throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!"); } } + + /// + /// Create a new ForgeInfo from the given data. + /// + /// The modinfo JSON tag. + /// Forge protocol version + internal ForgeInfo(ForgeMod[] mods, FMLVersion fmlVersion) + { + Mods = new(mods); + Version = fmlVersion; + } } } diff --git a/MinecraftClient/Protocol/Handlers/Packet/s2c/DeclareCommands.cs b/MinecraftClient/Protocol/Handlers/Packet/s2c/DeclareCommands.cs index 22835b616e..8ae673fd77 100644 --- a/MinecraftClient/Protocol/Handlers/Packet/s2c/DeclareCommands.cs +++ b/MinecraftClient/Protocol/Handlers/Packet/s2c/DeclareCommands.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; +using MinecraftClient.Protocol.PacketPipeline; namespace MinecraftClient.Protocol.Handlers.packet.s2c { @@ -8,7 +10,7 @@ internal static class DeclareCommands private static int RootIdx; private static CommandNode[] Nodes = Array.Empty(); - public static void Read(DataTypes dataTypes, Queue packetData) + public static async Task Read(DataTypes dataTypes, PacketStream packetData) { int count = dataTypes.ReadNextVarInt(packetData); Nodes = new CommandNode[count]; @@ -23,7 +25,7 @@ public static void Read(DataTypes dataTypes, Queue packetData) int redirectNode = ((flags & 0x08) > 0) ? dataTypes.ReadNextVarInt(packetData) : -1; - string? name = ((flags & 0x03) == 1 || (flags & 0x03) == 2) ? dataTypes.ReadNextString(packetData) : null; + string? name = ((flags & 0x03) == 1 || (flags & 0x03) == 2) ? (await dataTypes.ReadNextStringAsync(packetData)) : null; int paserId = ((flags & 0x03) == 2) ? dataTypes.ReadNextVarInt(packetData) : -1; Paser? paser = null; @@ -50,7 +52,7 @@ public static void Read(DataTypes dataTypes, Queue packetData) }; } - string? suggestionsType = ((flags & 0x10) > 0) ? dataTypes.ReadNextString(packetData) : null; + string? suggestionsType = ((flags & 0x10) > 0) ? (await dataTypes.ReadNextStringAsync(packetData)) : null; Nodes[i] = new(flags, childs, redirectNode, name, paser, suggestionsType); } @@ -158,7 +160,7 @@ internal abstract class Paser internal class PaserEmpty : Paser { - public PaserEmpty(DataTypes dataTypes, Queue packetData) { } + public PaserEmpty(DataTypes dataTypes, PacketStream packetData) { } public override bool Check(string text) { @@ -181,7 +183,7 @@ internal class PaserFloat : Paser private byte Flags; private float Min = float.MinValue, Max = float.MaxValue; - public PaserFloat(DataTypes dataTypes, Queue packetData) + public PaserFloat(DataTypes dataTypes, PacketStream packetData) { Flags = dataTypes.ReadNextByte(packetData); if ((Flags & 0x01) > 0) @@ -211,7 +213,7 @@ internal class PaserDouble : Paser private byte Flags; private double Min = double.MinValue, Max = double.MaxValue; - public PaserDouble(DataTypes dataTypes, Queue packetData) + public PaserDouble(DataTypes dataTypes, PacketStream packetData) { Flags = dataTypes.ReadNextByte(packetData); if ((Flags & 0x01) > 0) @@ -241,7 +243,7 @@ internal class PaserInteger : Paser private byte Flags; private int Min = int.MinValue, Max = int.MaxValue; - public PaserInteger(DataTypes dataTypes, Queue packetData) + public PaserInteger(DataTypes dataTypes, PacketStream packetData) { Flags = dataTypes.ReadNextByte(packetData); if ((Flags & 0x01) > 0) @@ -271,7 +273,7 @@ internal class PaserLong : Paser private byte Flags; private long Min = long.MinValue, Max = long.MaxValue; - public PaserLong(DataTypes dataTypes, Queue packetData) + public PaserLong(DataTypes dataTypes, PacketStream packetData) { Flags = dataTypes.ReadNextByte(packetData); if ((Flags & 0x01) > 0) @@ -302,7 +304,7 @@ internal class PaserString : Paser private enum StringType { SINGLE_WORD, QUOTABLE_PHRASE, GREEDY_PHRASE }; - public PaserString(DataTypes dataTypes, Queue packetData) + public PaserString(DataTypes dataTypes, PacketStream packetData) { Type = (StringType)dataTypes.ReadNextVarInt(packetData); } @@ -327,7 +329,7 @@ internal class PaserEntity : Paser { private byte Flags; - public PaserEntity(DataTypes dataTypes, Queue packetData) + public PaserEntity(DataTypes dataTypes, PacketStream packetData) { Flags = dataTypes.ReadNextByte(packetData); } @@ -351,7 +353,7 @@ public override string GetName() internal class PaserBlockPos : Paser { - public PaserBlockPos(DataTypes dataTypes, Queue packetData) { } + public PaserBlockPos(DataTypes dataTypes, PacketStream packetData) { } public override bool Check(string text) { @@ -372,7 +374,7 @@ public override string GetName() internal class PaserColumnPos : Paser { - public PaserColumnPos(DataTypes dataTypes, Queue packetData) { } + public PaserColumnPos(DataTypes dataTypes, PacketStream packetData) { } public override bool Check(string text) { @@ -393,7 +395,7 @@ public override string GetName() internal class PaserVec3 : Paser { - public PaserVec3(DataTypes dataTypes, Queue packetData) { } + public PaserVec3(DataTypes dataTypes, PacketStream packetData) { } public override bool Check(string text) { @@ -414,7 +416,7 @@ public override string GetName() internal class PaserVec2 : Paser { - public PaserVec2(DataTypes dataTypes, Queue packetData) { } + public PaserVec2(DataTypes dataTypes, PacketStream packetData) { } public override bool Check(string text) { @@ -435,7 +437,7 @@ public override string GetName() internal class PaserRotation : Paser { - public PaserRotation(DataTypes dataTypes, Queue packetData) { } + public PaserRotation(DataTypes dataTypes, PacketStream packetData) { } public override bool Check(string text) { @@ -455,7 +457,7 @@ public override string GetName() internal class PaserMessage : Paser { - public PaserMessage(DataTypes dataTypes, Queue packetData) { } + public PaserMessage(DataTypes dataTypes, PacketStream packetData) { } public override bool Check(string text) { @@ -477,7 +479,7 @@ internal class PaserScoreHolder : Paser { private byte Flags; - public PaserScoreHolder(DataTypes dataTypes, Queue packetData) + public PaserScoreHolder(DataTypes dataTypes, PacketStream packetData) { Flags = dataTypes.ReadNextByte(packetData); } @@ -502,7 +504,7 @@ internal class PaserRange : Paser { private bool Decimals; - public PaserRange(DataTypes dataTypes, Queue packetData) + public PaserRange(DataTypes dataTypes, PacketStream packetData) { Decimals = dataTypes.ReadNextBool(packetData); } @@ -527,9 +529,11 @@ internal class PaserResourceOrTag : Paser { private string Registry; - public PaserResourceOrTag(DataTypes dataTypes, Queue packetData) + public PaserResourceOrTag(DataTypes dataTypes, PacketStream packetData) { - Registry = dataTypes.ReadNextString(packetData); + var task = dataTypes.ReadNextStringAsync(packetData); + task.Wait(); + Registry = task.Result; } public override bool Check(string text) @@ -552,9 +556,11 @@ internal class PaserResource : Paser { private string Registry; - public PaserResource(DataTypes dataTypes, Queue packetData) + public PaserResource(DataTypes dataTypes, PacketStream packetData) { - Registry = dataTypes.ReadNextString(packetData); + var task = dataTypes.ReadNextStringAsync(packetData); + task.Wait(); + Registry = task.Result; } public override bool Check(string text) diff --git a/MinecraftClient/Protocol/Handlers/Protocol16.cs b/MinecraftClient/Protocol/Handlers/Protocol16.cs index 9bb7efcf98..c903571472 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol16.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol16.cs @@ -2,18 +2,22 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Net.Http; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; using System.Threading; +using System.Threading.Tasks; using MinecraftClient.Crypto; using MinecraftClient.Inventory; using MinecraftClient.Mapping; using MinecraftClient.Protocol.Message; +using MinecraftClient.Protocol.PacketPipeline; using MinecraftClient.Protocol.ProfileKey; using MinecraftClient.Protocol.Session; using MinecraftClient.Proxy; using MinecraftClient.Scripting; +using static ConsoleInteractive.ConsoleReader; using static MinecraftClient.Settings; namespace MinecraftClient.Protocol.Handlers @@ -27,12 +31,15 @@ class Protocol16Handler : IMinecraftCom readonly IMinecraftComHandler handler; private bool encrypted = false; private readonly int protocolversion; - private Tuple? netRead = null; - Crypto.AesCfb8Stream? s; + + AesStream? s; readonly TcpClient c; - public Protocol16Handler(TcpClient Client, int ProtocolVersion, IMinecraftComHandler Handler) + private readonly CancellationToken CancelToken; + + public Protocol16Handler(CancellationToken cancelToken, TcpClient Client, int ProtocolVersion, IMinecraftComHandler Handler) { + CancelToken = cancelToken; ConsoleIO.SetAutoCompleteEngine(this); if (protocolversion >= 72) ChatParser.InitTranslations(); @@ -67,390 +74,398 @@ private Protocol16Handler(TcpClient Client) c = Client; } - private void Updater(object? o) + public async Task StartUpdating() { - if (((CancellationToken)o!).IsCancellationRequested) + if (CancelToken.IsCancellationRequested) return; try { - while (!((CancellationToken)o!).IsCancellationRequested) + while (!CancelToken.IsCancellationRequested) { do { Thread.Sleep(100); - } while (Update()); + } while (await Update()); } } catch (System.IO.IOException) { } catch (SocketException) { } catch (ObjectDisposedException) { } - if (((CancellationToken)o!).IsCancellationRequested) + if (CancelToken.IsCancellationRequested) return; handler.OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, ""); } - private bool Update() + private async Task Update() { - handler.OnUpdate(); + await handler.OnUpdate(); bool connection_ok = true; while (c.Client.Available > 0 && connection_ok) { - byte id = ReadNextByte(); - connection_ok = ProcessPacket(id); + byte id = await ReadNextByte(); + connection_ok = await ProcessPacket(id); } return connection_ok; } - private bool ProcessPacket(byte id) + private async Task ProcessPacket(byte id) { int nbr; switch (id) { case 0x00: byte[] keepalive = new byte[5] { 0, 0, 0, 0, 0 }; - Receive(keepalive, 1, 4, SocketFlags.None); + await ReceiveAsync(keepalive, 1, 4); handler.OnServerKeepAlive(); - Send(keepalive); break; - case 0x01: ReadData(4); ReadNextString(); ReadData(5); break; - case 0x02: ReadData(1); ReadNextString(); ReadNextString(); ReadData(4); break; + await Send(keepalive); break; + case 0x01: await ReadData(4); await ReadNextString(); await ReadData(5); break; + case 0x02: await ReadData(1); await ReadNextString(); await ReadNextString(); await ReadData(4); break; case 0x03: - string message = ReadNextString(); - handler.OnTextReceived(new ChatMessage(message, protocolversion >= 72, 0, Guid.Empty)); break; - case 0x04: ReadData(16); break; - case 0x05: ReadData(6); ReadNextItemSlot(); break; - case 0x06: ReadData(12); break; - case 0x07: ReadData(9); break; - case 0x08: if (protocolversion >= 72) { ReadData(10); } else ReadData(8); break; - case 0x09: ReadData(8); ReadNextString(); break; - case 0x0A: ReadData(1); break; - case 0x0B: ReadData(33); break; - case 0x0C: ReadData(9); break; - case 0x0D: ReadData(41); break; - case 0x0E: ReadData(11); break; - case 0x0F: ReadData(10); ReadNextItemSlot(); ReadData(3); break; - case 0x10: ReadData(2); break; - case 0x11: ReadData(14); break; - case 0x12: ReadData(5); break; - case 0x13: if (protocolversion >= 72) { ReadData(9); } else ReadData(5); break; - case 0x14: ReadData(4); ReadNextString(); ReadData(16); ReadNextEntityMetaData(); break; - case 0x16: ReadData(8); break; - case 0x17: ReadData(19); ReadNextObjectData(); break; - case 0x18: ReadData(26); ReadNextEntityMetaData(); break; - case 0x19: ReadData(4); ReadNextString(); ReadData(16); break; - case 0x1A: ReadData(18); break; - case 0x1B: if (protocolversion >= 72) { ReadData(10); } break; - case 0x1C: ReadData(10); break; - case 0x1D: nbr = (int)ReadNextByte(); ReadData(nbr * 4); break; - case 0x1E: ReadData(4); break; - case 0x1F: ReadData(7); break; - case 0x20: ReadData(6); break; - case 0x21: ReadData(9); break; - case 0x22: ReadData(18); break; - case 0x23: ReadData(5); break; - case 0x26: ReadData(5); break; - case 0x27: if (protocolversion >= 72) { ReadData(9); } else ReadData(8); break; - case 0x28: ReadData(4); ReadNextEntityMetaData(); break; - case 0x29: ReadData(8); break; - case 0x2A: ReadData(5); break; - case 0x2B: ReadData(8); break; - case 0x2C: if (protocolversion >= 72) { ReadNextEntityProperties(protocolversion); } break; - case 0x33: ReadData(13); nbr = ReadNextInt(); ReadData(nbr); break; - case 0x34: ReadData(10); nbr = ReadNextInt(); ReadData(nbr); break; - case 0x35: ReadData(12); break; - case 0x36: ReadData(14); break; - case 0x37: ReadData(17); break; - case 0x38: ReadNextChunkBulkData(); break; - case 0x3C: ReadData(28); nbr = ReadNextInt(); ReadData(3 * nbr); ReadData(12); break; - case 0x3D: ReadData(18); break; - case 0x3E: ReadNextString(); ReadData(17); break; - case 0x3F: if (protocolversion > 51) { ReadNextString(); ReadData(32); } break; - case 0x46: ReadData(2); break; - case 0x47: ReadData(17); break; - case 0x64: ReadNextWindowData(protocolversion); break; - case 0x65: ReadData(1); break; - case 0x66: ReadData(7); ReadNextItemSlot(); break; - case 0x67: ReadData(3); ReadNextItemSlot(); break; - case 0x68: ReadData(1); for (nbr = ReadNextShort(); nbr > 0; nbr--) { ReadNextItemSlot(); } break; - case 0x69: ReadData(5); break; - case 0x6A: ReadData(4); break; - case 0x6B: ReadData(2); ReadNextItemSlot(); break; - case 0x6C: ReadData(2); break; - case 0x82: ReadData(10); ReadNextString(); ReadNextString(); ReadNextString(); ReadNextString(); break; - case 0x83: ReadData(4); nbr = ReadNextShort(); ReadData(nbr); break; - case 0x84: ReadData(11); nbr = ReadNextShort(); if (nbr > 0) { ReadData(nbr); } break; - case 0x85: if (protocolversion >= 74) { ReadData(13); } break; + string message = await ReadNextString(); + await handler.OnTextReceivedAsync(new ChatMessage(message, protocolversion >= 72, 0, Guid.Empty)); break; + case 0x04: await ReadData(16); break; + case 0x05: await ReadData(6); await ReadNextItemSlot(); break; + case 0x06: await ReadData(12); break; + case 0x07: await ReadData(9); break; + case 0x08: if (protocolversion >= 72) { await ReadData(10); } else await ReadData(8); break; + case 0x09: await ReadData(8); await ReadNextString(); break; + case 0x0A: await ReadData(1); break; + case 0x0B: await ReadData(33); break; + case 0x0C: await ReadData(9); break; + case 0x0D: await ReadData(41); break; + case 0x0E: await ReadData(11); break; + case 0x0F: await ReadData(10); await ReadNextItemSlot(); await ReadData(3); break; + case 0x10: await ReadData(2); break; + case 0x11: await ReadData(14); break; + case 0x12: await ReadData(5); break; + case 0x13: if (protocolversion >= 72) { await ReadData(9); } else await ReadData(5); break; + case 0x14: await ReadData(4); await ReadNextString(); await ReadData(16); await ReadNextEntityMetaData(); break; + case 0x16: await ReadData(8); break; + case 0x17: await ReadData(19); await ReadNextObjectData(); break; + case 0x18: await ReadData(26); await ReadNextEntityMetaData(); break; + case 0x19: await ReadData(4); await ReadNextString(); await ReadData(16); break; + case 0x1A: await ReadData(18); break; + case 0x1B: if (protocolversion >= 72) { await ReadData(10); } break; + case 0x1C: await ReadData(10); break; + case 0x1D: nbr = (int)(await ReadNextByte()); await ReadData(nbr * 4); break; + case 0x1E: await ReadData(4); break; + case 0x1F: await ReadData(7); break; + case 0x20: await ReadData(6); break; + case 0x21: await ReadData(9); break; + case 0x22: await ReadData(18); break; + case 0x23: await ReadData(5); break; + case 0x26: await ReadData(5); break; + case 0x27: if (protocolversion >= 72) { await ReadData(9); } else await ReadData(8); break; + case 0x28: await ReadData(4); await ReadNextEntityMetaData(); break; + case 0x29: await ReadData(8); break; + case 0x2A: await ReadData(5); break; + case 0x2B: await ReadData(8); break; + case 0x2C: if (protocolversion >= 72) { await ReadNextEntityProperties(protocolversion); } break; + case 0x33: await ReadData(13); nbr = await ReadNextInt(); await ReadData(nbr); break; + case 0x34: await ReadData(10); nbr = await ReadNextInt(); await ReadData(nbr); break; + case 0x35: await ReadData(12); break; + case 0x36: await ReadData(14); break; + case 0x37: await ReadData(17); break; + case 0x38: await ReadNextChunkBulkData(); break; + case 0x3C: await ReadData(28); nbr = await ReadNextInt(); await ReadData(3 * nbr); await ReadData(12); break; + case 0x3D: await ReadData(18); break; + case 0x3E: await ReadNextString(); await ReadData(17); break; + case 0x3F: if (protocolversion > 51) { await ReadNextString(); await ReadData(32); } break; + case 0x46: await ReadData(2); break; + case 0x47: await ReadData(17); break; + case 0x64: await ReadNextWindowData(protocolversion); break; + case 0x65: await ReadData(1); break; + case 0x66: await ReadData(7); await ReadNextItemSlot(); break; + case 0x67: await ReadData(3); await ReadNextItemSlot(); break; + case 0x68: await ReadData(1); for (nbr = await ReadNextShort(); nbr > 0; nbr--) { await ReadNextItemSlot(); } break; + case 0x69: await ReadData(5); break; + case 0x6A: await ReadData(4); break; + case 0x6B: await ReadData(2); await ReadNextItemSlot(); break; + case 0x6C: await ReadData(2); break; + case 0x82: await ReadData(10); await ReadNextString(); await ReadNextString(); await ReadNextString(); await ReadNextString(); break; + case 0x83: await ReadData(4); nbr = await ReadNextShort(); await ReadData(nbr); break; + case 0x84: await ReadData(11); nbr = await ReadNextShort(); if (nbr > 0) { await ReadData(nbr); } break; + case 0x85: if (protocolversion >= 74) { await ReadData(13); } break; case 0xC8: - if (ReadNextInt() == 2022) { ConsoleIO.WriteLogLine(Translations.mcc_player_dead, acceptnewlines: true); } - if (protocolversion >= 72) { ReadData(4); } else ReadData(1); + if (await ReadNextInt() == 2022) { ConsoleIO.WriteLogLine(Translations.mcc_player_dead, acceptnewlines: true); } + if (protocolversion >= 72) { await ReadData(4); } else await ReadData(1); break; case 0xC9: - string name = ReadNextString(); bool online = ReadNextByte() != 0x00; ReadData(2); - Guid FakeUUID = new(MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(name)).Take(16).ToArray()); - if (online) { handler.OnPlayerJoin(new PlayerInfo(name, FakeUUID)); } else { handler.OnPlayerLeave(FakeUUID); } + string name = await ReadNextString(); bool online = await ReadNextByte() != 0x00; await ReadData(2); + Guid FakeUUID = new(MD5.HashData(Encoding.UTF8.GetBytes(name)).Take(16).ToArray()); + if (online) { await handler.OnPlayerJoinAsync(new PlayerInfo(name, FakeUUID)); } else { await handler.OnPlayerLeaveAsync(FakeUUID); } break; - case 0xCA: if (protocolversion >= 72) { ReadData(9); } else ReadData(3); break; + case 0xCA: if (protocolversion >= 72) { await ReadData(9); } else await ReadData(3); break; case 0xCB: - string resultString = ReadNextString(); + string resultString = await ReadNextString(); if (!string.IsNullOrEmpty(resultString)) { string[] result = resultString.Split((char)0x00); handler.OnAutoCompleteDone(0, result); } break; - case 0xCC: ReadNextString(); ReadData(4); break; - case 0xCD: ReadData(1); break; - case 0xCE: if (protocolversion > 51) { ReadNextString(); ReadNextString(); ReadData(1); } break; - case 0xCF: if (protocolversion > 51) { ReadNextString(); ReadData(1); ReadNextString(); } ReadData(4); break; - case 0xD0: if (protocolversion > 51) { ReadData(1); ReadNextString(); } break; - case 0xD1: if (protocolversion > 51) { ReadNextTeamData(); } break; + case 0xCC: await ReadNextString(); await ReadData(4); break; + case 0xCD: await ReadData(1); break; + case 0xCE: if (protocolversion > 51) { await ReadNextString(); await ReadNextString(); await ReadData(1); } break; + case 0xCF: if (protocolversion > 51) { await ReadNextString(); await ReadData(1); await ReadNextString(); } await ReadData(4); break; + case 0xD0: if (protocolversion > 51) { await ReadData(1); await ReadNextString(); } break; + case 0xD1: if (protocolversion > 51) { await ReadNextTeamData(); } break; case 0xFA: - string channel = ReadNextString(); - byte[] payload = ReadNextByteArray(); + string channel = await ReadNextString(); + byte[] payload = await ReadNextByteArray(); handler.OnPluginChannelMessage(channel, payload); break; case 0xFF: - string reason = ReadNextString(); + string reason = await ReadNextString(); handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick, reason); break; default: return false; //unknown packet! } return true; //packet has been successfully skipped } - private void StartUpdating() - { - netRead = new(new Thread(new ParameterizedThreadStart(Updater)), new CancellationTokenSource()); - netRead.Item1.Name = "ProtocolPacketHandler"; - netRead.Item1.Start(netRead.Item2.Token); - } - - /// - /// Get net read thread (main thread) ID - /// - /// Net read thread ID - public int GetNetMainThreadId() - { - return netRead != null ? netRead.Item1.ManagedThreadId : -1; - } - public void Dispose() { try { - if (netRead != null) - { - netRead.Item2.Cancel(); - c.Close(); - } + c.Close(); } catch { } } - private void ReadData(int offset) + private async Task ReadData(int offset) { if (offset > 0) { try { byte[] cache = new byte[offset]; - Receive(cache, 0, offset, SocketFlags.None); + await ReceiveAsync(cache, 0, offset); } catch (OutOfMemoryException) { } } } - private string ReadNextString() + private async Task ReadNextString() + { + ushort length = (ushort)(await ReadNextShort()); + if (length > 0) + { + byte[] cache = new byte[length * 2]; + await ReceiveAsync(cache, 0, length * 2); + string result = Encoding.BigEndianUnicode.GetString(cache); + return result; + } + else + return string.Empty; + } + + private async Task ReadNextStringAsync(CancellationToken cancellationToken = default) { - ushort length = (ushort)ReadNextShort(); + ushort length = (ushort)(await ReadNextShortAsync()); if (length > 0) { byte[] cache = new byte[length * 2]; - Receive(cache, 0, length * 2, SocketFlags.None); + await ReceiveAsync(cache, 0, length * 2); + if (cancellationToken.IsCancellationRequested) + return string.Empty; string result = Encoding.BigEndianUnicode.GetString(cache); return result; } - else return ""; + else + return string.Empty; } - public bool SendEntityAction(int PlayerEntityID, int ActionID) + public async Task SendEntityAction(int PlayerEntityID, int ActionID) { - return false; + return await Task.FromResult(false); } - private byte[] ReadNextByteArray() + private async Task ReadNextByteArray() { - short len = ReadNextShort(); + short len = await ReadNextShort(); byte[] data = new byte[len]; - Receive(data, 0, len, SocketFlags.None); + await ReceiveAsync(data, 0, len); return data; } - private short ReadNextShort() + private async Task ReadNextShort() { byte[] tmp = new byte[2]; - Receive(tmp, 0, 2, SocketFlags.None); + await ReceiveAsync(tmp, 0, 2); Array.Reverse(tmp); return BitConverter.ToInt16(tmp, 0); } - private int ReadNextInt() + private async Task ReadNextShortAsync() + { + byte[] tmp = new byte[2]; + await ReceiveAsync(tmp, 0, 2); + Array.Reverse(tmp); + return BitConverter.ToInt16(tmp, 0); + } + + private async Task ReadNextInt() { byte[] tmp = new byte[4]; - Receive(tmp, 0, 4, SocketFlags.None); + await ReceiveAsync(tmp, 0, 4); Array.Reverse(tmp); return BitConverter.ToInt32(tmp, 0); } - private byte ReadNextByte() + private async Task ReadNextByte() { byte[] result = new byte[1]; - Receive(result, 0, 1, SocketFlags.None); + await ReceiveAsync(result, 0, 1); return result[0]; } - private void ReadNextItemSlot() + private async Task ReadNextItemSlot() { - short itemid = ReadNextShort(); + short itemid = await ReadNextShort(); //If slot not empty (item ID != -1) if (itemid != -1) { - ReadData(1); //Item count - ReadData(2); //Item damage - short length = ReadNextShort(); + await ReadData(1); //Item count + await ReadData(2); //Item damage + short length = await ReadNextShort(); //If length of optional NBT data > 0, read it - if (length > 0) { ReadData(length); } + if (length > 0) { await ReadData(length); } } } - private void ReadNextEntityMetaData() + private async Task ReadNextEntityMetaData() { do { byte[] id = new byte[1]; - Receive(id, 0, 1, SocketFlags.None); + await ReceiveAsync(id, 0, 1); if (id[0] == 0x7F) { break; } int index = id[0] & 0x1F; int type = id[0] >> 5; switch (type) { - case 0: ReadData(1); break; //Byte - case 1: ReadData(2); break; //Short - case 2: ReadData(4); break; //Int - case 3: ReadData(4); break; //Float - case 4: ReadNextString(); break; //String - case 5: ReadNextItemSlot(); break; //Slot - case 6: ReadData(12); break; //Vector (3 Int) + case 0: await ReadData(1); break; //Byte + case 1: await ReadData(2); break; //Short + case 2: await ReadData(4); break; //Int + case 3: await ReadData(4); break; //Float + case 4: await ReadNextString(); break; //String + case 5: await ReadNextItemSlot(); break; //Slot + case 6: await ReadData(12); break; //Vector (3 Int) } } while (true); } - private void ReadNextObjectData() + private async Task ReadNextObjectData() { - int id = ReadNextInt(); - if (id != 0) { ReadData(6); } + int id = await ReadNextInt(); + if (id != 0) { await ReadData(6); } } - private void ReadNextTeamData() + private async Task ReadNextTeamData() { - ReadNextString(); //Internal Name - byte mode = ReadNextByte(); + await ReadNextString(); //Internal Name + byte mode = await ReadNextByte(); if (mode == 0 || mode == 2) { - ReadNextString(); //Display Name - ReadNextString(); //Prefix - ReadNextString(); //Suffix - ReadData(1); //Friendly Fire + await ReadNextString(); //Display Name + await ReadNextString(); //Prefix + await ReadNextString(); //Suffix + await ReadData(1); //Friendly Fire } if (mode == 0 || mode == 3 || mode == 4) { - short count = ReadNextShort(); + short count = await ReadNextShort(); for (int i = 0; i < count; i++) { - ReadNextString(); //Players + await ReadNextString(); //Players } } } - private void ReadNextEntityProperties(int protocolversion) + private async Task ReadNextEntityProperties(int protocolversion) { if (protocolversion >= 72) { if (protocolversion >= 74) { //Minecraft 1.6.2 - ReadNextInt(); //Entity ID - int count = ReadNextInt(); + await ReadNextInt(); //Entity ID + int count = await ReadNextInt(); for (int i = 0; i < count; i++) { - ReadNextString(); //Property name - ReadData(8); //Property value (Double) - short othercount = ReadNextShort(); - ReadData(25 * othercount); + await ReadNextString(); //Property name + await ReadData(8); //Property value (Double) + short othercount = await ReadNextShort(); + await ReadData(25 * othercount); } } else { //Minecraft 1.6.0 / 1.6.1 - ReadNextInt(); //Entity ID - int count = ReadNextInt(); + await ReadNextInt(); //Entity ID + int count = await ReadNextInt(); for (int i = 0; i < count; i++) { - ReadNextString(); //Property name - ReadData(8); //Property value (Double) + await ReadNextString(); //Property name + await ReadData(8); //Property value (Double) } } } } - private void ReadNextWindowData(int protocolversion) + private async Task ReadNextWindowData(int protocolversion) { - ReadData(1); - byte windowtype = ReadNextByte(); - ReadNextString(); - ReadData(1); + await ReadData(1); + byte windowtype = await ReadNextByte(); + await ReadNextString(); + await ReadData(1); if (protocolversion > 51) { - ReadData(1); + await ReadData(1); if (protocolversion >= 72 && windowtype == 0xb) { - ReadNextInt(); + await ReadNextInt(); } } } - private void ReadNextChunkBulkData() + private async Task ReadNextChunkBulkData() { - short chunkcount = ReadNextShort(); - int datalen = ReadNextInt(); - ReadData(1); - ReadData(datalen); - ReadData(12 * (chunkcount)); + short chunkcount = await ReadNextShort(); + int datalen = await ReadNextInt(); + await ReadData(1); + await ReadData(datalen); + await ReadData(12 * (chunkcount)); } - private void Receive(byte[] buffer, int start, int offset, SocketFlags f) + /// + /// Network reading method. Read bytes from the socket or encrypted socket. + /// + private async Task ReceiveAsync(byte[] buffer, int start, int offset) { int read = 0; while (read < offset) { if (encrypted) - read += s!.Read(buffer, start + read, offset - read); + read += await s!.ReadAsync(buffer.AsMemory().Slice(start + read, offset - read)); else - read += c.Client.Receive(buffer, start + read, offset - read, f); + read += await c.Client.ReceiveAsync(new ArraySegment(buffer, start + read, offset - read)); } } - private void Send(byte[] buffer) + private async Task Send(byte[] buffer) { if (encrypted) - s!.Write(buffer, 0, buffer.Length); + await s!.WriteAsync(buffer); else - c.Client.Send(buffer); + await c.Client.SendAsync(buffer); } - private bool Handshake(string uuid, string username, string sessionID, string host, int port, SessionToken session) + private async Task Handshake(HttpClient httpClient, string uuid, string username, string sessionID, string host, int port, SessionToken session) { //array byte[] data = new byte[10 + (username.Length + host.Length) * 2]; @@ -484,27 +499,27 @@ private bool Handshake(string uuid, string username, string sessionID, string ho Array.Reverse(sh); sh.CopyTo(data, 6 + (username.Length * 2) + (host.Length * 2)); - Send(data); + await Send(data); byte[] pid = new byte[1]; - Receive(pid, 0, 1, SocketFlags.None); + await ReceiveAsync(pid, 0, 1); while (pid[0] == 0xFA) //Skip some early plugin messages { - ProcessPacket(pid[0]); - Receive(pid, 0, 1, SocketFlags.None); + await ProcessPacket(pid[0]); + await ReceiveAsync(pid, 0, 1); } if (pid[0] == 0xFD) { - string serverID = ReadNextString(); - byte[] PublicServerkey = ReadNextByteArray(); - byte[] token = ReadNextByteArray(); + string serverID = await ReadNextString(); + byte[] PublicServerkey = await ReadNextByteArray(); + byte[] token = await ReadNextByteArray(); if (serverID == "-") ConsoleIO.WriteLineFormatted("§8" + Translations.mcc_server_offline, acceptnewlines: true); else if (Settings.Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_handshake, serverID)); - return StartEncryption(uuid, username, sessionID, token, serverID, PublicServerkey, session); + return await StartEncryption(httpClient, uuid, sessionID, token, serverID, PublicServerkey, session); } else { @@ -513,7 +528,7 @@ private bool Handshake(string uuid, string username, string sessionID, string ho } } - private bool StartEncryption(string uuid, string username, string sessionID, byte[] token, string serverIDhash, byte[] serverPublicKey, SessionToken session) + private async Task StartEncryption(HttpClient httpClient, string uuid, string sessionID, byte[] token, string serverIDhash, byte[] serverPublicKey, SessionToken session) { RSACryptoServiceProvider RSAService = CryptoHandler.DecodeRSAPublicKey(serverPublicKey)!; byte[] secretKey = CryptoHandler.ClientAESPrivateKey ?? CryptoHandler.GenerateAESPrivateKey(); @@ -524,28 +539,28 @@ private bool StartEncryption(string uuid, string username, string sessionID, byt if (serverIDhash != "-") { ConsoleIO.WriteLine(Translations.mcc_session); - string serverHash = CryptoHandler.GetServerHash(serverIDhash, serverPublicKey, secretKey); - bool needCheckSession = true; - if (session.ServerPublicKey != null && session.SessionPreCheckTask != null - && serverIDhash == session.ServerIDhash && Enumerable.SequenceEqual(serverPublicKey, session.ServerPublicKey)) - { - session.SessionPreCheckTask.Wait(); - if (session.SessionPreCheckTask.Result) // PreCheck Successed - needCheckSession = false; - } - - if (needCheckSession) + string serverHash = CryptoHandler.GetServerHash(serverIDhash, serverPublicKey, secretKey); + if (session.SessionPreCheckTask != null && session.ServerInfoHash != null && serverHash == session.ServerInfoHash) { - if (ProtocolHandler.SessionCheck(uuid, sessionID, serverHash)) + (bool preCheckResult, string? error) = await session.SessionPreCheckTask; + if (!preCheckResult) { - session.ServerIDhash = serverIDhash; - session.ServerPublicKey = serverPublicKey; - SessionCache.Store(InternalConfig.Account.Login.ToLower(), session); + handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, + string.IsNullOrEmpty(error) ? Translations.mcc_session_fail : $"{Translations.mcc_session_fail} Error: {error}."); + return false; } + session.SessionPreCheckTask = null; + } + else + { + (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; } } @@ -570,15 +585,15 @@ private bool StartEncryption(string uuid, string username, string sessionID, byt token_enc.CopyTo(data, 5 + (short)key_enc.Length); //Send it back - Send(data); + await Send(data); //Getting the next packet byte[] pid = new byte[1]; - Receive(pid, 0, 1, SocketFlags.None); + await ReceiveAsync(pid, 0, 1); if (pid[0] == 0xFC) { - ReadData(4); - s = new AesCfb8Stream(c.GetStream(), secretKey); + await ReadData(4); + s = new AesStream(c.Client, secretKey); encrypted = true; return true; } @@ -589,11 +604,11 @@ private bool StartEncryption(string uuid, string username, string sessionID, byt } } - public bool Login(PlayerKeyPair? playerKeyPair, SessionToken session) + public async Task Login(HttpClient httpClient, PlayerKeyPair? playerKeyPair, SessionToken session) { - if (Handshake(handler.GetUserUuidStr(), handler.GetUsername(), handler.GetSessionID(), handler.GetServerHost(), handler.GetServerPort(), session)) + if (await Handshake(httpClient, handler.GetUserUuidStr(), handler.GetUsername(), handler.GetSessionID(), handler.GetServerHost(), handler.GetServerPort(), session)) { - Send(new byte[] { 0xCD, 0 }); + await Send(new byte[] { 0xCD, 0 }); try { byte[] pid = new byte[1]; @@ -601,21 +616,20 @@ public bool Login(PlayerKeyPair? playerKeyPair, SessionToken session) { if (c.Connected) { - Receive(pid, 0, 1, SocketFlags.None); + await ReceiveAsync(pid, 0, 1); while (pid[0] >= 0xC0 && pid[0] != 0xFF) //Skip some early packets or plugin messages { - ProcessPacket(pid[0]); - Receive(pid, 0, 1, SocketFlags.None); + await ProcessPacket(pid[0]); + await ReceiveAsync(pid, 0, 1); } if (pid[0] == (byte)1) { - ReadData(4); ReadNextString(); ReadData(5); - StartUpdating(); + await ReadData(4); await ReadNextString(); await ReadData(5); return true; //The Server accepted the request } else if (pid[0] == (byte)0xFF) { - string reason = ReadNextString(); + string reason = await ReadNextString(); handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, reason); return false; } @@ -659,7 +673,7 @@ public void Disconnect() msg.CopyTo(reason, 3); } - Send(reason); + Send(reason).Wait(); } catch (SocketException) { } catch (System.IO.IOException) { } @@ -675,9 +689,9 @@ public int GetProtocolVersion() return protocolversion; } - public bool SendChatMessage(string message, PlayerKeyPair? playerKeyPair) + public async Task SendChatMessage(string message, PlayerKeyPair? playerKeyPair) { - if (String.IsNullOrEmpty(message)) + if (string.IsNullOrEmpty(message)) return true; try @@ -694,111 +708,111 @@ public bool SendChatMessage(string message, PlayerKeyPair? playerKeyPair) msg = Encoding.BigEndianUnicode.GetBytes(message); msg.CopyTo(chat, 3); - Send(chat); + await Send(chat); return true; } catch (SocketException) { return false; } catch (System.IO.IOException) { return false; } } - public bool SendRespawnPacket() + public async Task SendRespawnPacket() { try { - Send(new byte[] { 0xCD, 1 }); + await Send(new byte[] { 0xCD, 1 }); return true; } catch (SocketException) { return false; } } - public bool SendUpdateSign(Location location, string line1, string line2, string line3, string line4) + public async Task SendUpdateSign(Location location, string line1, string line2, string line3, string line4) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } - public bool SendBrandInfo(string brandInfo) + public async Task SendBrandInfo(string brandInfo) { - return false; //Only supported since MC 1.7 + return await Task.FromResult(false); //Only supported since MC 1.7 } - public bool SendClientSettings(string language, byte viewDistance, byte difficulty, byte chatMode, bool chatColors, byte skinParts, byte mainHand) + public async Task SendClientSettings(string language, byte viewDistance, byte difficulty, byte chatMode, bool chatColors, byte skinParts, byte mainHand) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } - public bool SendLocationUpdate(Location location, bool onGround, float? yaw, float? pitch) + public async Task SendLocationUpdate(Location location, bool onGround, float? yaw, float? pitch) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } - public bool SendInteractEntity(int EntityID, int type) + public async Task SendInteractEntity(int EntityID, int type) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } - public bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z, int hand) + public async Task SendInteractEntity(int EntityID, int type, float X, float Y, float Z, int hand) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } - public bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z) + public async Task SendInteractEntity(int EntityID, int type, float X, float Y, float Z) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } - public bool SendInteractEntity(int EntityID, int type, int hand) + public async Task SendInteractEntity(int EntityID, int type, int hand) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } - public bool UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags) + public async Task UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } - public bool SendUseItem(int hand, int sequenceId) + public async Task SendUseItem(int hand, int sequenceId) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } - public bool SendWindowAction(int windowId, int slotId, WindowActionType action, Item? item, List> changedSlots, int stateId) + public async Task SendWindowAction(int windowId, int slotId, WindowActionType action, Item? item, List> changedSlots, int stateId) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } - public bool SendAnimation(int animation, int playerid) + public async Task SendAnimation(int animation, int playerid) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } - public bool SendCreativeInventoryAction(int slot, ItemType item, int count, Dictionary? nbt) + public async Task SendCreativeInventoryAction(int slot, ItemType item, int count, Dictionary? nbt) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } - public bool ClickContainerButton(int windowId, int buttonId) + public async Task ClickContainerButton(int windowId, int buttonId) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } - public bool SendCloseWindow(int windowId) + public async Task SendCloseWindow(int windowId) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } - public bool SendPlayerBlockPlacement(int hand, Location location, Direction face, int sequenceId) + public async Task SendPlayerBlockPlacement(int hand, Location location, Direction face, int sequenceId) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } - public bool SendHeldItemChange(short slot) + public async Task SendHeldItemChange(short slot) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } - public bool SendPlayerDigging(int status, Location location, Direction face, int sequenceId) + public async Task SendPlayerDigging(int status, Location location, Direction face, int sequenceId) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } /// @@ -806,7 +820,7 @@ public bool SendPlayerDigging(int status, Location location, Direction face, int /// /// Channel to send packet on /// packet Data - public bool SendPluginChannelPacket(string channel, byte[] data) + public async Task SendPluginChannelPacket(string channel, byte[] data) { try { @@ -818,7 +832,7 @@ public bool SendPluginChannelPacket(string channel, byte[] data) byte[] dataLength = BitConverter.GetBytes((short)data.Length); Array.Reverse(dataLength); - Send(ConcatBytes(new byte[] { 0xFA }, channelLength, channelData, dataLength, data)); + await Send(ConcatBytes(new byte[] { 0xFA }, channelLength, channelData, dataLength, data)); return true; } @@ -826,7 +840,7 @@ public bool SendPluginChannelPacket(string channel, byte[] data) catch (System.IO.IOException) { return false; } } - int IAutoComplete.AutoComplete(string BehindCursor) + async Task IAutoComplete.AutoComplete(string BehindCursor) { if (String.IsNullOrEmpty(BehindCursor)) return -1; @@ -839,7 +853,7 @@ int IAutoComplete.AutoComplete(string BehindCursor) byte[] msg = Encoding.BigEndianUnicode.GetBytes(BehindCursor); msg.CopyTo(autocomplete, 3); ConsoleIO.AutoCompleteDone = false; - Send(autocomplete); + await Send(autocomplete); return 0; } @@ -851,24 +865,25 @@ private static byte[] ConcatBytes(params byte[][] bytes) return result.ToArray(); } - public static bool DoPing(string host, int port, ref int protocolversion) + public static async Task> DoPing(string host, int port, CancellationToken cancelToken) { try { + TcpClient tcpClient = ProxyHandler.NewTcpClient(host, port, ProxyHandler.ClientType.Ingame); + tcpClient.ReceiveBufferSize = 1024 * 1024; + tcpClient.ReceiveTimeout = 5000; // MC 1.7.2+ SpigotMC servers won't respond, so we need a reasonable timeout. + string version = ""; - TcpClient tcp = ProxyHandler.NewTcpClient(host, port); - tcp.ReceiveTimeout = 30000; // 30 seconds - tcp.ReceiveTimeout = 5000; //MC 1.7.2+ SpigotMC servers won't respond, so we need a reasonable timeout. byte[] ping = new byte[2] { 0xfe, 0x01 }; - tcp.Client.Send(ping, SocketFlags.None); - tcp.Client.Receive(ping, 0, 1, SocketFlags.None); + await tcpClient.Client.SendAsync(ping, SocketFlags.None, cancelToken); + await tcpClient.Client.ReceiveAsync(new ArraySegment(ping, 0, 1), cancelToken); if (ping[0] == 0xff) { - Protocol16Handler ComTmp = new(tcp); - string result = ComTmp.ReadNextString(); + Protocol16Handler ComTmp = new(tcpClient); + string result = await ComTmp.ReadNextStringAsync(cancelToken); - if (Settings.Config.Logging.DebugMessages) + if (Config.Logging.DebugMessages) { // May contain formatting codes, cannot use WriteLineFormatted Console.ForegroundColor = ConsoleColor.DarkGray; @@ -876,38 +891,39 @@ public static bool DoPing(string host, int port, ref int protocolversion) Console.ForegroundColor = ConsoleColor.Gray; } + int protocolversion; if (result.Length > 2 && result[0] == '§' && result[1] == '1') { string[] tmp = result.Split((char)0x00); - protocolversion = (byte)Int16.Parse(tmp[1], NumberStyles.Any, CultureInfo.CurrentCulture); + protocolversion = short.Parse(tmp[1], NumberStyles.Any, CultureInfo.CurrentCulture); version = tmp[2]; - if (protocolversion == 127) //MC 1.7.2+ - return false; + if (protocolversion == 127) // MC 1.7.2+ + return new(false, 0, null); } else { - protocolversion = (byte)39; + protocolversion = 39; version = "B1.8.1 - 1.3.2"; } ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_version, version, protocolversion)); - return true; + return new(true, protocolversion, null); } - else return false; } - catch { return false; } + catch { } + return new(false, 0, null); } - public bool SelectTrade(int selectedSlot) + public async Task SelectTrade(int selectedSlot) { - return false; //MC 1.13+ + return await Task.FromResult(false); //MC 1.13+ } - public bool SendSpectate(Guid UUID) + public async Task SendSpectate(Guid UUID) { - return false; //Currently not implemented + return await Task.FromResult(false); //Currently not implemented } } } diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 1a3123c17b..501bad3d1f 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -3,13 +3,20 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using System.IO; +using System.IO.Compression; using System.Linq; +using System.Net.Http; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; +using System.Threading.Tasks; +using MinecraftClient.Commands; using MinecraftClient.Crypto; +using MinecraftClient.EntityHandler; using MinecraftClient.Inventory; using MinecraftClient.Inventory.ItemPalettes; using MinecraftClient.Logger; @@ -20,6 +27,7 @@ using MinecraftClient.Protocol.Handlers.packet.s2c; using MinecraftClient.Protocol.Handlers.PacketPalettes; using MinecraftClient.Protocol.Message; +using MinecraftClient.Protocol.PacketPipeline; using MinecraftClient.Protocol.ProfileKey; using MinecraftClient.Protocol.Session; using MinecraftClient.Proxy; @@ -38,7 +46,7 @@ namespace MinecraftClient.Protocol.Handlers /// - Add the packet type palette for that Minecraft version. Please see PacketTypePalette.cs for more information /// - Also see Material.cs and ItemType.cs for updating block and item data inside MCC /// - class Protocol18Handler : IMinecraftCom + partial class Protocol18Handler : IMinecraftCom { internal const int MC_1_8_Version = 47; internal const int MC_1_9_Version = 107; @@ -64,14 +72,14 @@ class Protocol18Handler : IMinecraftCom internal const int MC_1_19_Version = 759; internal const int MC_1_19_2_Version = 760; - private int compression_treshold = 0; + private ulong CurrentTick = 0; + private int autocomplete_transaction_id = 0; private readonly Dictionary window_actions = new(); private bool login_phase = true; private readonly int protocolVersion; private int currentDimension; private bool isOnlineMode = false; - private readonly BlockingCollection>> packetQueue = new(); private float LastYaw, LastPitch; private int pendingAcknowledgments = 0; @@ -85,13 +93,14 @@ class Protocol18Handler : IMinecraftCom readonly PacketTypePalette packetPalette; readonly SocketWrapper socketWrapper; readonly DataTypes dataTypes; - Tuple? netMain = null; // main thread - Tuple? netReader = null; // reader thread readonly ILogger log; readonly RandomNumberGenerator randomGen; - public Protocol18Handler(TcpClient Client, int protocolVersion, IMinecraftComHandler handler, ForgeInfo? forgeInfo) + private readonly CancellationToken CancelToken; + + public Protocol18Handler(TcpClient Client, int protocolVersion, IMinecraftComHandler handler, ForgeInfo? forgeInfo, CancellationToken cancelToken) { + CancelToken = cancelToken; ConsoleIO.SetAutoCompleteEngine(this); ChatParser.InitTranslations(); socketWrapper = new SocketWrapper(Client); @@ -205,116 +214,68 @@ public Protocol18Handler(TcpClient Client, int protocolVersion, IMinecraftComHan }; } - /// - /// Separate thread. Network reading loop. - /// - private void Updater(object? o) + private async Task MainTicker() { - CancellationToken cancelToken = (CancellationToken)o!; - - if (cancelToken.IsCancellationRequested) - return; - + using PeriodicTimer periodicTimer = new(TimeSpan.FromMilliseconds(1000 / 20)); try { - Stopwatch stopWatch = new(); - while (!packetQueue.IsAddingCompleted) + while (await periodicTimer.WaitForNextTickAsync(CancelToken) && !CancelToken.IsCancellationRequested) { - cancelToken.ThrowIfCancellationRequested(); - - handler.OnUpdate(); - stopWatch.Restart(); - - while (packetQueue.TryTake(out Tuple>? packetInfo)) + ++CurrentTick; + try { - (int packetID, Queue packetData) = packetInfo; - HandlePacket(packetID, packetData); - - if (stopWatch.Elapsed.Milliseconds >= 100) + await handler.OnUpdate(); + } + catch (Exception e) + { + if (Config.Logging.DebugMessages) { - handler.OnUpdate(); - stopWatch.Restart(); + ConsoleIO.WriteLine($"{e.GetType().Name} on tick: {e.Message}"); + if (e.StackTrace != null) + ConsoleIO.WriteLine(e.StackTrace); } } - - int sleepLength = 100 - stopWatch.Elapsed.Milliseconds; - if (sleepLength > 0) - Thread.Sleep(sleepLength); } } - catch (ObjectDisposedException) { } + catch (AggregateException e) + { + if (e.InnerException is not OperationCanceledException) + throw; + } catch (OperationCanceledException) { } - catch (NullReferenceException) { } - - if (cancelToken.IsCancellationRequested) - return; - - handler.OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, ""); } /// - /// Read and decompress packets. + /// Start the updating thread. Should be called after login success. /// - internal void PacketReader(object? o) + public async Task StartUpdating() { - CancellationToken cancelToken = (CancellationToken)o!; - while (socketWrapper.IsConnected() && !cancelToken.IsCancellationRequested) + Task MainTickerTask = Task.Run(MainTicker); + + while (!CancelToken.IsCancellationRequested) { try { - while (socketWrapper.HasDataAvailable()) - { - packetQueue.Add(ReadNextPacket()); - - if (cancelToken.IsCancellationRequested) - break; - } + (int packetID, PacketStream packetStream) = await socketWrapper.GetNextPacket(handleCompress: protocolVersion >= MC_1_8_Version); + // ConsoleIO.WriteLine("Len: " + packetData.Count + ", Packet: " + packetPalette.GetIncommingTypeById(packetID).ToString()); + await HandlePacket(packetID, packetStream); } - catch (System.IO.IOException) { break; } + catch (IOException) { break; } catch (SocketException) { break; } + catch (ObjectDisposedException) { break; } + catch (OperationCanceledException) { break; } catch (NullReferenceException) { break; } - catch (Ionic.Zlib.ZlibException) { break; } - - if (cancelToken.IsCancellationRequested) - break; - - Thread.Sleep(10); - } - packetQueue.CompleteAdding(); - } - - /// - /// Read the next packet from the network - /// - /// will contain packet ID - /// will contain raw packet Data - internal Tuple> ReadNextPacket() - { - int size = dataTypes.ReadNextVarIntRAW(socketWrapper); //Packet size - Queue packetData = new(socketWrapper.ReadDataRAW(size)); //Packet contents - - //Handle packet decompression - if (protocolVersion >= MC_1_8_Version - && compression_treshold > 0) - { - int sizeUncompressed = dataTypes.ReadNextVarInt(packetData); - if (sizeUncompressed != 0) // != 0 means compressed, let's decompress + catch (AggregateException e) { - byte[] toDecompress = packetData.ToArray(); - byte[] uncompressed = ZlibUtils.Decompress(toDecompress, sizeUncompressed); - packetData = new(uncompressed); + if (e.InnerException is TaskCanceledException) + break; } } - int packetID = dataTypes.ReadNextVarInt(packetData); //Packet ID + if (!CancelToken.IsCancellationRequested) + handler.OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, string.Empty); - if (handler.GetNetworkPacketCaptureEnabled()) - { - List clone = packetData.ToList(); - handler.OnNetworkPacket(packetID, clone, login_phase, true); - } - - return new(packetID, packetData); + await MainTickerTask; } /// @@ -323,56 +284,57 @@ internal Tuple> ReadNextPacket() /// Packet ID /// Packet contents /// TRUE if the packet was processed, FALSE if ignored or unknown - internal bool HandlePacket(int packetID, Queue packetData) + internal async Task HandlePacket(int packetID, PacketStream packetData) { try { if (login_phase) { - switch (packetID) //Packet IDs are different while logging in + switch (packetID) // Packet IDs are different while logging in { case 0x03: if (protocolVersion >= MC_1_8_Version) - compression_treshold = dataTypes.ReadNextVarInt(packetData); + socketWrapper.CompressionThreshold = await dataTypes.ReadNextVarIntAsync(packetData); break; case 0x04: - int messageId = dataTypes.ReadNextVarInt(packetData); - string channel = dataTypes.ReadNextString(packetData); - List responseData = new(); - bool understood = pForge.HandleLoginPluginRequest(channel, packetData, ref responseData); - SendLoginPluginResponse(messageId, understood, responseData.ToArray()); + int messageId = await dataTypes.ReadNextVarIntAsync(packetData); + string channel = await dataTypes.ReadNextStringAsync(packetData); + (bool understood, List responseData) = await pForge.HandleLoginPluginRequest(channel, packetData); + await SendLoginPluginResponse(messageId, understood, responseData.ToArray()); return understood; default: - return false; //Ignored packet + return false; // Ignored packet } } // Regular in-game packets else switch (packetPalette.GetIncommingTypeById(packetID)) { case PacketTypesIn.KeepAlive: - SendPacket(PacketTypesOut.KeepAlive, packetData); handler.OnServerKeepAlive(); + await SendPacket(PacketTypesOut.KeepAlive, packetData); break; case PacketTypesIn.Ping: - SendPacket(PacketTypesOut.Pong, packetData); + handler.OnServerKeepAlive(); + await SendPacket(PacketTypesOut.Pong, packetData); break; case PacketTypesIn.JoinGame: - handler.OnGameJoined(); - int playerEntityID = dataTypes.ReadNextInt(packetData); + Task OnGameJoinedTask = handler.OnGameJoinedAsync(); + + int playerEntityID = await dataTypes.ReadNextIntAsync(packetData); handler.OnReceivePlayerEntityID(playerEntityID); if (protocolVersion >= MC_1_16_2_Version) - dataTypes.ReadNextBool(packetData); // Is hardcore - 1.16.2 and above + await dataTypes.ReadNextBoolAsync(packetData); // Is hardcore - 1.16.2 and above - handler.OnGamemodeUpdate(Guid.Empty, dataTypes.ReadNextByte(packetData)); + Task OnGamemodeUpdateTask = handler.OnGamemodeUpdate(Guid.Empty, await dataTypes.ReadNextByteAsync(packetData)); if (protocolVersion >= MC_1_16_Version) { - dataTypes.ReadNextByte(packetData); // Previous Gamemode - 1.16 and above - int worldCount = dataTypes.ReadNextVarInt(packetData); // Dimension Count (World Count) - 1.16 and above + await dataTypes.ReadNextByteAsync(packetData); // Previous Gamemode - 1.16 and above + int worldCount = await dataTypes.ReadNextVarIntAsync(packetData); // Dimension Count (World Count) - 1.16 and above for (int i = 0; i < worldCount; i++) - dataTypes.ReadNextString(packetData); // Dimension Names (World Names) - 1.16 and above - var registryCodec = dataTypes.ReadNextNbt(packetData); // Registry Codec (Dimension Codec) - 1.16 and above + await dataTypes.SkipNextStringAsync(packetData); // Dimension Names (World Names) - 1.16 and above + var registryCodec = await dataTypes.ReadNextNbtAsync(packetData); // Registry Codec (Dimension Codec) - 1.16 and above if (handler.GetTerrainEnabled()) World.StoreDimensionList(registryCodec); } @@ -388,24 +350,24 @@ internal bool HandlePacket(int packetID, Queue packetData) if (protocolVersion >= MC_1_16_Version) { if (protocolVersion >= MC_1_19_Version) - dimensionTypeName = dataTypes.ReadNextString(packetData); // Dimension Type: Identifier + dimensionTypeName = await dataTypes.ReadNextStringAsync(packetData); // Dimension Type: Identifier else if (protocolVersion >= MC_1_16_2_Version) - dimensionType = dataTypes.ReadNextNbt(packetData); // Dimension Type: NBT Tag Compound + dimensionType = await dataTypes.ReadNextNbtAsync(packetData); // Dimension Type: NBT Tag Compound else - dataTypes.ReadNextString(packetData); + await dataTypes.SkipNextStringAsync(packetData); currentDimension = 0; } else if (protocolVersion >= MC_1_9_1_Version) - currentDimension = dataTypes.ReadNextInt(packetData); + currentDimension = await dataTypes.ReadNextIntAsync(packetData); else - currentDimension = (sbyte)dataTypes.ReadNextByte(packetData); + currentDimension = (sbyte)await dataTypes.ReadNextByteAsync(packetData); if (protocolVersion < MC_1_14_Version) - dataTypes.ReadNextByte(packetData); // Difficulty - 1.13 and below + await dataTypes.ReadNextByteAsync(packetData); // Difficulty - 1.13 and below if (protocolVersion >= MC_1_16_Version) { - string dimensionName = dataTypes.ReadNextString(packetData); // Dimension Name (World Name) - 1.16 and above + string dimensionName = await dataTypes.ReadNextStringAsync(packetData); // Dimension Name (World Name) - 1.16 and above if (handler.GetTerrainEnabled()) { if (protocolVersion >= MC_1_16_2_Version && protocolVersion <= MC_1_18_2_Version) @@ -421,88 +383,91 @@ internal bool HandlePacket(int packetID, Queue packetData) } if (protocolVersion >= MC_1_15_Version) - dataTypes.ReadNextLong(packetData); // Hashed world seed - 1.15 and above + await dataTypes.SkipNextLongAsync(packetData); // Hashed world seed - 1.15 and above if (protocolVersion >= MC_1_16_2_Version) - dataTypes.ReadNextVarInt(packetData); // Max Players - 1.16.2 and above + await dataTypes.SkipNextVarIntAsync(packetData); // Max Players - 1.16.2 and above else - dataTypes.ReadNextByte(packetData); // Max Players - 1.16.1 and below + await dataTypes.ReadNextByteAsync(packetData); // Max Players - 1.16.1 and below if (protocolVersion < MC_1_16_Version) - dataTypes.SkipNextString(packetData); // Level Type - 1.15 and below + await dataTypes.SkipNextStringAsync(packetData); // Level Type - 1.15 and below if (protocolVersion >= MC_1_14_Version) - dataTypes.ReadNextVarInt(packetData); // View distance - 1.14 and above + await dataTypes.SkipNextVarIntAsync(packetData); // View distance - 1.14 and above if (protocolVersion >= MC_1_18_1_Version) - dataTypes.ReadNextVarInt(packetData); // Simulation Distance - 1.18 and above + await dataTypes.SkipNextVarIntAsync(packetData); // Simulation Distance - 1.18 and above if (protocolVersion >= MC_1_8_Version) - dataTypes.ReadNextBool(packetData); // Reduced debug info - 1.8 and above + await dataTypes.ReadNextBoolAsync(packetData); // Reduced debug info - 1.8 and above if (protocolVersion >= MC_1_15_Version) - dataTypes.ReadNextBool(packetData); // Enable respawn screen - 1.15 and above + await dataTypes.ReadNextBoolAsync(packetData); // Enable respawn screen - 1.15 and above if (protocolVersion >= MC_1_16_Version) { - dataTypes.ReadNextBool(packetData); // Is Debug - 1.16 and above - dataTypes.ReadNextBool(packetData); // Is Flat - 1.16 and above + await dataTypes.ReadNextBoolAsync(packetData); // Is Debug - 1.16 and above + await dataTypes.ReadNextBoolAsync(packetData); // Is Flat - 1.16 and above } if (protocolVersion >= MC_1_19_Version) { - bool hasDeathLocation = dataTypes.ReadNextBool(packetData); // Has death location + bool hasDeathLocation = await dataTypes.ReadNextBoolAsync(packetData); // Has death location if (hasDeathLocation) { - dataTypes.SkipNextString(packetData); // Death dimension name: Identifier - dataTypes.ReadNextLocation(packetData); // Death location + await dataTypes.SkipNextStringAsync(packetData); // Death dimension name: Identifier + await dataTypes.SkipNextLocationAsync(packetData); // Death location } } + + await OnGameJoinedTask; + await OnGamemodeUpdateTask; break; case PacketTypesIn.DeclareCommands: if (protocolVersion >= MC_1_19_Version) - DeclareCommands.Read(dataTypes, packetData); + await DeclareCommands.Read(dataTypes, packetData); break; case PacketTypesIn.ChatMessage: int messageType = 0; if (protocolVersion <= MC_1_18_2_Version) // 1.18 and bellow { - string message = dataTypes.ReadNextString(packetData); + string message = await dataTypes.ReadNextStringAsync(packetData); Guid senderUUID; if (protocolVersion >= MC_1_8_Version) { //Hide system messages or xp bar messages? - messageType = dataTypes.ReadNextByte(packetData); - if ((messageType == 1 && !Config.Main.Advanced.ShowSystemMessages) - || (messageType == 2 && !Config.Main.Advanced.ShowSystemMessages)) + messageType = await dataTypes.ReadNextByteAsync(packetData); + if (messageType == 1 && !Config.Main.Advanced.ShowSystemMessages + || messageType == 2 && !Config.Main.Advanced.ShowSystemMessages) break; if (protocolVersion >= MC_1_16_5_Version) - senderUUID = dataTypes.ReadNextUUID(packetData); + senderUUID = await dataTypes.ReadNextUUIDAsync(packetData); else senderUUID = Guid.Empty; } else senderUUID = Guid.Empty; - handler.OnTextReceived(new(message, true, messageType, senderUUID)); + await handler.OnTextReceivedAsync(new(message, true, messageType, senderUUID)); } else if (protocolVersion == MC_1_19_Version) // 1.19 { - string signedChat = dataTypes.ReadNextString(packetData); + string signedChat = await dataTypes.ReadNextStringAsync(packetData); - bool hasUnsignedChatContent = dataTypes.ReadNextBool(packetData); - string? unsignedChatContent = hasUnsignedChatContent ? dataTypes.ReadNextString(packetData) : null; + bool hasUnsignedChatContent = await dataTypes.ReadNextBoolAsync(packetData); + string? unsignedChatContent = hasUnsignedChatContent ? await dataTypes.ReadNextStringAsync(packetData) : null; - messageType = dataTypes.ReadNextVarInt(packetData); - if ((messageType == 1 && !Config.Main.Advanced.ShowSystemMessages) - || (messageType == 2 && !Config.Main.Advanced.ShowXPBarMessages)) + messageType = await dataTypes.ReadNextVarIntAsync(packetData); + if (messageType == 1 && !Config.Main.Advanced.ShowSystemMessages + || messageType == 2 && !Config.Main.Advanced.ShowXPBarMessages) break; - Guid senderUUID = dataTypes.ReadNextUUID(packetData); - string senderDisplayName = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + Guid senderUUID = await dataTypes.ReadNextUUIDAsync(packetData); + string senderDisplayName = ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetData)); - bool hasSenderTeamName = dataTypes.ReadNextBool(packetData); - string? senderTeamName = hasSenderTeamName ? ChatParser.ParseText(dataTypes.ReadNextString(packetData)) : null; + bool hasSenderTeamName = await dataTypes.ReadNextBoolAsync(packetData); + string? senderTeamName = hasSenderTeamName ? ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetData)) : null; - long timestamp = dataTypes.ReadNextLong(packetData); + long timestamp = await dataTypes.ReadNextLongAsync(packetData); - long salt = dataTypes.ReadNextLong(packetData); + long salt = await dataTypes.ReadNextLongAsync(packetData); - byte[] messageSignature = dataTypes.ReadNextByteArray(packetData); + byte[] messageSignature = await dataTypes.ReadNextByteArrayAsync(packetData); bool verifyResult; if (!isOnlineMode) @@ -516,42 +481,42 @@ internal bool HandlePacket(int packetID, Queue packetData) } ChatMessage chat = new(signedChat, true, messageType, senderUUID, unsignedChatContent, senderDisplayName, senderTeamName, timestamp, messageSignature, verifyResult); - handler.OnTextReceived(chat); + await handler.OnTextReceivedAsync(chat); } else // 1.19.1 + { - byte[]? precedingSignature = dataTypes.ReadNextBool(packetData) ? dataTypes.ReadNextByteArray(packetData) : null; - Guid senderUUID = dataTypes.ReadNextUUID(packetData); - byte[] headerSignature = dataTypes.ReadNextByteArray(packetData); + byte[]? precedingSignature = await dataTypes.ReadNextBoolAsync(packetData) ? await dataTypes.ReadNextByteArrayAsync(packetData) : null; + Guid senderUUID = await dataTypes.ReadNextUUIDAsync(packetData); + byte[] headerSignature = await dataTypes.ReadNextByteArrayAsync(packetData); - string signedChat = dataTypes.ReadNextString(packetData); - string? decorated = dataTypes.ReadNextBool(packetData) ? dataTypes.ReadNextString(packetData) : null; + string signedChat = await dataTypes.ReadNextStringAsync(packetData); + string? decorated = await dataTypes.ReadNextBoolAsync(packetData) ? await dataTypes.ReadNextStringAsync(packetData) : null; - long timestamp = dataTypes.ReadNextLong(packetData); - long salt = dataTypes.ReadNextLong(packetData); + long timestamp = await dataTypes.ReadNextLongAsync(packetData); + long salt = await dataTypes.ReadNextLongAsync(packetData); - int lastSeenMessageListLen = dataTypes.ReadNextVarInt(packetData); + int lastSeenMessageListLen = await dataTypes.ReadNextVarIntAsync(packetData); LastSeenMessageList.Entry[] lastSeenMessageList = new LastSeenMessageList.Entry[lastSeenMessageListLen]; for (int i = 0; i < lastSeenMessageListLen; ++i) { - Guid user = dataTypes.ReadNextUUID(packetData); - byte[] lastSignature = dataTypes.ReadNextByteArray(packetData); + Guid user = await dataTypes.ReadNextUUIDAsync(packetData); + byte[] lastSignature = await dataTypes.ReadNextByteArrayAsync(packetData); lastSeenMessageList[i] = new(user, lastSignature); } LastSeenMessageList lastSeenMessages = new(lastSeenMessageList); - string? unsignedChatContent = dataTypes.ReadNextBool(packetData) ? dataTypes.ReadNextString(packetData) : null; + string? unsignedChatContent = await dataTypes.ReadNextBoolAsync(packetData) ? await dataTypes.ReadNextStringAsync(packetData) : null; - int filterEnum = dataTypes.ReadNextVarInt(packetData); + int filterEnum = await dataTypes.ReadNextVarIntAsync(packetData); if (filterEnum == 2) // PARTIALLY_FILTERED - dataTypes.ReadNextULongArray(packetData); + await dataTypes.SkipNextULongArray(packetData); - int chatTypeId = dataTypes.ReadNextVarInt(packetData); - string chatName = dataTypes.ReadNextString(packetData); - string? targetName = dataTypes.ReadNextBool(packetData) ? dataTypes.ReadNextString(packetData) : null; + int chatTypeId = await dataTypes.ReadNextVarIntAsync(packetData); + string chatName = await dataTypes.ReadNextStringAsync(packetData); + string? targetName = await dataTypes.ReadNextBoolAsync(packetData) ? await dataTypes.ReadNextStringAsync(packetData) : null; Dictionary chatInfo = Json.ParseJson(chatName).Properties; - string senderDisplayName = (chatInfo.ContainsKey("insertion") ? chatInfo["insertion"] : chatInfo["text"]).StringValue; + string senderDisplayName = (chatInfo.TryGetValue("insertion", out Json.JSONData? insertion) ? insertion : chatInfo["text"]).StringValue; string? senderTeamName = null; ChatParser.MessageType messageTypeEnum = ChatParser.ChatId2Type!.GetValueOrDefault(chatTypeId, ChatParser.MessageType.CHAT); if (targetName != null && @@ -592,44 +557,44 @@ internal bool HandlePacket(int packetID, Queue packetData) ChatMessage chat = new(signedChat, false, chatTypeId, senderUUID, unsignedChatContent, senderDisplayName, senderTeamName, timestamp, headerSignature, verifyResult); if (isOnlineMode && !chat.LacksSender()) - Acknowledge(chat); - handler.OnTextReceived(chat); + await Acknowledge(chat); + await handler.OnTextReceivedAsync(chat); } break; case PacketTypesIn.CombatEvent: // 1.8 - 1.16.5 if (protocolVersion >= MC_1_8_Version && protocolVersion <= MC_1_16_5_Version) { - CombatEventType eventType = (CombatEventType)dataTypes.ReadNextVarInt(packetData); + CombatEventType eventType = (CombatEventType)await dataTypes.ReadNextVarIntAsync(packetData); if (eventType == CombatEventType.EntityDead) { - dataTypes.SkipNextVarInt(packetData); + await dataTypes.SkipNextVarIntAsync(packetData); - handler.OnPlayerKilled( - dataTypes.ReadNextInt(packetData), - ChatParser.ParseText(dataTypes.ReadNextString(packetData)) + await handler.OnPlayerKilledAsync( + await dataTypes.ReadNextIntAsync(packetData), + ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetData)) ); } } break; case PacketTypesIn.DeathCombatEvent: - dataTypes.SkipNextVarInt(packetData); + await dataTypes.SkipNextVarIntAsync(packetData); - handler.OnPlayerKilled( - dataTypes.ReadNextInt(packetData), - ChatParser.ParseText(dataTypes.ReadNextString(packetData)) + await handler.OnPlayerKilledAsync( + await dataTypes.ReadNextIntAsync(packetData), + ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetData)) ); break; case PacketTypesIn.MessageHeader: if (protocolVersion >= MC_1_19_2_Version) { - byte[]? precedingSignature = dataTypes.ReadNextBool(packetData) ? dataTypes.ReadNextByteArray(packetData) : null; - Guid senderUUID = dataTypes.ReadNextUUID(packetData); - byte[] headerSignature = dataTypes.ReadNextByteArray(packetData); - byte[] bodyDigest = dataTypes.ReadNextByteArray(packetData); + byte[]? precedingSignature = await dataTypes.ReadNextBoolAsync(packetData) ? await dataTypes.ReadNextByteArrayAsync(packetData) : null; + Guid senderUUID = await dataTypes.ReadNextUUIDAsync(packetData); + byte[] headerSignature = await dataTypes.ReadNextByteArrayAsync(packetData); + byte[] bodyDigest = await dataTypes.ReadNextByteArrayAsync(packetData); bool verifyResult; @@ -659,21 +624,21 @@ internal bool HandlePacket(int packetID, Queue packetData) if (protocolVersion >= MC_1_16_Version) { if (protocolVersion >= MC_1_19_Version) - dimensionTypeNameRespawn = dataTypes.ReadNextString(packetData); // Dimension Type: Identifier + dimensionTypeNameRespawn = await dataTypes.ReadNextStringAsync(packetData); // Dimension Type: Identifier else if (protocolVersion >= MC_1_16_2_Version) - dimensionTypeRespawn = dataTypes.ReadNextNbt(packetData); // Dimension Type: NBT Tag Compound + dimensionTypeRespawn = await dataTypes.ReadNextNbtAsync(packetData); // Dimension Type: NBT Tag Compound else - dataTypes.ReadNextString(packetData); + await dataTypes.SkipNextStringAsync(packetData); currentDimension = 0; } else { // 1.15 and below - currentDimension = dataTypes.ReadNextInt(packetData); + currentDimension = await dataTypes.ReadNextIntAsync(packetData); } if (protocolVersion >= MC_1_16_Version) { - string dimensionName = dataTypes.ReadNextString(packetData); // Dimension Name (World Name) - 1.16 and above + string dimensionName = await dataTypes.ReadNextStringAsync(packetData); // Dimension Name (World Name) - 1.16 and above if (handler.GetTerrainEnabled()) { if (protocolVersion >= MC_1_16_2_Version && protocolVersion <= MC_1_18_2_Version) @@ -689,41 +654,41 @@ internal bool HandlePacket(int packetID, Queue packetData) } if (protocolVersion < MC_1_14_Version) - dataTypes.ReadNextByte(packetData); // Difficulty - 1.13 and below + await dataTypes.ReadNextByteAsync(packetData); // Difficulty - 1.13 and below if (protocolVersion >= MC_1_15_Version) - dataTypes.ReadNextLong(packetData); // Hashed world seed - 1.15 and above - dataTypes.ReadNextByte(packetData); // Gamemode + await dataTypes.SkipNextLongAsync(packetData); // Hashed world seed - 1.15 and above + await dataTypes.ReadNextByteAsync(packetData); // Gamemode if (protocolVersion >= MC_1_16_Version) - dataTypes.ReadNextByte(packetData); // Previous Game mode - 1.16 and above + await dataTypes.ReadNextByteAsync(packetData); // Previous Game mode - 1.16 and above if (protocolVersion < MC_1_16_Version) - dataTypes.SkipNextString(packetData); // Level Type - 1.15 and below + await dataTypes.SkipNextStringAsync(packetData); // Level Type - 1.15 and below if (protocolVersion >= MC_1_16_Version) { - dataTypes.ReadNextBool(packetData); // Is Debug - 1.16 and above - dataTypes.ReadNextBool(packetData); // Is Flat - 1.16 and above - dataTypes.ReadNextBool(packetData); // Copy metadata - 1.16 and above + await dataTypes.ReadNextBoolAsync(packetData); // Is Debug - 1.16 and above + await dataTypes.ReadNextBoolAsync(packetData); // Is Flat - 1.16 and above + await dataTypes.ReadNextBoolAsync(packetData); // Copy metadata - 1.16 and above } if (protocolVersion >= MC_1_19_Version) { - bool hasDeathLocation = dataTypes.ReadNextBool(packetData); // Has death location + bool hasDeathLocation = await dataTypes.ReadNextBoolAsync(packetData); // Has death location if (hasDeathLocation) { - dataTypes.ReadNextString(packetData); // Death dimension name: Identifier - dataTypes.ReadNextLocation(packetData); // Death location + await dataTypes.SkipNextStringAsync(packetData); // Death dimension name: Identifier + await dataTypes.SkipNextLocationAsync(packetData); // Death location } } - handler.OnRespawn(); + await handler.OnRespawnAsync(); break; case PacketTypesIn.PlayerPositionAndLook: { // These always need to be read, since we need the field after them for teleport confirm - double x = dataTypes.ReadNextDouble(packetData); - double y = dataTypes.ReadNextDouble(packetData); - double z = dataTypes.ReadNextDouble(packetData); + double x = await dataTypes.ReadNextDoubleAsync(packetData); + double y = await dataTypes.ReadNextDoubleAsync(packetData); + double z = await dataTypes.ReadNextDoubleAsync(packetData); Location location = new(x, y, z); - float yaw = dataTypes.ReadNextFloat(packetData); - float pitch = dataTypes.ReadNextFloat(packetData); - byte locMask = dataTypes.ReadNextByte(packetData); + float yaw = await dataTypes.ReadNextFloatAsync(packetData); + float pitch = await dataTypes.ReadNextFloatAsync(packetData); + byte locMask = await dataTypes.ReadNextByteAsync(packetData); // entity handling require player pos for distance calculating if (handler.GetTerrainEnabled() || handler.GetEntityHandlingEnabled()) @@ -739,7 +704,7 @@ internal bool HandlePacket(int packetID, Queue packetData) if (protocolVersion >= MC_1_9_Version) { - int teleportID = dataTypes.ReadNextVarInt(packetData); + int teleportID = await dataTypes.ReadNextVarIntAsync(packetData); if (teleportID < 0) { yaw = LastYaw; pitch = LastPitch; } else { LastYaw = yaw; LastPitch = pitch; } @@ -747,12 +712,12 @@ internal bool HandlePacket(int packetID, Queue packetData) handler.UpdateLocation(location, yaw, pitch); // Teleport confirm packet - SendPacket(PacketTypesOut.TeleportConfirm, dataTypes.GetVarInt(teleportID)); + await SendPacket(PacketTypesOut.TeleportConfirm, dataTypes.GetVarInt(teleportID)); if (Config.Main.Advanced.TemporaryFixBadpacket) { - SendLocationUpdate(location, true, yaw, pitch, true); + await SendLocationUpdate(location, true, yaw, pitch, true); if (teleportID == 1) - SendLocationUpdate(location, true, yaw, pitch, true); + await SendLocationUpdate(location, true, yaw, pitch, true); } } else @@ -762,7 +727,7 @@ internal bool HandlePacket(int packetID, Queue packetData) } if (protocolVersion >= MC_1_17_Version) - dataTypes.ReadNextBool(packetData); // Dismount Vehicle - 1.17 and above + await dataTypes.ReadNextBoolAsync(packetData); // Dismount Vehicle - 1.17 and above } break; case PacketTypesIn.ChunkData: @@ -771,27 +736,27 @@ internal bool HandlePacket(int packetID, Queue packetData) Interlocked.Increment(ref handler.GetWorld().chunkCnt); Interlocked.Increment(ref handler.GetWorld().chunkLoadNotCompleted); - int chunkX = dataTypes.ReadNextInt(packetData); - int chunkZ = dataTypes.ReadNextInt(packetData); + int chunkX = await dataTypes.ReadNextIntAsync(packetData); + int chunkZ = await dataTypes.ReadNextIntAsync(packetData); if (protocolVersion >= MC_1_17_Version) { ulong[]? verticalStripBitmask = null; if (protocolVersion == MC_1_17_Version || protocolVersion == MC_1_17_1_Version) - verticalStripBitmask = dataTypes.ReadNextULongArray(packetData); // Bit Mask Length and Primary Bit Mask + verticalStripBitmask = await dataTypes.ReadNextULongArrayAsync(packetData); // Bit Mask Length and Primary Bit Mask - dataTypes.ReadNextNbt(packetData); // Heightmaps + await dataTypes.ReadNextNbtAsync(packetData); // Heightmaps if (protocolVersion == MC_1_17_Version || protocolVersion == MC_1_17_1_Version) { - int biomesLength = dataTypes.ReadNextVarInt(packetData); // Biomes length + int biomesLength = await dataTypes.ReadNextVarIntAsync(packetData); // Biomes length for (int i = 0; i < biomesLength; i++) - dataTypes.SkipNextVarInt(packetData); // Biomes + await dataTypes.SkipNextVarIntAsync(packetData); // Biomes } - int dataSize = dataTypes.ReadNextVarInt(packetData); // Size + int dataSize = await dataTypes.ReadNextVarIntAsync(packetData); // Size - pTerrain.ProcessChunkColumnData(chunkX, chunkZ, verticalStripBitmask, packetData); + await pTerrain.ProcessChunkColumnData(chunkX, chunkZ, verticalStripBitmask, packetData); Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); // Block Entity data: ignored @@ -799,30 +764,31 @@ internal bool HandlePacket(int packetID, Queue packetData) } else { - bool chunksContinuous = dataTypes.ReadNextBool(packetData); + bool chunksContinuous = await dataTypes.ReadNextBoolAsync(packetData); if (protocolVersion >= MC_1_16_Version && protocolVersion <= MC_1_16_1_Version) - dataTypes.ReadNextBool(packetData); // Ignore old data - 1.16 to 1.16.1 only + await dataTypes.ReadNextBoolAsync(packetData); // Ignore old data - 1.16 to 1.16.1 only ushort chunkMask = protocolVersion >= MC_1_9_Version - ? (ushort)dataTypes.ReadNextVarInt(packetData) - : dataTypes.ReadNextUShort(packetData); + ? (ushort)await dataTypes.ReadNextVarIntAsync(packetData) + : await dataTypes.ReadNextUShortAsync(packetData); if (protocolVersion < MC_1_8_Version) { - ushort addBitmap = dataTypes.ReadNextUShort(packetData); - int compressedDataSize = dataTypes.ReadNextInt(packetData); - byte[] compressed = dataTypes.ReadData(compressedDataSize, packetData); - byte[] decompressed = ZlibUtils.Decompress(compressed); - - pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, addBitmap, currentDimension == 0, chunksContinuous, currentDimension, new Queue(decompressed)); + ushort addBitmap = await dataTypes.ReadNextUShortAsync(packetData); + int compressedDataSize = await dataTypes.ReadNextIntAsync(packetData); + byte[] compressed = await dataTypes.ReadDataAsync(compressedDataSize, packetData); + ZLibStream zlibStream = new(new MemoryStream(compressed, false), CompressionMode.Decompress, leaveOpen: false); + PacketStream chunkDataStream = new(zlibStream, compressedDataSize); + await pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, addBitmap, currentDimension == 0, chunksContinuous, currentDimension, chunkDataStream); + await chunkDataStream.DisposeAsync(); Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); } else { if (protocolVersion >= MC_1_14_Version) - dataTypes.ReadNextNbt(packetData); // Heightmaps - 1.14 and above + await dataTypes.ReadNextNbtAsync(packetData); // Heightmaps - 1.14 and above int biomesLength = 0; if (protocolVersion >= MC_1_16_2_Version) if (chunksContinuous) - biomesLength = dataTypes.ReadNextVarInt(packetData); // Biomes length - 1.16.2 and above + biomesLength = await dataTypes.ReadNextVarIntAsync(packetData); // Biomes length - 1.16.2 and above if (protocolVersion >= MC_1_15_Version && chunksContinuous) { if (protocolVersion >= MC_1_16_2_Version) @@ -831,14 +797,15 @@ internal bool HandlePacket(int packetID, Queue packetData) { // Biomes - 1.16.2 and above // Don't use ReadNextVarInt because it cost too much time - dataTypes.SkipNextVarInt(packetData); + await dataTypes.SkipNextVarIntAsync(packetData); } } - else dataTypes.DropData(1024 * 4, packetData); // Biomes - 1.15 and above + else + await dataTypes.DropDataAsync(1024 * 4, packetData); // Biomes - 1.15 and above } - int dataSize = dataTypes.ReadNextVarInt(packetData); + int dataSize = await dataTypes.ReadNextVarIntAsync(packetData); - pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, 0, false, chunksContinuous, currentDimension, packetData); + await pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, 0, false, chunksContinuous, currentDimension, packetData); Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); } } @@ -848,8 +815,8 @@ internal bool HandlePacket(int packetID, Queue packetData) if (protocolVersion < MC_1_8_Version) break; - int mapid = dataTypes.ReadNextVarInt(packetData); - byte scale = dataTypes.ReadNextByte(packetData); + int mapid = await dataTypes.ReadNextVarIntAsync(packetData); + byte scale = await dataTypes.ReadNextByteAsync(packetData); // 1.9 + @@ -862,18 +829,18 @@ internal bool HandlePacket(int packetID, Queue packetData) if (protocolVersion >= MC_1_17_Version) { if (protocolVersion >= MC_1_14_Version) - locked = dataTypes.ReadNextBool(packetData); + locked = await dataTypes.ReadNextBoolAsync(packetData); if (protocolVersion >= MC_1_9_Version) - trackingPosition = dataTypes.ReadNextBool(packetData); + trackingPosition = await dataTypes.ReadNextBoolAsync(packetData); } else { if (protocolVersion >= MC_1_9_Version) - trackingPosition = dataTypes.ReadNextBool(packetData); + trackingPosition = await dataTypes.ReadNextBoolAsync(packetData); if (protocolVersion >= MC_1_14_Version) - locked = dataTypes.ReadNextBool(packetData); + locked = await dataTypes.ReadNextBoolAsync(packetData); } int iconcount = 0; @@ -882,7 +849,7 @@ internal bool HandlePacket(int packetID, Queue packetData) // 1,9 + = needs tracking position to be true to get the icons if (protocolVersion <= MC_1_16_5_Version || trackingPosition) { - iconcount = dataTypes.ReadNextVarInt(packetData); + iconcount = await dataTypes.ReadNextVarIntAsync(packetData); for (int i = 0; i < iconcount; i++) { @@ -891,18 +858,18 @@ internal bool HandlePacket(int packetID, Queue packetData) // 1.8 - 1.13 if (protocolVersion < MC_1_13_2_Version) { - byte directionAndtype = dataTypes.ReadNextByte(packetData); + byte directionAndtype = await dataTypes.ReadNextByteAsync(packetData); byte direction, type; // 1.12.2+ if (protocolVersion >= MC_1_12_2_Version) { direction = (byte)(directionAndtype & 0xF); - type = (byte)((directionAndtype >> 4) & 0xF); + type = (byte)(directionAndtype >> 4 & 0xF); } else // 1.8 - 1.12 { - direction = (byte)((directionAndtype >> 4) & 0xF); + direction = (byte)(directionAndtype >> 4 & 0xF); type = (byte)(directionAndtype & 0xF); } @@ -912,25 +879,25 @@ internal bool HandlePacket(int packetID, Queue packetData) // 1.13.2+ if (protocolVersion >= MC_1_13_2_Version) - mapIcon.Type = (MapIconType)dataTypes.ReadNextVarInt(packetData); + mapIcon.Type = (MapIconType)await dataTypes.ReadNextVarIntAsync(packetData); - mapIcon.X = dataTypes.ReadNextByte(packetData); - mapIcon.Z = dataTypes.ReadNextByte(packetData); + mapIcon.X = await dataTypes.ReadNextByteAsync(packetData); + mapIcon.Z = await dataTypes.ReadNextByteAsync(packetData); // 1.13.2+ if (protocolVersion >= MC_1_13_2_Version) { - mapIcon.Direction = dataTypes.ReadNextByte(packetData); + mapIcon.Direction = await dataTypes.ReadNextByteAsync(packetData); - if (dataTypes.ReadNextBool(packetData)) // Has Display Name? - mapIcon.DisplayName = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + if (await dataTypes.ReadNextBoolAsync(packetData)) // Has Display Name? + mapIcon.DisplayName = ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetData)); } icons.Add(mapIcon); } } - byte columnsUpdated = dataTypes.ReadNextByte(packetData); // width + byte columnsUpdated = await dataTypes.ReadNextByteAsync(packetData); // width byte rowsUpdated = 0; // height byte mapCoulmnX = 0; byte mapRowZ = 0; @@ -938,32 +905,46 @@ internal bool HandlePacket(int packetID, Queue packetData) if (columnsUpdated > 0) { - rowsUpdated = dataTypes.ReadNextByte(packetData); // height - mapCoulmnX = dataTypes.ReadNextByte(packetData); - mapRowZ = dataTypes.ReadNextByte(packetData); - colors = dataTypes.ReadNextByteArray(packetData); + rowsUpdated = await dataTypes.ReadNextByteAsync(packetData); // height + mapCoulmnX = await dataTypes.ReadNextByteAsync(packetData); + mapRowZ = await dataTypes.ReadNextByteAsync(packetData); + colors = await dataTypes.ReadNextByteArrayAsync(packetData); } - handler.OnMapData(mapid, scale, trackingPosition, locked, icons, columnsUpdated, rowsUpdated, mapCoulmnX, mapRowZ, colors); + MapData mapData = new() + { + MapId = mapid, + Scale = scale, + TrackingPosition = trackingPosition, + Locked = locked, + Icons = icons, + ColumnsUpdated = columnsUpdated, + RowsUpdated = rowsUpdated, + MapCoulmnX = mapCoulmnX, + MapRowZ = mapRowZ, + Colors = colors, + }; + + await handler.OnMapData(mapData); break; case PacketTypesIn.TradeList: - if ((protocolVersion >= MC_1_14_Version) && (handler.GetInventoryEnabled())) + if (protocolVersion >= MC_1_14_Version && handler.GetInventoryEnabled()) { // MC 1.14 or greater - int windowID = dataTypes.ReadNextVarInt(packetData); - int size = dataTypes.ReadNextByte(packetData); + int windowID = await dataTypes.ReadNextVarIntAsync(packetData); + int size = await dataTypes.ReadNextByteAsync(packetData); List trades = new(); for (int tradeId = 0; tradeId < size; tradeId++) { - VillagerTrade trade = dataTypes.ReadNextTrade(packetData, itemPalette); + VillagerTrade trade = await dataTypes.ReadNextTradeAsync(packetData, itemPalette); trades.Add(trade); } VillagerInfo villagerInfo = new() { - Level = dataTypes.ReadNextVarInt(packetData), - Experience = dataTypes.ReadNextVarInt(packetData), - IsRegularVillager = dataTypes.ReadNextBool(packetData), - CanRestock = dataTypes.ReadNextBool(packetData) + Level = await dataTypes.ReadNextVarIntAsync(packetData), + Experience = await dataTypes.ReadNextVarIntAsync(packetData), + IsRegularVillager = await dataTypes.ReadNextBoolAsync(packetData), + CanRestock = await dataTypes.ReadNextBoolAsync(packetData) }; handler.OnTradeList(windowID, trades, villagerInfo); } @@ -971,11 +952,11 @@ internal bool HandlePacket(int packetID, Queue packetData) case PacketTypesIn.Title: if (protocolVersion >= MC_1_8_Version) { - int action2 = dataTypes.ReadNextVarInt(packetData); - string titletext = String.Empty; - string subtitletext = String.Empty; - string actionbartext = String.Empty; - string json = String.Empty; + int action2 = await dataTypes.ReadNextVarIntAsync(packetData); + string titletext = string.Empty; + string subtitletext = string.Empty; + string actionbartext = string.Empty; + string json = string.Empty; int fadein = -1; int stay = -1; int fadeout = -1; @@ -984,23 +965,23 @@ internal bool HandlePacket(int packetID, Queue packetData) if (action2 == 0) { json = titletext; - titletext = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + titletext = ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetData)); } else if (action2 == 1) { json = subtitletext; - subtitletext = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + subtitletext = ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetData)); } else if (action2 == 2) { json = actionbartext; - actionbartext = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + actionbartext = ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetData)); } else if (action2 == 3) { - fadein = dataTypes.ReadNextInt(packetData); - stay = dataTypes.ReadNextInt(packetData); - fadeout = dataTypes.ReadNextInt(packetData); + fadein = await dataTypes.ReadNextIntAsync(packetData); + stay = await dataTypes.ReadNextIntAsync(packetData); + fadeout = await dataTypes.ReadNextIntAsync(packetData); } } else @@ -1008,21 +989,34 @@ internal bool HandlePacket(int packetID, Queue packetData) if (action2 == 0) { json = titletext; - titletext = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + titletext = ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetData)); } else if (action2 == 1) { json = subtitletext; - subtitletext = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + subtitletext = ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetData)); } else if (action2 == 2) { - fadein = dataTypes.ReadNextInt(packetData); - stay = dataTypes.ReadNextInt(packetData); - fadeout = dataTypes.ReadNextInt(packetData); + fadein = await dataTypes.ReadNextIntAsync(packetData); + stay = await dataTypes.ReadNextIntAsync(packetData); + fadeout = await dataTypes.ReadNextIntAsync(packetData); } } - handler.OnTitle(action2, titletext, subtitletext, actionbartext, fadein, stay, fadeout, json); + + TitlePacket title = new() + { + Action = action2, + TitleText = titletext, + SubtitleText = subtitletext, + ActionbarText = actionbartext, + Stay = stay, + FadeIn = fadein, + FadeOut = fadeout, + JsonText = json, + }; + + await handler.OnTitle(title); } break; case PacketTypesIn.MultiBlockChange: @@ -1030,24 +1024,24 @@ internal bool HandlePacket(int packetID, Queue packetData) { if (protocolVersion >= MC_1_16_2_Version) { - long chunkSection = dataTypes.ReadNextLong(packetData); + long chunkSection = await dataTypes.ReadNextLongAsync(packetData); int sectionX = (int)(chunkSection >> 42); - int sectionY = (int)((chunkSection << 44) >> 44); - int sectionZ = (int)((chunkSection << 22) >> 42); - dataTypes.ReadNextBool(packetData); // Useless boolean (Related to light update) - int blocksSize = dataTypes.ReadNextVarInt(packetData); + int sectionY = (int)(chunkSection << 44 >> 44); + int sectionZ = (int)(chunkSection << 22 >> 42); + await dataTypes.ReadNextBoolAsync(packetData); // Useless boolean (Related to light update) + int blocksSize = await dataTypes.ReadNextVarIntAsync(packetData); for (int i = 0; i < blocksSize; i++) { ulong chunkSectionPosition = (ulong)dataTypes.ReadNextVarLong(packetData); int blockId = (int)(chunkSectionPosition >> 12); - int localX = (int)((chunkSectionPosition >> 8) & 0x0F); - int localZ = (int)((chunkSectionPosition >> 4) & 0x0F); + int localX = (int)(chunkSectionPosition >> 8 & 0x0F); + int localZ = (int)(chunkSectionPosition >> 4 & 0x0F); int localY = (int)(chunkSectionPosition & 0x0F); Block block = new((ushort)blockId); - int blockX = (sectionX * 16) + localX; - int blockY = (sectionY * 16) + localY; - int blockZ = (sectionZ * 16) + localZ; + int blockX = sectionX * 16 + localX; + int blockY = sectionY * 16 + localY; + int blockZ = sectionZ * 16 + localZ; Location location = new(blockX, blockY, blockZ); @@ -1056,11 +1050,11 @@ internal bool HandlePacket(int packetID, Queue packetData) } else { - int chunkX = dataTypes.ReadNextInt(packetData); - int chunkZ = dataTypes.ReadNextInt(packetData); + int chunkX = await dataTypes.ReadNextIntAsync(packetData); + int chunkZ = await dataTypes.ReadNextIntAsync(packetData); int recordCount = protocolVersion < MC_1_8_Version - ? (int)dataTypes.ReadNextShort(packetData) - : dataTypes.ReadNextVarInt(packetData); + ? await dataTypes.ReadNextShortAsync(packetData) + : await dataTypes.ReadNextVarIntAsync(packetData); for (int i = 0; i < recordCount; i++) { @@ -1070,15 +1064,15 @@ internal bool HandlePacket(int packetID, Queue packetData) if (protocolVersion < MC_1_8_Version) { - blockIdMeta = dataTypes.ReadNextUShort(packetData); - blockY = (ushort)dataTypes.ReadNextByte(packetData); - locationXZ = dataTypes.ReadNextByte(packetData); + blockIdMeta = await dataTypes.ReadNextUShortAsync(packetData); + blockY = await dataTypes.ReadNextByteAsync(packetData); + locationXZ = await dataTypes.ReadNextByteAsync(packetData); } else { - locationXZ = dataTypes.ReadNextByte(packetData); - blockY = (ushort)dataTypes.ReadNextByte(packetData); - blockIdMeta = (ushort)dataTypes.ReadNextVarInt(packetData); + locationXZ = await dataTypes.ReadNextByteAsync(packetData); + blockY = await dataTypes.ReadNextByteAsync(packetData); + blockIdMeta = (ushort)await dataTypes.ReadNextVarIntAsync(packetData); } int blockX = locationXZ >> 4; @@ -1093,16 +1087,16 @@ internal bool HandlePacket(int packetID, Queue packetData) break; case PacketTypesIn.ServerData: string motd = "-"; - bool hasMotd = dataTypes.ReadNextBool(packetData); + bool hasMotd = await dataTypes.ReadNextBoolAsync(packetData); if (hasMotd) - motd = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + motd = ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetData)); string iconBase64 = "-"; - bool hasIcon = dataTypes.ReadNextBool(packetData); + bool hasIcon = await dataTypes.ReadNextBoolAsync(packetData); if (hasIcon) - iconBase64 = dataTypes.ReadNextString(packetData); + iconBase64 = await dataTypes.ReadNextStringAsync(packetData); - bool previewsChat = dataTypes.ReadNextBool(packetData); + bool previewsChat = await dataTypes.ReadNextBoolAsync(packetData); handler.OnServerDataRecived(hasMotd, motd, hasIcon, iconBase64, previewsChat); break; @@ -1111,11 +1105,11 @@ internal bool HandlePacket(int packetID, Queue packetData) { if (protocolVersion < MC_1_8_Version) { - int blockX = dataTypes.ReadNextInt(packetData); - int blockY = dataTypes.ReadNextByte(packetData); - int blockZ = dataTypes.ReadNextInt(packetData); - short blockId = (short)dataTypes.ReadNextVarInt(packetData); - byte blockMeta = dataTypes.ReadNextByte(packetData); + int blockX = await dataTypes.ReadNextIntAsync(packetData); + int blockY = await dataTypes.ReadNextByteAsync(packetData); + int blockZ = await dataTypes.ReadNextIntAsync(packetData); + short blockId = (short)await dataTypes.ReadNextVarIntAsync(packetData); + byte blockMeta = await dataTypes.ReadNextByteAsync(packetData); Location location = new(blockX, blockY, blockZ); Block block = new(blockId, blockMeta); @@ -1123,19 +1117,19 @@ internal bool HandlePacket(int packetID, Queue packetData) } else { - Location location = dataTypes.ReadNextLocation(packetData); - Block block = new((ushort)dataTypes.ReadNextVarInt(packetData)); + Location location = await dataTypes.ReadNextLocationAsync(packetData); + Block block = new((ushort)await dataTypes.ReadNextVarIntAsync(packetData)); handler.OnBlockChange(location, block); } } break; case PacketTypesIn.SetDisplayChatPreview: - bool previewsChatSetting = dataTypes.ReadNextBool(packetData); + bool previewsChatSetting = await dataTypes.ReadNextBoolAsync(packetData); handler.OnChatPreviewSettingUpdate(previewsChatSetting); break; case PacketTypesIn.ChatPreview: - int queryID = dataTypes.ReadNextInt(packetData); - bool componentIsPresent = dataTypes.ReadNextBool(packetData); + int queryID = await dataTypes.ReadNextIntAsync(packetData); + bool componentIsPresent = await dataTypes.ReadNextBoolAsync(packetData); // Currently noy implemented log.Debug("New chat preview: "); @@ -1143,7 +1137,7 @@ internal bool HandlePacket(int packetID, Queue packetData) log.Debug(">> Component is present: " + componentIsPresent); if (componentIsPresent) { - string message = dataTypes.ReadNextString(packetData); + string message = await dataTypes.ReadNextStringAsync(packetData); log.Debug(">> Component: " + ChatParser.ParseText(message)); //handler.OnTextReceived(message, true); } @@ -1156,22 +1150,25 @@ internal bool HandlePacket(int packetID, Queue packetData) { int chunkCount; bool hasSkyLight; - Queue chunkData = packetData; + + bool needDispose = false; + PacketStream chunkDataStream = packetData; //Read global fields if (protocolVersion < MC_1_8_Version) { - chunkCount = dataTypes.ReadNextShort(packetData); - int compressedDataSize = dataTypes.ReadNextInt(packetData); - hasSkyLight = dataTypes.ReadNextBool(packetData); - byte[] compressed = dataTypes.ReadData(compressedDataSize, packetData); - byte[] decompressed = ZlibUtils.Decompress(compressed); - chunkData = new Queue(decompressed); + chunkCount = await dataTypes.ReadNextShortAsync(packetData); + int compressedDataSize = await dataTypes.ReadNextIntAsync(packetData); + hasSkyLight = await dataTypes.ReadNextBoolAsync(packetData); + byte[] compressed = await dataTypes.ReadDataAsync(compressedDataSize, packetData); + ZLibStream zlibStream = new(new MemoryStream(compressed, false), CompressionMode.Decompress, leaveOpen: false); + chunkDataStream = new PacketStream(zlibStream, compressedDataSize); + needDispose = true; } else { - hasSkyLight = dataTypes.ReadNextBool(packetData); - chunkCount = dataTypes.ReadNextVarInt(packetData); + hasSkyLight = await dataTypes.ReadNextBoolAsync(packetData); + chunkCount = await dataTypes.ReadNextVarIntAsync(packetData); } //Read chunk records @@ -1181,28 +1178,31 @@ internal bool HandlePacket(int packetID, Queue packetData) ushort[] addBitmaps = new ushort[chunkCount]; for (int chunkColumnNo = 0; chunkColumnNo < chunkCount; chunkColumnNo++) { - chunkXs[chunkColumnNo] = dataTypes.ReadNextInt(packetData); - chunkZs[chunkColumnNo] = dataTypes.ReadNextInt(packetData); - chunkMasks[chunkColumnNo] = dataTypes.ReadNextUShort(packetData); + chunkXs[chunkColumnNo] = await dataTypes.ReadNextIntAsync(packetData); + chunkZs[chunkColumnNo] = await dataTypes.ReadNextIntAsync(packetData); + chunkMasks[chunkColumnNo] = await dataTypes.ReadNextUShortAsync(packetData); addBitmaps[chunkColumnNo] = protocolVersion < MC_1_8_Version - ? dataTypes.ReadNextUShort(packetData) + ? await dataTypes.ReadNextUShortAsync(packetData) : (ushort)0; } //Process chunk records for (int chunkColumnNo = 0; chunkColumnNo < chunkCount; chunkColumnNo++) { - pTerrain.ProcessChunkColumnData(chunkXs[chunkColumnNo], chunkZs[chunkColumnNo], chunkMasks[chunkColumnNo], addBitmaps[chunkColumnNo], hasSkyLight, true, currentDimension, chunkData); + await pTerrain.ProcessChunkColumnData(chunkXs[chunkColumnNo], chunkZs[chunkColumnNo], chunkMasks[chunkColumnNo], addBitmaps[chunkColumnNo], hasSkyLight, true, currentDimension, chunkDataStream); Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); } + if (needDispose) + await chunkDataStream.DisposeAsync(); + } break; case PacketTypesIn.UnloadChunk: if (protocolVersion >= MC_1_9_Version && handler.GetTerrainEnabled()) { - int chunkX = dataTypes.ReadNextInt(packetData); - int chunkZ = dataTypes.ReadNextInt(packetData); + int chunkX = await dataTypes.ReadNextIntAsync(packetData); + int chunkZ = await dataTypes.ReadNextIntAsync(packetData); // Warning: It is legal to include unloaded chunks in the UnloadChunk packet. // Since chunks that have not been loaded are not recorded, this may result @@ -1216,8 +1216,8 @@ internal bool HandlePacket(int packetID, Queue packetData) case PacketTypesIn.ChangeGameState: if (protocolVersion >= MC_1_15_2_Version) { - byte reason = dataTypes.ReadNextByte(packetData); - float state = dataTypes.ReadNextFloat(packetData); + byte reason = await dataTypes.ReadNextByteAsync(packetData); + float state = await dataTypes.ReadNextFloatAsync(packetData); handler.OnGameEvent(reason, state); } @@ -1225,18 +1225,18 @@ internal bool HandlePacket(int packetID, Queue packetData) case PacketTypesIn.PlayerInfo: if (protocolVersion >= MC_1_8_Version) { - int action = dataTypes.ReadNextVarInt(packetData); // Action Name - int numberOfPlayers = dataTypes.ReadNextVarInt(packetData); // Number Of Players + int action = await dataTypes.ReadNextVarIntAsync(packetData); // Action Name + int numberOfPlayers = await dataTypes.ReadNextVarIntAsync(packetData); // Number Of Players for (int i = 0; i < numberOfPlayers; i++) { - Guid uuid = dataTypes.ReadNextUUID(packetData); // Player UUID + Guid uuid = await dataTypes.ReadNextUUIDAsync(packetData); // Player UUID switch (action) { case 0x00: //Player Join (Add player since 1.19) - string name = dataTypes.ReadNextString(packetData); // Player name - int propNum = dataTypes.ReadNextVarInt(packetData); // Number of properties in the following array + string name = await dataTypes.ReadNextStringAsync(packetData); // Player name + int propNum = await dataTypes.ReadNextVarIntAsync(packetData); // Number of properties in the following array // Property: Tuple packetData) new Tuple[propNum] : null; for (int p = 0; p < propNum; p++) { - string propertyName = dataTypes.ReadNextString(packetData); // Name: String (32767) - string val = dataTypes.ReadNextString(packetData); // Value: String (32767) + string propertyName = await dataTypes.ReadNextStringAsync(packetData); // Name: String (32767) + string val = await dataTypes.ReadNextStringAsync(packetData); // Value: String (32767) string? propertySignature = null; - if (dataTypes.ReadNextBool(packetData)) // Is Signed - propertySignature = dataTypes.ReadNextString(packetData); // Signature: String (32767) + if (await dataTypes.ReadNextBoolAsync(packetData)) // Is Signed + propertySignature = await dataTypes.ReadNextStringAsync(packetData); // Signature: String (32767) if (useProperty) properties![p] = new(propertyName, val, propertySignature); } #pragma warning restore CS0162 // Unreachable code detected - int gameMode = dataTypes.ReadNextVarInt(packetData); // Gamemode - handler.OnGamemodeUpdate(uuid, gameMode); + int gameMode = await dataTypes.ReadNextVarIntAsync(packetData); // Gamemode + await handler.OnGamemodeUpdate(uuid, gameMode); - int ping = dataTypes.ReadNextVarInt(packetData); // Ping + int ping = await dataTypes.ReadNextVarIntAsync(packetData); // Ping string? displayName = null; - if (dataTypes.ReadNextBool(packetData)) // Has display name - displayName = dataTypes.ReadNextString(packetData); // Display name + if (await dataTypes.ReadNextBoolAsync(packetData)) // Has display name + displayName = await dataTypes.ReadNextStringAsync(packetData); // Display name // 1.19 Additions long? keyExpiration = null; byte[]? publicKey = null, signature = null; if (protocolVersion >= MC_1_19_Version) { - if (dataTypes.ReadNextBool(packetData)) // Has Sig Data (if true, red the following fields) + if (await dataTypes.ReadNextBoolAsync(packetData)) // Has Sig Data (if true, red the following fields) { - keyExpiration = dataTypes.ReadNextLong(packetData); // Timestamp + keyExpiration = await dataTypes.ReadNextLongAsync(packetData); // Timestamp - int publicKeyLength = dataTypes.ReadNextVarInt(packetData); // Public Key Length + int publicKeyLength = await dataTypes.ReadNextVarIntAsync(packetData); // Public Key Length if (publicKeyLength > 0) - publicKey = dataTypes.ReadData(publicKeyLength, packetData); // Public key + publicKey = await dataTypes.ReadDataAsync(publicKeyLength, packetData); // Public key - int signatureLength = dataTypes.ReadNextVarInt(packetData); // Signature Length + int signatureLength = await dataTypes.ReadNextVarIntAsync(packetData); // Signature Length if (signatureLength > 0) - signature = dataTypes.ReadData(signatureLength, packetData); // Public key + signature = await dataTypes.ReadDataAsync(signatureLength, packetData); // Public key } } - handler.OnPlayerJoin(new PlayerInfo(uuid, name, properties, gameMode, ping, displayName, keyExpiration, publicKey, signature)); + await handler.OnPlayerJoinAsync(new PlayerInfo(uuid, name, properties, gameMode, ping, displayName, keyExpiration, publicKey, signature)); break; case 0x01: //Update gamemode - handler.OnGamemodeUpdate(uuid, dataTypes.ReadNextVarInt(packetData)); + await handler.OnGamemodeUpdate(uuid, await dataTypes.ReadNextVarIntAsync(packetData)); break; case 0x02: //Update latency - int latency = dataTypes.ReadNextVarInt(packetData); - handler.OnLatencyUpdate(uuid, latency); //Update latency; + int latency = await dataTypes.ReadNextVarIntAsync(packetData); + await handler.OnLatencyUpdate(uuid, latency); //Update latency; break; case 0x03: //Update display name - if (dataTypes.ReadNextBool(packetData)) + if (await dataTypes.ReadNextBoolAsync(packetData)) { PlayerInfo? player = handler.GetPlayerInfo(uuid); if (player != null) - player.DisplayName = dataTypes.ReadNextString(packetData); + player.DisplayName = await dataTypes.ReadNextStringAsync(packetData); else - dataTypes.SkipNextString(packetData); + await dataTypes.SkipNextStringAsync(packetData); } break; case 0x04: //Player Leave - handler.OnPlayerLeave(uuid); + await handler.OnPlayerLeaveAsync(uuid); break; default: //Unknown player list item type @@ -1314,52 +1314,55 @@ internal bool HandlePacket(int packetID, Queue packetData) } else //MC 1.7.X does not provide UUID in tab-list updates { - string name = dataTypes.ReadNextString(packetData); - bool online = dataTypes.ReadNextBool(packetData); - short ping = dataTypes.ReadNextShort(packetData); - Guid FakeUUID = new(MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(name)).Take(16).ToArray()); + string name = await dataTypes.ReadNextStringAsync(packetData); + bool online = await dataTypes.ReadNextBoolAsync(packetData); + short ping = await dataTypes.ReadNextShortAsync(packetData); + Guid FakeUUID = new(MD5.HashData(Encoding.UTF8.GetBytes(name)).Take(16).ToArray()); if (online) - handler.OnPlayerJoin(new PlayerInfo(name, FakeUUID)); - else handler.OnPlayerLeave(FakeUUID); + await handler.OnPlayerJoinAsync(new PlayerInfo(name, FakeUUID)); + else + await handler.OnPlayerLeaveAsync(FakeUUID); } break; case PacketTypesIn.TabComplete: int old_transaction_id = autocomplete_transaction_id; if (protocolVersion >= MC_1_13_Version) { - autocomplete_transaction_id = dataTypes.ReadNextVarInt(packetData); - dataTypes.ReadNextVarInt(packetData); // Start of text to replace - dataTypes.ReadNextVarInt(packetData); // Length of text to replace + autocomplete_transaction_id = await dataTypes.ReadNextVarIntAsync(packetData); + await dataTypes.SkipNextVarIntAsync(packetData); // Start of text to replace + await dataTypes.SkipNextVarIntAsync(packetData); // Length of text to replace } - int autocomplete_count = dataTypes.ReadNextVarInt(packetData); + int autocomplete_count = await dataTypes.ReadNextVarIntAsync(packetData); string[] autocomplete_result = new string[autocomplete_count]; for (int i = 0; i < autocomplete_count; i++) { - autocomplete_result[i] = dataTypes.ReadNextString(packetData); + autocomplete_result[i] = await dataTypes.ReadNextStringAsync(packetData); if (protocolVersion >= MC_1_13_Version) { // Skip optional tooltip for each tab-complete resul`t - if (dataTypes.ReadNextBool(packetData)) - dataTypes.SkipNextString(packetData); + if (await dataTypes.ReadNextBoolAsync(packetData)) + await dataTypes.SkipNextStringAsync(packetData); } } handler.OnAutoCompleteDone(old_transaction_id, autocomplete_result); break; case PacketTypesIn.PluginMessage: - String channel = dataTypes.ReadNextString(packetData); + string channel = await dataTypes.ReadNextStringAsync(packetData); // Length is unneeded as the whole remaining packetData is the entire payload of the packet. if (protocolVersion < MC_1_8_Version) pForge.ReadNextVarShort(packetData); - handler.OnPluginChannelMessage(channel, packetData.ToArray()); - return pForge.HandlePluginMessage(channel, packetData, ref currentDimension); + byte[] pluginMessage = await packetData.ReadFullPacket(); + handler.OnPluginChannelMessage(channel, pluginMessage); + (bool handleForgePlusinMsgStatus, currentDimension) = await pForge.HandlePluginMessage(channel, pluginMessage, currentDimension); + return handleForgePlusinMsgStatus; case PacketTypesIn.Disconnect: - handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick, ChatParser.ParseText(dataTypes.ReadNextString(packetData))); + handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick, ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetData))); return false; case PacketTypesIn.SetCompression: if (protocolVersion >= MC_1_8_Version && protocolVersion < MC_1_9_Version) - compression_treshold = dataTypes.ReadNextVarInt(packetData); + socketWrapper.CompressionThreshold = await dataTypes.ReadNextVarIntAsync(packetData); break; case PacketTypesIn.OpenWindow: if (handler.GetInventoryEnabled()) @@ -1367,105 +1370,105 @@ internal bool HandlePacket(int packetID, Queue packetData) if (protocolVersion < MC_1_14_Version) { // MC 1.13 or lower - byte windowID = dataTypes.ReadNextByte(packetData); - string type = dataTypes.ReadNextString(packetData).Replace("minecraft:", "").ToUpper(); + byte windowID = await dataTypes.ReadNextByteAsync(packetData); + string type = (await dataTypes.ReadNextStringAsync(packetData)).Replace("minecraft:", "").ToUpper(); ContainerTypeOld inventoryType = (ContainerTypeOld)Enum.Parse(typeof(ContainerTypeOld), type); - string title = dataTypes.ReadNextString(packetData); - byte slots = dataTypes.ReadNextByte(packetData); + string title = await dataTypes.ReadNextStringAsync(packetData); + byte slots = await dataTypes.ReadNextByteAsync(packetData); Container inventory = new(windowID, inventoryType, ChatParser.ParseText(title)); - handler.OnInventoryOpen(windowID, inventory); + await handler.OnInventoryOpenAsync(windowID, inventory); } else { // MC 1.14 or greater - int windowID = dataTypes.ReadNextVarInt(packetData); - int windowType = dataTypes.ReadNextVarInt(packetData); - string title = dataTypes.ReadNextString(packetData); + int windowID = await dataTypes.ReadNextVarIntAsync(packetData); + int windowType = await dataTypes.ReadNextVarIntAsync(packetData); + string title = await dataTypes.ReadNextStringAsync(packetData); Container inventory = new(windowID, windowType, ChatParser.ParseText(title)); - handler.OnInventoryOpen(windowID, inventory); + await handler.OnInventoryOpenAsync(windowID, inventory); } } break; case PacketTypesIn.CloseWindow: if (handler.GetInventoryEnabled()) { - byte windowID = dataTypes.ReadNextByte(packetData); + byte windowID = await dataTypes.ReadNextByteAsync(packetData); lock (window_actions) { window_actions[windowID] = 0; } - handler.OnInventoryClose(windowID); + await handler.OnInventoryCloseAsync(windowID); } break; case PacketTypesIn.WindowItems: if (handler.GetInventoryEnabled()) { - byte windowId = dataTypes.ReadNextByte(packetData); + byte windowId = await dataTypes.ReadNextByteAsync(packetData); int stateId = -1; int elements = 0; if (protocolVersion >= MC_1_17_1_Version) { // State ID and Elements as VarInt - 1.17.1 and above - stateId = dataTypes.ReadNextVarInt(packetData); - elements = dataTypes.ReadNextVarInt(packetData); + stateId = await dataTypes.ReadNextVarIntAsync(packetData); + elements = await dataTypes.ReadNextVarIntAsync(packetData); } else { // Elements as Short - 1.17.0 and below - dataTypes.ReadNextShort(packetData); + await dataTypes.ReadNextShortAsync(packetData); } Dictionary inventorySlots = new(); for (int slotId = 0; slotId < elements; slotId++) { - Item? item = dataTypes.ReadNextItemSlot(packetData, itemPalette); + Item? item = await dataTypes.ReadNextItemSlotAsync(packetData, itemPalette); if (item != null) inventorySlots[slotId] = item; } if (protocolVersion >= MC_1_17_1_Version) // Carried Item - 1.17.1 and above - dataTypes.ReadNextItemSlot(packetData, itemPalette); + await dataTypes.ReadNextItemSlotAsync(packetData, itemPalette); - handler.OnWindowItems(windowId, inventorySlots, stateId); + await handler.OnWindowItemsAsync(windowId, inventorySlots, stateId); } break; case PacketTypesIn.WindowProperty: - byte containerId = dataTypes.ReadNextByte(packetData); - short propertyId = dataTypes.ReadNextShort(packetData); - short propertyValue = dataTypes.ReadNextShort(packetData); + byte containerId = await dataTypes.ReadNextByteAsync(packetData); + short propertyId = await dataTypes.ReadNextShortAsync(packetData); + short propertyValue = await dataTypes.ReadNextShortAsync(packetData); - handler.OnWindowProperties(containerId, propertyId, propertyValue); + await handler.OnWindowPropertiesAsync(containerId, propertyId, propertyValue); break; case PacketTypesIn.SetSlot: if (handler.GetInventoryEnabled()) { - byte windowID = dataTypes.ReadNextByte(packetData); + byte windowID = await dataTypes.ReadNextByteAsync(packetData); int stateId = -1; if (protocolVersion >= MC_1_17_1_Version) - stateId = dataTypes.ReadNextVarInt(packetData); // State ID - 1.17.1 and above - short slotID = dataTypes.ReadNextShort(packetData); - Item? item = dataTypes.ReadNextItemSlot(packetData, itemPalette); - handler.OnSetSlot(windowID, slotID, item, stateId); + stateId = await dataTypes.ReadNextVarIntAsync(packetData); // State ID - 1.17.1 and above + short slotID = await dataTypes.ReadNextShortAsync(packetData); + Item? item = await dataTypes.ReadNextItemSlotAsync(packetData, itemPalette); + await handler.OnSetSlotAsync(windowID, slotID, item, stateId); } break; case PacketTypesIn.WindowConfirmation: if (handler.GetInventoryEnabled()) { - byte windowID = dataTypes.ReadNextByte(packetData); - short actionID = dataTypes.ReadNextShort(packetData); - bool accepted = dataTypes.ReadNextBool(packetData); + byte windowID = await dataTypes.ReadNextByteAsync(packetData); + short actionID = await dataTypes.ReadNextShortAsync(packetData); + bool accepted = await dataTypes.ReadNextBoolAsync(packetData); if (!accepted) - SendWindowConfirmation(windowID, actionID, true); + await SendWindowConfirmation(windowID, actionID, true); } break; case PacketTypesIn.ResourcePackSend: - string url = dataTypes.ReadNextString(packetData); - string hash = dataTypes.ReadNextString(packetData); + string url = await dataTypes.ReadNextStringAsync(packetData); + string hash = await dataTypes.ReadNextStringAsync(packetData); bool forced = true; // Assume forced for MC 1.16 and below if (protocolVersion >= MC_1_17_Version) { - forced = dataTypes.ReadNextBool(packetData); - bool hasPromptMessage = dataTypes.ReadNextBool(packetData); // Has Prompt Message (Boolean) - 1.17 and above + forced = await dataTypes.ReadNextBoolAsync(packetData); + bool hasPromptMessage = await dataTypes.ReadNextBoolAsync(packetData); // Has Prompt Message (Boolean) - 1.17 and above if (hasPromptMessage) - dataTypes.SkipNextString(packetData); // Prompt Message (Optional Chat) - 1.17 and above + await dataTypes.SkipNextStringAsync(packetData); // Prompt Message (Optional Chat) - 1.17 and above } // Some server plugins may send invalid resource packs to probe the client and we need to ignore them (issue #1056) if (!url.StartsWith("http") && hash.Length != 40) // Some server may have null hash value @@ -1474,91 +1477,103 @@ internal bool HandlePacket(int packetID, Queue packetData) byte[] responseHeader = Array.Empty(); if (protocolVersion < MC_1_10_Version) //MC 1.10 does not include resource pack hash in responses responseHeader = dataTypes.ConcatBytes(dataTypes.GetVarInt(hash.Length), Encoding.UTF8.GetBytes(hash)); - SendPacket(PacketTypesOut.ResourcePackStatus, dataTypes.ConcatBytes(responseHeader, dataTypes.GetVarInt(3))); //Accepted pack - SendPacket(PacketTypesOut.ResourcePackStatus, dataTypes.ConcatBytes(responseHeader, dataTypes.GetVarInt(0))); //Successfully loaded + await SendPacket(PacketTypesOut.ResourcePackStatus, dataTypes.ConcatBytes(responseHeader, dataTypes.GetVarInt(3))); //Accepted pack + await SendPacket(PacketTypesOut.ResourcePackStatus, dataTypes.ConcatBytes(responseHeader, dataTypes.GetVarInt(0))); //Successfully loaded break; case PacketTypesIn.SpawnEntity: if (handler.GetEntityHandlingEnabled()) { - Entity entity = dataTypes.ReadNextEntity(packetData, entityPalette, false); - handler.OnSpawnEntity(entity); + Entity entity = await dataTypes.ReadNextEntity(packetData, entityPalette, false); + await handler.OnSpawnEntity(entity); } break; case PacketTypesIn.EntityEquipment: if (handler.GetEntityHandlingEnabled()) { - int entityid = dataTypes.ReadNextVarInt(packetData); + int entityid = await dataTypes.ReadNextVarIntAsync(packetData); if (protocolVersion >= MC_1_16_Version) { bool hasNext; do { - byte bitsData = dataTypes.ReadNextByte(packetData); + byte bitsData = await dataTypes.ReadNextByteAsync(packetData); // Top bit set if another entry follows, and otherwise unset if this is the last item in the array - hasNext = (bitsData >> 7) == 1; + hasNext = bitsData >> 7 == 1; int slot2 = bitsData >> 1; - Item? item = dataTypes.ReadNextItemSlot(packetData, itemPalette); - handler.OnEntityEquipment(entityid, slot2, item); + Item? item = await dataTypes.ReadNextItemSlotAsync(packetData, itemPalette); + await handler.OnEntityEquipment(entityid, slot2, item); } while (hasNext); } else { - int slot2 = dataTypes.ReadNextVarInt(packetData); - Item? item = dataTypes.ReadNextItemSlot(packetData, itemPalette); - handler.OnEntityEquipment(entityid, slot2, item); + int slot2 = await dataTypes.ReadNextVarIntAsync(packetData); + Item? item = await dataTypes.ReadNextItemSlotAsync(packetData, itemPalette); + await handler.OnEntityEquipment(entityid, slot2, item); } } break; case PacketTypesIn.SpawnLivingEntity: if (handler.GetEntityHandlingEnabled()) { - Entity entity = dataTypes.ReadNextEntity(packetData, entityPalette, true); + Entity entity = await dataTypes.ReadNextEntity(packetData, entityPalette, true); // packet before 1.15 has metadata at the end // this is not handled in dataTypes.ReadNextEntity() // we are simply ignoring leftover data in packet - handler.OnSpawnEntity(entity); + await handler.OnSpawnEntity(entity); } break; case PacketTypesIn.SpawnPlayer: if (handler.GetEntityHandlingEnabled()) { - int EntityID = dataTypes.ReadNextVarInt(packetData); - Guid UUID = dataTypes.ReadNextUUID(packetData); - double X = dataTypes.ReadNextDouble(packetData); - double Y = dataTypes.ReadNextDouble(packetData); - double Z = dataTypes.ReadNextDouble(packetData); - byte Yaw = dataTypes.ReadNextByte(packetData); - byte Pitch = dataTypes.ReadNextByte(packetData); + int EntityID = await dataTypes.ReadNextVarIntAsync(packetData); + Guid UUID = await dataTypes.ReadNextUUIDAsync(packetData); + double X = await dataTypes.ReadNextDoubleAsync(packetData); + double Y = await dataTypes.ReadNextDoubleAsync(packetData); + double Z = await dataTypes.ReadNextDoubleAsync(packetData); + byte Yaw = await dataTypes.ReadNextByteAsync(packetData); + byte Pitch = await dataTypes.ReadNextByteAsync(packetData); Location EntityLocation = new(X, Y, Z); - handler.OnSpawnPlayer(EntityID, UUID, EntityLocation, Yaw, Pitch); + await handler.OnSpawnPlayer(EntityID, UUID, EntityLocation, Yaw, Pitch); } break; case PacketTypesIn.EntityEffect: if (handler.GetEntityHandlingEnabled()) { - int entityid = dataTypes.ReadNextVarInt(packetData); - Inventory.Effects effect = Effects.Speed; + int entityid = await dataTypes.ReadNextVarIntAsync(packetData); + EffectType effectType = EffectType.Speed; int effectId = protocolVersion >= MC_1_18_2_Version ? - dataTypes.ReadNextVarInt(packetData) : dataTypes.ReadNextByte(packetData); - if (Enum.TryParse(effectId.ToString(), out effect)) + await dataTypes.ReadNextVarIntAsync(packetData) : await dataTypes.ReadNextByteAsync(packetData); + if (Enum.TryParse(effectId.ToString(), out effectType)) { - int amplifier = dataTypes.ReadNextByte(packetData); - int duration = dataTypes.ReadNextVarInt(packetData); - byte flags = dataTypes.ReadNextByte(packetData); + int amplifier = await dataTypes.ReadNextByteAsync(packetData); + int duration = await dataTypes.ReadNextVarIntAsync(packetData); + byte flags = await dataTypes.ReadNextByteAsync(packetData); bool hasFactorData = false; Dictionary? factorCodec = null; if (protocolVersion >= MC_1_19_Version) { - hasFactorData = dataTypes.ReadNextBool(packetData); + hasFactorData = await dataTypes.ReadNextBoolAsync(packetData); if (hasFactorData) - factorCodec = dataTypes.ReadNextNbt(packetData); + factorCodec = await dataTypes.ReadNextNbtAsync(packetData); } - handler.OnEntityEffect(entityid, effect, amplifier, duration, flags, hasFactorData, factorCodec); + Effect effect = new() + { + Type = effectType, + EffectLevel = amplifier, + StartTick = CurrentTick, + DurationInTick = duration, + IsFromBeacon = (flags & 0x01) > 0, + ShowParticles = (flags & 0x02) > 0, + ShowIcon = (flags & 0x04) > 0, + FactorData = factorCodec, + }; + + await handler.OnEntityEffect(entityid, effect); } } break; @@ -1567,65 +1582,65 @@ internal bool HandlePacket(int packetID, Queue packetData) { int entityCount = 1; // 1.17.0 has only one entity per packet if (protocolVersion != MC_1_17_Version) - entityCount = dataTypes.ReadNextVarInt(packetData); // All other versions have a "count" field + entityCount = await dataTypes.ReadNextVarIntAsync(packetData); // All other versions have a "count" field int[] entityList = new int[entityCount]; for (int i = 0; i < entityCount; i++) { - entityList[i] = dataTypes.ReadNextVarInt(packetData); + entityList[i] = await dataTypes.ReadNextVarIntAsync(packetData); } - handler.OnDestroyEntities(entityList); + await handler.OnDestroyEntities(entityList); } break; case PacketTypesIn.EntityPosition: if (handler.GetEntityHandlingEnabled()) { - int EntityID = dataTypes.ReadNextVarInt(packetData); - Double DeltaX = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); - Double DeltaY = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); - Double DeltaZ = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); - bool OnGround = dataTypes.ReadNextBool(packetData); - DeltaX /= (128 * 32); - DeltaY /= (128 * 32); - DeltaZ /= (128 * 32); - handler.OnEntityPosition(EntityID, DeltaX, DeltaY, DeltaZ, OnGround); + int EntityID = await dataTypes.ReadNextVarIntAsync(packetData); + double DeltaX = Convert.ToDouble(await dataTypes.ReadNextShortAsync(packetData)); + double DeltaY = Convert.ToDouble(await dataTypes.ReadNextShortAsync(packetData)); + double DeltaZ = Convert.ToDouble(await dataTypes.ReadNextShortAsync(packetData)); + bool OnGround = await dataTypes.ReadNextBoolAsync(packetData); + DeltaX /= 128 * 32; + DeltaY /= 128 * 32; + DeltaZ /= 128 * 32; + await handler.OnEntityPosition(EntityID, DeltaX, DeltaY, DeltaZ, OnGround); } break; case PacketTypesIn.EntityPositionAndRotation: if (handler.GetEntityHandlingEnabled()) { - int EntityID = dataTypes.ReadNextVarInt(packetData); - Double DeltaX = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); - Double DeltaY = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); - Double DeltaZ = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); - byte _yaw = dataTypes.ReadNextByte(packetData); - byte _pitch = dataTypes.ReadNextByte(packetData); - bool OnGround = dataTypes.ReadNextBool(packetData); - DeltaX /= (128 * 32); - DeltaY /= (128 * 32); - DeltaZ /= (128 * 32); - handler.OnEntityPosition(EntityID, DeltaX, DeltaY, DeltaZ, OnGround); + int EntityID = await dataTypes.ReadNextVarIntAsync(packetData); + double DeltaX = Convert.ToDouble(await dataTypes.ReadNextShortAsync(packetData)); + double DeltaY = Convert.ToDouble(await dataTypes.ReadNextShortAsync(packetData)); + double DeltaZ = Convert.ToDouble(await dataTypes.ReadNextShortAsync(packetData)); + byte _yaw = await dataTypes.ReadNextByteAsync(packetData); + byte _pitch = await dataTypes.ReadNextByteAsync(packetData); + bool OnGround = await dataTypes.ReadNextBoolAsync(packetData); + DeltaX /= 128 * 32; + DeltaY /= 128 * 32; + DeltaZ /= 128 * 32; + await handler.OnEntityPosition(EntityID, DeltaX, DeltaY, DeltaZ, OnGround); } break; case PacketTypesIn.EntityProperties: if (handler.GetEntityHandlingEnabled()) { - int EntityID = dataTypes.ReadNextVarInt(packetData); - int NumberOfProperties = protocolVersion >= MC_1_17_Version ? dataTypes.ReadNextVarInt(packetData) : dataTypes.ReadNextInt(packetData); - Dictionary keys = new(); + int EntityID = await dataTypes.ReadNextVarIntAsync(packetData); + int NumberOfProperties = protocolVersion >= MC_1_17_Version ? await dataTypes.ReadNextVarIntAsync(packetData) : await dataTypes.ReadNextIntAsync(packetData); + Dictionary keys = new(); for (int i = 0; i < NumberOfProperties; i++) { - string _key = dataTypes.ReadNextString(packetData); - Double _value = dataTypes.ReadNextDouble(packetData); + string _key = await dataTypes.ReadNextStringAsync(packetData); + double _value = await dataTypes.ReadNextDoubleAsync(packetData); List op0 = new(); List op1 = new(); List op2 = new(); - int NumberOfModifiers = dataTypes.ReadNextVarInt(packetData); + int NumberOfModifiers = await dataTypes.ReadNextVarIntAsync(packetData); for (int j = 0; j < NumberOfModifiers; j++) { - dataTypes.ReadNextUUID(packetData); - Double amount = dataTypes.ReadNextDouble(packetData); - byte operation = dataTypes.ReadNextByte(packetData); + await dataTypes.SkipNextUUIDAsync(packetData); + double amount = await dataTypes.ReadNextDoubleAsync(packetData); + byte operation = await dataTypes.ReadNextByteAsync(packetData); switch (operation) { case 0: op0.Add(amount); break; @@ -1638,14 +1653,14 @@ internal bool HandlePacket(int packetID, Queue packetData) if (op2.Count > 0) _value *= op2.Aggregate((a, _x) => a * _x); keys.Add(_key, _value); } - handler.OnEntityProperties(EntityID, keys); + await handler.OnEntityProperties(EntityID, keys); } break; case PacketTypesIn.EntityMetadata: if (handler.GetEntityHandlingEnabled()) { - int EntityID = dataTypes.ReadNextVarInt(packetData); - Dictionary metadata = dataTypes.ReadNextMetadata(packetData, itemPalette); + int EntityID = await dataTypes.ReadNextVarIntAsync(packetData); + Dictionary metadata = await dataTypes.ReadNextMetadataAsync(packetData, itemPalette); int healthField; // See https://wiki.vg/Entity_metadata#Living_Entity if (protocolVersion > MC_1_19_2_Version) @@ -1668,115 +1683,115 @@ internal bool HandlePacket(int packetID, Queue packetData) case PacketTypesIn.EntityStatus: if (handler.GetEntityHandlingEnabled()) { - int entityId = dataTypes.ReadNextInt(packetData); - byte status = dataTypes.ReadNextByte(packetData); - handler.OnEntityStatus(entityId, status); + int entityId = await dataTypes.ReadNextIntAsync(packetData); + byte status = await dataTypes.ReadNextByteAsync(packetData); + await handler.OnEntityStatus(entityId, status); } break; case PacketTypesIn.TimeUpdate: - long WorldAge = dataTypes.ReadNextLong(packetData); - long TimeOfday = dataTypes.ReadNextLong(packetData); - handler.OnTimeUpdate(WorldAge, TimeOfday); + long WorldAge = await dataTypes.ReadNextLongAsync(packetData); + long TimeOfday = await dataTypes.ReadNextLongAsync(packetData); + await handler.OnTimeUpdate(WorldAge, TimeOfday); break; case PacketTypesIn.SystemChat: - string systemMessage = dataTypes.ReadNextString(packetData); - int msgType = dataTypes.ReadNextVarInt(packetData); - if ((msgType == 1 && !Config.Main.Advanced.ShowSystemMessages)) + string systemMessage = await dataTypes.ReadNextStringAsync(packetData); + int msgType = await dataTypes.ReadNextVarIntAsync(packetData); + if (msgType == 1 && !Config.Main.Advanced.ShowSystemMessages) break; - handler.OnTextReceived(new(systemMessage, true, msgType, Guid.Empty, true)); + await handler.OnTextReceivedAsync(new(systemMessage, true, msgType, Guid.Empty, true)); break; case PacketTypesIn.EntityTeleport: if (handler.GetEntityHandlingEnabled()) { - int EntityID = dataTypes.ReadNextVarInt(packetData); - Double X = dataTypes.ReadNextDouble(packetData); - Double Y = dataTypes.ReadNextDouble(packetData); - Double Z = dataTypes.ReadNextDouble(packetData); - byte EntityYaw = dataTypes.ReadNextByte(packetData); - byte EntityPitch = dataTypes.ReadNextByte(packetData); - bool OnGround = dataTypes.ReadNextBool(packetData); - handler.OnEntityTeleport(EntityID, X, Y, Z, OnGround); + int EntityID = await dataTypes.ReadNextVarIntAsync(packetData); + double X = await dataTypes.ReadNextDoubleAsync(packetData); + double Y = await dataTypes.ReadNextDoubleAsync(packetData); + double Z = await dataTypes.ReadNextDoubleAsync(packetData); + byte EntityYaw = await dataTypes.ReadNextByteAsync(packetData); + byte EntityPitch = await dataTypes.ReadNextByteAsync(packetData); + bool OnGround = await dataTypes.ReadNextBoolAsync(packetData); + await handler.OnEntityTeleport(EntityID, X, Y, Z, OnGround); } break; case PacketTypesIn.UpdateHealth: - float health = dataTypes.ReadNextFloat(packetData); + float health = await dataTypes.ReadNextFloatAsync(packetData); int food; if (protocolVersion >= MC_1_8_Version) - food = dataTypes.ReadNextVarInt(packetData); + food = await dataTypes.ReadNextVarIntAsync(packetData); else - food = dataTypes.ReadNextShort(packetData); - dataTypes.ReadNextFloat(packetData); // Food Saturation - handler.OnUpdateHealth(health, food); + food = await dataTypes.ReadNextShortAsync(packetData); + await dataTypes.SkipNextFloatAsync(packetData); // Food Saturation + await handler.OnUpdateHealth(health, food); break; case PacketTypesIn.SetExperience: - float experiencebar = dataTypes.ReadNextFloat(packetData); - int level = dataTypes.ReadNextVarInt(packetData); - int totalexperience = dataTypes.ReadNextVarInt(packetData); - handler.OnSetExperience(experiencebar, level, totalexperience); + float experiencebar = await dataTypes.ReadNextFloatAsync(packetData); + int level = await dataTypes.ReadNextVarIntAsync(packetData); + int totalexperience = await dataTypes.ReadNextVarIntAsync(packetData); + await handler.OnSetExperience(experiencebar, level, totalexperience); break; case PacketTypesIn.Explosion: - Location explosionLocation = new(dataTypes.ReadNextFloat(packetData), dataTypes.ReadNextFloat(packetData), dataTypes.ReadNextFloat(packetData)); + Location explosionLocation = new(await dataTypes.ReadNextFloatAsync(packetData), await dataTypes.ReadNextFloatAsync(packetData), await dataTypes.ReadNextFloatAsync(packetData)); - float explosionStrength = dataTypes.ReadNextFloat(packetData); + float explosionStrength = await dataTypes.ReadNextFloatAsync(packetData); int explosionBlockCount = protocolVersion >= MC_1_17_Version - ? dataTypes.ReadNextVarInt(packetData) - : dataTypes.ReadNextInt(packetData); + ? await dataTypes.ReadNextVarIntAsync(packetData) + : await dataTypes.ReadNextIntAsync(packetData); for (int i = 0; i < explosionBlockCount; i++) - dataTypes.ReadData(3, packetData); + await dataTypes.DropDataAsync(3, packetData); - float playerVelocityX = dataTypes.ReadNextFloat(packetData); - float playerVelocityY = dataTypes.ReadNextFloat(packetData); - float playerVelocityZ = dataTypes.ReadNextFloat(packetData); + float playerVelocityX = await dataTypes.ReadNextFloatAsync(packetData); + float playerVelocityY = await dataTypes.ReadNextFloatAsync(packetData); + float playerVelocityZ = await dataTypes.ReadNextFloatAsync(packetData); - handler.OnExplosion(explosionLocation, explosionStrength, explosionBlockCount); + await handler.OnExplosion(explosionLocation, explosionStrength, explosionBlockCount); break; case PacketTypesIn.HeldItemChange: - byte slot = dataTypes.ReadNextByte(packetData); - handler.OnHeldItemChange(slot); + byte slot = await dataTypes.ReadNextByteAsync(packetData); + await handler.OnHeldItemChange(slot); break; case PacketTypesIn.ScoreboardObjective: - string objectivename = dataTypes.ReadNextString(packetData); - byte mode = dataTypes.ReadNextByte(packetData); - string objectivevalue = String.Empty; + string objectivename = await dataTypes.ReadNextStringAsync(packetData); + byte mode = await dataTypes.ReadNextByteAsync(packetData); + string objectivevalue = string.Empty; int type2 = -1; if (mode == 0 || mode == 2) { - objectivevalue = dataTypes.ReadNextString(packetData); - type2 = dataTypes.ReadNextVarInt(packetData); + objectivevalue = await dataTypes.ReadNextStringAsync(packetData); + type2 = await dataTypes.ReadNextVarIntAsync(packetData); } handler.OnScoreboardObjective(objectivename, mode, objectivevalue, type2); break; case PacketTypesIn.UpdateScore: - string entityname = dataTypes.ReadNextString(packetData); + string entityname = await dataTypes.ReadNextStringAsync(packetData); int action3 = protocolVersion >= MC_1_18_2_Version - ? dataTypes.ReadNextVarInt(packetData) - : dataTypes.ReadNextByte(packetData); + ? await dataTypes.ReadNextVarIntAsync(packetData) + : await dataTypes.ReadNextByteAsync(packetData); string objectivename2 = string.Empty; int value = -1; if (action3 != 1 || protocolVersion >= MC_1_8_Version) - objectivename2 = dataTypes.ReadNextString(packetData); + objectivename2 = await dataTypes.ReadNextStringAsync(packetData); if (action3 != 1) - value = dataTypes.ReadNextVarInt(packetData); + value = await dataTypes.ReadNextVarIntAsync(packetData); handler.OnUpdateScore(entityname, action3, objectivename2, value); break; case PacketTypesIn.BlockChangedAck: - handler.OnBlockChangeAck(dataTypes.ReadNextVarInt(packetData)); + handler.OnBlockChangeAck(await dataTypes.ReadNextVarIntAsync(packetData)); break; case PacketTypesIn.BlockBreakAnimation: if (handler.GetEntityHandlingEnabled() && handler.GetTerrainEnabled()) { - int playerId = dataTypes.ReadNextVarInt(packetData); - Location blockLocation = dataTypes.ReadNextLocation(packetData); - byte stage = dataTypes.ReadNextByte(packetData); + int playerId = await dataTypes.ReadNextVarIntAsync(packetData); + Location blockLocation = await dataTypes.ReadNextLocationAsync(packetData); + byte stage = await dataTypes.ReadNextByteAsync(packetData); handler.OnBlockBreakAnimation(playerId, blockLocation, stage); } break; case PacketTypesIn.EntityAnimation: if (handler.GetEntityHandlingEnabled()) { - int playerId2 = dataTypes.ReadNextVarInt(packetData); - byte animation = dataTypes.ReadNextByte(packetData); + int playerId2 = await dataTypes.ReadNextVarIntAsync(packetData); + byte animation = await dataTypes.ReadNextByteAsync(packetData); handler.OnEntityAnimation(playerId2, animation); } break; @@ -1789,7 +1804,7 @@ internal bool HandlePacket(int packetID, Queue packetData) { if (innerException is ThreadAbortException || innerException is SocketException || innerException.InnerException is SocketException) throw; //Thread abort or Connection lost rather than invalid data - throw new System.IO.InvalidDataException( + throw new InvalidDataException( string.Format(Translations.exception_packet_process, packetPalette.GetIncommingTypeById(packetID), packetID, @@ -1801,52 +1816,27 @@ internal bool HandlePacket(int packetID, Queue packetData) } /// - /// Start the updating thread. Should be called after login success. + /// /// - private void StartUpdating() + /// + public ulong GetCurrentTick() { - Thread threadUpdater = new(new ParameterizedThreadStart(Updater)) - { - Name = "ProtocolPacketHandler" - }; - netMain = new Tuple(threadUpdater, new CancellationTokenSource()); - threadUpdater.Start(netMain.Item2.Token); - - Thread threadReader = new(new ParameterizedThreadStart(PacketReader)) - { - Name = "ProtocolPacketReader" - }; - netReader = new Tuple(threadReader, new CancellationTokenSource()); - threadReader.Start(netReader.Item2.Token); + return CurrentTick; } /// - /// Get net read thread (main thread) ID + /// Disconnect from the server, cancel network reading. /// - /// Net read thread ID - public int GetNetMainThreadId() - { - return netMain != null ? netMain.Item1.ManagedThreadId : -1; - } + public void Dispose() { } /// - /// Disconnect from the server, cancel network reading. + /// Send a packet to the server. Packet ID, compression, and encryption will be handled automatically. /// - public void Dispose() + /// packet type + /// packet Data + private async Task SendPacket(PacketTypesOut packet, IEnumerable packetData) { - try - { - if (netMain != null) - { - netMain.Item2.Cancel(); - } - if (netReader != null) - { - netReader.Item2.Cancel(); - socketWrapper.Disconnect(); - } - } - catch { } + await SendPacket(packetPalette.GetOutgoingIdByType(packet), packetData); } /// @@ -1854,9 +1844,9 @@ public void Dispose() /// /// packet type /// packet Data - private void SendPacket(PacketTypesOut packet, IEnumerable packetData) + private async Task SendPacket(PacketTypesOut packet, PacketStream packetData) { - SendPacket(packetPalette.GetOutgoingIdByType(packet), packetData); + await SendPacket(packetPalette.GetOutgoingIdByType(packet), await packetData.ReadFullPacket()); } /// @@ -1864,23 +1854,37 @@ private void SendPacket(PacketTypesOut packet, IEnumerable packetData) /// /// packet ID /// packet Data - private void SendPacket(int packetID, IEnumerable packetData) + private async Task SendPacket(int packetID, IEnumerable packetData) { if (handler.GetNetworkPacketCaptureEnabled()) { - List clone = packetData.ToList(); - handler.OnNetworkPacket(packetID, clone, login_phase, false); + await handler.OnNetworkPacketAsync(packetID, packetData.ToArray(), login_phase, false); } // log.Info("[C -> S] Sending packet " + packetID + " > " + dataTypes.ByteArrayToString(packetData.ToArray())); //The inner packet byte[] the_packet = dataTypes.ConcatBytes(dataTypes.GetVarInt(packetID), packetData.ToArray()); - if (compression_treshold > 0) //Compression enabled? + if (socketWrapper.CompressionThreshold > 0) //Compression enabled? { - if (the_packet.Length >= compression_treshold) //Packet long enough for compressing? + if (the_packet.Length >= socketWrapper.CompressionThreshold) //Packet long enough for compressing? { - byte[] compressed_packet = ZlibUtils.Compress(the_packet); + //var compressed = new MemoryStream(); + //var zLibStream = new ZLibStream(compressed, CompressionMode.Compress, false); + //zLibStream.Write(the_packet); + //byte[] compressed_packet = compressed.ToArray(); + //zLibStream.Dispose(); + + byte[] compressed_packet; + using (MemoryStream memstream = new()) + { + using (ZLibStream stream = new(memstream, CompressionMode.Compress)) + { + stream.Write(the_packet); + } + compressed_packet = memstream.ToArray(); + } + the_packet = dataTypes.ConcatBytes(dataTypes.GetVarInt(the_packet.Length), compressed_packet); } else @@ -1891,21 +1895,21 @@ private void SendPacket(int packetID, IEnumerable packetData) } //log.Debug("[C -> S] Sending packet " + packetID + " > " + dataTypes.ByteArrayToString(dataTypes.ConcatBytes(dataTypes.GetVarInt(the_packet.Length), the_packet))); - socketWrapper.SendDataRAW(dataTypes.ConcatBytes(dataTypes.GetVarInt(the_packet.Length), the_packet)); + await socketWrapper.SendAsync(dataTypes.ConcatBytes(dataTypes.GetVarInt(the_packet.Length), the_packet)); } /// /// Do the Minecraft login. /// /// True if login successful - public bool Login(PlayerKeyPair? playerKeyPair, SessionToken session) + public async Task Login(HttpClient httpClient, PlayerKeyPair? playerKeyPair, SessionToken session) { byte[] protocol_version = dataTypes.GetVarInt(protocolVersion); string server_address = pForge.GetServerAddress(handler.GetServerHost()); byte[] server_port = dataTypes.GetUShort((ushort)handler.GetServerPort()); byte[] next_state = dataTypes.GetVarInt(2); byte[] handshake_packet = dataTypes.ConcatBytes(protocol_version, dataTypes.GetString(server_address), server_port, next_state); - SendPacket(0x00, handshake_packet); + await SendPacket(0x00, handshake_packet); List fullLoginPacket = new(); fullLoginPacket.AddRange(dataTypes.GetString(handler.GetUsername())); // Username @@ -1935,39 +1939,42 @@ public bool Login(PlayerKeyPair? playerKeyPair, SessionToken session) fullLoginPacket.AddRange(dataTypes.GetUUID(uuid)); // UUID } } - SendPacket(0x00, fullLoginPacket); + await SendPacket(0x00, fullLoginPacket); while (true) { - (int packetID, Queue packetData) = ReadNextPacket(); + (int packetID, PacketStream packetData) = await socketWrapper.GetNextPacket(handleCompress: protocolVersion >= MC_1_8_Version); if (packetID == 0x00) //Login rejected { - handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, ChatParser.ParseText(dataTypes.ReadNextString(packetData))); + handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetData))); return false; } else if (packetID == 0x01) //Encryption request { isOnlineMode = true; - string serverID = dataTypes.ReadNextString(packetData); - byte[] serverPublicKey = dataTypes.ReadNextByteArray(packetData); - byte[] token = dataTypes.ReadNextByteArray(packetData); - return StartEncryption(handler.GetUserUuidStr(), handler.GetSessionID(), token, serverID, serverPublicKey, playerKeyPair, session); + string serverID = await dataTypes.ReadNextStringAsync(packetData); + byte[] serverPublicKey = await dataTypes.ReadNextByteArrayAsync(packetData); + byte[] token = await dataTypes.ReadNextByteArrayAsync(packetData); + return await StartEncryption(httpClient, handler.GetUserUuidStr(), handler.GetSessionID(), token, serverID, serverPublicKey, playerKeyPair, session); } else if (packetID == 0x02) //Login successful { log.Info("§8" + Translations.mcc_server_offline); login_phase = false; - if (!pForge.CompleteForgeHandshake()) + if (!await pForge.CompleteForgeHandshake(socketWrapper)) { log.Error("§8" + Translations.error_forge); return false; } - StartUpdating(); + // StartUpdating(); return true; //No need to check session or start encryption } - else HandlePacket(packetID, packetData); + else + { + await HandlePacket(packetID, packetData); + } } } @@ -1975,7 +1982,7 @@ public bool Login(PlayerKeyPair? playerKeyPair, SessionToken session) /// Start network encryption. Automatically called by Login() if the server requests encryption. /// /// True if encryption was successful - private bool StartEncryption(string uuid, string sessionID, byte[] token, string serverIDhash, byte[] serverPublicKey, PlayerKeyPair? playerKeyPair, SessionToken session) + private async Task StartEncryption(HttpClient httpClient, string uuid, string sessionID, byte[] token, string serverIDhash, byte[] serverPublicKey, PlayerKeyPair? playerKeyPair, SessionToken session) { RSACryptoServiceProvider RSAService = CryptoHandler.DecodeRSAPublicKey(serverPublicKey)!; byte[] secretKey = CryptoHandler.ClientAESPrivateKey ?? CryptoHandler.GenerateAESPrivateKey(); @@ -1986,28 +1993,27 @@ private bool StartEncryption(string uuid, string sessionID, byte[] token, string { log.Info(Translations.mcc_session); - bool needCheckSession = true; - if (session.ServerPublicKey != null && session.SessionPreCheckTask != null - && serverIDhash == session.ServerIDhash && Enumerable.SequenceEqual(serverPublicKey, session.ServerPublicKey)) - { - session.SessionPreCheckTask.Wait(); - if (session.SessionPreCheckTask.Result) // PreCheck Successed - needCheckSession = false; - } - - if (needCheckSession) + string serverHash = CryptoHandler.GetServerHash(serverIDhash, serverPublicKey, secretKey); + if (session.SessionPreCheckTask != null && session.ServerInfoHash != null && serverHash == session.ServerInfoHash) { - string serverHash = CryptoHandler.GetServerHash(serverIDhash, serverPublicKey, secretKey); - - if (ProtocolHandler.SessionCheck(uuid, sessionID, serverHash)) + (bool preCheckResult, string? error) = await session.SessionPreCheckTask; + if (!preCheckResult) { - session.ServerIDhash = serverIDhash; - session.ServerPublicKey = serverPublicKey; - SessionCache.Store(InternalConfig.Account.Login.ToLower(), session); + handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, + string.IsNullOrEmpty(error) ? Translations.mcc_session_fail : $"{Translations.mcc_session_fail} Error: {error}."); + return false; } + session.SessionPreCheckTask = null; + } + else + { + (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; } } @@ -2016,7 +2022,7 @@ private bool StartEncryption(string uuid, string sessionID, byte[] token, string // Encryption Response packet List encryptionResponse = new(); encryptionResponse.AddRange(dataTypes.GetArray(RSAService.Encrypt(secretKey, false))); // Shared Secret - if (protocolVersion >= Protocol18Handler.MC_1_19_Version) + if (protocolVersion >= MC_1_19_Version) { if (playerKeyPair == null) { @@ -2037,16 +2043,16 @@ private bool StartEncryption(string uuid, string sessionID, byte[] token, string { encryptionResponse.AddRange(dataTypes.GetArray(RSAService.Encrypt(token, false))); // Verify Token } - SendPacket(0x01, encryptionResponse); + await SendPacket(0x01, encryptionResponse); - //Start client-side encryption + // Start client-side encryption socketWrapper.SwitchToEncrypted(secretKey); // pre switch - //Process the next packet - int loopPrevention = UInt16.MaxValue; + // Process the next packet + int loopPrevention = ushort.MaxValue; while (true) { - (int packetID, Queue packetData) = ReadNextPacket(); + (int packetID, PacketStream packetData) = await socketWrapper.GetNextPacket(handleCompress: protocolVersion >= MC_1_8_Version); if (packetID < 0 || loopPrevention-- < 0) // Failed to read packet or too many iterations (issue #1150) { handler.OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, "§8" + Translations.error_invalid_encrypt); @@ -2054,28 +2060,28 @@ private bool StartEncryption(string uuid, string sessionID, byte[] token, string } else if (packetID == 0x00) //Login rejected { - handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, ChatParser.ParseText(dataTypes.ReadNextString(packetData))); + handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetData))); return false; } else if (packetID == 0x02) //Login successful { Guid uuidReceived; - if (protocolVersion >= Protocol18Handler.MC_1_16_Version) - uuidReceived = dataTypes.ReadNextUUID(packetData); + if (protocolVersion >= MC_1_16_Version) + uuidReceived = await dataTypes.ReadNextUUIDAsync(packetData); else - uuidReceived = Guid.Parse(dataTypes.ReadNextString(packetData)); - string userName = dataTypes.ReadNextString(packetData); + uuidReceived = Guid.Parse(await dataTypes.ReadNextStringAsync(packetData)); + string userName = await dataTypes.ReadNextStringAsync(packetData); Tuple[]? playerProperty = null; - if (protocolVersion >= Protocol18Handler.MC_1_19_Version) + if (protocolVersion >= MC_1_19_Version) { - int count = dataTypes.ReadNextVarInt(packetData); // Number Of Properties + int count = await dataTypes.ReadNextVarIntAsync(packetData); // Number Of Properties playerProperty = new Tuple[count]; for (int i = 0; i < count; ++i) { - string name = dataTypes.ReadNextString(packetData); - string value = dataTypes.ReadNextString(packetData); - bool isSigned = dataTypes.ReadNextBool(packetData); - string signature = isSigned ? dataTypes.ReadNextString(packetData) : String.Empty; + string name = await dataTypes.ReadNextStringAsync(packetData); + string value = await dataTypes.ReadNextStringAsync(packetData); + bool isSigned = await dataTypes.ReadNextBoolAsync(packetData); + string signature = isSigned ? await dataTypes.ReadNextStringAsync(packetData) : string.Empty; playerProperty[i] = new Tuple(name, value, signature); } } @@ -2083,16 +2089,19 @@ private bool StartEncryption(string uuid, string sessionID, byte[] token, string login_phase = false; - if (!pForge.CompleteForgeHandshake()) + if (!await pForge.CompleteForgeHandshake(socketWrapper)) { log.Error("§8" + Translations.error_forge_encrypt); return false; } - StartUpdating(); + // StartUpdating(); return true; } - else HandlePacket(packetID, packetData); + else + { + await HandlePacket(packetID, packetData); + } } } @@ -2109,7 +2118,7 @@ public void Disconnect() /// /// Text behind cursor /// Completed text - int IAutoComplete.AutoComplete(string BehindCursor) + async Task IAutoComplete.AutoComplete(string BehindCursor) { if (string.IsNullOrEmpty(BehindCursor)) return -1; @@ -2142,21 +2151,67 @@ int IAutoComplete.AutoComplete(string BehindCursor) tabcomplete_packet = dataTypes.ConcatBytes(dataTypes.GetString(BehindCursor)); } ConsoleIO.AutoCompleteDone = false; - SendPacket(PacketTypesOut.TabComplete, tabcomplete_packet); + await SendPacket(PacketTypesOut.TabComplete, tabcomplete_packet); return autocomplete_transaction_id; } + internal record PingResult + { +#pragma warning disable IDE1006 // Naming Styles + public bool previewsChat { init; get; } + public bool enforcesSecureChat { init; get; } + public Dictionary? description { init; get; } + public PlayerInfo? players { init; get; } + public VersionInfo? version { init; get; } + public string? favicon { init; get; } + public FrogeInfoFML1? modinfo { init; get; } + public FrogeInfoFML2? forgeData { init; get; } + + public record PlayerInfo + { + public int max { init; get; } + public int online { init; get; } + } + + public record VersionInfo + { + public string? name { init; get; } + public int protocol { init; get; } + } + + public record FrogeInfoFML1 + { + public string? type { init; get; } + public ForgeInfo.ForgeMod[]? modList { init; get; } + } + + public record FrogeInfoFML2 + { + public FrogeChannelInfo[]? channels { init; get; } + public ForgeInfo.ForgeMod[]? mods { init; get; } + public string? fmlNetworkVersion { init; get; } + + public record FrogeChannelInfo + { + public string? res { init; get; } + public string? version { init; get; } + public bool required { init; get; } + } + } +#pragma warning restore IDE1006 // Naming Styles + } + /// /// Ping a Minecraft server to get information about the server /// /// True if ping was successful - public static bool DoPing(string host, int port, ref int protocolVersion, ref ForgeInfo? forgeInfo) + public static async Task> DoPing(string host, int port, CancellationToken cancelToken) { - string version = ""; - TcpClient tcp = ProxyHandler.NewTcpClient(host, port); - tcp.ReceiveTimeout = 30000; // 30 seconds - tcp.ReceiveBufferSize = 1024 * 1024; - SocketWrapper socketWrapper = new(tcp); + TcpClient tcpClient = ProxyHandler.NewTcpClient(host, port, ProxyHandler.ClientType.Ingame); + tcpClient.ReceiveBufferSize = 1024 * 1024; + tcpClient.ReceiveTimeout = Config.Main.Advanced.TcpTimeout * 1000; + + SocketWrapper socketWrapper = new(tcpClient); DataTypes dataTypes = new(MC_1_8_Version); byte[] packet_id = dataTypes.GetVarInt(0); @@ -2166,55 +2221,55 @@ public static bool DoPing(string host, int port, ref int protocolVersion, ref Fo byte[] packet = dataTypes.ConcatBytes(packet_id, protocol_version, dataTypes.GetString(host), server_port, next_state); byte[] tosend = dataTypes.ConcatBytes(dataTypes.GetVarInt(packet.Length), packet); - socketWrapper.SendDataRAW(tosend); + await socketWrapper.SendAsync(tosend, cancelToken); byte[] status_request = dataTypes.GetVarInt(0); byte[] request_packet = dataTypes.ConcatBytes(dataTypes.GetVarInt(status_request.Length), status_request); - socketWrapper.SendDataRAW(request_packet); + await socketWrapper.SendAsync(request_packet, cancelToken); - int packetLength = dataTypes.ReadNextVarIntRAW(socketWrapper); - if (packetLength > 0) //Read Response length + (int packetID, PacketStream packetData) = await socketWrapper.GetNextPacket(false, cancelToken); + if (packetID == 0x00) { - Queue packetData = new(socketWrapper.ReadDataRAW(packetLength)); - if (dataTypes.ReadNextVarInt(packetData) == 0x00) //Read Packet ID - { - string result = dataTypes.ReadNextString(packetData); //Get the Json data + string result = await dataTypes.ReadNextStringAsync(packetData); //Get the Json data - if (Config.Logging.DebugMessages) - { - // May contain formatting codes, cannot use WriteLineFormatted - Console.ForegroundColor = ConsoleColor.DarkGray; - ConsoleIO.WriteLine(result); - Console.ForegroundColor = ConsoleColor.Gray; - } + if (Config.Logging.DebugMessages) + { + // May contain formatting codes, cannot use WriteLineFormatted + Console.ForegroundColor = ConsoleColor.DarkGray; + ConsoleIO.WriteLine(result); + Console.ForegroundColor = ConsoleColor.Gray; + } - if (!String.IsNullOrEmpty(result) && result.StartsWith("{") && result.EndsWith("}")) + if (!string.IsNullOrEmpty(result)) + { + try { - Json.JSONData jsonData = Json.ParseJson(result); - if (jsonData.Type == Json.JSONData.DataType.Object && jsonData.Properties.ContainsKey("version")) + PingResult? jsonData = JsonSerializer.Deserialize(result); + if (jsonData != null && jsonData.version != null) { - Json.JSONData versionData = jsonData.Properties["version"]; - - //Retrieve display name of the Minecraft version - if (versionData.Properties.ContainsKey("name")) - version = versionData.Properties["name"].StringValue; + // Retrieve display name of the Minecraft version + string version = jsonData.version.name ?? string.Empty; - //Retrieve protocol version number for handling this server - if (versionData.Properties.ContainsKey("protocol")) - protocolVersion = int.Parse(versionData.Properties["protocol"].StringValue, NumberStyles.Any, CultureInfo.CurrentCulture); + // Retrieve protocol version number for handling this server + int protocolVersion = jsonData.version.protocol; // Check for forge on the server. + ForgeInfo? forgeInfo = null; Protocol18Forge.ServerInfoCheckForge(jsonData, ref forgeInfo); - ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_server_protocol, version, protocolVersion + (forgeInfo != null ? Translations.mcc_with_forge : ""))); + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_server_protocol, + version, protocolVersion + (forgeInfo == null ? string.Empty : Translations.mcc_with_forge))); - return true; + return new(true, protocolVersion, forgeInfo); } } + catch (JsonException) { } + catch (ArgumentNullException) { } } } - return false; + + return new(false, 0, null); } /// @@ -2245,18 +2300,18 @@ public int GetProtocolVersion() /// /// Message acknowledgment /// True if properly sent - public bool SendMessageAcknowledgment(LastSeenMessageList.Acknowledgment acknowledgment) + public async Task SendMessageAckAsync(LastSeenMessageList.Acknowledgment acknowledgment) { try { byte[] fields = dataTypes.GetAcknowledgment(acknowledgment, isOnlineMode && Config.Signature.LoginWithSecureProfile); - SendPacket(PacketTypesOut.MessageAcknowledgment, fields); + await SendPacket(PacketTypesOut.MessageAcknowledgment, fields); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } @@ -2266,7 +2321,7 @@ public LastSeenMessageList.Acknowledgment ConsumeAcknowledgment() return new LastSeenMessageList.Acknowledgment(lastSeenMessagesCollector.GetLastSeenMessages(), lastReceivedMessage); } - public void Acknowledge(ChatMessage message) + public async Task Acknowledge(ChatMessage message) { LastSeenMessageList.Entry? entry = message.ToLastSeenMessageEntry(); @@ -2276,7 +2331,7 @@ public void Acknowledge(ChatMessage message) lastReceivedMessage = null; if (pendingAcknowledgments++ > 64) - SendMessageAcknowledgment(ConsumeAcknowledgment()); + await SendMessageAckAsync(ConsumeAcknowledgment()); } } @@ -2286,20 +2341,20 @@ public void Acknowledge(ChatMessage message) /// Command /// PlayerKeyPair /// True if properly sent - public bool SendChatCommand(string command, PlayerKeyPair? playerKeyPair) + public async Task SendChatCommand(string command, PlayerKeyPair? playerKeyPair) { - if (String.IsNullOrEmpty(command)) + if (string.IsNullOrEmpty(command)) return true; - command = Regex.Replace(command, @"\s+", " "); - command = Regex.Replace(command, @"\s$", string.Empty); + command = GetCharFilterRegex1().Replace(command, " "); + command = GetCharFilterRegex2().Replace(command, string.Empty); log.Debug("chat command = " + command); try { LastSeenMessageList.Acknowledgment? acknowledgment = - (protocolVersion >= MC_1_19_2_Version) ? ConsumeAcknowledgment() : null; + protocolVersion >= MC_1_19_2_Version ? ConsumeAcknowledgment() : null; List fields = new(); @@ -2329,7 +2384,7 @@ public bool SendChatCommand(string command, PlayerKeyPair? playerKeyPair) foreach ((string argName, string message) in needSigned) { fields.AddRange(dataTypes.GetString(argName)); // Argument name: String - byte[] sign = (protocolVersion >= MC_1_19_2_Version) ? + byte[] sign = protocolVersion >= MC_1_19_2_Version ? playerKeyPair!.PrivateKey.SignMessage(message, uuid, timeNow, ref salt, acknowledgment!.lastSeen) : playerKeyPair!.PrivateKey.SignMessage(message, uuid, timeNow, ref salt); fields.AddRange(dataTypes.GetVarInt(sign.Length)); // Signature length: VarInt @@ -2346,11 +2401,11 @@ public bool SendChatCommand(string command, PlayerKeyPair? playerKeyPair) fields.AddRange(dataTypes.GetAcknowledgment(acknowledgment!, isOnlineMode && Config.Signature.LoginWithSecureProfile)); } - SendPacket(PacketTypesOut.ChatCommand, fields); + await SendPacket(PacketTypesOut.ChatCommand, fields); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } @@ -2360,14 +2415,14 @@ public bool SendChatCommand(string command, PlayerKeyPair? playerKeyPair) /// Message /// PlayerKeyPair /// True if properly sent - public bool SendChatMessage(string message, PlayerKeyPair? playerKeyPair) + public async Task SendChatMessage(string message, PlayerKeyPair? playerKeyPair) { - if (String.IsNullOrEmpty(message)) + if (string.IsNullOrEmpty(message)) return true; // Process Chat Command - 1.19 and above if (protocolVersion >= MC_1_19_Version && message.StartsWith('/')) - return SendChatCommand(message[1..], playerKeyPair); + return await SendChatCommand(message[1..], playerKeyPair); try { @@ -2379,7 +2434,7 @@ public bool SendChatMessage(string message, PlayerKeyPair? playerKeyPair) if (protocolVersion >= MC_1_19_Version) { LastSeenMessageList.Acknowledgment? acknowledgment = - (protocolVersion >= MC_1_19_2_Version) ? ConsumeAcknowledgment() : null; + protocolVersion >= MC_1_19_2_Version ? ConsumeAcknowledgment() : null; // Timestamp: Instant(Long) DateTimeOffset timeNow = DateTimeOffset.UtcNow; @@ -2398,7 +2453,7 @@ public bool SendChatMessage(string message, PlayerKeyPair? playerKeyPair) // Signature Length & Signature: (VarInt) and Byte Array Guid uuid = handler.GetUserUuid(); - byte[] sign = (protocolVersion >= MC_1_19_2_Version) ? + byte[] sign = protocolVersion >= MC_1_19_2_Version ? playerKeyPair.PrivateKey.SignMessage(message, uuid, timeNow, ref salt, acknowledgment!.lastSeen) : playerKeyPair.PrivateKey.SignMessage(message, uuid, timeNow, ref salt); fields.AddRange(dataTypes.GetVarInt(sign.Length)); @@ -2414,15 +2469,15 @@ public bool SendChatMessage(string message, PlayerKeyPair? playerKeyPair) fields.AddRange(dataTypes.GetAcknowledgment(acknowledgment!, isOnlineMode && Config.Signature.LoginWithSecureProfile)); } } - SendPacket(PacketTypesOut.ChatMessage, fields); + await SendPacket(PacketTypesOut.ChatMessage, fields); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } - public bool SendEntityAction(int PlayerEntityID, int ActionID) + public async Task SendEntityAction(int PlayerEntityID, int ActionID) { try { @@ -2430,11 +2485,11 @@ public bool SendEntityAction(int PlayerEntityID, int ActionID) fields.AddRange(dataTypes.GetVarInt(PlayerEntityID)); fields.AddRange(dataTypes.GetVarInt(ActionID)); fields.AddRange(dataTypes.GetVarInt(0)); - SendPacket(PacketTypesOut.EntityAction, fields); + await SendPacket(PacketTypesOut.EntityAction, fields); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } @@ -2442,15 +2497,15 @@ public bool SendEntityAction(int PlayerEntityID, int ActionID) /// Send a respawn packet to the server /// /// True if properly sent - public bool SendRespawnPacket() + public async Task SendRespawnPacket() { try { - SendPacket(PacketTypesOut.ClientStatus, new byte[] { 0 }); + await SendPacket(PacketTypesOut.ClientStatus, new byte[] { 0 }); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } @@ -2459,19 +2514,19 @@ public bool SendRespawnPacket() /// /// Client string describing the client /// True if brand info was successfully sent - public bool SendBrandInfo(string brandInfo) + public async Task SendBrandInfo(string brandInfo) { - if (String.IsNullOrEmpty(brandInfo)) + if (string.IsNullOrEmpty(brandInfo)) return false; // Plugin channels were significantly changed between Minecraft 1.12 and 1.13 // https://wiki.vg/index.php?title=Pre-release_protocol&oldid=14132#Plugin_Channels if (protocolVersion >= MC_1_13_Version) { - return SendPluginChannelPacket("minecraft:brand", dataTypes.GetString(brandInfo)); + return await SendPluginChannelPacket("minecraft:brand", dataTypes.GetString(brandInfo)); } else { - return SendPluginChannelPacket("MC|Brand", dataTypes.GetString(brandInfo)); + return await SendPluginChannelPacket("MC|Brand", dataTypes.GetString(brandInfo)); } } @@ -2486,7 +2541,7 @@ public bool SendBrandInfo(string brandInfo) /// Show skin layers /// 1.9+ main hand /// True if client settings were successfully sent - public bool SendClientSettings(string language, byte viewDistance, byte difficulty, byte chatMode, bool chatColors, byte skinParts, byte mainHand) + public async Task SendClientSettings(string language, byte viewDistance, byte difficulty, byte chatMode, bool chatColors, byte skinParts, byte mainHand) { try { @@ -2517,10 +2572,10 @@ public bool SendClientSettings(string language, byte viewDistance, byte difficul } if (protocolVersion >= MC_1_18_1_Version) fields.Add(1); // 1.18 and above - Allow server listings - SendPacket(PacketTypesOut.ClientSettings, fields); + await SendPacket(PacketTypesOut.ClientSettings, fields); } catch (SocketException) { } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } return false; } @@ -2534,12 +2589,12 @@ public bool SendClientSettings(string language, byte viewDistance, byte difficul /// Optional new yaw for updating player look /// Optional new pitch for updating player look /// True if the location update was successfully sent - public bool SendLocationUpdate(Location location, bool onGround, float? yaw, float? pitch) + public async Task SendLocationUpdate(Location location, bool onGround, float? yaw, float? pitch) { - return SendLocationUpdate(location, onGround, yaw, pitch, true); + return await SendLocationUpdate(location, onGround, yaw, pitch, true); } - public bool SendLocationUpdate(Location location, bool onGround, float? yaw = null, float? pitch = null, bool forceUpdate = false) + public async Task SendLocationUpdate(Location location, bool onGround, float? yaw = null, float? pitch = null, bool forceUpdate = false) { if (handler.GetTerrainEnabled()) { @@ -2569,7 +2624,7 @@ public bool SendLocationUpdate(Location location, bool onGround, float? yaw = nu try { - SendPacket(packetType, dataTypes.ConcatBytes( + await SendPacket(packetType, dataTypes.ConcatBytes( dataTypes.GetDouble(location.X), dataTypes.GetDouble(location.Y), protocolVersion < MC_1_8_Version @@ -2581,7 +2636,7 @@ public bool SendLocationUpdate(Location location, bool onGround, float? yaw = nu return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } else return false; @@ -2592,7 +2647,7 @@ public bool SendLocationUpdate(Location location, bool onGround, float? yaw = nu /// /// Channel to send packet on /// packet Data - public bool SendPluginChannelPacket(string channel, byte[] data) + public async Task SendPluginChannelPacket(string channel, byte[] data) { try { @@ -2603,17 +2658,17 @@ public bool SendPluginChannelPacket(string channel, byte[] data) byte[] length = BitConverter.GetBytes((short)data.Length); Array.Reverse(length); - SendPacket(PacketTypesOut.PluginMessage, dataTypes.ConcatBytes(dataTypes.GetString(channel), length, data)); + await SendPacket(PacketTypesOut.PluginMessage, dataTypes.ConcatBytes(dataTypes.GetString(channel), length, data)); } else { - SendPacket(PacketTypesOut.PluginMessage, dataTypes.ConcatBytes(dataTypes.GetString(channel), data)); + await SendPacket(PacketTypesOut.PluginMessage, dataTypes.ConcatBytes(dataTypes.GetString(channel), data)); } return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } @@ -2624,15 +2679,15 @@ public bool SendPluginChannelPacket(string channel, byte[] data) /// TRUE if the request was understood /// Response to the request /// TRUE if successfully sent - public bool SendLoginPluginResponse(int messageId, bool understood, byte[] data) + public async Task SendLoginPluginResponse(int messageId, bool understood, byte[] data) { try { - SendPacket(0x02, dataTypes.ConcatBytes(dataTypes.GetVarInt(messageId), dataTypes.GetBool(understood), data)); + await SendPacket(0x02, dataTypes.ConcatBytes(dataTypes.GetVarInt(messageId), dataTypes.GetBool(understood), data)); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } @@ -2642,7 +2697,7 @@ public bool SendLoginPluginResponse(int messageId, bool understood, byte[] data) /// /// /// - public bool SendInteractEntity(int EntityID, int type) + public async Task SendInteractEntity(int EntityID, int type) { try { @@ -2656,16 +2711,16 @@ public bool SendInteractEntity(int EntityID, int type) if (protocolVersion >= MC_1_16_Version) fields.AddRange(dataTypes.GetBool(false)); - SendPacket(PacketTypesOut.InteractEntity, fields); + await SendPacket(PacketTypesOut.InteractEntity, fields); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } // TODO: Interact at block location (e.g. chest minecart) - public bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z, int hand) + public async Task SendInteractEntity(int EntityID, int type, float X, float Y, float Z, int hand) { try { @@ -2681,14 +2736,14 @@ public bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z // TODO: Update to reflect the real player state if (protocolVersion >= MC_1_16_Version) fields.AddRange(dataTypes.GetBool(false)); - SendPacket(PacketTypesOut.InteractEntity, fields); + await SendPacket(PacketTypesOut.InteractEntity, fields); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } - public bool SendInteractEntity(int EntityID, int type, int hand) + public async Task SendInteractEntity(int EntityID, int type, int hand) { try { @@ -2701,19 +2756,20 @@ public bool SendInteractEntity(int EntityID, int type, int hand) // TODO: Update to reflect the real player state if (protocolVersion >= MC_1_16_Version) fields.AddRange(dataTypes.GetBool(false)); - SendPacket(PacketTypesOut.InteractEntity, fields); + await SendPacket(PacketTypesOut.InteractEntity, fields); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } - public bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z) + + public async Task SendInteractEntity(int EntityID, int type, float X, float Y, float Z) { - return false; + return await Task.FromResult(false); } - public bool SendUseItem(int hand, int sequenceId) + public async Task SendUseItem(int hand, int sequenceId) { if (protocolVersion < MC_1_9_Version) return false; // Packet does not exist prior to MC 1.9 @@ -2726,15 +2782,15 @@ public bool SendUseItem(int hand, int sequenceId) packet.AddRange(dataTypes.GetVarInt(hand)); if (protocolVersion >= MC_1_19_Version) packet.AddRange(dataTypes.GetVarInt(sequenceId)); - SendPacket(PacketTypesOut.UseItem, packet); + await SendPacket(PacketTypesOut.UseItem, packet); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } - public bool SendPlayerDigging(int status, Location location, Direction face, int sequenceId) + public async Task SendPlayerDigging(int status, Location location, Direction face, int sequenceId) { try { @@ -2744,15 +2800,15 @@ public bool SendPlayerDigging(int status, Location location, Direction face, int packet.AddRange(dataTypes.GetVarInt(dataTypes.GetBlockFace(face))); if (protocolVersion >= MC_1_19_Version) packet.AddRange(dataTypes.GetVarInt(sequenceId)); - SendPacket(PacketTypesOut.PlayerDigging, packet); + await SendPacket(PacketTypesOut.PlayerDigging, packet); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } - public bool SendPlayerBlockPlacement(int hand, Location location, Direction face, int sequenceId) + public async Task SendPlayerBlockPlacement(int hand, Location location, Direction face, int sequenceId) { if (protocolVersion < MC_1_14_Version) return false; // NOT IMPLEMENTED for older MC versions @@ -2768,29 +2824,29 @@ public bool SendPlayerBlockPlacement(int hand, Location location, Direction face packet.Add(0); // insideBlock = false; if (protocolVersion >= MC_1_19_Version) packet.AddRange(dataTypes.GetVarInt(sequenceId)); - SendPacket(PacketTypesOut.PlayerBlockPlacement, packet); + await SendPacket(PacketTypesOut.PlayerBlockPlacement, packet); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } - public bool SendHeldItemChange(short slot) + public async Task SendHeldItemChange(short slot) { try { List packet = new(); packet.AddRange(dataTypes.GetShort(slot)); - SendPacket(PacketTypesOut.HeldItemChange, packet); + await SendPacket(PacketTypesOut.HeldItemChange, packet); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } - public bool SendWindowAction(int windowId, int slotId, WindowActionType action, Item? item, List> changedSlots, int stateId) + public async Task SendWindowAction(int windowId, int slotId, WindowActionType action, Item? item, List> changedSlots, int stateId) { try { @@ -2870,45 +2926,47 @@ public bool SendWindowAction(int windowId, int slotId, WindowActionType action, packet.AddRange(dataTypes.GetItemSlot(item, itemPalette)); // Carried item (Clicked item) - SendPacket(PacketTypesOut.ClickWindow, packet); + await SendPacket(PacketTypesOut.ClickWindow, packet); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } - public bool SendCreativeInventoryAction(int slot, ItemType itemType, int count, Dictionary? nbt) + public async Task SendCreativeInventoryAction(int slot, ItemType itemType, int count, Dictionary? nbt) { try { List packet = new(); packet.AddRange(dataTypes.GetShort((short)slot)); packet.AddRange(dataTypes.GetItemSlot(new Item(itemType, count, nbt), itemPalette)); - SendPacket(PacketTypesOut.CreativeInventoryAction, packet); + await SendPacket(PacketTypesOut.CreativeInventoryAction, packet); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } - public bool ClickContainerButton(int windowId, int buttonId) + public async Task ClickContainerButton(int windowId, int buttonId) { try { - List packet = new(); - packet.Add((byte)windowId); - packet.Add((byte)buttonId); - SendPacket(PacketTypesOut.ClickWindowButton, packet); + List packet = new() + { + (byte)windowId, + (byte)buttonId + }; + await SendPacket(PacketTypesOut.ClickWindowButton, packet); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } - public bool SendAnimation(int animation, int playerid) + public async Task SendAnimation(int animation, int playerid) { try { @@ -2919,7 +2977,7 @@ public bool SendAnimation(int animation, int playerid) if (protocolVersion < MC_1_8_Version) { packet.AddRange(dataTypes.GetInt(playerid)); - packet.Add((byte)1); // Swing arm + packet.Add(1); // Swing arm } else if (protocolVersion < MC_1_9_Version) { @@ -2930,7 +2988,7 @@ public bool SendAnimation(int animation, int playerid) packet.AddRange(dataTypes.GetVarInt(animation)); } - SendPacket(PacketTypesOut.Animation, packet); + await SendPacket(PacketTypesOut.Animation, packet); return true; } else @@ -2939,11 +2997,11 @@ public bool SendAnimation(int animation, int playerid) } } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } - public bool SendCloseWindow(int windowId) + public async Task SendCloseWindow(int windowId) { try { @@ -2952,15 +3010,15 @@ public bool SendCloseWindow(int windowId) if (window_actions.ContainsKey(windowId)) window_actions[windowId] = 0; } - SendPacket(PacketTypesOut.CloseWindow, new[] { (byte)windowId }); + await SendPacket(PacketTypesOut.CloseWindow, new[] { (byte)windowId }); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } - public bool SendUpdateSign(Location sign, string line1, string line2, string line3, string line4) + public async Task SendUpdateSign(Location sign, string line1, string line2, string line3, string line4) { try { @@ -2979,15 +3037,15 @@ public bool SendUpdateSign(Location sign, string line1, string line2, string lin packet.AddRange(dataTypes.GetString(line2)); packet.AddRange(dataTypes.GetString(line3)); packet.AddRange(dataTypes.GetString(line4)); - SendPacket(PacketTypesOut.UpdateSign, packet); + await SendPacket(PacketTypesOut.UpdateSign, packet); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } - public bool UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags) + public async Task UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags) { if (protocolVersion <= MC_1_13_Version) { @@ -2998,33 +3056,35 @@ public bool UpdateCommandBlock(Location location, string command, CommandBlockMo packet.AddRange(dataTypes.GetString(command)); packet.AddRange(dataTypes.GetVarInt((int)mode)); packet.Add((byte)flags); - SendPacket(PacketTypesOut.UpdateSign, packet); + await SendPacket(PacketTypesOut.UpdateSign, packet); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } else { return false; } } - public bool SendWindowConfirmation(byte windowID, short actionID, bool accepted) + public async Task SendWindowConfirmation(byte windowID, short actionID, bool accepted) { try { - List packet = new(); - packet.Add(windowID); + List packet = new() + { + windowID + }; packet.AddRange(dataTypes.GetShort(actionID)); packet.Add(accepted ? (byte)1 : (byte)0); - SendPacket(PacketTypesOut.WindowConfirmation, packet); + await SendPacket(PacketTypesOut.WindowConfirmation, packet); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } - public bool SelectTrade(int selectedSlot) + public async Task SelectTrade(int selectedSlot) { // MC 1.13 or greater if (protocolVersion >= MC_1_13_Version) @@ -3033,17 +3093,17 @@ public bool SelectTrade(int selectedSlot) { List packet = new(); packet.AddRange(dataTypes.GetVarInt(selectedSlot)); - SendPacket(PacketTypesOut.SelectTrade, packet); + await SendPacket(PacketTypesOut.SelectTrade, packet); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } else { return false; } } - public bool SendSpectate(Guid UUID) + public async Task SendSpectate(Guid UUID) { // MC 1.8 or greater if (protocolVersion >= MC_1_8_Version) @@ -3052,11 +3112,11 @@ public bool SendSpectate(Guid UUID) { List packet = new(); packet.AddRange(dataTypes.GetUUID(UUID)); - SendPacket(PacketTypesOut.Spectate, packet); + await SendPacket(PacketTypesOut.Spectate, packet); return true; } catch (SocketException) { return false; } - catch (System.IO.IOException) { return false; } + catch (IOException) { return false; } catch (ObjectDisposedException) { return false; } } else { return false; } @@ -3068,5 +3128,11 @@ private byte[] GenerateSalt() randomGen.GetNonZeroBytes(salt); return salt; } + + [GeneratedRegex("\\s+")] + private static partial Regex GetCharFilterRegex1(); + + [GeneratedRegex("\\s$")] + private static partial Regex GetCharFilterRegex2(); } } diff --git a/MinecraftClient/Protocol/Handlers/Protocol18Forge.cs b/MinecraftClient/Protocol/Handlers/Protocol18Forge.cs index e7ff7cc2b2..9b7eca6791 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18Forge.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18Forge.cs @@ -3,9 +3,12 @@ using System.Linq; using System.Text; using System.Threading; +using System.Threading.Tasks; using MinecraftClient.Protocol.Handlers.Forge; using MinecraftClient.Protocol.Message; +using MinecraftClient.Protocol.PacketPipeline; using MinecraftClient.Scripting; +using static MinecraftClient.Protocol.Handlers.Protocol18Handler; namespace MinecraftClient.Protocol.Handlers { @@ -54,23 +57,23 @@ public string GetServerAddress(string serverAddress) /// Completes the Minecraft Forge handshake (Forge Protocol version 1: FML) /// /// Whether the handshake was successful. - public bool CompleteForgeHandshake() + public async Task CompleteForgeHandshake(SocketWrapper socketWrapper) { if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML) { while (fmlHandshakeState != FMLHandshakeClientState.DONE) { - (int packetID, Queue packetData) = protocol18.ReadNextPacket(); + (int packetID, PacketStream packetStream) = await socketWrapper.GetNextPacket(handleCompress: true); if (packetID == 0x40) // Disconnect { - mcHandler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, ChatParser.ParseText(dataTypes.ReadNextString(packetData))); + mcHandler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetStream))); return false; } else { // Send back regular packet to the vanilla protocol handler - protocol18.HandlePacket(packetID, packetData); + await protocol18.HandlePacket(packetID, packetStream); } } } @@ -82,7 +85,7 @@ public bool CompleteForgeHandshake() /// /// Packet data to read from /// Length from packet data - public int ReadNextVarShort(Queue packetData) + public int ReadNextVarShort(PacketStream packetData) { if (ForgeEnabled()) { @@ -103,10 +106,11 @@ public int ReadNextVarShort(Queue packetData) /// Plugin message data /// Current world dimension /// TRUE if the plugin message was recognized and handled - public bool HandlePluginMessage(string channel, Queue packetData, ref int currentDimension) + public async Task> HandlePluginMessage(string channel, byte[] packetDataArr, int currentDimension) { if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML && fmlHandshakeState != FMLHandshakeClientState.DONE) { + Queue packetData = new(packetDataArr); if (channel == "FML|HS") { FMLHandshakeDiscriminator discriminator = (FMLHandshakeDiscriminator)dataTypes.ReadNextByte(packetData); @@ -114,21 +118,21 @@ public bool HandlePluginMessage(string channel, Queue packetData, ref int if (discriminator == FMLHandshakeDiscriminator.HandshakeReset) { fmlHandshakeState = FMLHandshakeClientState.START; - return true; + return new(true, currentDimension); } switch (fmlHandshakeState) { case FMLHandshakeClientState.START: if (discriminator != FMLHandshakeDiscriminator.ServerHello) - return false; + return new(false, currentDimension); // Send the plugin channel registration. // REGISTER is somewhat special in that it doesn't actually include length information, // and is also \0-separated. // Also, yes, "FML" is there twice. Don't ask me why, but that's the way forge does it. string[] channels = { "FML|HS", "FML", "FML|MP", "FML", "FORGE" }; - protocol18.SendPluginChannelPacket("REGISTER", Encoding.UTF8.GetBytes(string.Join("\0", channels))); + await protocol18.SendPluginChannelPacket("REGISTER", Encoding.UTF8.GetBytes(string.Join("\0", channels))); byte fmlProtocolVersion = dataTypes.ReadNextByte(packetData); @@ -139,7 +143,7 @@ public bool HandlePluginMessage(string channel, Queue packetData, ref int currentDimension = dataTypes.ReadNextInt(packetData); // Tell the server we're running the same version. - SendForgeHandshakePacket(FMLHandshakeDiscriminator.ClientHello, new byte[] { fmlProtocolVersion }); + await SendForgeHandshakePacket(FMLHandshakeDiscriminator.ClientHello, new byte[] { fmlProtocolVersion }); // Then tell the server that we're running the same mods. if (Settings.Config.Logging.DebugMessages) @@ -148,17 +152,17 @@ public bool HandlePluginMessage(string channel, Queue packetData, ref int for (int i = 0; i < forgeInfo.Mods.Count; i++) { ForgeInfo.ForgeMod mod = forgeInfo.Mods[i]; - mods[i] = dataTypes.ConcatBytes(dataTypes.GetString(mod.ModID), dataTypes.GetString(mod.Version)); + mods[i] = dataTypes.ConcatBytes(dataTypes.GetString(mod.ModID!), dataTypes.GetString(mod.Version ?? mod.ModMarker ?? string.Empty)); } - SendForgeHandshakePacket(FMLHandshakeDiscriminator.ModList, + await SendForgeHandshakePacket(FMLHandshakeDiscriminator.ModList, dataTypes.ConcatBytes(dataTypes.GetVarInt(forgeInfo.Mods.Count), dataTypes.ConcatBytes(mods))); fmlHandshakeState = FMLHandshakeClientState.WAITINGSERVERDATA; - return true; + return new(true, currentDimension); case FMLHandshakeClientState.WAITINGSERVERDATA: if (discriminator != FMLHandshakeDiscriminator.ModList) - return false; + return new(false, currentDimension); Thread.Sleep(2000); @@ -167,16 +171,16 @@ public bool HandlePluginMessage(string channel, Queue packetData, ref int // Tell the server that yes, we are OK with the mods it has // even though we don't actually care what mods it has. - SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck, + await SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck, new byte[] { (byte)FMLHandshakeClientState.WAITINGSERVERDATA }); fmlHandshakeState = FMLHandshakeClientState.WAITINGSERVERCOMPLETE; - return false; + return new(false, currentDimension); case FMLHandshakeClientState.WAITINGSERVERCOMPLETE: // The server now will tell us a bunch of registry information. // We need to read it all, though, until it says that there is no more. if (discriminator != FMLHandshakeDiscriminator.RegistryData) - return false; + return new(false, currentDimension); if (protocolversion < Protocol18Handler.MC_1_8_Version) { @@ -202,34 +206,34 @@ public bool HandlePluginMessage(string channel, Queue packetData, ref int fmlHandshakeState = FMLHandshakeClientState.PENDINGCOMPLETE; } - return false; + return new(false, currentDimension); case FMLHandshakeClientState.PENDINGCOMPLETE: // The server will ask us to accept the registries. // Just say yes. if (discriminator != FMLHandshakeDiscriminator.HandshakeAck) - return false; + return new(false, currentDimension); if (Settings.Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted("§8" + Translations.forge_accept_registry, acceptnewlines: true); - SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck, + await SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck, new byte[] { (byte)FMLHandshakeClientState.PENDINGCOMPLETE }); fmlHandshakeState = FMLHandshakeClientState.COMPLETE; - return true; + return new(true, currentDimension); case FMLHandshakeClientState.COMPLETE: // One final "OK". On the actual forge source, a packet is sent from // the client to the client saying that the connection was complete, but // we don't need to do that. - SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck, + await SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck, new byte[] { (byte)FMLHandshakeClientState.COMPLETE }); if (Settings.Config.Logging.DebugMessages) ConsoleIO.WriteLine(Translations.forge_complete); fmlHandshakeState = FMLHandshakeClientState.DONE; - return true; + return new(true, currentDimension); } } } - return false; + return new(false, currentDimension); } /// @@ -239,8 +243,9 @@ public bool HandlePluginMessage(string channel, Queue packetData, ref int /// Plugin message data /// Response data to return to server /// TRUE/FALSE depending on whether the packet was understood or not - public bool HandleLoginPluginRequest(string channel, Queue packetData, ref List responseData) + public async Task>> HandleLoginPluginRequest(string channel, PacketStream packetData) { + List responseData = new(); if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML2 && channel == "fml:loginwrapper") { // Forge Handshake handler source code used to implement the FML2 packets: @@ -278,8 +283,8 @@ public bool HandleLoginPluginRequest(string channel, Queue packetData, ref // The content of each message is mapped into a class inside FMLHandshakeMessages.java // FMLHandshakeHandler will then process the packet, e.g. handleServerModListOnClient() for Server Mod List. - string fmlChannel = dataTypes.ReadNextString(packetData); - dataTypes.ReadNextVarInt(packetData); // Packet length + string fmlChannel = await dataTypes.ReadNextStringAsync(packetData); + dataTypes.SkipNextVarInt(packetData); // Packet length int packetID = dataTypes.ReadNextVarInt(packetData); if (fmlChannel == "fml:handshake") @@ -308,17 +313,17 @@ public bool HandleLoginPluginRequest(string channel, Queue packetData, ref List mods = new(); int modCount = dataTypes.ReadNextVarInt(packetData); for (int i = 0; i < modCount; i++) - mods.Add(dataTypes.ReadNextString(packetData)); + mods.Add(await dataTypes.ReadNextStringAsync(packetData)); Dictionary channels = new(); int channelCount = dataTypes.ReadNextVarInt(packetData); for (int i = 0; i < channelCount; i++) - channels.Add(dataTypes.ReadNextString(packetData), dataTypes.ReadNextString(packetData)); + channels.Add(await dataTypes.ReadNextStringAsync(packetData), await dataTypes.ReadNextStringAsync(packetData)); List registries = new(); int registryCount = dataTypes.ReadNextVarInt(packetData); for (int i = 0; i < registryCount; i++) - registries.Add(dataTypes.ReadNextString(packetData)); + registries.Add(await dataTypes.ReadNextStringAsync(packetData)); // Server Mod List Reply: FMLHandshakeMessages.java > C2SModListReply > encode() // @@ -372,7 +377,7 @@ public bool HandleLoginPluginRequest(string channel, Queue packetData, ref if (Settings.Config.Logging.DebugMessages) { - string registryName = dataTypes.ReadNextString(packetData); + string registryName = await dataTypes.ReadNextStringAsync(packetData); ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_registry, registryName)); } @@ -391,7 +396,7 @@ public bool HandleLoginPluginRequest(string channel, Queue packetData, ref if (Settings.Config.Logging.DebugMessages) { - string configName = dataTypes.ReadNextString(packetData); + string configName = await dataTypes.ReadNextStringAsync(packetData); ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_config, configName)); } @@ -408,11 +413,10 @@ public bool HandleLoginPluginRequest(string channel, Queue packetData, ref if (fmlResponseReady) { // Wrap our FML packet into a LoginPluginResponse payload - responseData.Clear(); responseData.AddRange(dataTypes.GetString(fmlChannel)); responseData.AddRange(dataTypes.GetVarInt(fmlResponsePacket.Count)); responseData.AddRange(fmlResponsePacket); - return true; + return new(true, responseData); } } else if (Settings.Config.Logging.DebugMessages) @@ -420,7 +424,7 @@ public bool HandleLoginPluginRequest(string channel, Queue packetData, ref ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_unknown_channel, fmlChannel)); } } - return false; + return new(false, responseData); } /// @@ -428,9 +432,9 @@ public bool HandleLoginPluginRequest(string channel, Queue packetData, ref /// /// Discriminator to use. /// packet Data - private void SendForgeHandshakePacket(FMLHandshakeDiscriminator discriminator, byte[] data) + private async Task SendForgeHandshakePacket(FMLHandshakeDiscriminator discriminator, byte[] data) { - protocol18.SendPluginChannelPacket("FML|HS", dataTypes.ConcatBytes(new byte[] { (byte)discriminator }, data)); + await protocol18.SendPluginChannelPacket("FML|HS", dataTypes.ConcatBytes(new byte[] { (byte)discriminator }, data)); } /// @@ -439,10 +443,10 @@ private void SendForgeHandshakePacket(FMLHandshakeDiscriminator discriminator, b /// JSON data returned by the server /// ForgeInfo to populate /// True if the server is running Forge - public static bool ServerInfoCheckForge(Json.JSONData jsonData, ref ForgeInfo? forgeInfo) + public static bool ServerInfoCheckForge(PingResult jsonData, ref ForgeInfo? forgeInfo) { - return ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML) // MC 1.12 and lower - || ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML2); // MC 1.13 and greater + return ServerInfoCheckForgeSubFML1(jsonData, ref forgeInfo) // MC 1.12 and lower + || ServerInfoCheckForgeSubFML2(jsonData, ref forgeInfo); // MC 1.13 and greater } /// @@ -474,38 +478,21 @@ public static ForgeInfo ServerForceForge(int protocolVersion) /// /// JSON data returned by the server /// ForgeInfo to populate - /// Forge protocol version /// True if the server is running Forge - private static bool ServerInfoCheckForgeSub(Json.JSONData jsonData, ref ForgeInfo? forgeInfo, FMLVersion fmlVersion) + private static bool ServerInfoCheckForgeSubFML1(PingResult jsonData, ref ForgeInfo? forgeInfo) { - string forgeDataTag; - string versionField; - string versionString; - - switch (fmlVersion) + if (jsonData.modinfo != null) { - case FMLVersion.FML: - forgeDataTag = "modinfo"; - versionField = "type"; - versionString = "FML"; - break; - case FMLVersion.FML2: - forgeDataTag = "forgeData"; - versionField = "fmlNetworkVersion"; - versionString = "2"; - break; - default: - throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!"); - } - - if (jsonData.Properties.ContainsKey(forgeDataTag) && jsonData.Properties[forgeDataTag].Type == Json.JSONData.DataType.Object) - { - Json.JSONData modData = jsonData.Properties[forgeDataTag]; - if (modData.Properties.ContainsKey(versionField) && modData.Properties[versionField].StringValue == versionString) + if (jsonData.modinfo.type == "FML") { - forgeInfo = new ForgeInfo(modData, fmlVersion); - if (forgeInfo.Mods.Any()) + if (jsonData.modinfo.modList == null || jsonData.modinfo.modList.Length == 0) { + forgeInfo = null; + ConsoleIO.WriteLineFormatted("§8" + Translations.forge_no_mod, acceptnewlines: true); + } + else + { + forgeInfo = new ForgeInfo(jsonData.modinfo.modList, FMLVersion.FML); ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_with_mod, forgeInfo.Mods.Count)); if (Settings.Config.Logging.DebugMessages) { @@ -515,10 +502,39 @@ private static bool ServerInfoCheckForgeSub(Json.JSONData jsonData, ref ForgeInf } return true; } - else + } + } + return false; + } + + /// + /// Server Info: Check for For Forge on a Minecraft server Ping result (Handles FML and FML2 + /// + /// JSON data returned by the server + /// ForgeInfo to populate + /// True if the server is running Forge + private static bool ServerInfoCheckForgeSubFML2(PingResult jsonData, ref ForgeInfo? forgeInfo) + { + if (jsonData.forgeData != null) + { + if (jsonData.forgeData.fmlNetworkVersion == "2") + { + if (jsonData.forgeData.mods == null || jsonData.forgeData.mods.Length == 0) { - ConsoleIO.WriteLineFormatted("§8" + Translations.forge_no_mod, acceptnewlines: true); forgeInfo = null; + ConsoleIO.WriteLineFormatted("§8" + Translations.forge_no_mod, acceptnewlines: true); + } + else + { + forgeInfo = new ForgeInfo(jsonData.forgeData.mods, FMLVersion.FML2); + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_with_mod, forgeInfo.Mods.Count)); + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLineFormatted("§8" + Translations.forge_mod_list, acceptnewlines: true); + foreach (ForgeInfo.ForgeMod mod in forgeInfo.Mods) + ConsoleIO.WriteLineFormatted("§8 " + mod.ToString()); + } + return true; } } } diff --git a/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs b/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs index d144f37b96..bc7e54e670 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs @@ -3,9 +3,11 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading.Tasks; //using System.Linq; //using System.Text; using MinecraftClient.Mapping; +using MinecraftClient.Protocol.PacketPipeline; namespace MinecraftClient.Protocol.Handlers { @@ -33,21 +35,21 @@ public Protocol18Terrain(int protocolVersion, DataTypes dataTypes, IMinecraftCom /// /// Reading the "Block states" field: consists of 4096 entries, representing all the blocks in the chunk section. /// - /// Cache for reading data + /// Cache for reading data [MethodImpl(MethodImplOptions.AggressiveOptimization)] - private Chunk? ReadBlockStatesField(Queue cache) + private async Task ReadBlockStatesFieldAsync(PacketStream stream) { // read Block states (Type: Paletted Container) - byte bitsPerEntry = dataTypes.ReadNextByte(cache); + byte bitsPerEntry = await dataTypes.ReadNextByteAsync(stream); // 1.18(1.18.1) add a pattle named "Single valued" to replace the vertical strip bitmask in the old if (bitsPerEntry == 0 && protocolversion >= Protocol18Handler.MC_1_18_1_Version) { // Palettes: Single valued - 1.18(1.18.1) and above - ushort blockId = (ushort)dataTypes.ReadNextVarInt(cache); + ushort blockId = (ushort)(await dataTypes.ReadNextVarIntAsync(stream)); Block block = new(blockId); - dataTypes.SkipNextVarInt(cache); // Data Array Length will be zero + dataTypes.SkipNextVarInt(stream); // Data Array Length will be zero // Empty chunks will not be stored if (block.Type == Material.Air) @@ -73,16 +75,16 @@ public Protocol18Terrain(int protocolVersion, DataTypes dataTypes, IMinecraftCom // EG, if bitsPerEntry = 5, valueMask = 00011111 in binary uint valueMask = (uint)((1 << bitsPerEntry) - 1); - int paletteLength = usePalette ? dataTypes.ReadNextVarInt(cache) : 0; // Assume zero when length is absent + int paletteLength = usePalette ? await dataTypes.ReadNextVarIntAsync(stream) : 0; // Assume zero when length is absent - Span palette = paletteLength < 256 ? stackalloc uint[paletteLength] : new uint[paletteLength]; + uint[] palette = new uint[paletteLength]; for (int i = 0; i < paletteLength; i++) - palette[i] = (uint)dataTypes.ReadNextVarInt(cache); + palette[i] = (uint)(await dataTypes.ReadNextVarIntAsync(stream)); //// Block IDs are packed in the array of 64-bits integers - dataTypes.SkipNextVarInt(cache); // Entry length - Span entryDataByte = stackalloc byte[8]; - Span entryDataLong = MemoryMarshal.Cast(entryDataByte); // Faster than MemoryMarshal.Read + dataTypes.SkipNextVarInt(stream); // Entry length + + long entryData = 0; Chunk chunk = new(); int startOffset = 64; // Read the first data immediately @@ -101,10 +103,10 @@ public Protocol18Terrain(int protocolVersion, DataTypes dataTypes, IMinecraftCom // When overlapping, move forward to the beginning of the next Long startOffset = 0; - dataTypes.ReadDataReverse(cache, entryDataByte); // read long + entryData = await dataTypes.ReadNextLongAsync(stream); } - uint blockId = (uint)(entryDataLong[0] >> startOffset) & valueMask; + uint blockId = (uint)(entryData >> startOffset) & valueMask; // Map small IDs to actual larger block IDs if (usePalette) @@ -141,10 +143,10 @@ public Protocol18Terrain(int protocolVersion, DataTypes dataTypes, IMinecraftCom /// Chunk X location /// Chunk Z location /// Chunk mask for reading data, store in bitset, used in 1.17 and 1.17.1 - /// Cache for reading chunk data + /// Cache for reading chunk data /// token to cancel the task [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public void ProcessChunkColumnData(int chunkX, int chunkZ, ulong[]? verticalStripBitmask, Queue cache) + public async Task ProcessChunkColumnData(int chunkX, int chunkZ, ulong[]? verticalStripBitmask, PacketStream stream) { World world = handler.GetWorld(); @@ -181,10 +183,10 @@ public void ProcessChunkColumnData(int chunkX, int chunkZ, ulong[]? verticalStri ((verticalStripBitmask![chunkY / 64] & (1UL << (chunkY % 64))) != 0)) { // Non-air block count inside chunk section, for lighting purposes - int blockCnt = dataTypes.ReadNextShort(cache); + int blockCnt = await dataTypes.ReadNextShortAsync(stream); // Read Block states (Type: Paletted Container) - Chunk? chunk = ReadBlockStatesField(cache); + Chunk? chunk = await ReadBlockStatesFieldAsync(stream); //We have our chunk, save the chunk into the world world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == lastChunkY); @@ -192,23 +194,23 @@ public void ProcessChunkColumnData(int chunkX, int chunkZ, ulong[]? verticalStri // Skip Read Biomes (Type: Paletted Container) - 1.18(1.18.1) and above if (protocolversion >= Protocol18Handler.MC_1_18_1_Version) { - byte bitsPerEntryBiome = dataTypes.ReadNextByte(cache); // Bits Per Entry + byte bitsPerEntryBiome = await dataTypes.ReadNextByteAsync(stream); // Bits Per Entry if (bitsPerEntryBiome == 0) { - dataTypes.SkipNextVarInt(cache); // Value - dataTypes.SkipNextVarInt(cache); // Data Array Length + dataTypes.SkipNextVarInt(stream); // Value + dataTypes.SkipNextVarInt(stream); // Data Array Length // Data Array must be empty } else { if (bitsPerEntryBiome <= 3) { - int paletteLength = dataTypes.ReadNextVarInt(cache); // Palette Length + int paletteLength = await dataTypes.ReadNextVarIntAsync(stream); // Palette Length for (int i = 0; i < paletteLength; i++) - dataTypes.SkipNextVarInt(cache); // Palette + dataTypes.SkipNextVarInt(stream); // Palette } - int dataArrayLength = dataTypes.ReadNextVarInt(cache); // Data Array Length - dataTypes.DropData(dataArrayLength * 8, cache); // Data Array + int dataArrayLength = await dataTypes.ReadNextVarIntAsync(stream); // Data Array Length + await dataTypes.DropDataAsync(dataArrayLength * 8, stream); // Data Array } } } @@ -228,10 +230,10 @@ public void ProcessChunkColumnData(int chunkX, int chunkZ, ulong[]? verticalStri /// Contains skylight info /// Are the chunk continuous /// Current dimension type (0 = overworld) - /// Cache for reading chunk data + /// Cache for reading chunk data /// token to cancel the task [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ushort chunkMask2, bool hasSkyLight, bool chunksContinuous, int currentDimension, Queue cache) + public async Task ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ushort chunkMask2, bool hasSkyLight, bool chunksContinuous, int currentDimension, PacketStream stream) { World world = handler.GetWorld(); @@ -247,9 +249,9 @@ public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ush { // 1.14 and above Non-air block count inside chunk section, for lighting purposes if (protocolversion >= Protocol18Handler.MC_1_14_Version) - dataTypes.ReadNextShort(cache); + await dataTypes.SkipNextShortAsync(stream); - byte bitsPerBlock = dataTypes.ReadNextByte(cache); + byte bitsPerBlock = await dataTypes.ReadNextByteAsync(stream); bool usePalette = (bitsPerBlock <= 8); // Vanilla Minecraft will use at least 4 bits per block @@ -260,12 +262,12 @@ public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ush // is not used, MC 1.13+ does not send the field at all in this case int paletteLength = 0; // Assume zero when length is absent if (usePalette || protocolversion < Protocol18Handler.MC_1_13_Version) - paletteLength = dataTypes.ReadNextVarInt(cache); + paletteLength = await dataTypes.ReadNextVarIntAsync(stream); int[] palette = new int[paletteLength]; for (int i = 0; i < paletteLength; i++) { - palette[i] = dataTypes.ReadNextVarInt(cache); + palette[i] = await dataTypes.ReadNextVarIntAsync(stream); } // Bit mask covering bitsPerBlock bits @@ -273,7 +275,7 @@ public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ush uint valueMask = (uint)((1 << bitsPerBlock) - 1); // Block IDs are packed in the array of 64-bits integers - ulong[] dataArray = dataTypes.ReadNextULongArray(cache); + ulong[] dataArray = await dataTypes.ReadNextULongArrayAsync(stream); Chunk chunk = new(); @@ -358,19 +360,19 @@ public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ush } } - //We have our chunk, save the chunk into the world + // We have our chunk, save the chunk into the world world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == maxChunkY); - //Pre-1.14 Lighting data + // Pre-1.14 Lighting data if (protocolversion < Protocol18Handler.MC_1_14_Version) { - //Skip block light - dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache); + // Skip block light + await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream); - //Skip sky light + // Skip sky light if (currentDimension == 0) // Sky light is not sent in the nether or the end - dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache); + await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream); } } } @@ -383,15 +385,12 @@ public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ush // 1.8 chunk format if (chunksContinuous && chunkMask == 0) { - //Unload the entire chunk column - handler.InvokeOnMainThread(() => - { - world[chunkX, chunkZ] = null; - }); + // Unload the entire chunk column + world[chunkX, chunkZ] = null; } else { - //Load chunk data from the server + // Load chunk data from the server int maxChunkY = sizeof(int) * 8 - 1 - BitOperations.LeadingZeroCount(chunkMask); for (int chunkY = 0; chunkY <= maxChunkY; chunkY++) { @@ -399,35 +398,34 @@ public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ush { Chunk chunk = new(); - //Read chunk data, all at once for performance reasons, and build the chunk object - Queue queue = new(dataTypes.ReadNextUShortsLittleEndian(Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ, cache)); + // Read chunk data, all at once for performance reasons, and build the chunk object for (int blockY = 0; blockY < Chunk.SizeY; blockY++) for (int blockZ = 0; blockZ < Chunk.SizeZ; blockZ++) for (int blockX = 0; blockX < Chunk.SizeX; blockX++) - chunk.SetWithoutCheck(blockX, blockY, blockZ, new Block(queue.Dequeue())); + chunk.SetWithoutCheck(blockX, blockY, blockZ, new Block(await dataTypes.ReadNextUShortAsync(stream))); - //We have our chunk, save the chunk into the world + // We have our chunk, save the chunk into the world world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == maxChunkY); } } - //Skip light information + // Skip light information for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++) { if ((chunkMask & (1 << chunkY)) != 0) { - //Skip block light - dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache); + // Skip block light + await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream); - //Skip sky light + // Skip sky light if (hasSkyLight) - dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache); + await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream); } } - //Skip biome metadata + // Skip biome metadata if (chunksContinuous) - dataTypes.DropData(Chunk.SizeX * Chunk.SizeZ, cache); + await dataTypes.DropDataAsync(Chunk.SizeX * Chunk.SizeZ, stream); } } else @@ -435,15 +433,12 @@ public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ush // 1.7 chunk format if (chunksContinuous && chunkMask == 0) { - //Unload the entire chunk column - handler.InvokeOnMainThread(() => - { - world[chunkX, chunkZ] = null; - }); + // Unload the entire chunk column + world[chunkX, chunkZ] = null; } else { - //Count chunk sections + // Count chunk sections int sectionCount = 0; int addDataSectionCount = 0; for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++) @@ -454,10 +449,10 @@ public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ush addDataSectionCount++; } - //Read chunk data, unpacking 4-bit values into 8-bit values for block metadata - Queue blockTypes = new(dataTypes.ReadData(Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount, cache)); + // Read chunk data, unpacking 4-bit values into 8-bit values for block metadata + Queue blockTypes = new(await dataTypes.ReadDataAsync(Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount, stream)); Queue blockMeta = new(); - foreach (byte packed in dataTypes.ReadData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, cache)) + foreach (byte packed in await dataTypes.ReadDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, stream)) { byte hig = (byte)(packed >> 4); byte low = (byte)(packed & (byte)0x0F); @@ -465,15 +460,15 @@ public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ush blockMeta.Enqueue(low); } - //Skip data we don't need - dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, cache); //Block light + // Skip data we don't need + await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, stream); //Block light if (hasSkyLight) - dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, cache); //Sky light - dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * addDataSectionCount) / 2, cache); //BlockAdd + await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, stream); //Sky light + await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * addDataSectionCount) / 2, stream); //BlockAdd if (chunksContinuous) - dataTypes.DropData(Chunk.SizeX * Chunk.SizeZ, cache); //Biomes + await dataTypes.DropDataAsync(Chunk.SizeX * Chunk.SizeZ, stream); //Biomes - //Load chunk data + // Load chunk data int maxChunkY = sizeof(int) * 8 - 1 - BitOperations.LeadingZeroCount(chunkMask); for (int chunkY = 0; chunkY <= maxChunkY; chunkY++) { diff --git a/MinecraftClient/Protocol/Handlers/SocketWrapper.cs b/MinecraftClient/Protocol/Handlers/SocketWrapper.cs deleted file mode 100644 index d9793024b7..0000000000 --- a/MinecraftClient/Protocol/Handlers/SocketWrapper.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Net.Sockets; -using MinecraftClient.Crypto; - -namespace MinecraftClient.Protocol.Handlers -{ - /// - /// Wrapper for handling unencrypted & encrypted socket - /// - class SocketWrapper - { - readonly TcpClient c; - AesCfb8Stream? s; - bool encrypted = false; - - /// - /// Initialize a new SocketWrapper - /// - /// TcpClient connected to the server - public SocketWrapper(TcpClient client) - { - c = client; - } - - /// - /// Check if the socket is still connected - /// - /// TRUE if still connected - /// Silently dropped connection can only be detected by attempting to read/write data - public bool IsConnected() - { - return c.Client != null && c.Connected; - } - - /// - /// Check if the socket has data available to read - /// - /// TRUE if data is available to read - public bool HasDataAvailable() - { - return c.Client.Available > 0; - } - - /// - /// Switch network reading/writing to an encrypted stream - /// - /// AES secret key - public void SwitchToEncrypted(byte[] secretKey) - { - if (encrypted) - throw new InvalidOperationException("Stream is already encrypted!?"); - s = new AesCfb8Stream(c.GetStream(), secretKey); - encrypted = true; - } - - /// - /// Network reading method. Read bytes from the socket or encrypted socket. - /// - private void Receive(byte[] buffer, int start, int offset, SocketFlags f) - { - int read = 0; - while (read < offset) - { - if (encrypted) - read += s!.Read(buffer, start + read, offset - read); - else - read += c.Client.Receive(buffer, start + read, offset - read, f); - } - } - - /// - /// Read some data from the server. - /// - /// Amount of bytes to read - /// The data read from the network as an array - public byte[] ReadDataRAW(int length) - { - if (length > 0) - { - byte[] cache = new byte[length]; - Receive(cache, 0, length, SocketFlags.None); - return cache; - } - return Array.Empty(); - } - - /// - /// Send raw data to the server. - /// - /// data to send - public void SendDataRAW(byte[] buffer) - { - if (encrypted) - s!.Write(buffer, 0, buffer.Length); - else - c.Client.Send(buffer); - } - - /// - /// Disconnect from the server - /// - public void Disconnect() - { - try - { - c.Close(); - } - catch (SocketException) { } - catch (System.IO.IOException) { } - catch (NullReferenceException) { } - catch (ObjectDisposedException) { } - } - } -} diff --git a/MinecraftClient/Protocol/Handlers/ZlibUtils.cs b/MinecraftClient/Protocol/Handlers/ZlibUtils.cs deleted file mode 100644 index 62f8bf8555..0000000000 --- a/MinecraftClient/Protocol/Handlers/ZlibUtils.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Ionic.Zlib; - -namespace MinecraftClient.Protocol.Handlers -{ - /// - /// Quick Zlib compression handling for network packet compression. - /// Note: Underlying compression handling is taken from the DotNetZip Library. - /// This library is open source and provided under the Microsoft Public License. - /// More info about DotNetZip at dotnetzip.codeplex.com. - /// - public static class ZlibUtils - { - /// - /// Compress a byte array into another bytes array using Zlib compression - /// - /// Data to compress - /// Compressed data as a byte array - public static byte[] Compress(byte[] to_compress) - { - byte[] data; - using (System.IO.MemoryStream memstream = new()) - { - using (ZlibStream stream = new(memstream, CompressionMode.Compress)) - { - stream.Write(to_compress, 0, to_compress.Length); - } - data = memstream.ToArray(); - } - return data; - } - - /// - /// Decompress a byte array into another byte array of the specified size - /// - /// Data to decompress - /// Size of the data once decompressed - /// Decompressed data as a byte array - public static byte[] Decompress(byte[] to_decompress, int size_uncompressed) - { - ZlibStream stream = new(new System.IO.MemoryStream(to_decompress, false), CompressionMode.Decompress); - byte[] packetData_decompressed = new byte[size_uncompressed]; - stream.Read(packetData_decompressed, 0, size_uncompressed); - stream.Close(); - return packetData_decompressed; - } - - /// - /// Decompress a byte array into another byte array of a potentially unlimited size (!) - /// - /// Data to decompress - /// Decompressed data as byte array - public static byte[] Decompress(byte[] to_decompress) - { - ZlibStream stream = new(new System.IO.MemoryStream(to_decompress, false), CompressionMode.Decompress); - byte[] buffer = new byte[16 * 1024]; - using System.IO.MemoryStream decompressedBuffer = new(); - int read; - while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) - decompressedBuffer.Write(buffer, 0, read); - return decompressedBuffer.ToArray(); - } - } -} diff --git a/MinecraftClient/Protocol/IMinecraftCom.cs b/MinecraftClient/Protocol/IMinecraftCom.cs index 4c0513dd12..11202eb8c6 100644 --- a/MinecraftClient/Protocol/IMinecraftCom.cs +++ b/MinecraftClient/Protocol/IMinecraftCom.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; using MinecraftClient.Inventory; using MinecraftClient.Mapping; using MinecraftClient.Protocol.ProfileKey; @@ -19,7 +22,12 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// Start the login procedure once connected to the server /// /// True if login was successful - bool Login(PlayerKeyPair? playerKeyPair, Session.SessionToken session); + Task Login(HttpClient httpClient, PlayerKeyPair? playerKeyPair, Session.SessionToken session); + + /// + /// Start processing game packets. + /// + Task StartUpdating(); /// /// Disconnect from the server @@ -46,20 +54,20 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// /// Text to send /// True if successfully sent - bool SendChatMessage(string message, PlayerKeyPair? playerKeyPair = null); + Task SendChatMessage(string message, PlayerKeyPair? playerKeyPair = null); /// /// Allow to respawn after death /// /// True if packet successfully sent - bool SendRespawnPacket(); + Task SendRespawnPacket(); /// /// Inform the server of the client being used to connect /// /// Client string describing the client /// True if brand info was successfully sent - bool SendBrandInfo(string brandInfo); + Task SendBrandInfo(string brandInfo); /// /// Inform the server of the client's Minecraft settings @@ -72,7 +80,7 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// Show skin layers /// 1.9+ main hand /// True if client settings were successfully sent - bool SendClientSettings(string language, byte viewDistance, byte difficulty, byte chatMode, bool chatColors, byte skinParts, byte mainHand); + Task SendClientSettings(string language, byte viewDistance, byte difficulty, byte chatMode, bool chatColors, byte skinParts, byte mainHand); /// /// Send a location update telling that we moved to that location @@ -82,7 +90,7 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// The new yaw (optional) /// The new pitch (optional) /// True if packet was successfully sent - bool SendLocationUpdate(Location location, bool onGround, float? yaw, float? pitch); + Task SendLocationUpdate(Location location, bool onGround, float? yaw, float? pitch); /// /// Send a plugin channel packet to the server. @@ -91,7 +99,7 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// Channel to send packet on /// packet Data /// True if message was successfully sent - bool SendPluginChannelPacket(string channel, byte[] data); + Task SendPluginChannelPacket(string channel, byte[] data); /// /// Send Entity Action packet to the server. @@ -99,14 +107,14 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// PlayerID /// Type of packet to send /// True if packet was successfully sent - bool SendEntityAction(int EntityID, int type); + Task SendEntityAction(int EntityID, int type); /// /// Send a held item change packet to the server. /// /// New active slot in the inventory hotbar /// True if packet was successfully sent - bool SendHeldItemChange(short slot); + Task SendHeldItemChange(short slot); /// /// Send an entity interaction packet to the server. @@ -114,7 +122,7 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// Entity ID to interact with /// Type of interaction (0: interact, 1: attack, 2: interact at) /// True if packet was successfully sent - bool SendInteractEntity(int EntityID, int type); + Task SendInteractEntity(int EntityID, int type); /// /// Send an entity interaction packet to the server. @@ -126,7 +134,7 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// Z coordinate for "interact at" /// Player hand (0: main hand, 1: off hand) /// True if packet was successfully sent - bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z, int hand); + Task SendInteractEntity(int EntityID, int type, float X, float Y, float Z, int hand); /// /// Send an entity interaction packet to the server. @@ -137,7 +145,7 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// Y coordinate for "interact at" /// Z coordinate for "interact at" /// True if packet was successfully sent - bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z); + Task SendInteractEntity(int EntityID, int type, float X, float Y, float Z); /// /// Send an entity interaction packet to the server. @@ -146,7 +154,7 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// Type of interaction (0: interact, 1: attack, 2: interact at) /// Only if Type is interact or interact at; 0: main hand, 1: off hand /// True if packet was successfully sent - bool SendInteractEntity(int EntityID, int type, int hand); + Task SendInteractEntity(int EntityID, int type, int hand); /// /// Send a use item packet to the server @@ -154,7 +162,7 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// 0: main hand, 1: off hand /// Sequence ID used for synchronization /// True if packet was successfully sent - bool SendUseItem(int hand, int sequenceId); + Task SendUseItem(int hand, int sequenceId); /// /// Send a click window slot packet to the server @@ -166,7 +174,7 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// Slots that have been changed in this event: List /// Inventory's stateId /// True if packet was successfully sent - bool SendWindowAction(int windowId, int slotId, WindowActionType action, Item? item, List> changedSlots, int stateId); + Task SendWindowAction(int windowId, int slotId, WindowActionType action, Item? item, List> changedSlots, int stateId); /// /// Request Creative Mode item creation into regular/survival Player Inventory @@ -177,7 +185,7 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// Item count /// Optional item NBT /// TRUE if item given successfully - bool SendCreativeInventoryAction(int slot, ItemType itemType, int count, Dictionary? nbt); + Task SendCreativeInventoryAction(int slot, ItemType itemType, int count, Dictionary? nbt); /// /// Send a click container button packet to the server. @@ -187,7 +195,7 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// Id of the clicked button /// True if packet was successfully sent - bool ClickContainerButton(int windowId, int buttonId); + Task ClickContainerButton(int windowId, int buttonId); /// /// Plays animation @@ -195,13 +203,13 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// 0 for left arm, 1 for right arm /// Player Entity ID /// TRUE if item given successfully - bool SendAnimation(int animation, int playerid); + Task SendAnimation(int animation, int playerid); /// /// Send a close window packet to the server /// /// Id of the window being closed - bool SendCloseWindow(int windowId); + Task SendCloseWindow(int windowId); /// /// Send player block placement packet to the server @@ -211,7 +219,7 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// Block face /// Sequence ID (use for synchronization) /// True if packet was successfully sent - bool SendPlayerBlockPlacement(int hand, Location location, Direction face, int sequenceId); + Task SendPlayerBlockPlacement(int hand, Location location, Direction face, int sequenceId); /// /// Send player blog digging packet to the server. This packet needs to be called at least twice: Once to begin digging, then a second time to finish digging @@ -221,7 +229,7 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// Block face /// Sequence ID (use for synchronization) /// True if packet was succcessfully sent - bool SendPlayerDigging(int status, Location location, Direction face, int sequenceId); + Task SendPlayerDigging(int status, Location location, Direction face, int sequenceId); /// /// Change text on a sign @@ -232,7 +240,7 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// New line 3 /// New line 4 /// True if packet was succcessfully sent - bool SendUpdateSign(Location location, string line1, string line2, string line3, string line4); + Task SendUpdateSign(Location location, string line1, string line2, string line3, string line4); /// /// Update command block @@ -241,24 +249,18 @@ public interface IMinecraftCom : IDisposable, IAutoComplete /// command /// command block mode /// command block flags - bool UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags); + Task UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags); /// /// Select villager trade /// /// The slot of the trade, starts at 0. - bool SelectTrade(int selectedSlot); + Task SelectTrade(int selectedSlot); /// /// Spectate a player/entity /// /// The uuid of the player/entity to spectate/teleport to. - bool SendSpectate(Guid uuid); - - /// - /// Get net read thread (main thread) ID - /// - /// Net read thread ID - int GetNetMainThreadId(); + Task SendSpectate(Guid uuid); } } diff --git a/MinecraftClient/Protocol/IMinecraftComHandler.cs b/MinecraftClient/Protocol/IMinecraftComHandler.cs index 61e24642f7..25f8452c0d 100644 --- a/MinecraftClient/Protocol/IMinecraftComHandler.cs +++ b/MinecraftClient/Protocol/IMinecraftComHandler.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; +using MinecraftClient.EntityHandler; using MinecraftClient.Inventory; using MinecraftClient.Logger; using MinecraftClient.Mapping; @@ -18,7 +20,6 @@ public interface IMinecraftComHandler { /* The MinecraftCom Handler must * provide these getters */ - int GetServerPort(); string GetServerHost(); string GetUsername(); @@ -43,26 +44,6 @@ public interface IMinecraftComHandler Container? GetInventory(int inventoryID); ILogger GetLogger(); - /// - /// Invoke a task on the main thread, wait for completion and retrieve return value. - /// - /// Task to run with any type or return value - /// Any result returned from task, result type is inferred from the task - /// bool result = InvokeOnMainThread(methodThatReturnsAbool); - /// bool result = InvokeOnMainThread(() => methodThatReturnsAbool(argument)); - /// int result = InvokeOnMainThread(() => { yourCode(); return 42; }); - /// Type of the return value - T InvokeOnMainThread(Func task); - - /// - /// Invoke a task on the main thread and wait for completion - /// - /// Task to run without return value - /// InvokeOnMainThread(methodThatReturnsNothing); - /// InvokeOnMainThread(() => methodThatReturnsNothing(argument)); - /// InvokeOnMainThread(() => { yourCode(); }); - void InvokeOnMainThread(Action task); - /// /// Called when a network packet received or sent /// @@ -73,18 +54,18 @@ public interface IMinecraftComHandler /// A copy of Packet Data /// The packet is login phase or playing phase /// The packet is received from server or sent by client - void OnNetworkPacket(int packetID, List packetData, bool isLogin, bool isInbound); + Task OnNetworkPacketAsync(int packetID, byte[] packetData, bool isLogin, bool isInbound); /// /// Called when a server was successfully joined /// - void OnGameJoined(); + Task OnGameJoinedAsync(); /// /// Received chat/system message from the server /// /// Message received - public void OnTextReceived(ChatMessage message); + Task OnTextReceivedAsync(ChatMessage message); /// /// Will be called every animations of the hit and place block @@ -110,7 +91,7 @@ public interface IMinecraftComHandler /// /// This method is called when the protocol handler receives a title /// - void OnTitle(int action, string titletext, string subtitletext, string actionbartext, int fadein, int stay, int fadeout, string json); + Task OnTitle(TitlePacket title); /// /// Called when receiving a connection keep-alive from the server @@ -136,36 +117,36 @@ public interface IMinecraftComHandler /// /// Called when an inventory is opened /// - void OnInventoryOpen(int inventoryID, Container inventory); + Task OnInventoryOpenAsync(int inventoryID, Container inventory); /// /// Called when an inventory is closed /// - void OnInventoryClose(int inventoryID); + Task OnInventoryCloseAsync(int inventoryID); /// /// Called when the player respawns, which happens on login, respawn and world change. /// - void OnRespawn(); + Task OnRespawnAsync(); /// /// Triggered when a new player joins the game /// /// player info - public void OnPlayerJoin(PlayerInfo player); + Task OnPlayerJoinAsync(PlayerInfo player); /// /// This method is called when a player has left the game /// /// UUID of the player - void OnPlayerLeave(Guid uuid); + Task OnPlayerLeaveAsync(Guid uuid); /// /// This method is called when a player has been killed by another entity /// /// Killer's entity if /// message sent in chat when player is killed - void OnPlayerKilled(int killerEntityId, string chatMessage); + Task OnPlayerKilledAsync(int killerEntityId, string chatMessage); /// /// Called when the server sets the new location for the player @@ -181,24 +162,24 @@ public interface IMinecraftComHandler void OnConnectionLost(ChatBot.DisconnectReason reason, string message); /// - /// Called ~10 times per second (10 ticks per second) + /// Called ~20 times per second (20 ticks per second) /// Useful for updating bots in other parts of the program /// - void OnUpdate(); + Task OnUpdate(); /// /// Registers the given plugin channel for the given bot. /// /// The channel to register. /// The bot to register the channel for. - void RegisterPluginChannel(string channel, ChatBot bot); + Task RegisterPluginChannelAsync(string channel, ChatBot bot); /// /// Unregisters the given plugin channel for the given bot. /// /// The channel to unregister. /// The bot to unregister the channel for. - void UnregisterPluginChannel(string channel, ChatBot bot); + Task UnregisterPluginChannelAsync(string channel, ChatBot bot); /// /// Sends a plugin channel packet to the server. @@ -208,7 +189,7 @@ public interface IMinecraftComHandler /// The payload for the packet. /// Whether the packet should be sent even if the server or the client hasn't registered it yet. /// Whether the packet was sent: true if it was sent, false if there was a connection error or it wasn't registered. - bool SendPluginChannelMessage(string channel, byte[] data, bool sendEvenIfNotRegistered = false); + Task SendPluginChannelMessageAsync(string channel, byte[] data, bool sendEvenIfNotRegistered = false); /// /// Called when a plugin channel message was sent from the server. @@ -221,7 +202,7 @@ public interface IMinecraftComHandler /// Called when an entity has spawned /// /// Spawned entity - void OnSpawnEntity(Entity entity); + Task OnSpawnEntity(Entity entity); /// /// Called when an entity has spawned @@ -229,7 +210,7 @@ public interface IMinecraftComHandler /// Entity id /// Equipment slot. 0: main hand, 1: off hand, 2–5: armor slot (2: boots, 3: leggings, 4: chestplate, 5: helmet)/param> /// Item/param> - void OnEntityEquipment(int entityid, int slot, Item? item); + Task OnEntityEquipment(int entityid, int slot, Item? item); /// /// Called when a player spawns or enters the client's render distance @@ -239,13 +220,13 @@ public interface IMinecraftComHandler /// Entity location /// Player head yaw /// Player head pitch - void OnSpawnPlayer(int entityID, Guid uuid, Location location, byte yaw, byte pitch); + Task OnSpawnPlayer(int entityID, Guid uuid, Location location, byte yaw, byte pitch); /// /// Called when entities have despawned /// /// List of Entity ID that have despawned - void OnDestroyEntities(int[] EntityID); + Task OnDestroyEntities(int[] EntityID); /// /// Called when an entity moved by coordinate offset @@ -255,7 +236,7 @@ public interface IMinecraftComHandler /// Y offset /// Z offset /// TRUE if on ground - void OnEntityPosition(int entityID, Double dx, Double dy, Double dz, bool onGround); + Task OnEntityPosition(int entityID, Double dx, Double dy, Double dz, bool onGround); /// /// Called when an entity moved to fixed coordinates @@ -265,28 +246,28 @@ public interface IMinecraftComHandler /// Y /// Z /// TRUE if on ground - void OnEntityTeleport(int entityID, Double x, Double y, Double z, bool onGround); + Task OnEntityTeleport(int entityID, Double x, Double y, Double z, bool onGround); /// /// Called when additional properties have been received for an entity /// /// Entity ID /// Dictionary of properties - void OnEntityProperties(int entityID, Dictionary prop); + Task OnEntityProperties(int entityID, Dictionary prop); /// /// Called when the status of an entity have been changed /// /// Entity ID /// Status ID - void OnEntityStatus(int entityID, byte status); + Task OnEntityStatus(int entityID, byte status); /// /// Called when the world age has been updated /// /// World age /// Time of Day - void OnTimeUpdate(long worldAge, long timeOfDay); + Task OnTimeUpdate(long worldAge, long timeOfDay); /// /// When received window properties from server. @@ -295,7 +276,7 @@ public interface IMinecraftComHandler /// Inventory ID /// Property ID /// Property Value - public void OnWindowProperties(byte inventoryID, short propertyId, short propertyValue); + Task OnWindowPropertiesAsync(byte inventoryID, short propertyId, short propertyValue); /// /// Called when inventory items have been received @@ -303,7 +284,7 @@ public interface IMinecraftComHandler /// Inventory ID /// Item list /// State ID - void OnWindowItems(byte inventoryID, Dictionary itemList, int stateId); + Task OnWindowItemsAsync(byte inventoryID, Dictionary itemList, int stateId); /// /// Called when a single slot has been updated inside an inventory @@ -312,14 +293,14 @@ public interface IMinecraftComHandler /// Slot ID /// Item (may be null for empty slot) /// State ID - void OnSetSlot(byte inventoryID, short slotID, Item? item, int stateId); + Task OnSetSlotAsync(byte inventoryID, short slotID, Item? item, int stateId); /// /// Called when player health or hunger changed. /// /// /// - void OnUpdateHealth(float health, int food); + Task OnUpdateHealth(float health, int food); /// /// Called when the health of an entity changed @@ -341,21 +322,21 @@ public interface IMinecraftComHandler /// Explosion location /// Explosion strength /// Amount of affected blocks - void OnExplosion(Location location, float strength, int affectedBlocks); + Task OnExplosion(Location location, float strength, int affectedBlocks); /// /// Called when a player's game mode has changed /// /// Affected player's UUID /// New game mode - void OnGamemodeUpdate(Guid uuid, int gamemode); + Task OnGamemodeUpdate(Guid uuid, int gamemode); /// /// Called when a player's latency has changed /// /// Affected player's UUID /// latency - void OnLatencyUpdate(Guid uuid, int latency); + Task OnLatencyUpdate(Guid uuid, int latency); /// /// Called when Experience bar is updated @@ -363,14 +344,14 @@ public interface IMinecraftComHandler /// Experience bar level /// Player Level /// Total experience - void OnSetExperience(float Experiencebar, int Level, int TotalExperience); + Task OnSetExperience(float Experiencebar, int Level, int TotalExperience); /// /// Called when client need to change slot. /// /// Used for setting player slot after joining game /// - void OnHeldItemChange(byte slot); + Task OnHeldItemChange(byte slot); /// /// Called when an update of the map is sent by the server, take a look at https://wiki.vg/Protocol#Map_Data for more info on the fields @@ -386,7 +367,7 @@ public interface IMinecraftComHandler /// x offset of the westernmost column /// z offset of the northernmost row /// a byte array of colors on the map - void OnMapData(int mapid, byte scale, bool trackingPosition, bool locked, List icons, byte columnsUpdated, byte rowsUpdated, byte mapCoulmnX, byte mapRowZ, byte[]? colors); + Task OnMapData(MapData mapData); /// /// Called when the Player entity ID has been received from the server @@ -404,7 +385,7 @@ public interface IMinecraftComHandler /// effect flags /// has factor data /// factorCodec - void OnEntityEffect(int entityid, Effects effect, int amplifier, int duration, byte flags, bool hasFactorData, Dictionary? factorCodec); + Task OnEntityEffect(int entityid, Effect effect); /// /// Called when coreboardObjective @@ -472,6 +453,6 @@ public interface IMinecraftComHandler /// Id of the clicked button /// True if packet was successfully sent - bool ClickContainerButton(int windowId, int buttonId); + public Task ClickContainerButton(int windowId, int buttonId); } } diff --git a/MinecraftClient/Protocol/JwtPayloadDecode.cs b/MinecraftClient/Protocol/JwtPayloadDecode.cs index a103a4b83f..8e299ff349 100644 --- a/MinecraftClient/Protocol/JwtPayloadDecode.cs +++ b/MinecraftClient/Protocol/JwtPayloadDecode.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Text; namespace MinecraftClient.Protocol @@ -6,11 +7,10 @@ namespace MinecraftClient.Protocol // Thanks to https://stackoverflow.com/questions/60404612/parse-jwt-token-to-get-the-payload-content-only-without-external-library-in-c-sh public static class JwtPayloadDecode { - public static string GetPayload(string token) + public static MemoryStream GetPayload(string token) { var content = token.Split('.')[1]; - var jsonPayload = Encoding.UTF8.GetString(Decode(content)); - return jsonPayload; + return new MemoryStream(Decode(content)); } private static byte[] Decode(string input) @@ -23,7 +23,7 @@ private static byte[] Decode(string input) case 0: break; // No pad chars in this case case 2: output += "=="; break; // Two pad chars case 3: output += "="; break; // One pad char - default: throw new System.ArgumentOutOfRangeException(nameof(input), "Illegal base64url string!"); + default: throw new ArgumentOutOfRangeException(nameof(input), "Illegal base64url string!"); } var converted = Convert.FromBase64String(output); // Standard base64 decoder return converted; diff --git a/MinecraftClient/Protocol/MicrosoftAuthentication.cs b/MinecraftClient/Protocol/MicrosoftAuthentication.cs index b782ea3db5..318d56e4dc 100644 --- a/MinecraftClient/Protocol/MicrosoftAuthentication.cs +++ b/MinecraftClient/Protocol/MicrosoftAuthentication.cs @@ -3,9 +3,18 @@ using System.Collections.Specialized; using System.Diagnostics; using System.Globalization; +using System.IO; using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using System.Text.RegularExpressions; +using System.Threading.Tasks; +using MinecraftClient.Protocol.ProfileKey; using static MinecraftClient.Settings; using static MinecraftClient.Settings.MainConfigHealper.MainConfig.GeneralConfig; @@ -13,9 +22,10 @@ namespace MinecraftClient.Protocol { static class Microsoft { - private static readonly string clientId = "54473e32-df8f-42e9-a649-9419b0dab9d3"; - private static readonly string signinUrl = string.Format("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={0}&response_type=code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&scope=XboxLive.signin%20offline_access%20openid%20email&prompt=select_account&response_mode=fragment", clientId); - private static readonly string tokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; + private const string clientId = "54473e32-df8f-42e9-a649-9419b0dab9d3"; + private const string tokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; + private const string signinUrl = $"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={clientId}&response_type=code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&scope=XboxLive.signin%20offline_access%20openid%20email&prompt=select_account&response_mode=fragment"; + private const string certificates = "https://api.minecraftservices.com/player/certificates"; public static string SignInUrl { get { return signinUrl; } } @@ -26,7 +36,7 @@ static class Microsoft /// Sign-in URL with email pre-filled public static string GetSignInUrlWithHint(string loginHint) { - return SignInUrl + "&login_hint=" + Uri.EscapeDataString(loginHint); + return $"{SignInUrl}&login_hint={Uri.EscapeDataString(loginHint)}"; } /// @@ -34,11 +44,16 @@ public static string GetSignInUrlWithHint(string loginHint) /// /// Auth code obtained after user signing in /// Access token and refresh token - public static LoginResponse RequestAccessToken(string code) + public static async Task RequestAccessTokenAsync(HttpClient httpClient, string code) { - string postData = "client_id={0}&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&code={1}"; - postData = string.Format(postData, clientId, code); - return RequestToken(postData); + FormUrlEncodedContent postData = new(new KeyValuePair[] + { + new("client_id", clientId), + new("grant_type", "authorization_code"), + new("redirect_uri", "https://mccteam.github.io/redirect.html"), + new("code", code), + }); + return await RequestTokenAsync(httpClient, postData); } /// @@ -46,11 +61,43 @@ public static LoginResponse RequestAccessToken(string code) /// /// Refresh token /// Access token and new refresh token - public static LoginResponse RefreshAccessToken(string refreshToken) + public static async Task RefreshAccessTokenAsync(HttpClient httpClient, string refreshToken) + { + FormUrlEncodedContent postData = new(new KeyValuePair[] + { + new("client_id", clientId), + new("grant_type", "refresh_token"), + new("redirect_uri", "https://mccteam.github.io/redirect.html"), + new("refresh_token", refreshToken), + }); + return await RequestTokenAsync(httpClient, postData); + } + + private record TokenInfo + { + public string? token_type { init; get; } + public string? scope { init; get; } + public int expires_in { init; get; } + public int ext_expires_in { init; get; } + public string? access_token { init; get; } + public string? refresh_token { init; get; } + public string? id_token { init; get; } + public string? error { init; get; } + public string? error_description { init; get; } + } + + private record JwtPayloadInIdToken { - string postData = "client_id={0}&grant_type=refresh_token&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&refresh_token={1}"; - postData = string.Format(postData, clientId, refreshToken); - return RequestToken(postData); + public string? ver { init; get; } + public string? iss { init; get; } + public string? sub { init; get; } + public string? aud { init; get; } + public long exp { init; get; } + public long iat { init; get; } + public long nbf { init; get; } + public string? email { init; get; } + public string? tid { init; get; } + public string? aio { init; get; } } /// @@ -58,45 +105,88 @@ public static LoginResponse RefreshAccessToken(string refreshToken) /// /// Complete POST data for the request /// - private static LoginResponse RequestToken(string postData) + private static async Task RequestTokenAsync(HttpClient httpClient, FormUrlEncodedContent postData) { - var request = new ProxiedWebRequest(tokenUrl) - { - UserAgent = "MCC/" + Program.Version - }; - var response = request.Post("application/x-www-form-urlencoded", postData); - var jsonData = Json.ParseJson(response.Body); + using HttpResponseMessage response = await httpClient.PostAsync(tokenUrl, postData); + + TokenInfo jsonData = (await response.Content.ReadFromJsonAsync())!; // Error handling - if (jsonData.Properties.ContainsKey("error")) + if (!string.IsNullOrEmpty(jsonData.error)) { - throw new Exception(jsonData.Properties["error_description"].StringValue); + throw new Exception(jsonData.error_description); } else { - string accessToken = jsonData.Properties["access_token"].StringValue; - string refreshToken = jsonData.Properties["refresh_token"].StringValue; - int expiresIn = int.Parse(jsonData.Properties["expires_in"].StringValue, NumberStyles.Any, CultureInfo.CurrentCulture); - // Extract email from JWT - string payload = JwtPayloadDecode.GetPayload(jsonData.Properties["id_token"].StringValue); - var jsonPayload = Json.ParseJson(payload); - string email = jsonPayload.Properties["email"].StringValue; + Stream payload = JwtPayloadDecode.GetPayload(jsonData.id_token!); + JwtPayloadInIdToken jsonPayload = (await JsonSerializer.DeserializeAsync(payload))!; + return new LoginResponse() { - Email = email, - AccessToken = accessToken, - RefreshToken = refreshToken, - ExpiresIn = expiresIn + Email = jsonPayload.email!, + AccessToken = jsonData.access_token!, + RefreshToken = jsonData.refresh_token!, + ExpiresIn = jsonData.expires_in, }; } } + private record ProfileKeyResult + { + public KeyPair? keyPair { init; get; } + public string? publicKeySignature { init; get; } + public string? publicKeySignatureV2 { init; get; } + public DateTime expiresAt { init; get; } + public DateTime refreshedAfter { init; get; } + + public record KeyPair + { + public string? privateKey { init; get; } + public string? publicKey { init; get; } + } + } + + /// + /// Request the key to be used for message signing. + /// + /// Access token in session + /// Profile key + public static async Task RequestProfileKeyAsync(HttpClient httpClient, string accessToken) + { + try + { + using HttpRequestMessage request = new(HttpMethod.Post, certificates); + + request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken)); + + using HttpResponseMessage response = await httpClient.SendAsync(request); + + if (Settings.Config.Logging.DebugMessages) + ConsoleIO.WriteLine(response.ToString()); + + ProfileKeyResult jsonData = (await response.Content.ReadFromJsonAsync())!; + + PublicKey publicKey = new(jsonData.keyPair!.publicKey!, jsonData.publicKeySignature, jsonData.publicKeySignatureV2); + + PrivateKey privateKey = new(jsonData.keyPair!.privateKey!); + + return new PlayerKeyPair(publicKey, privateKey, jsonData.expiresAt, jsonData.refreshedAfter); + } + catch (HttpRequestException e) + { + ConsoleIO.WriteLineFormatted("§cFetch profile key failed: " + e.Message); + if (Settings.Config.Logging.DebugMessages) + ConsoleIO.WriteLineFormatted("§c" + e.StackTrace); + return null; + } + } + public static void OpenBrowser(string link) { try { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (OperatingSystem.IsWindows()) { var ps = new ProcessStartInfo(link) { @@ -106,11 +196,11 @@ public static void OpenBrowser(string link) Process.Start(ps); } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + else if (OperatingSystem.IsLinux()) { Process.Start("xdg-open", link); } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + else if (OperatingSystem.IsMacOS()) { Process.Start("open", link); } @@ -134,53 +224,86 @@ public struct LoginResponse } } - static class XboxLive + static partial class XboxLive { - private static readonly string authorize = "https://login.live.com/oauth20_authorize.srf?client_id=000000004C12AE6F&redirect_uri=https://login.live.com/oauth20_desktop.srf&scope=service::user.auth.xboxlive.com::MBI_SSL&display=touch&response_type=token&locale=en"; - private static readonly string xbl = "https://user.auth.xboxlive.com/user/authenticate"; - private static readonly string xsts = "https://xsts.auth.xboxlive.com/xsts/authorize"; + internal const string UserAgent = "Mozilla/5.0 (XboxReplay; XboxLiveAuth/3.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"; - private static readonly string userAgent = "Mozilla/5.0 (XboxReplay; XboxLiveAuth/3.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"; + private const string xsts = "https://xsts.auth.xboxlive.com/xsts/authorize"; + private const string xbl = "https://user.auth.xboxlive.com/user/authenticate"; + private const string authorize = "https://login.live.com/oauth20_authorize.srf?client_id=000000004C12AE6F&redirect_uri=https://login.live.com/oauth20_desktop.srf&scope=service::user.auth.xboxlive.com::MBI_SSL&display=touch&response_type=token&locale=en"; - private static readonly Regex ppft = new("sFTTag:'.*value=\"(.*)\"\\/>'"); - private static readonly Regex urlPost = new("urlPost:'(.+?(?=\'))"); - private static readonly Regex confirm = new("identity\\/confirm"); - private static readonly Regex invalidAccount = new("Sign in to", RegexOptions.IgnoreCase); - private static readonly Regex twoFA = new("Help us protect your account", RegexOptions.IgnoreCase); + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.General) + { + AllowTrailingCommas = true, + PropertyNameCaseInsensitive = false, + ReadCommentHandling = JsonCommentHandling.Skip, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; public static string SignInUrl { get { return authorize; } } + private record AuthPayload + { + public Propertie? Properties { init; get; } + public string? RelyingParty { init; get; } + public string? TokenType { init; get; } + + public record Propertie + { + public string? AuthMethod { init; get; } + public string? SiteName { init; get; } + public string? RpsTicket { init; get; } + public string? SandboxId { init; get; } + public string[]? UserTokens { init; get; } + } + } + + private record AuthResult + { + public DateTime IssueInstant { init; get; } + public DateTime NotAfter { init; get; } + public string? Token { init; get; } + public DisplayClaim? DisplayClaims { init; get; } + + public record DisplayClaim + { + public Dictionary[]? xui { init; get; } + } + } + + private record AuthError + { + public string? Identity { init; get; } + public long XErr { init; get; } + public string? Message { init; get; } + public string? Redirect { init; get; } + } + /// /// Pre-authentication /// /// This step is to get the login page for later use /// - public static PreAuthResponse PreAuth() + public static async Task PreAuthAsync(HttpClient httpClient) { - var request = new ProxiedWebRequest(authorize) - { - UserAgent = userAgent - }; - var response = request.Get(); + using HttpResponseMessage response = await httpClient.GetAsync(authorize); + + string html = await response.Content.ReadAsStringAsync(); - string html = response.Body; + string PPFT = GetPpftRegex().Match(html).Groups[1].Value; - string PPFT = ppft.Match(html).Groups[1].Value; - string urlPost = XboxLive.urlPost.Match(html).Groups[1].Value; + string urlPost = GetUrlPostRegex().Match(html).Groups[1].Value; if (string.IsNullOrEmpty(PPFT) || string.IsNullOrEmpty(urlPost)) { throw new Exception("Fail to extract PPFT or urlPost"); } - //Console.WriteLine("PPFT: {0}", PPFT); - //Console.WriteLine(); - //Console.WriteLine("urlPost: {0}", urlPost); return new PreAuthResponse() { UrlPost = urlPost, PPFT = PPFT, - Cookie = response.Cookies + Cookie = new()// response.Cookies }; } @@ -192,69 +315,54 @@ public static PreAuthResponse PreAuth() /// Account password /// /// - public static Microsoft.LoginResponse UserLogin(string email, string password, PreAuthResponse preAuth) + public static async Task UserLoginAsync(HttpClient httpClient, string email, string password, PreAuthResponse preAuth) { - var request = new ProxiedWebRequest(preAuth.UrlPost, preAuth.Cookie) + FormUrlEncodedContent postData = new(new KeyValuePair[] { - UserAgent = userAgent - }; - - string postData = "login=" + Uri.EscapeDataString(email) - + "&loginfmt=" + Uri.EscapeDataString(email) - + "&passwd=" + Uri.EscapeDataString(password) - + "&PPFT=" + Uri.EscapeDataString(preAuth.PPFT); + new("login", email), + new("loginfmt", email), + new("passwd", password), + new("PPFT", preAuth.PPFT), + }); - var response = request.Post("application/x-www-form-urlencoded", postData); + using HttpResponseMessage response = await httpClient.PostAsync(preAuth.UrlPost, postData); - if (Settings.Config.Logging.DebugMessages) - { + if (Config.Logging.DebugMessages) ConsoleIO.WriteLine(response.ToString()); - } - if (response.StatusCode >= 300 && response.StatusCode <= 399) + if (response.IsSuccessStatusCode) { - string url = response.Headers.Get("Location")!; - string hash = url.Split('#')[1]; - - var request2 = new ProxiedWebRequest(url); - var response2 = request2.Get(); - - if (response2.StatusCode != 200) - { - throw new Exception("Authentication failed"); - } + string hash = response.RequestMessage!.RequestUri!.Fragment[1..]; if (string.IsNullOrEmpty(hash)) - { throw new Exception("Cannot extract access token"); - } - var dict = Request.ParseQueryString(hash); - //foreach (var pair in dict) - //{ - // Console.WriteLine("{0}: {1}", pair.Key, pair.Value); - //} + var dict = Request.ParseQueryString(hash); return new Microsoft.LoginResponse() { Email = email, AccessToken = dict["access_token"], RefreshToken = dict["refresh_token"], - ExpiresIn = int.Parse(dict["expires_in"], NumberStyles.Any, CultureInfo.CurrentCulture) + ExpiresIn = int.Parse(dict["expires_in"]) }; } else { - if (twoFA.IsMatch(response.Body)) + string body = await response.Content.ReadAsStringAsync(); + if (GetTwoFARegex().IsMatch(body)) { // TODO: Handle 2FA throw new Exception("2FA enabled but not supported yet. Use browser sign-in method or try to disable 2FA in Microsoft account settings"); } - else if (invalidAccount.IsMatch(response.Body)) + else if (GetInvalidAccountRegex().IsMatch(body)) { throw new Exception("Invalid credentials. Check your credentials"); } - else throw new Exception("Unexpected response. Check your credentials. Response code: " + response.StatusCode); + else + { + throw new Exception("Unexpected response. Check your credentials. Response code: " + response.StatusCode); + } } } @@ -263,54 +371,54 @@ public static Microsoft.LoginResponse UserLogin(string email, string password, P /// /// /// - public static XblAuthenticateResponse XblAuthenticate(Microsoft.LoginResponse loginResponse) + public static async Task XblAuthenticateAsync(HttpClient httpClient, Microsoft.LoginResponse loginResponse) { - var request = new ProxiedWebRequest(xbl) - { - UserAgent = userAgent, - Accept = "application/json" - }; - request.Headers.Add("x-xbl-contract-version", "0"); - - var accessToken = loginResponse.AccessToken; + string accessToken; if (Config.Main.General.Method == LoginMethod.browser) { // Our own client ID must have d= in front of the token or HTTP status 400 // "Stolen" client ID must not have d= in front of the token or HTTP status 400 - accessToken = "d=" + accessToken; + accessToken = "d=" + loginResponse.AccessToken; + } + else + { + accessToken = loginResponse.AccessToken; } - string payload = "{" - + "\"Properties\": {" - + "\"AuthMethod\": \"RPS\"," - + "\"SiteName\": \"user.auth.xboxlive.com\"," - + "\"RpsTicket\": \"" + accessToken + "\"" - + "}," - + "\"RelyingParty\": \"http://auth.xboxlive.com\"," - + "\"TokenType\": \"JWT\"" - + "}"; - var response = request.Post("application/json", payload); - if (Settings.Config.Logging.DebugMessages) + AuthPayload payload = new() { + Properties = new AuthPayload.Propertie() + { + AuthMethod = "RPS", + SiteName = "user.auth.xboxlive.com", + RpsTicket = accessToken, + }, + RelyingParty = "http://auth.xboxlive.com", + TokenType = "JWT", + }; + + using StringContent httpContent = new(JsonSerializer.Serialize(payload, JsonOptions), Encoding.UTF8, "application/json"); + + httpContent.Headers.Add("x-xbl-contract-version", "0"); + + using HttpResponseMessage response = await httpClient.PostAsync(xbl, httpContent); + + if (Config.Logging.DebugMessages) ConsoleIO.WriteLine(response.ToString()); - } - if (response.StatusCode == 200) + + if (response.IsSuccessStatusCode) { - string jsonString = response.Body; - //Console.WriteLine(jsonString); + AuthResult jsonData = (await response.Content.ReadFromJsonAsync())!; - Json.JSONData json = Json.ParseJson(jsonString); - string token = json.Properties["Token"].StringValue; - string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue; return new XblAuthenticateResponse() { - Token = token, - UserHash = userHash + Token = jsonData.Token!, + UserHash = jsonData.DisplayClaims!.xui![0]["uhs"], }; } else { - throw new Exception("XBL Authentication failed"); + throw new Exception("XBL Authentication failed, code = " + response.StatusCode.ToString()); } } @@ -320,56 +428,53 @@ public static XblAuthenticateResponse XblAuthenticate(Microsoft.LoginResponse lo /// (Don't ask me what is XSTS, I DONT KNOW) /// /// - public static XSTSAuthenticateResponse XSTSAuthenticate(XblAuthenticateResponse xblResponse) + public static async Task XSTSAuthenticateAsync(HttpClient httpClient, XblAuthenticateResponse xblResponse) { - var request = new ProxiedWebRequest(xsts) + AuthPayload payload = new() { - UserAgent = userAgent, - Accept = "application/json" + Properties = new AuthPayload.Propertie() + { + SandboxId = "RETAIL", + UserTokens = new string[] { xblResponse.Token }, + }, + RelyingParty = "rp://api.minecraftservices.com/", + TokenType = "JWT", }; - request.Headers.Add("x-xbl-contract-version", "1"); - - string payload = "{" - + "\"Properties\": {" - + "\"SandboxId\": \"RETAIL\"," - + "\"UserTokens\": [" - + "\"" + xblResponse.Token + "\"" - + "]" - + "}," - + "\"RelyingParty\": \"rp://api.minecraftservices.com/\"," - + "\"TokenType\": \"JWT\"" - + "}"; - var response = request.Post("application/json", payload); - if (Settings.Config.Logging.DebugMessages) - { + + using StringContent httpContent = new(JsonSerializer.Serialize(payload, JsonOptions), Encoding.UTF8, "application/json"); + + httpContent.Headers.Add("x-xbl-contract-version", "1"); + + using HttpResponseMessage response = await httpClient.PostAsync(xsts, httpContent); + + if (Config.Logging.DebugMessages) ConsoleIO.WriteLine(response.ToString()); - } - if (response.StatusCode == 200) + + if (response.IsSuccessStatusCode) { - string jsonString = response.Body; - Json.JSONData json = Json.ParseJson(jsonString); - string token = json.Properties["Token"].StringValue; - string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue; + AuthResult jsonData = (await response.Content.ReadFromJsonAsync())!; + return new XSTSAuthenticateResponse() { - Token = token, - UserHash = userHash + Token = jsonData.Token!, + UserHash = jsonData.DisplayClaims!.xui![0]["uhs"], }; } else { - if (response.StatusCode == 401) + if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { - Json.JSONData json = Json.ParseJson(response.Body); - if (json.Properties["XErr"].StringValue == "2148916233") - { + AuthError jsonData = (await response.Content.ReadFromJsonAsync())!; + if (jsonData.XErr == 2148916233) throw new Exception("The account doesn't have an Xbox account"); - } - else if (json.Properties["XErr"].StringValue == "2148916238") - { + else if (jsonData.XErr == 2148916235) + throw new Exception("The account is from a country where Xbox Live is not available/banned"); + else if (jsonData.XErr == 2148916236 || jsonData.XErr == 2148916237) + throw new Exception("The account needs adult verification on Xbox page. (South Korea)"); + else if (jsonData.XErr == 2148916238) throw new Exception("The account is a child (under 18) and cannot proceed unless the account is added to a Family by an adult"); - } - else throw new Exception("Unknown XSTS error code: " + json.Properties["XErr"].StringValue); + else + throw new Exception("Unknown XSTS error code: " + jsonData.XErr.ToString() + ", Check " + jsonData.Redirect); } else { @@ -396,13 +501,63 @@ public struct XSTSAuthenticateResponse public string Token; public string UserHash; } + + [GeneratedRegex("sFTTag:'.*value=\"(.*)\"\\/>'")] + private static partial Regex GetPpftRegex(); + + [GeneratedRegex("urlPost:'(.+?(?='))")] + private static partial Regex GetUrlPostRegex(); + + [GeneratedRegex("identity\\/confirm")] + private static partial Regex GetConfirmRegex(); + + [GeneratedRegex("Sign in to", RegexOptions.IgnoreCase, "zh-CN")] + private static partial Regex GetInvalidAccountRegex(); + + [GeneratedRegex("Help us protect your account", RegexOptions.IgnoreCase, "zh-CN")] + private static partial Regex GetTwoFARegex(); } static class MinecraftWithXbox { - private static readonly string loginWithXbox = "https://api.minecraftservices.com/authentication/login_with_xbox"; - private static readonly string ownership = "https://api.minecraftservices.com/entitlements/mcstore"; - private static readonly string profile = "https://api.minecraftservices.com/minecraft/profile"; + private const string profile = "https://api.minecraftservices.com/minecraft/profile"; + private const string ownership = "https://api.minecraftservices.com/entitlements/mcstore"; + private const string loginWithXbox = "https://api.minecraftservices.com/authentication/login_with_xbox"; + + private record LoginPayload + { + public string? identityToken { init; get; } + } + + private record LoginResult + { + public string? username { init; get; } + public string[]? roles { init; get; } + public string? access_token { init; get; } + public string? token_type { init; get; } + public int expires_in { init; get; } + } + + private record GameOwnershipResult + { + public Dictionary[]? items { init; get; } + public string? signature { init; get; } + public string? keyId { init; get; } + } + + private record GameProfileResult + { + public string? id { init; get; } + public string? name { init; get; } + public Dictionary[]? skins { init; get; } + public Dictionary[]? capes { init; get; } + /* Error */ + public string? path { init; get; } + public string? errorType { init; get; } + public string? error { init; get; } + public string? errorMessage { init; get; } + public string? developerMessage { init; get; } + } /// /// Login to Minecraft using the XSTS token and user hash obtained before @@ -410,25 +565,23 @@ static class MinecraftWithXbox /// /// /// - public static string LoginWithXbox(string userHash, string xstsToken) + public static async Task LoginWithXboxAsync(HttpClient httpClient, string userHash, string xstsToken) { - var request = new ProxiedWebRequest(loginWithXbox) + LoginPayload payload = new() { - Accept = "application/json" + identityToken = $"XBL3.0 x={userHash};{xstsToken}", }; - string payload = "{\"identityToken\": \"XBL3.0 x=" + userHash + ";" + xstsToken + "\"}"; - var response = request.Post("application/json", payload); + using StringContent httpContent = new(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); - if (Settings.Config.Logging.DebugMessages) - { + using HttpResponseMessage response = await httpClient.PostAsync(loginWithXbox, httpContent); + + if (Config.Logging.DebugMessages) ConsoleIO.WriteLine(response.ToString()); - } - string jsonString = response.Body; - Json.JSONData json = Json.ParseJson(jsonString); + LoginResult jsonData = (await response.Content.ReadFromJsonAsync())!; - return json.Properties["access_token"].StringValue; + return jsonData.access_token!; } /// @@ -436,39 +589,40 @@ public static string LoginWithXbox(string userHash, string xstsToken) /// /// /// True if the user own the game - public static bool UserHasGame(string accessToken) + public static async Task CheckUserHasGameAsync(HttpClient httpClient, string accessToken) { - var request = new ProxiedWebRequest(ownership); + using HttpRequestMessage request = new(HttpMethod.Get, ownership); request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken)); - var response = request.Get(); - if (Settings.Config.Logging.DebugMessages) - { + using HttpResponseMessage response = await httpClient.SendAsync(request); + + if (Config.Logging.DebugMessages) ConsoleIO.WriteLine(response.ToString()); - } - string jsonString = response.Body; - Json.JSONData json = Json.ParseJson(jsonString); - return json.Properties["items"].DataArray.Count > 0; + GameOwnershipResult jsonData = (await response.Content.ReadFromJsonAsync())!; + + return jsonData.items!.Length > 0; } - public static UserProfile GetUserProfile(string accessToken) + public static async Task GetUserProfileAsync(HttpClient httpClient, string accessToken) { - var request = new ProxiedWebRequest(profile); + using HttpRequestMessage request = new(HttpMethod.Get, profile); request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken)); - var response = request.Get(); - if (Settings.Config.Logging.DebugMessages) - { + using HttpResponseMessage response = await httpClient.SendAsync(request); + + if (Config.Logging.DebugMessages) ConsoleIO.WriteLine(response.ToString()); - } - string jsonString = response.Body; - Json.JSONData json = Json.ParseJson(jsonString); + GameProfileResult jsonData = (await response.Content.ReadFromJsonAsync())!; + + if (!string.IsNullOrEmpty(jsonData.error)) + throw new Exception($"{jsonData.errorType}: {jsonData.error}. {jsonData.errorMessage}"); + return new UserProfile() { - UUID = json.Properties["id"].StringValue, - UserName = json.Properties["name"].StringValue + UUID = jsonData.id!, + UserName = jsonData.name!, }; } diff --git a/MinecraftClient/Protocol/PacketPipeline/AesStream.cs b/MinecraftClient/Protocol/PacketPipeline/AesStream.cs new file mode 100644 index 0000000000..63093819aa --- /dev/null +++ b/MinecraftClient/Protocol/PacketPipeline/AesStream.cs @@ -0,0 +1,213 @@ +using System; +using System.IO; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using MinecraftClient.Crypto; +using MinecraftClient.Crypto.AesHandler; +using static ConsoleInteractive.ConsoleReader; + +namespace MinecraftClient.Protocol.PacketPipeline +{ + public class AesStream : Stream + { + public const int BlockSize = 16; + private const int BufferSize = 1024; + + public Socket Client; + public bool HwAccelerateEnable { init; get; } + private bool inStreamEnded = false; + + private readonly IAesHandler Aes; + + private int InputBufPos = 0, OutputBufPos = 0; + private readonly Memory InputBuf, OutputBuf; + private readonly Memory AesBufRead, AesBufSend; + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length => throw new NotSupportedException(); + + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + public AesStream(Socket socket, byte[] key) + { + Client = socket; + + InputBuf = new byte[BufferSize + BlockSize]; + OutputBuf = new byte[BufferSize + BlockSize]; + + AesBufRead = new byte[BlockSize]; + AesBufSend = new byte[BlockSize]; + + if (FasterAesX86.IsSupported()) + { + HwAccelerateEnable = true; + Aes = new FasterAesX86(key); + } + else if (false && FasterAesArm.IsSupported()) // Further testing required + { + HwAccelerateEnable = true; + Aes = new FasterAesArm(key); + } + else + { + HwAccelerateEnable = false; + Aes = new BasicAes(key); + } + + key.CopyTo(InputBuf.Slice(0, BlockSize)); + key.CopyTo(OutputBuf.Slice(0, BlockSize)); + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + var task = ReadAsync(buffer.AsMemory(offset, count)).AsTask(); + task.Wait(); + return task.Result; + } + + public override int ReadByte() + { + if (inStreamEnded) + return -1; + + var task = Client.ReceiveAsync(InputBuf.Slice(InputBufPos + BlockSize, 1)).AsTask(); + task.Wait(); + if (task.Result == 0) + { + inStreamEnded = true; + return -1; + } + + Aes.EncryptEcb(InputBuf.Slice(InputBufPos, BlockSize).Span, AesBufRead.Span); + byte result = (byte)(AesBufRead.Span[0] ^ InputBuf.Span[InputBufPos + BlockSize]); + + InputBufPos++; + if (InputBufPos == BufferSize) + { + InputBuf.Slice(BufferSize, BlockSize).CopyTo(InputBuf[..BlockSize]); + InputBufPos = 0; + } + + return result; + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (inStreamEnded) + return 0; + + int readLimit = Math.Min(buffer.Length, BufferSize - InputBufPos); + int curRead = await Client.ReceiveAsync(InputBuf.Slice(InputBufPos + BlockSize, readLimit), cancellationToken); + + if (curRead == 0 || cancellationToken.IsCancellationRequested) + { + if (curRead == 0) + inStreamEnded = true; + return curRead; + } + + for (int idx = 0; idx < curRead; idx++) + { + Aes.EncryptEcb(InputBuf.Slice(InputBufPos + idx, BlockSize).Span, AesBufRead.Span); + buffer.Span[idx] = (byte)(AesBufRead.Span[0] ^ InputBuf.Span[InputBufPos + BlockSize + idx]); + } + + InputBufPos += curRead; + if (InputBufPos == BufferSize) + { + InputBuf.Slice(BufferSize, BlockSize).CopyTo(InputBuf[..BlockSize]); + InputBufPos = 0; + } + + return curRead; + } + + public new async ValueTask ReadExactlyAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (inStreamEnded) + return; + + for (int readed = 0, curRead; readed < buffer.Length; readed += curRead) + { + int readLimit = Math.Min(buffer.Length - readed, BufferSize - InputBufPos); + curRead = await Client.ReceiveAsync(InputBuf.Slice(InputBufPos + BlockSize, readLimit), cancellationToken); + + if (curRead == 0 || cancellationToken.IsCancellationRequested) + { + if (curRead == 0) + inStreamEnded = true; + return; + } + + for (int idx = 0; idx < curRead; idx++) + { + Aes.EncryptEcb(InputBuf.Slice(InputBufPos + idx, BlockSize).Span, AesBufRead.Span); + buffer.Span[readed + idx] = (byte)(AesBufRead.Span[0] ^ InputBuf.Span[InputBufPos + BlockSize + idx]); + } + + InputBufPos += curRead; + if (InputBufPos == BufferSize) + { + InputBuf.Slice(BufferSize, BlockSize).CopyTo(InputBuf.Slice(0, BlockSize)); + InputBufPos = 0; + } + } + } + + public async ValueTask ReadRawAsync(Memory buffer, CancellationToken cancellationToken = default) + { + return await Client.ReceiveAsync(buffer, cancellationToken); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + WriteAsync(buffer.AsMemory(offset, count)).AsTask().Wait(); + } + + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + int outputStartPos = OutputBufPos; + for (int wirtten = 0; wirtten < buffer.Length; ++wirtten) + { + if (cancellationToken.IsCancellationRequested) + return; + + Aes.EncryptEcb(OutputBuf.Slice(OutputBufPos, BlockSize).Span, AesBufSend.Span); + OutputBuf.Span[OutputBufPos + BlockSize] = (byte)(AesBufSend.Span[0] ^ buffer.Span[wirtten]); + + if (++OutputBufPos == BufferSize) + { + await Client.SendAsync(OutputBuf.Slice(outputStartPos + BlockSize, BufferSize - outputStartPos), cancellationToken); + OutputBuf.Slice(BufferSize, BlockSize).CopyTo(OutputBuf.Slice(0, BlockSize)); + OutputBufPos = outputStartPos = 0; + } + } + + if (OutputBufPos > outputStartPos) + await Client.SendAsync(OutputBuf.Slice(outputStartPos + BlockSize, OutputBufPos - outputStartPos), cancellationToken); + + return; + } + } +} diff --git a/MinecraftClient/Protocol/PacketPipeline/PacketStream.cs b/MinecraftClient/Protocol/PacketPipeline/PacketStream.cs new file mode 100644 index 0000000000..ece8b07499 --- /dev/null +++ b/MinecraftClient/Protocol/PacketPipeline/PacketStream.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using static ConsoleInteractive.ConsoleReader; + +namespace MinecraftClient.Protocol.PacketPipeline +{ + internal class PacketStream : Stream + { + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length => throw new NotSupportedException(); + + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + readonly CancellationToken CancelToken; + + private readonly Stream baseStream; + private readonly AesStream? aesStream; + private ZLibStream? zlibStream; + + private int packetSize, packetReaded; + + internal const int DropBufSize = 1024; + internal static readonly Memory DropBuf = new byte[DropBufSize]; + + private static readonly byte[] SingleByteBuf = new byte[1]; + + public PacketStream(ZLibStream zlibStream, int packetSize, CancellationToken cancellationToken = default) + { + CancelToken = cancellationToken; + + this.aesStream = null; + this.zlibStream = zlibStream; + this.baseStream = zlibStream; + + this.packetReaded = 0; + this.packetSize = packetSize; + } + + public PacketStream(AesStream aesStream, int packetSize, CancellationToken cancellationToken = default) + { + CancelToken = cancellationToken; + + this.aesStream = aesStream; + this.zlibStream = null; + this.baseStream = aesStream; + + this.packetReaded = 0; + this.packetSize = packetSize; + } + + public PacketStream(Stream baseStream, int packetSize, CancellationToken cancellationToken = default) + { + CancelToken = cancellationToken; + + this.aesStream = null; + this.zlibStream = null; + this.baseStream = baseStream; + + this.packetReaded = 0; + this.packetSize = packetSize; + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public new byte ReadByte() + { + ++packetReaded; + if (packetReaded > packetSize) + throw new OverflowException("Reach the end of the packet!"); + baseStream.Read(SingleByteBuf, 0, 1); + return SingleByteBuf[0]; + } + + public async Task ReadByteAsync() + { + ++packetReaded; + if (packetReaded > packetSize) + throw new OverflowException("Reach the end of the packet!"); + await baseStream.ReadExactlyAsync(SingleByteBuf, CancelToken); + return SingleByteBuf[0]; + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (packetReaded + buffer.Length > packetSize) + throw new OverflowException("Reach the end of the packet!"); + int readed = baseStream.Read(buffer, offset, count); + packetReaded += readed; + return readed; + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (packetReaded + buffer.Length > packetSize) + throw new OverflowException("Reach the end of the packet!"); + int readed = await baseStream.ReadAsync(buffer, CancelToken); + packetReaded += readed; + return readed; + } + + public new async ValueTask ReadExactlyAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (packetReaded + buffer.Length > packetSize) + throw new OverflowException("Reach the end of the packet!"); + await baseStream.ReadExactlyAsync(buffer, CancelToken); + packetReaded += buffer.Length; + } + + public async Task ReadFullPacket() + { + byte[] buffer = new byte[packetSize - packetReaded]; + await ReadExactlyAsync(buffer); + packetReaded = packetSize; + return buffer; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public async Task Skip(int length) + { + if (zlibStream != null) + { + for (int readed = 0, curRead; readed < length; readed += curRead) + curRead = await zlibStream.ReadAsync(DropBuf[..Math.Min(DropBufSize, length - readed)]); + } + else if (aesStream != null) + { + int skipRaw = length - AesStream.BlockSize; + for (int readed = 0, curRead; readed < skipRaw; readed += curRead) + curRead = await aesStream.ReadRawAsync(DropBuf[..Math.Min(DropBufSize, skipRaw - readed)]); + await aesStream.ReadAsync(DropBuf[..Math.Min(length, AesStream.BlockSize)]); + } + else + { + for (int readed = 0, curRead; readed < length; readed += curRead) + curRead = await baseStream.ReadAsync(DropBuf[..Math.Min(DropBufSize, length - readed)]); + } + packetReaded += length; + } + + public override async ValueTask DisposeAsync() + { + if (CancelToken.IsCancellationRequested) + return; + + if (zlibStream != null) + { + await zlibStream.DisposeAsync(); + zlibStream = null; + packetReaded = packetSize; + } + else + { + if (packetSize - packetReaded > 0) + { + // ConsoleIO.WriteLine("Plain readed " + packetReaded + ", last " + (packetSize - packetReaded)); + await Skip(packetSize - packetReaded); + } + } + } + } +} diff --git a/MinecraftClient/Protocol/PacketPipeline/SocketWrapper.cs b/MinecraftClient/Protocol/PacketPipeline/SocketWrapper.cs new file mode 100644 index 0000000000..b96098eea8 --- /dev/null +++ b/MinecraftClient/Protocol/PacketPipeline/SocketWrapper.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using MinecraftClient.Crypto; + +namespace MinecraftClient.Protocol.PacketPipeline +{ + /// + /// Wrapper for handling unencrypted & encrypted socket + /// + class SocketWrapper + { + private TcpClient tcpClient; + + private AesStream? AesStream; + + private PacketStream? packetStream = null; + + private Stream ReadStream, WriteStream; + + private bool Encrypted = false; + + public int CompressionThreshold { get; set; } = 0; + + + private SemaphoreSlim SendSemaphore = new(1, 1); + + private Task LastSendTask = Task.CompletedTask; + + /// + /// Initialize a new SocketWrapper + /// + /// TcpClient connected to the server + public SocketWrapper(TcpClient client) + { + tcpClient = client; + ReadStream = WriteStream = client.GetStream(); + } + + /// + /// Check if the socket is still connected + /// + /// TRUE if still connected + /// Silently dropped connection can only be detected by attempting to read/write data + public bool IsConnected() + { + return tcpClient.Client != null && tcpClient.Connected; + } + + /// + /// Check if the socket has data available to read + /// + /// TRUE if data is available to read + public bool HasDataAvailable() + { + return tcpClient.Client.Available > 0; + } + + /// + /// Switch network reading/writing to an encrypted stream + /// + /// AES secret key + public void SwitchToEncrypted(byte[] secretKey) + { + if (Encrypted) + throw new InvalidOperationException("Stream is already encrypted!?"); + Encrypted = true; + ReadStream = WriteStream = AesStream = new AesStream(tcpClient.Client, secretKey); + } + + /// + /// Send raw data to the server. + /// + /// data to send + public async Task SendAsync(Memory buffer, CancellationToken cancellationToken = default) + { + await SendSemaphore.WaitAsync(cancellationToken); + if (cancellationToken.IsCancellationRequested) return; + await LastSendTask; + LastSendTask = WriteStream.WriteAsync(buffer, cancellationToken).AsTask(); + SendSemaphore.Release(); + } + + public async Task> GetNextPacket(bool handleCompress, CancellationToken cancellationToken = default) + { + if (packetStream != null) + { + await packetStream.DisposeAsync(); + packetStream = null; + } + + int readed = 0; + (int packetSize, _) = await ReceiveVarIntRaw(ReadStream, cancellationToken); + + int packetID; + if (handleCompress && CompressionThreshold > 0) + { + (int sizeUncompressed, readed) = await ReceiveVarIntRaw(ReadStream, cancellationToken); + if (sizeUncompressed != 0) + { + ZlibBaseStream zlibBaseStream = new(AesStream ?? ReadStream, packetSize: packetSize - readed); + ZLibStream zlibStream = new(zlibBaseStream, CompressionMode.Decompress, leaveOpen: false); + + if (AesStream == null || AesStream.HwAccelerateEnable) + { + zlibBaseStream.BufferSize = 64; + (packetID, readed) = await ReceiveVarIntRaw(zlibStream, cancellationToken); + zlibBaseStream.BufferSize = 1024; + } + else + { + zlibBaseStream.BufferSize = 16; + (packetID, readed) = await ReceiveVarIntRaw(zlibStream, cancellationToken); + zlibBaseStream.BufferSize = 256; + } + + // ConsoleIO.WriteLine("packetID = " + packetID + ", readed = " + zlibBaseStream.packetReaded + ", size = " + packetSize + " -> " + sizeUncompressed); + + packetStream = new(zlibStream, sizeUncompressed - readed, cancellationToken); + + return new(packetID, packetStream); + } + } + + (packetID, int readed2) = await ReceiveVarIntRaw(ReadStream, cancellationToken); + + packetStream = new(AesStream ?? ReadStream, packetSize - readed - readed2, cancellationToken); + + return new(packetID, packetStream); + } + + private async Task> ReceiveVarIntRaw(Stream stream, CancellationToken cancellationToken = default) + { + int i = 0; + int j = 0; + byte[] b = new byte[1]; + while (true) + { + await stream.ReadAsync(b, cancellationToken); + i |= (b[0] & 0x7F) << j++ * 7; + if (j > 5) throw new OverflowException("VarInt too big"); + if ((b[0] & 0x80) != 128) break; + } + return new(i, j); + } + + /// + /// Disconnect from the server + /// + public void Disconnect() + { + try + { + tcpClient.Close(); + } + catch (SocketException) { } + catch (IOException) { } + catch (NullReferenceException) { } + catch (ObjectDisposedException) { } + } + } +} diff --git a/MinecraftClient/Protocol/PacketPipeline/ZlibBaseStream.cs b/MinecraftClient/Protocol/PacketPipeline/ZlibBaseStream.cs new file mode 100644 index 0000000000..4eeecba1f8 --- /dev/null +++ b/MinecraftClient/Protocol/PacketPipeline/ZlibBaseStream.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MinecraftClient.Crypto; +using static ConsoleInteractive.ConsoleReader; + +namespace MinecraftClient.Protocol.PacketPipeline +{ + internal class ZlibBaseStream : Stream + { + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length => throw new NotSupportedException(); + + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + public int BufferSize { get; set; } = 16; + public int packetSize = 0, packetReaded = 0; + + private Stream baseStream; + private AesStream? aesStream; + + public ZlibBaseStream(Stream baseStream, int packetSize) + { + packetReaded = 0; + this.packetSize = packetSize; + this.baseStream = baseStream; + aesStream = null; + } + + public ZlibBaseStream(AesStream aesStream, int packetSize) + { + packetReaded = 0; + this.packetSize = packetSize; + baseStream = this.aesStream = aesStream; + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (packetReaded == packetSize) + return 0; + int readed = baseStream.Read(buffer, offset, Math.Min(BufferSize, Math.Min(count, packetSize - packetReaded))); + packetReaded += readed; + return readed; + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + int readLen = Math.Min(BufferSize, Math.Min(buffer.Length, packetSize - packetReaded)); + if (packetReaded + readLen > packetSize) + throw new OverflowException("Reach the end of the packet!"); + await baseStream.ReadExactlyAsync(buffer[..readLen], cancellationToken); + packetReaded += readLen; + return readLen; + } + + public new async ValueTask ReadExactlyAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (packetReaded + buffer.Length > packetSize) + throw new OverflowException("Reach the end of the packet!"); + await baseStream.ReadExactlyAsync(buffer, cancellationToken); + packetReaded += buffer.Length; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public async Task Skip(int length) + { + if (aesStream != null) + { + int skipRaw = length - AesStream.BlockSize; + for (int readed = 0, curRead; readed < skipRaw; readed += curRead) + curRead = await aesStream.ReadRawAsync(PacketStream.DropBuf[..Math.Min(PacketStream.DropBufSize, skipRaw - readed)]); + await aesStream.ReadAsync(PacketStream.DropBuf[..Math.Min(length, AesStream.BlockSize)]); + } + else + { + for (int readed = 0, curRead; readed < length; readed += curRead) + curRead = await baseStream.ReadAsync(PacketStream.DropBuf[..Math.Min(PacketStream.DropBufSize, length - readed)]); + } + packetReaded += length; + } + + public override async ValueTask DisposeAsync() + { + if (packetSize - packetReaded > 0) + { + // ConsoleIO.WriteLine("Zlib readed " + packetReaded + ", last " + (packetSize - packetReaded)); + await Skip(packetSize - packetReaded); + } + } + } +} diff --git a/MinecraftClient/Protocol/PlayerInfo.cs b/MinecraftClient/Protocol/PlayerInfo.cs index 31e4300e97..ab782d0c9b 100644 --- a/MinecraftClient/Protocol/PlayerInfo.cs +++ b/MinecraftClient/Protocol/PlayerInfo.cs @@ -22,7 +22,7 @@ public class PlayerInfo // Entity info - public Mapping.Entity? entity; + public EntityHandler.Entity? entity; // For message signature diff --git a/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs b/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs index fdd685606f..3f1dfbd140 100644 --- a/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs +++ b/MinecraftClient/Protocol/ProfileKey/KeyUtils.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Security.Cryptography; using System.Text; +using System.Threading.Tasks; using MinecraftClient.Protocol.Message; namespace MinecraftClient.Protocol.ProfileKey @@ -10,51 +11,6 @@ static class KeyUtils { private static readonly SHA256 sha256Hash = SHA256.Create(); - private static readonly string certificates = "https://api.minecraftservices.com/player/certificates"; - - public static PlayerKeyPair? GetNewProfileKeys(string accessToken) - { - ProxiedWebRequest.Response? response = null; - try - { - var request = new ProxiedWebRequest(certificates) - { - Accept = "application/json" - }; - request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken)); - - response = request.Post("application/json", ""); - - if (Settings.Config.Logging.DebugMessages) - { - ConsoleIO.WriteLine(response.Body.ToString()); - } - - string jsonString = response.Body; - Json.JSONData json = Json.ParseJson(jsonString); - - PublicKey publicKey = new(pemKey: json.Properties["keyPair"].Properties["publicKey"].StringValue, - sig: json.Properties["publicKeySignature"].StringValue, - sigV2: json.Properties["publicKeySignatureV2"].StringValue); - - PrivateKey privateKey = new(pemKey: json.Properties["keyPair"].Properties["privateKey"].StringValue); - - return new PlayerKeyPair(publicKey, privateKey, - expiresAt: json.Properties["expiresAt"].StringValue, - refreshedAfter: json.Properties["refreshedAfter"].StringValue); - } - catch (Exception e) - { - int code = response == null ? 0 : response.StatusCode; - ConsoleIO.WriteLineFormatted("§cFetch profile key failed: HttpCode = " + code + ", Error = " + e.Message); - if (Settings.Config.Logging.DebugMessages) - { - ConsoleIO.WriteLineFormatted("§c" + e.StackTrace); - } - return null; - } - } - public static byte[] DecodePemKey(string key, string prefix, string suffix) { int i = key.IndexOf(prefix); diff --git a/MinecraftClient/Protocol/ProfileKey/KeysCache.cs b/MinecraftClient/Protocol/ProfileKey/KeysCache.cs deleted file mode 100644 index a6ea062001..0000000000 --- a/MinecraftClient/Protocol/ProfileKey/KeysCache.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.Serialization.Formatters.Binary; -using System.Timers; -using static MinecraftClient.Settings; -using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig; - -namespace MinecraftClient.Protocol.ProfileKey -{ - /// - /// Handle keys caching and storage. - /// - public static class KeysCache - { - private const string KeysCacheFilePlaintext = "ProfileKeyCache.ini"; - - private static FileMonitor? cachemonitor; - private static readonly Dictionary keys = new(); - private static readonly Timer updatetimer = new(100); - private static readonly List> pendingadds = new(); - private static readonly BinaryFormatter formatter = new(); - - /// - /// Retrieve whether KeysCache contains a keys for the given login. - /// - /// User login used with Minecraft.net - /// TRUE if keys are available - public static bool Contains(string login) - { - return keys.ContainsKey(login); - } - - /// - /// Store keys and save it to disk if required. - /// - /// User login used with Minecraft.net - /// User keys - public static void Store(string login, PlayerKeyPair playerKeyPair) - { - if (Contains(login)) - { - keys[login] = playerKeyPair; - } - else - { - keys.Add(login, playerKeyPair); - } - - if (Config.Main.Advanced.ProfileKeyCache == CacheType.disk && updatetimer.Enabled == true) - { - pendingadds.Add(new KeyValuePair(login, playerKeyPair)); - } - else if (Config.Main.Advanced.ProfileKeyCache == CacheType.disk) - { - SaveToDisk(); - } - } - - /// - /// Retrieve keys for the given login. - /// - /// User login used with Minecraft.net - /// PlayerKeyPair for given login - public static PlayerKeyPair Get(string login) - { - return keys[login]; - } - - /// - /// Initialize cache monitoring to keep cache updated with external changes. - /// - /// TRUE if keys are seeded from file - public static bool InitializeDiskCache() - { - cachemonitor = new FileMonitor(AppDomain.CurrentDomain.BaseDirectory, KeysCacheFilePlaintext, new FileSystemEventHandler(OnChanged)); - updatetimer.Elapsed += HandlePending; - return LoadFromDisk(); - } - - /// - /// Reloads cache on external cache file change. - /// - /// Sender - /// Event data - private static void OnChanged(object sender, FileSystemEventArgs e) - { - updatetimer.Stop(); - updatetimer.Start(); - } - - /// - /// Called after timer elapsed. Reads disk cache and adds new/modified keys back. - /// - /// Sender - /// Event data - private static void HandlePending(object? sender, ElapsedEventArgs e) - { - updatetimer.Stop(); - LoadFromDisk(); - - foreach (KeyValuePair pending in pendingadds.ToArray()) - { - Store(pending.Key, pending.Value); - pendingadds.Remove(pending); - } - } - - /// - /// Reads cache file and loads KeysInfos into KeysCache. - /// - /// True if data is successfully loaded - private static bool LoadFromDisk() - { - //User-editable keys cache file in text format - if (File.Exists(KeysCacheFilePlaintext)) - { - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_keys, KeysCacheFilePlaintext)); - - try - { - foreach (string line in FileMonitor.ReadAllLinesWithRetries(KeysCacheFilePlaintext)) - { - if (!line.TrimStart().StartsWith("#")) - { - - int separatorIdx = line.IndexOf('='); - if (separatorIdx >= 1 && line.Length > separatorIdx + 1) - { - string login = line[..separatorIdx]; - string value = line[(separatorIdx + 1)..]; - try - { - PlayerKeyPair playerKeyPair = PlayerKeyPair.FromString(value); - keys[login] = playerKeyPair; - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded_keys, playerKeyPair.ExpiresAt.ToString())); - } - catch (InvalidDataException e) - { - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string_keys, value, e.Message)); - } - catch (FormatException e) - { - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string_keys, value, e.Message)); - } - catch (ArgumentNullException e) - { - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string_keys, value, e.Message)); - - } - } - else if (Config.Logging.DebugMessages) - { - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_line_keys, line)); - } - } - } - } - catch (IOException e) - { - ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain_keys, e.Message)); - } - } - - return keys.Count > 0; - } - - /// - /// Saves player's keypair from KeysCache into cache file. - /// - private static void SaveToDisk() - { - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted("§8" + Translations.cache_saving_keys, acceptnewlines: true); - - List KeysCacheLines = new() - { - "# Generated by MCC v" + Program.Version + " - Keep it secret & Edit at own risk!", - "# ProfileKey=PublicKey(base64),PublicKeySignature(base64),PublicKeySignatureV2(base64),PrivateKey(base64),ExpiresAt,RefreshAfter" - }; - foreach (KeyValuePair entry in keys) - KeysCacheLines.Add(entry.Key + '=' + entry.Value.ToString()); - - try - { - FileMonitor.WriteAllLinesWithRetries(KeysCacheFilePlaintext, KeysCacheLines); - } - catch (IOException e) - { - ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail_keys, e.Message)); - } - } - } -} diff --git a/MinecraftClient/Protocol/ProfileKey/PlayerKeyPair.cs b/MinecraftClient/Protocol/ProfileKey/PlayerKeyPair.cs index 572b0d0610..d1a59682f2 100644 --- a/MinecraftClient/Protocol/ProfileKey/PlayerKeyPair.cs +++ b/MinecraftClient/Protocol/ProfileKey/PlayerKeyPair.cs @@ -1,35 +1,38 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text.Json.Serialization; namespace MinecraftClient.Protocol.ProfileKey { public class PlayerKeyPair { + [JsonInclude] + [JsonPropertyName("PublicKey")] public PublicKey PublicKey; + [JsonInclude] + [JsonPropertyName("PrivateKey")] public PrivateKey PrivateKey; + [JsonInclude] + [JsonPropertyName("ExpiresAt")] public DateTime ExpiresAt; - public DateTime RefreshedAfter; // Todo: add a timer + [JsonInclude] + [JsonPropertyName("RefreshedAfter")] + public DateTime RefreshedAfter; + [JsonIgnore] private const string DataTimeFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ"; - public PlayerKeyPair(PublicKey keyPublic, PrivateKey keyPrivate, string expiresAt, string refreshedAfter) + [JsonConstructor] + public PlayerKeyPair(PublicKey PublicKey, PrivateKey PrivateKey, DateTime ExpiresAt, DateTime RefreshedAfter) { - PublicKey = keyPublic; - PrivateKey = keyPrivate; - try - { - ExpiresAt = DateTime.ParseExact(expiresAt, DataTimeFormat, System.Globalization.CultureInfo.InvariantCulture).ToUniversalTime(); - RefreshedAfter = DateTime.ParseExact(refreshedAfter, DataTimeFormat, System.Globalization.CultureInfo.InvariantCulture).ToUniversalTime(); - } - catch - { - ExpiresAt = DateTime.Parse(expiresAt).ToUniversalTime(); - RefreshedAfter = DateTime.Parse(refreshedAfter).ToUniversalTime(); - } + this.PublicKey = PublicKey; + this.PrivateKey = PrivateKey; + this.ExpiresAt = ExpiresAt; + this.RefreshedAfter = RefreshedAfter; } public bool NeedRefresh() @@ -54,21 +57,6 @@ public long GetExpirationSeconds() return timeOffset.ToUnixTimeSeconds(); } - public static PlayerKeyPair FromString(string tokenString) - { - string[] fields = tokenString.Split(','); - - if (fields.Length < 6) - throw new InvalidDataException("Invalid string format"); - - PublicKey publicKey = new(pemKey: fields[0].Trim(), - sig: fields[1].Trim(), sigV2: fields[2].Trim()); - - PrivateKey privateKey = new(pemKey: fields[3].Trim()); - - return new PlayerKeyPair(publicKey, privateKey, fields[4].Trim(), fields[5].Trim()); - } - public override string ToString() { List datas = new(); diff --git a/MinecraftClient/Protocol/ProfileKey/PrivateKey.cs b/MinecraftClient/Protocol/ProfileKey/PrivateKey.cs index 6cb8103b7a..e0d5f1b895 100644 --- a/MinecraftClient/Protocol/ProfileKey/PrivateKey.cs +++ b/MinecraftClient/Protocol/ProfileKey/PrivateKey.cs @@ -1,15 +1,20 @@ using System; using System.Security.Cryptography; +using System.Text.Json.Serialization; using MinecraftClient.Protocol.Message; namespace MinecraftClient.Protocol.ProfileKey { public class PrivateKey { + [JsonInclude] + [JsonPropertyName("Key")] public byte[] Key { get; set; } + [JsonIgnore] private readonly RSA rsa; + [JsonIgnore] private byte[]? precedingSignature = null; public PrivateKey(string pemKey) @@ -20,6 +25,14 @@ public PrivateKey(string pemKey) rsa.ImportPkcs8PrivateKey(Key, out _); } + [JsonConstructor] + public PrivateKey(byte[] Key) + { + this.Key = Key; + rsa = RSA.Create(); + rsa.ImportPkcs8PrivateKey(Key, out _); + } + public byte[] SignData(byte[] data) { return rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); diff --git a/MinecraftClient/Protocol/ProfileKey/PublicKey.cs b/MinecraftClient/Protocol/ProfileKey/PublicKey.cs index 2f01fa843a..53280a228d 100644 --- a/MinecraftClient/Protocol/ProfileKey/PublicKey.cs +++ b/MinecraftClient/Protocol/ProfileKey/PublicKey.cs @@ -1,15 +1,26 @@ using System; +using System.Collections.Generic; using System.Security.Cryptography; +using System.Text.Json.Serialization; using MinecraftClient.Protocol.Message; namespace MinecraftClient.Protocol.ProfileKey { public class PublicKey { + [JsonInclude] + [JsonPropertyName("Key")] public byte[] Key { get; set; } + + [JsonInclude] + [JsonPropertyName("Signature")] public byte[]? Signature { get; set; } + + [JsonInclude] + [JsonPropertyName("SignatureV2")] public byte[]? SignatureV2 { get; set; } + [JsonIgnore] private readonly RSA rsa; public PublicKey(string pemKey, string? sig = null, string? sigV2 = null) @@ -36,6 +47,12 @@ public PublicKey(byte[] key, byte[] signature) Signature = signature; } + [JsonConstructor] + public PublicKey(byte[] Key, byte[]? Signature, byte[]? SignatureV2) : this(Key, Signature!) + { + this.SignatureV2 = SignatureV2; + } + public bool VerifyData(byte[] data, byte[] signature) { return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index 3328faf098..11f931f5e3 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -1,16 +1,27 @@ using System; using System.Collections.Generic; 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; using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Services.Description; using DnsClient; using MinecraftClient.Protocol.Handlers; 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; @@ -32,46 +43,36 @@ public static class ProtocolHandler /// Input domain name, updated with target host if any, else left untouched /// Updated with target port if any, else left untouched /// TRUE if a Minecraft Service was found. - public static bool MinecraftServiceLookup(ref string domain, ref ushort port) + public static async Task> MinecraftServiceLookupAsync(string domain) { - bool foundService = false; - string domainVal = domain; - ushort portVal = port; - - if (!String.IsNullOrEmpty(domain) && domain.Any(c => char.IsLetter(c))) + if (!string.IsNullOrEmpty(domain) && domain.Any(char.IsLetter)) { - AutoTimeout.Perform(() => + CancellationTokenSource cancelToken = new(1000 * + (Config.Main.Advanced.ResolveSrvRecords == MainConfigHealper.MainConfig.AdvancedConfig.ResolveSrvRecordType.fast ? 10 : 30)); + try { - try + ConsoleIO.WriteLine(string.Format(Translations.mcc_resolve, domain)); + var lookupClient = new LookupClient(); + var response = await lookupClient.QueryAsync($"_minecraft._tcp.{domain}", QueryType.SRV, cancellationToken: cancelToken.Token); + if (!cancelToken.IsCancellationRequested && !response.HasError && response.Answers.SrvRecords().Any()) { - ConsoleIO.WriteLine(string.Format(Translations.mcc_resolve, domainVal)); - var lookupClient = new LookupClient(); - var response = lookupClient.Query(new DnsQuestion($"_minecraft._tcp.{domainVal}", QueryType.SRV)); - if (response.HasError != true && response.Answers.SrvRecords().Any()) - { - //Order SRV records by priority and weight, then randomly - var result = response.Answers.SrvRecords() - .OrderBy(record => record.Priority) - .ThenByDescending(record => record.Weight) - .ThenBy(record => Guid.NewGuid()) - .First(); - string target = result.Target.Value.Trim('.'); - ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_found, target, result.Port, domainVal)); - domainVal = target; - portVal = result.Port; - foundService = true; - } + //Order SRV records by priority and weight, then randomly + var result = response.Answers.SrvRecords() + .OrderBy(record => record.Priority) + .ThenByDescending(record => record.Weight) + .ThenBy(record => Guid.NewGuid()) + .First(); + string target = result.Target.Value.Trim('.'); + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_found, target, result.Port, domain)); + return new(true, target, result.Port); } - catch (Exception e) - { - ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_not_found, domainVal, e.GetType().FullName, e.Message)); - } - }, TimeSpan.FromSeconds(Config.Main.Advanced.ResolveSrvRecords == MainConfigHealper.MainConfig.AdvancedConfig.ResolveSrvRecordType.fast ? 10 : 30)); + } + catch (Exception e) + { + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_not_found, domain, e.GetType().FullName, e.Message)); + } } - - domain = domainVal; - port = portVal; - return foundService; + return new(false, string.Empty, 25565); } /// @@ -81,43 +82,46 @@ public static bool MinecraftServiceLookup(ref string domain, ref ushort port) /// Server Port to ping /// Will contain protocol version, if ping successful /// TRUE if ping was successful - public static bool GetServerInfo(string serverIP, ushort serverPort, ref int protocolversion, ref ForgeInfo? forgeInfo) + public static async Task> GetServerInfoAsync(string serverIP, ushort serverPort, int protocolversion) { - bool success = false; - int protocolversionTmp = 0; - ForgeInfo? forgeInfoTmp = null; - if (AutoTimeout.Perform(() => + Tuple? result = null; + + CancellationTokenSource cancelTokenSource = new(1000 * + (Config.Main.Advanced.ResolveSrvRecords == MainConfigHealper.MainConfig.AdvancedConfig.ResolveSrvRecordType.fast ? 10 : 30)); + try { - try + result = await Protocol18Handler.DoPing(serverIP, serverPort, cancelTokenSource.Token); + if (!result.Item1) + result = await Protocol16Handler.DoPing(serverIP, serverPort, cancelTokenSource.Token); + } + catch (OperationCanceledException) + { + ConsoleIO.WriteLineFormatted("§8" + Translations.error_connection_timeout, acceptnewlines: true); + } + catch (Exception e) + { + ConsoleIO.WriteLineFormatted(string.Format("§8{0}: {1}", e.GetType().FullName, e.Message)); + } + + if (result != null) + { + if (!result.Item1) + ConsoleIO.WriteLineFormatted("§8" + Translations.error_unexpect_response, acceptnewlines: true); + + if (protocolversion != 0 && protocolversion != result.Item2) { - if (Protocol18Handler.DoPing(serverIP, serverPort, ref protocolversionTmp, ref forgeInfoTmp) - || Protocol16Handler.DoPing(serverIP, serverPort, ref protocolversionTmp)) - { - success = true; - } - else - ConsoleIO.WriteLineFormatted("§8" + Translations.error_unexpect_response, acceptnewlines: true); + ConsoleIO.WriteLineFormatted("§8" + Translations.error_version_different, acceptnewlines: true); + return new(true, protocolversion, result.Item3); } - catch (Exception e) + else { - ConsoleIO.WriteLineFormatted(String.Format("§8{0}: {1}", e.GetType().FullName, e.Message)); + if (result.Item2 <= 1) + ConsoleIO.WriteLineFormatted("§8" + Translations.error_no_version_report, acceptnewlines: true); + return result; } - }, TimeSpan.FromSeconds(Config.Main.Advanced.ResolveSrvRecords == MainConfigHealper.MainConfig.AdvancedConfig.ResolveSrvRecordType.fast ? 10 : 30))) - { - if (protocolversion != 0 && protocolversion != protocolversionTmp) - ConsoleIO.WriteLineFormatted("§8" + Translations.error_version_different, acceptnewlines: true); - if (protocolversion == 0 && protocolversionTmp <= 1) - ConsoleIO.WriteLineFormatted("§8" + Translations.error_no_version_report, acceptnewlines: true); - if (protocolversion == 0) - protocolversion = protocolversionTmp; - forgeInfo = forgeInfoTmp; - return success; - } - else - { - ConsoleIO.WriteLineFormatted("§8" + Translations.error_connection_timeout, acceptnewlines: true); - return false; } + + return new(false, 0, null); } /// @@ -127,17 +131,17 @@ public static bool GetServerInfo(string serverIP, ushort serverPort, ref int pro /// Protocol version to handle /// Handler with the appropriate callbacks /// - public static IMinecraftCom GetProtocolHandler(TcpClient Client, int ProtocolVersion, ForgeInfo? forgeInfo, IMinecraftComHandler Handler) + public static IMinecraftCom GetProtocolHandler(CancellationToken cancelToken, TcpClient Client, int ProtocolVersion, ForgeInfo? forgeInfo, IMinecraftComHandler Handler) { int[] supportedVersions_Protocol16 = { 51, 60, 61, 72, 73, 74, 78 }; if (Array.IndexOf(supportedVersions_Protocol16, ProtocolVersion) > -1) - return new Protocol16Handler(Client, ProtocolVersion, Handler); + return new Protocol16Handler(cancelToken, Client, ProtocolVersion, Handler); 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(Client, ProtocolVersion, Handler, forgeInfo); + return new Protocol18Handler(Client, ProtocolVersion, Handler, forgeInfo, cancelToken); throw new NotSupportedException(string.Format(Translations.exception_version_unsupport, ProtocolVersion)); } @@ -319,14 +323,10 @@ public static int MCVer2ProtocolVersion(string MCVersion) } else { - try - { - return Int32.Parse(MCVersion, NumberStyles.Any, CultureInfo.CurrentCulture); - } - catch - { + if (int.TryParse(MCVersion, NumberStyles.Any, CultureInfo.CurrentCulture, out int versionId)) + return versionId; + else return 0; - } } } @@ -338,59 +338,59 @@ public static int MCVer2ProtocolVersion(string MCVersion) /// The 1.X.X version number, or 0.0 if could not determine protocol version public static string ProtocolVersion2MCVer(int protocol) { - switch (protocol) - { - case 22: return "1.0"; - case 23: return "1.1"; - case 28: return "1.2.3"; - case 29: return "1.2.5"; - case 39: return "1.3.2"; - // case 47: return "1.4.2"; - case 48: return "1.4.3"; - case 49: return "1.4.5"; - case 51: return "1.4.6"; - case 60: return "1.5.1"; - case 62: return "1.5.2"; - case 72: return "1.6"; - case 73: return "1.6.1"; - case 3: return "1.7.1"; - case 4: return "1.7.2"; - case 5: return "1.7.6"; - case 47: return "1.8"; - case 107: return "1.9"; - case 108: return "1.9.1"; - case 109: return "1.9.2"; - case 110: return "1.9.3"; - case 210: return "1.10"; - case 315: return "1.11"; - case 316: return "1.11.1"; - case 335: return "1.12"; - case 338: return "1.12.1"; - case 340: return "1.12.2"; - case 393: return "1.13"; - case 401: return "1.13.1"; - case 404: return "1.13.2"; - case 477: return "1.14"; - case 480: return "1.14.1"; - case 485: return "1.14.2"; - case 490: return "1.14.3"; - case 498: return "1.14.4"; - case 573: return "1.15"; - case 575: return "1.15.1"; - case 578: return "1.15.2"; - case 735: return "1.16"; - case 736: return "1.16.1"; - case 751: return "1.16.2"; - case 753: return "1.16.3"; - case 754: return "1.16.5"; - case 755: return "1.17"; - case 756: return "1.17.1"; - case 757: return "1.18.1"; - case 758: return "1.18.2"; - case 759: return "1.19"; - case 760: return "1.19.2"; - default: return "0.0"; - } + return protocol switch + { + 22 => "1.0", + 23 => "1.1", + 28 => "1.2.3", + 29 => "1.2.5", + 39 => "1.3.2", + // 47 => "1.4.2", // 47 conflicts with 1.8 + 48 => "1.4.3", + 49 => "1.4.5", + 51 => "1.4.6", + 60 => "1.5.1", + 62 => "1.5.2", + 72 => "1.6", + 73 => "1.6.1", + 3 => "1.7.1", + 4 => "1.7.2", + 5 => "1.7.6", + 47 => "1.8", + 107 => "1.9", + 108 => "1.9.1", + 109 => "1.9.2", + 110 => "1.9.3", + 210 => "1.10", + 315 => "1.11", + 316 => "1.11.1", + 335 => "1.12", + 338 => "1.12.1", + 340 => "1.12.2", + 393 => "1.13", + 401 => "1.13.1", + 404 => "1.13.2", + 477 => "1.14", + 480 => "1.14.1", + 485 => "1.14.2", + 490 => "1.14.3", + 498 => "1.14.4", + 573 => "1.15", + 575 => "1.15.1", + 578 => "1.15.2", + 735 => "1.16", + 736 => "1.16.1", + 751 => "1.16.2", + 753 => "1.16.3", + 754 => "1.16.5", + 755 => "1.17", + 756 => "1.17.1", + 757 => "1.18.1", + 758 => "1.18.2", + 759 => "1.19", + 760 => "1.19.2", + _ => "0.0", + }; } /// @@ -423,20 +423,31 @@ public enum AccountType { Mojang, Microsoft }; /// Password /// In case of successful login, will contain session information for multiplayer /// Returns the status of the login (Success, Failure, etc.) - public static LoginResult GetLogin(string user, string pass, LoginType type, out SessionToken session) + public static async Task> GetLoginAsync(HttpClient httpClient, string user, string pass, LoginType type) { + httpClient.DefaultRequestHeaders.UserAgent.Clear(); if (type == LoginType.microsoft) { if (Config.Main.General.Method == LoginMethod.mcc) - return MicrosoftMCCLogin(user, pass, out session); + { + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"MCC/{Program.Version}"); + return await MicrosoftMCCLoginAsync(httpClient, user, pass); + } else - return MicrosoftBrowserLogin(out session, user); + { + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(XboxLive.UserAgent); + return await MicrosoftBrowserLoginAsync(httpClient, user); + } } else if (type == LoginType.mojang) { - return MojangLogin(user, pass, out session); + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"MCC/{Program.Version}"); + return await MojangLoginAsync(user, pass); + } + else + { + throw new InvalidOperationException("Account type must be Mojang or Microsoft"); } - else throw new InvalidOperationException("Account type must be Mojang or Microsoft"); } /// @@ -446,82 +457,77 @@ public static LoginResult GetLogin(string user, string pass, LoginType type, out /// /// /// - private static LoginResult MojangLogin(string user, string pass, out SessionToken session) + private static async Task> MojangLoginAsync(string user, string pass) { - session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; - try { - string result = ""; - string json_request = "{\"agent\": { \"name\": \"Minecraft\", \"version\": 1 }, \"username\": \"" + JsonEncode(user) + "\", \"password\": \"" + JsonEncode(pass) + "\", \"clientToken\": \"" + JsonEncode(session.ClientID) + "\" }"; - int code = DoHTTPSPost("authserver.mojang.com", "/authenticate", json_request, ref result); + string clientID = Guid.NewGuid().ToString().Replace("-", ""); + string json_request = $"{{\"agent\": {{ \"name\": \"Minecraft\", \"version\": 1 }}, \"username\": \"{JsonEncode(user)}\", \"password\": \"{JsonEncode(pass)}\", \"clientToken\": \"{JsonEncode(clientID)}\" }}"; + (int code, string result) = await DoHTTPSPost("authserver.mojang.com", "/authenticate", json_request); if (code == 200) { if (result.Contains("availableProfiles\":[]}")) { - return LoginResult.NotPremium; + return new(LoginResult.NotPremium, null); } else { Json.JSONData loginResponse = Json.ParseJson(result); - if (loginResponse.Properties.ContainsKey("accessToken") - && loginResponse.Properties.ContainsKey("selectedProfile") - && loginResponse.Properties["selectedProfile"].Properties.ContainsKey("id") - && loginResponse.Properties["selectedProfile"].Properties.ContainsKey("name")) + if (loginResponse.Properties.TryGetValue("accessToken", out Json.JSONData? accessToken) + && loginResponse.Properties.TryGetValue("selectedProfile", out Json.JSONData? selectedProfile) + && selectedProfile.Properties.TryGetValue("id", out Json.JSONData? selectedProfileId) + && selectedProfile.Properties.TryGetValue("name", out Json.JSONData? selectedProfileName)) { - session.ID = loginResponse.Properties["accessToken"].StringValue; - session.PlayerID = loginResponse.Properties["selectedProfile"].Properties["id"].StringValue; - session.PlayerName = loginResponse.Properties["selectedProfile"].Properties["name"].StringValue; - return LoginResult.Success; + SessionToken session = new() + { + ClientID = clientID, + ID = accessToken.StringValue, + PlayerID = selectedProfileId.StringValue, + PlayerName = selectedProfileName.StringValue + }; + return new(LoginResult.Success, session); } - else return LoginResult.InvalidResponse; + else + return new(LoginResult.InvalidResponse, null); } } else if (code == 403) { if (result.Contains("UserMigratedException")) - { - return LoginResult.AccountMigrated; - } - else return LoginResult.WrongPassword; + return new(LoginResult.AccountMigrated, null); + else + return new(LoginResult.WrongPassword, null); } else if (code == 503) { - return LoginResult.ServiceUnavailable; + return new(LoginResult.ServiceUnavailable, null); } else { ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.error_http_code, code)); - return LoginResult.OtherError; + return new(LoginResult.OtherError, null); } } - catch (System.Security.Authentication.AuthenticationException e) + catch (AuthenticationException e) { - if (Settings.Config.Logging.DebugMessages) - { + if (Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted("§8" + e.ToString()); - } - return LoginResult.SSLError; + return new(LoginResult.SSLError, null); } catch (System.IO.IOException e) { - if (Settings.Config.Logging.DebugMessages) - { + if (Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted("§8" + e.ToString()); - } if (e.Message.Contains("authentication")) - { - return LoginResult.SSLError; - } - else return LoginResult.OtherError; + return new(LoginResult.SSLError, null); + else + return new(LoginResult.OtherError, null); } catch (Exception e) { - if (Settings.Config.Logging.DebugMessages) - { + if (Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted("§8" + e.ToString()); - } - return LoginResult.OtherError; + return new(LoginResult.OtherError, null); } } @@ -533,24 +539,22 @@ private static LoginResult MojangLogin(string user, string pass, out SessionToke /// /// /// - private static LoginResult MicrosoftMCCLogin(string email, string password, out SessionToken session) + private static async Task> MicrosoftMCCLoginAsync(HttpClient httpClient, string email, string password) { try { - var msaResponse = XboxLive.UserLogin(email, password, XboxLive.PreAuth()); + var msaResponse = await XboxLive.UserLoginAsync(httpClient, email, password, await XboxLive.PreAuthAsync(httpClient)); // Remove refresh token for MCC sign method msaResponse.RefreshToken = string.Empty; - return MicrosoftLogin(msaResponse, out session); + return await MicrosoftLoginAsync(httpClient, msaResponse); } catch (Exception e) { - session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; + SessionToken session = new() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; ConsoleIO.WriteLineFormatted("§cMicrosoft authenticate failed: " + e.Message); - if (Settings.Config.Logging.DebugMessages) - { + if (Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted("§c" + e.StackTrace); - } - return LoginResult.WrongPassword; // Might not always be wrong password + return new(LoginResult.WrongPassword, session); // Might not always be wrong password } } @@ -564,92 +568,119 @@ private static LoginResult MicrosoftMCCLogin(string email, string password, out /// /// /// - public static LoginResult MicrosoftBrowserLogin(out SessionToken session, string loginHint = "") + public static async Task> MicrosoftBrowserLoginAsync(HttpClient httpClient, string? loginHint = null) { - if (string.IsNullOrEmpty(loginHint)) - Microsoft.OpenBrowser(Microsoft.SignInUrl); - else - Microsoft.OpenBrowser(Microsoft.GetSignInUrlWithHint(loginHint)); - ConsoleIO.WriteLine(Translations.mcc_browser_open); - ConsoleIO.WriteLine("\n" + Microsoft.SignInUrl + "\n"); + string link = string.IsNullOrEmpty(loginHint) ? Microsoft.SignInUrl : Microsoft.GetSignInUrlWithHint(loginHint); + + Microsoft.OpenBrowser(link); - ConsoleIO.WriteLine(Translations.mcc_browser_login_code); + ConsoleIO.SuppressPrinting(true); + ConsoleIO.WriteLine(Translations.mcc_browser_open, ignoreSuppress: true); + ConsoleIO.WriteLine($"\n{link}\n", ignoreSuppress: true); + + ConsoleIO.WriteLine(Translations.mcc_browser_login_code, ignoreSuppress: true); string code = ConsoleIO.ReadLine(); - ConsoleIO.WriteLine(string.Format(Translations.mcc_connecting, "Microsoft")); + ConsoleIO.WriteLine(string.Format(Translations.mcc_connecting, "Microsoft"), ignoreSuppress: true); + ConsoleIO.SuppressPrinting(false); - var msaResponse = Microsoft.RequestAccessToken(code); - return MicrosoftLogin(msaResponse, out session); + var msaResponse = await Microsoft.RequestAccessTokenAsync(httpClient, code); + return await MicrosoftLoginAsync(httpClient, msaResponse); } - public static LoginResult MicrosoftLoginRefresh(string refreshToken, out SessionToken session) + public static async Task> MicrosoftLoginRefreshAsync(HttpClient httpClient, string refreshToken) { - var msaResponse = Microsoft.RefreshAccessToken(refreshToken); - return MicrosoftLogin(msaResponse, out session); + var msaResponse = await Microsoft.RefreshAccessTokenAsync(httpClient, refreshToken); + return await MicrosoftLoginAsync(httpClient, msaResponse); } - private static LoginResult MicrosoftLogin(Microsoft.LoginResponse msaResponse, out SessionToken session) + private static async Task> MicrosoftLoginAsync(HttpClient httpClient, Microsoft.LoginResponse msaResponse) { - session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; - try { - var xblResponse = XboxLive.XblAuthenticate(msaResponse); - var xsts = XboxLive.XSTSAuthenticate(xblResponse); // Might throw even password correct + var xblResponse = await XboxLive.XblAuthenticateAsync(httpClient, msaResponse); + var xsts = await XboxLive.XSTSAuthenticateAsync(httpClient, xblResponse); // Might throw even password correct - string accessToken = MinecraftWithXbox.LoginWithXbox(xsts.UserHash, xsts.Token); - bool hasGame = MinecraftWithXbox.UserHasGame(accessToken); + string accessToken = await MinecraftWithXbox.LoginWithXboxAsync(httpClient, xsts.UserHash, xsts.Token); + bool hasGame = await MinecraftWithXbox.CheckUserHasGameAsync(httpClient, accessToken); if (hasGame) { - var profile = MinecraftWithXbox.GetUserProfile(accessToken); - session.PlayerName = profile.UserName; - session.PlayerID = profile.UUID; - session.ID = accessToken; - session.RefreshToken = msaResponse.RefreshToken; + var profile = await MinecraftWithXbox.GetUserProfileAsync(httpClient, accessToken); + SessionToken session = new() + { + ClientID = Guid.NewGuid().ToString().Replace("-", ""), + PlayerName = profile.UserName, + PlayerID = profile.UUID, + ID = accessToken, + RefreshToken = msaResponse.RefreshToken + }; InternalConfig.Account.Login = msaResponse.Email; - return LoginResult.Success; + return new(LoginResult.Success, session); } else { - return LoginResult.NotPremium; + return new(LoginResult.NotPremium, null); } } catch (Exception e) { ConsoleIO.WriteLineFormatted("§cMicrosoft authenticate failed: " + e.Message); - if (Settings.Config.Logging.DebugMessages) + if (Config.Logging.DebugMessages) { ConsoleIO.WriteLineFormatted("§c" + e.StackTrace); } - return LoginResult.WrongPassword; // Might not always be wrong password + return new(LoginResult.WrongPassword, null); // Might not always be wrong password } } + private record JwtPayloadInSessionId + { + public string? xuid { init; get; } + public string? agg { init; get; } + public string? sub { init; get; } + public long nbf { init; get; } + public string? auth { init; get; } + public string[]? roles { init; get; } + public string? iss { init; get; } + public long exp { init; get; } + public long iat { init; get; } + public string? platform { init; get; } + public string? yuid { init; get; } + } + /// /// Validates whether accessToken must be refreshed /// /// Session token to validate /// Returns the status of the token (Valid, Invalid, etc.) - public static LoginResult GetTokenValidation(SessionToken session) + public static async Task GetTokenValidation(SessionToken session) { - var payload = JwtPayloadDecode.GetPayload(session.ID); - var json = Json.ParseJson(payload); - var expTimestamp = long.Parse(json.Properties["exp"].StringValue, NumberStyles.Any, CultureInfo.CurrentCulture); - var now = DateTime.Now; - var tokenExp = UnixTimeStampToDateTime(expTimestamp); - if (Settings.Config.Logging.DebugMessages) - { - ConsoleIO.WriteLine("Access token expiration time is " + tokenExp.ToString()); - } - if (now < tokenExp) - { - // Still valid - return LoginResult.Success; - } - else + try { - // Token expired - return LoginResult.LoginRequired; + Stream payload = JwtPayloadDecode.GetPayload(session.ID); + JwtPayloadInSessionId jsonPayload = (await JsonSerializer.DeserializeAsync(payload))!; + + var now = DateTime.Now.AddMinutes(1); + var tokenExp = UnixTimeStampToDateTime(jsonPayload.exp); + + if (Config.Logging.DebugMessages) + ConsoleIO.WriteLine("Access token expiration time is " + tokenExp.ToString()); + + if (now < tokenExp) + { + // Still valid + return LoginResult.Success; + } + else + { + // Token expired + return LoginResult.LoginRequired; + } } + catch (JsonException) { } + catch (FormatException) { } + catch (ArgumentException) { } + catch (IndexOutOfRangeException) { } + return LoginResult.LoginRequired; } /// @@ -658,52 +689,68 @@ public static LoginResult GetTokenValidation(SessionToken session) /// Login /// In case of successful token refresh, will contain session information for multiplayer /// Returns the status of the new token request (Success, Failure, etc.) - public static LoginResult GetNewToken(SessionToken currentsession, out SessionToken session) + public static async Task> GetNewToken(SessionToken currentsession) { - session = new SessionToken(); try { - string result = ""; - string json_request = "{ \"accessToken\": \"" + JsonEncode(currentsession.ID) + "\", \"clientToken\": \"" + JsonEncode(currentsession.ClientID) + "\", \"selectedProfile\": { \"id\": \"" + JsonEncode(currentsession.PlayerID) + "\", \"name\": \"" + JsonEncode(currentsession.PlayerName) + "\" } }"; - int code = DoHTTPSPost("authserver.mojang.com", "/refresh", json_request, ref result); + string json_request = $"{{ \"accessToken\": \"{JsonEncode(currentsession.ID)}\", \"clientToken\": \"{JsonEncode(currentsession.ClientID)}\", \"selectedProfile\": {{ \"id\": \"{JsonEncode(currentsession.PlayerID)}\", \"name\": \"{JsonEncode(currentsession.PlayerName)}\" }} }}"; + (int code, string result) = await DoHTTPSPost("authserver.mojang.com", "/refresh", json_request); if (code == 200) { if (result == null) { - return LoginResult.NullError; + return new(LoginResult.NullError, null); } else { Json.JSONData loginResponse = Json.ParseJson(result); - if (loginResponse.Properties.ContainsKey("accessToken") - && loginResponse.Properties.ContainsKey("selectedProfile") - && loginResponse.Properties["selectedProfile"].Properties.ContainsKey("id") - && loginResponse.Properties["selectedProfile"].Properties.ContainsKey("name")) + if (loginResponse.Properties.TryGetValue("accessToken", out Json.JSONData? accessToken) + && loginResponse.Properties.TryGetValue("selectedProfile", out Json.JSONData? selectedProfile) + && selectedProfile.Properties.TryGetValue("id", out Json.JSONData? selectedProfileId) + && selectedProfile.Properties.TryGetValue("name", out Json.JSONData? selectedProfileName)) { - session.ID = loginResponse.Properties["accessToken"].StringValue; - session.PlayerID = loginResponse.Properties["selectedProfile"].Properties["id"].StringValue; - session.PlayerName = loginResponse.Properties["selectedProfile"].Properties["name"].StringValue; - return LoginResult.Success; + + SessionToken session = new() + { + ID = accessToken.StringValue, + PlayerID = selectedProfileId.StringValue, + PlayerName = selectedProfileName.StringValue + }; + return new(LoginResult.Success, session); } - else return LoginResult.InvalidResponse; + else + return new(LoginResult.InvalidResponse, null); } } else if (code == 403 && result.Contains("InvalidToken")) { - return LoginResult.InvalidToken; + return new(LoginResult.InvalidToken, null); } else { ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.error_auth, code)); - return LoginResult.OtherError; + return new(LoginResult.OtherError, null); } } catch { - return LoginResult.OtherError; + return new(LoginResult.OtherError, null); } } + private record SessionCheckPayload + { + public string? accessToken { init; get; } + public string? selectedProfile { init; get; } + 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 /// @@ -711,16 +758,42 @@ public static LoginResult GetNewToken(SessionToken currentsession, out SessionTo /// Session ID /// Server ID /// TRUE if session was successfully checked - public static bool SessionCheck(string uuid, string accesstoken, string serverhash) + public static async Task> SessionCheckAsync(HttpClient httpClient, string uuid, string accesstoken, string serverhash) { + SessionCheckPayload payload = new() + { + accessToken = accesstoken, + selectedProfile = uuid, + serverId = serverhash, + }; + try { - string result = ""; - string json_request = "{\"accessToken\":\"" + accesstoken + "\",\"selectedProfile\":\"" + uuid + "\",\"serverId\":\"" + serverhash + "\"}"; - int code = DoHTTPSPost("sessionserver.mojang.com", "/session/minecraft/join", json_request, ref result); - return (code >= 200 && code < 300); + 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.Headers.UserAgent.Clear(); + request.Headers.UserAgent.ParseAdd($"MCC/{Program.Version}"); + + using HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + 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}"); } - catch { return false; } } /// @@ -730,37 +803,30 @@ public static bool SessionCheck(string uuid, string accesstoken, string serverha /// Player UUID /// Access token /// List of ID of available Realms worlds - public static List RealmsListWorlds(string username, string uuid, string accesstoken) + public static async Task> RealmsListWorldsAsync(HttpClient httpClient, string username, string uuid, string accesstoken) { List realmsWorldsResult = new(); // Store world ID try { - string result = ""; string cookies = String.Format("sid=token:{0}:{1};user={2};version={3}", accesstoken, uuid, username, Program.MCHighestVersion); - DoHTTPSGet("pc.realms.minecraft.net", "/worlds", cookies, ref result); + (_, string result) = await DoHTTPSGet("pc.realms.minecraft.net", "/worlds", cookies); Json.JSONData realmsWorlds = Json.ParseJson(result); - if (realmsWorlds.Properties.ContainsKey("servers") - && realmsWorlds.Properties["servers"].Type == Json.JSONData.DataType.Array - && realmsWorlds.Properties["servers"].DataArray.Count > 0) + if (realmsWorlds.Properties.TryGetValue("servers", out Json.JSONData? servers) + && servers.Type == Json.JSONData.DataType.Array + && servers.DataArray.Count > 0) { List availableWorlds = new(); // Store string to print int index = 0; - foreach (Json.JSONData realmsServer in realmsWorlds.Properties["servers"].DataArray) + foreach (Json.JSONData realmsServer in servers.DataArray) { - if (realmsServer.Properties.ContainsKey("name") - && realmsServer.Properties.ContainsKey("owner") - && realmsServer.Properties.ContainsKey("id") - && realmsServer.Properties.ContainsKey("expired")) + if (realmsServer.Properties.TryGetValue("name", out Json.JSONData? name) + && realmsServer.Properties.TryGetValue("owner", out Json.JSONData? owner) + && realmsServer.Properties.TryGetValue("id", out Json.JSONData? id) + && realmsServer.Properties.TryGetValue("expired", out Json.JSONData? expired) + && expired.StringValue == "false") { - if (realmsServer.Properties["expired"].StringValue == "false") - { - availableWorlds.Add(String.Format("[{0}] {2} ({3}) - {1}", - index++, - realmsServer.Properties["id"].StringValue, - realmsServer.Properties["name"].StringValue, - realmsServer.Properties["owner"].StringValue)); - realmsWorldsResult.Add(realmsServer.Properties["id"].StringValue); - } + availableWorlds.Add($"[{index++}] {name.StringValue} ({owner.StringValue}) - {id.StringValue}"); + realmsWorldsResult.Add(id.StringValue); } } if (availableWorlds.Count > 0) @@ -792,13 +858,12 @@ public static List RealmsListWorlds(string username, string uuid, string /// Player UUID /// Access token /// Server address (host:port) or empty string if failure - public static string GetRealmsWorldServerAddress(string worldId, string username, string uuid, string accesstoken) + public static async Task GetRealmsWorldServerAddress(HttpClient httpClient, string worldId, string username, string uuid, string accesstoken) { try { - string result = ""; string cookies = String.Format("sid=token:{0}:{1};user={2};version={3}", accesstoken, uuid, username, Program.MCHighestVersion); - int statusCode = DoHTTPSGet("pc.realms.minecraft.net", "/worlds/v1/" + worldId + "/join/pc", cookies, ref result); + (int statusCode, string result) = await DoHTTPSGet("pc.realms.minecraft.net", $"/worlds/v1/{worldId}/join/pc", cookies); if (statusCode == 200) { Json.JSONData serverAddress = Json.ParseJson(result); @@ -835,9 +900,9 @@ public static string GetRealmsWorldServerAddress(string worldId, string username /// Cookies for making the request /// Request result /// HTTP Status code - private static int DoHTTPSGet(string host, string endpoint, string cookies, ref string result) + private static async Task> DoHTTPSGet(string host, string endpoint, string cookies) { - List http_request = new() + List http_request = new() { "GET " + endpoint + " HTTP/1.1", "Cookie: " + cookies, @@ -850,7 +915,7 @@ private static int DoHTTPSGet(string host, string endpoint, string cookies, ref "", "" }; - return DoHTTPSRequest(http_request, host, ref result); + return await DoHTTPSRequest(http_request, host); } /// @@ -861,20 +926,20 @@ private static int DoHTTPSGet(string host, string endpoint, string cookies, ref /// Request payload /// Request result /// HTTP Status code - private static int DoHTTPSPost(string host, string endpoint, string request, ref string result) + private static async Task> DoHTTPSPost(string host, string endpoint, string request) { - List http_request = new() + List http_request = new() { - "POST " + endpoint + " HTTP/1.1", - "Host: " + host, - "User-Agent: MCC/" + Program.Version, + $"POST {endpoint} HTTP/1.1", + $"Host: {host}", + $"User-Agent: MCC/{Program.Version}", "Content-Type: application/json", - "Content-Length: " + Encoding.ASCII.GetBytes(request).Length, + $"Content-Length: {Encoding.ASCII.GetBytes(request).Length}", "Connection: close", "", request }; - return DoHTTPSRequest(http_request, host, ref result); + return await DoHTTPSRequest(http_request, host); } /// @@ -885,57 +950,62 @@ private static int DoHTTPSPost(string host, string endpoint, string request, ref /// Host to connect to /// Request result /// HTTP Status code - private static int DoHTTPSRequest(List headers, string host, ref string result) + private static async Task> DoHTTPSRequest(List headers, string host) { - string? postResult = null; - int statusCode = 520; - Exception? exception = null; - AutoTimeout.Perform(() => + string postResult = string.Empty; + + var cancelToken = new CancellationTokenSource(30 * 1000).Token; + + if (Config.Logging.DebugMessages) + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.debug_request, host)); + + TcpClient client = ProxyHandler.NewTcpClient(host, 443, ProxyHandler.ClientType.Login); + SslStream stream = new(client.GetStream()); + + SslClientAuthenticationOptions sslOptions = new() // Enable TLS 1.2. Hotfix for #1780 { - try - { - if (Settings.Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.debug_request, host)); + TargetHost = host, + ClientCertificates = null, + EnabledSslProtocols = SslProtocols.Tls12, + CertificateRevocationCheckMode = X509RevocationMode.Online, + EncryptionPolicy = EncryptionPolicy.RequireEncryption, + }; + Task sslAuth = stream.AuthenticateAsClientAsync(sslOptions, cancelToken); + if (cancelToken.IsCancellationRequested) + throw new TimeoutException(string.Format(Translations.mcc_network_timeout, host)); - TcpClient client = ProxyHandler.NewTcpClient(host, 443, true); - SslStream stream = new(client.GetStream()); - stream.AuthenticateAsClient(host, null, SslProtocols.Tls12, true); // Enable TLS 1.2. Hotfix for #1780 + if (Config.Logging.DebugMessages) + foreach (string line in headers) + ConsoleIO.WriteLineFormatted("§8> " + line); - if (Settings.Config.Logging.DebugMessages) - foreach (string line in headers) - ConsoleIO.WriteLineFormatted("§8> " + line); + await sslAuth; + await stream.WriteAsync(Encoding.ASCII.GetBytes(string.Join("\r\n", headers.ToArray())), cancelToken); + if (cancelToken.IsCancellationRequested) + throw new TimeoutException(string.Format(Translations.mcc_network_timeout, host)); - stream.Write(Encoding.ASCII.GetBytes(String.Join("\r\n", headers.ToArray()))); - System.IO.StreamReader sr = new(stream); - string raw_result = sr.ReadToEnd(); + using System.IO.StreamReader sr = new(stream); + string raw_result = await sr.ReadToEndAsync(cancelToken); + sr.Dispose(); + if (cancelToken.IsCancellationRequested) + throw new TimeoutException(string.Format(Translations.mcc_network_timeout, host)); - if (Settings.Config.Logging.DebugMessages) - { - ConsoleIO.WriteLine(""); - foreach (string line in raw_result.Split('\n')) - ConsoleIO.WriteLineFormatted("§8< " + line); - } + if (Config.Logging.DebugMessages) + { + ConsoleIO.WriteLine(string.Empty); + foreach (string line in raw_result.Split('\n')) + ConsoleIO.WriteLineFormatted("§8< " + line); + } - if (raw_result.StartsWith("HTTP/1.1")) - { - postResult = raw_result[(raw_result.IndexOf("\r\n\r\n") + 4)..]; - statusCode = int.Parse(raw_result.Split(' ')[1], NumberStyles.Any, CultureInfo.CurrentCulture); - } - else statusCode = 520; //Web server is returning an unknown error - } - catch (Exception e) - { - if (e is not System.Threading.ThreadAbortException) - { - exception = e; - } - } - }, TimeSpan.FromSeconds(30)); - if (postResult != null) - result = postResult; - if (exception != null) - throw exception; - return statusCode; + int statusCode; + if (raw_result.StartsWith("HTTP/1.1")) + { + postResult = raw_result[(raw_result.IndexOf("\r\n\r\n") + 4)..]; + statusCode = int.Parse(raw_result.Split(' ')[1], NumberStyles.Any, CultureInfo.CurrentCulture); + } + else + statusCode = 520; //Web server is returning an unknown error + + return new(statusCode, postResult); } /// diff --git a/MinecraftClient/Protocol/ProxiedWebRequest.cs b/MinecraftClient/Protocol/ProxiedWebRequest.cs index 06e69a9d1f..13f2198c41 100644 --- a/MinecraftClient/Protocol/ProxiedWebRequest.cs +++ b/MinecraftClient/Protocol/ProxiedWebRequest.cs @@ -9,6 +9,7 @@ using System.Security.Authentication; using System.Text; using System.Threading; +using System.Threading.Tasks; using MinecraftClient.Proxy; namespace MinecraftClient.Protocol @@ -25,7 +26,7 @@ public interface ITcpFactory private readonly string httpVersion = "HTTP/1.1"; - private ITcpFactory? tcpFactory; + private readonly ITcpFactory? tcpFactory; private bool isProxied = false; // Send absolute Url in request if true private readonly Uri uri; @@ -45,7 +46,7 @@ public interface ITcpFactory /// Set to true to tell the http client proxy is enabled /// public bool IsProxy { get { return isProxied; } set { isProxied = value; } } - public bool Debug { get { return Settings.Config.Logging.DebugMessages; } } + public static bool Debug { get { return Settings.Config.Logging.DebugMessages; } } /// /// Create a new http request @@ -105,9 +106,9 @@ private void SetupBasicHeaders() /// Perform GET request and get the response. Proxy is handled automatically /// /// - public Response Get() + public async Task Get() { - return Send("GET"); + return await Send("GET"); } /// @@ -116,12 +117,12 @@ public Response Get() /// The content type of request body /// Request body /// - public Response Post(string contentType, string body) + public async Task Post(string contentType, string body) { Headers.Add("Content-Type", contentType); // Calculate length Headers.Add("Content-Length", Encoding.UTF8.GetBytes(body).Length.ToString()); - return Send("POST", body); + return await Send("POST", body); } /// @@ -130,35 +131,35 @@ public Response Post(string contentType, string body) /// Method in string representation /// Optional request body /// - private Response Send(string method, string body = "") + private async Task Send(string method, string body = "") { List requestMessage = new() { string.Format("{0} {1} {2}", method.ToUpper(), isProxied ? AbsoluteUrl : Path, httpVersion) // Request line }; + foreach (string key in Headers) // Headers { var value = Headers[key]; requestMessage.Add(string.Format("{0}: {1}", key, value)); } + requestMessage.Add(""); // + if (body != "") - { requestMessage.Add(body); - } - else requestMessage.Add(""); // + else + requestMessage.Add(""); // + if (Debug) - { foreach (string l in requestMessage) - { ConsoleIO.WriteLine("< " + l); - } - } + Response response = Response.Empty(); // FIXME: Use TcpFactory interface to avoid direct usage of the ProxyHandler class // TcpClient client = tcpFactory.CreateTcpClient(Host, Port); - TcpClient client = ProxyHandler.NewTcpClient(Host, Port, true); + TcpClient client = ProxyHandler.NewTcpClient(Host, Port, ProxyHandler.ClientType.Login); Stream stream; if (IsSecure) { @@ -171,35 +172,25 @@ private Response Send(string method, string body = "") } string h = string.Join("\r\n", requestMessage.ToArray()); byte[] data = Encoding.ASCII.GetBytes(h); - stream.Write(data, 0, data.Length); - stream.Flush(); + await stream.WriteAsync(data); + await stream.FlushAsync(); // Read response - int statusCode = ReadHttpStatus(stream); - var headers = ReadHeader(stream); - string? rbody; - if (headers.Get("transfer-encoding") == "chunked") - { - rbody = ReadBodyChunked(stream); - } - else - { - rbody = ReadBody(stream, int.Parse(headers.Get("content-length") ?? "0")); - } + int statusCode = await ReadHttpStatus(stream); + var headers = await ReadHeader(stream); + + Task rbody = (headers.Get("transfer-encoding") == "chunked") ? + ReadBodyChunked(stream) : ReadBody(stream, int.Parse(headers.Get("content-length") ?? "0")); + if (headers.Get("set-cookie") != null) - { response.Cookies = ParseSetCookie(headers.GetValues("set-cookie") ?? Array.Empty()); - } - response.Body = rbody ?? ""; + response.StatusCode = statusCode; response.Headers = headers; + response.Body = await rbody; - try - { - stream.Close(); - client.Close(); - } - catch { } + try { stream.Close(); } catch { } + try { client.Close(); } catch { } return response; } @@ -210,9 +201,9 @@ private Response Send(string method, string body = "") /// Stream to read /// /// If server return unknown data - private static int ReadHttpStatus(Stream s) + private static async Task ReadHttpStatus(Stream s) { - var httpHeader = ReadLine(s); // http header line + var httpHeader = await ReadLine(s); // http header line if (httpHeader.StartsWith("HTTP/1.1") || httpHeader.StartsWith("HTTP/1.0")) { return int.Parse(httpHeader.Split(' ')[1], NumberStyles.Any, CultureInfo.CurrentCulture); @@ -228,15 +219,15 @@ private static int ReadHttpStatus(Stream s) /// /// Stream to read /// Headers in lower-case - private static NameValueCollection ReadHeader(Stream s) + private static async Task ReadHeader(Stream s) { var headers = new NameValueCollection(); // Read headers string header; do { - header = ReadLine(s); - if (!String.IsNullOrEmpty(header)) + header = await ReadLine(s); + if (!string.IsNullOrEmpty(header)) { var tmp = header.Split(new char[] { ':' }, 2); var name = tmp[0].ToLower(); @@ -244,7 +235,7 @@ private static NameValueCollection ReadHeader(Stream s) headers.Add(name, value); } } - while (!String.IsNullOrEmpty(header)); + while (!string.IsNullOrEmpty(header)); return headers; } @@ -254,23 +245,19 @@ private static NameValueCollection ReadHeader(Stream s) /// Stream to read /// Length of the body (the Content-Length header) /// Body or null if length is zero - private static string? ReadBody(Stream s, int length) + private static async Task ReadBody(Stream s, int length) { if (length > 0) { byte[] buffer = new byte[length]; - int r = 0; - while (r < length) - { - var read = s.Read(buffer, r, length - r); - r += read; - Thread.Sleep(50); - } + int readed = 0; + while (readed < length) + readed += await s.ReadAsync(buffer.AsMemory(readed, length - readed)); return Encoding.UTF8.GetString(buffer); } else { - return null; + return string.Empty; } } @@ -279,13 +266,13 @@ private static NameValueCollection ReadHeader(Stream s) /// /// Stream to read /// Body or empty string if nothing is received - private static string ReadBodyChunked(Stream s) + private static async Task ReadBodyChunked(Stream s) { List buffer1 = new(); while (true) { - string l = ReadLine(s); - int size = Int32.Parse(l, NumberStyles.HexNumber); + string l = await ReadLine(s); + int size = int.Parse(l, NumberStyles.HexNumber); if (size == 0) break; byte[] buffer2 = new byte[size]; @@ -296,7 +283,7 @@ private static string ReadBodyChunked(Stream s) r += read; Thread.Sleep(50); } - ReadLine(s); + await ReadLine(s); buffer1.AddRange(buffer2); } return Encoding.UTF8.GetString(buffer1.ToArray()); @@ -366,17 +353,15 @@ private static NameValueCollection ParseSetCookie(IEnumerable headerValu /// /// Stream to read /// String - private static string ReadLine(Stream s) + private static async Task ReadLine(Stream s) { List buffer = new(); - byte c; + byte[] c = new byte[1]; while (true) { - int b = s.ReadByte(); - if (b == -1) - break; - c = (byte)b; - if (c == '\n') + try { await s.ReadExactlyAsync(c, 0, 1); } + catch { break; } + if (c[0] == '\n') { if (buffer.Last() == '\r') { @@ -384,7 +369,7 @@ private static string ReadLine(Stream s) break; } } - buffer.Add(c); + buffer.Add(c[0]); } return Encoding.UTF8.GetString(buffer.ToArray()); } diff --git a/MinecraftClient/Protocol/ReplayHandler.cs b/MinecraftClient/Protocol/ReplayHandler.cs index abaeecfc01..ebce0e8983 100644 --- a/MinecraftClient/Protocol/ReplayHandler.cs +++ b/MinecraftClient/Protocol/ReplayHandler.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Linq; -using Ionic.Zip; using MinecraftClient.Mapping; using MinecraftClient.Protocol.Handlers; using MinecraftClient.Protocol.Handlers.PacketPalettes; @@ -135,15 +135,19 @@ public void CreateReplayFile(string replayFileName) MetaData.duration = Convert.ToInt32((lastPacketTime - recordStartTime).TotalMilliseconds); MetaData.SaveToFile(); - using (Stream recordingFile = new FileStream(Path.Combine(temporaryCache, recordingTmpFileName), FileMode.Open)) + using (FileStream zipToOpen = new(Path.Combine(ReplayFileDirectory, replayFileName), FileMode.Open)) { + using ZipArchive archive = new(zipToOpen, ZipArchiveMode.Create); + + using (Stream recordingFile = new FileStream(Path.Combine(temporaryCache, recordingTmpFileName), FileMode.Open)) + { + ZipArchiveEntry recordingTmpFileEntry = archive.CreateEntry(recordingTmpFileName); + recordingFile.CopyTo(recordingTmpFileEntry.Open()); + } + using Stream metaDataFile = new FileStream(Path.Combine(temporaryCache, MetaData.MetaDataFileName), FileMode.Open); - using ZipOutputStream zs = new(Path.Combine(ReplayFileDirectory, replayFileName)); - zs.PutNextEntry(recordingTmpFileName); - recordingFile.CopyTo(zs); - zs.PutNextEntry(MetaData.MetaDataFileName); - metaDataFile.CopyTo(zs); - zs.Close(); + ZipArchiveEntry metaDataFileEntry = archive.CreateEntry(MetaData.MetaDataFileName); + metaDataFile.CopyTo(metaDataFileEntry.Open()); } File.Delete(Path.Combine(temporaryCache, recordingTmpFileName)); @@ -165,20 +169,21 @@ public void CreateBackupReplay(string replayFileName) MetaData.duration = Convert.ToInt32((lastPacketTime - recordStartTime).TotalMilliseconds); MetaData.SaveToFile(); - using (Stream metaDataFile = new FileStream(Path.Combine(temporaryCache, MetaData.MetaDataFileName), FileMode.Open)) + using (FileStream zipToOpen = new(replayFileName, FileMode.OpenOrCreate)) { - using ZipOutputStream zs = new(replayFileName); - zs.PutNextEntry(recordingTmpFileName); + using ZipArchive archive = new(zipToOpen, ZipArchiveMode.Create); + + ZipArchiveEntry recordingTmpFileEntry = archive.CreateEntry(recordingTmpFileName); // .CopyTo() method start from stream current position // We need to reset position in order to get full content var lastPosition = recordStream!.BaseStream.Position; recordStream.BaseStream.Position = 0; - recordStream.BaseStream.CopyTo(zs); + recordStream.BaseStream.CopyTo(recordingTmpFileEntry.Open()); recordStream.BaseStream.Position = lastPosition; - zs.PutNextEntry(MetaData.MetaDataFileName); - metaDataFile.CopyTo(zs); - zs.Close(); + using Stream metaDataFile = new FileStream(Path.Combine(temporaryCache, MetaData.MetaDataFileName), FileMode.Open); + ZipArchiveEntry metaDataFileEntry = archive.CreateEntry(MetaData.MetaDataFileName); + metaDataFile.CopyTo(metaDataFileEntry.Open()); } WriteDebugLog("Backup replay file created."); diff --git a/MinecraftClient/Protocol/Session/SessionCache.cs b/MinecraftClient/Protocol/Session/SessionCache.cs index 8fd17e95c2..fb701d30ac 100644 --- a/MinecraftClient/Protocol/Session/SessionCache.cs +++ b/MinecraftClient/Protocol/Session/SessionCache.cs @@ -1,9 +1,19 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Reflection; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; +using System.ServiceModel.Channels; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Threading.Tasks; using System.Timers; +using MinecraftClient.Protocol.ProfileKey; +using MinecraftClient.Scripting; using static MinecraftClient.Settings; using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig; @@ -12,261 +22,181 @@ namespace MinecraftClient.Protocol.Session /// /// Handle sessions caching and storage. /// - public static class SessionCache + public static partial class SessionCache { - private const string SessionCacheFilePlaintext = "SessionCache.ini"; - private const string SessionCacheFileSerialized = "SessionCache.db"; - private static readonly string SessionCacheFileMinecraft = String.Concat( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - Path.DirectorySeparatorChar, - ".minecraft", - Path.DirectorySeparatorChar, - "launcher_profiles.json" - ); + public class Cache + { + [JsonInclude] + public Dictionary SessionTokens = new(); - private static FileMonitor? cachemonitor; - private static readonly Dictionary sessions = new(); - private static readonly Timer updatetimer = new(100); - private static readonly List> pendingadds = new(); - private static readonly BinaryFormatter formatter = new(); + [JsonInclude] + public Dictionary ProfileKeys = new(); - /// - /// Retrieve whether SessionCache contains a session for the given login. - /// - /// User login used with Minecraft.net - /// TRUE if session is available - public static bool Contains(string login) - { - return sessions.ContainsKey(login); - } + [JsonInclude] + public Dictionary ServerKeys = new(); - /// - /// Store a session and save it to disk if required. - /// - /// User login used with Minecraft.net - /// User session token used with Minecraft.net - public static void Store(string login, SessionToken session) - { - if (Contains(login)) - { - sessions[login] = session; - } - else + public record ServerInfo { - sessions.Add(login, session); - } + public ServerInfo(string serverIDhash, byte[] serverPublicKey) + { + ServerIDhash = serverIDhash; + ServerPublicKey = serverPublicKey; + } - if (Config.Main.Advanced.SessionCache == CacheType.disk && updatetimer.Enabled == true) - { - pendingadds.Add(new KeyValuePair(login, session)); - } - else if (Config.Main.Advanced.SessionCache == CacheType.disk) - { - SaveToDisk(); + public string? ServerIDhash { init; get; } + public byte[]? ServerPublicKey { init; get; } } } - /// - /// Retrieve a session token for the given login. - /// - /// User login used with Minecraft.net - /// SessionToken for given login - public static SessionToken Get(string login) - { - return sessions[login]; - } - - /// - /// Initialize cache monitoring to keep cache updated with external changes. - /// - /// TRUE if session tokens are seeded from file - public static bool InitializeDiskCache() - { - cachemonitor = new FileMonitor(AppDomain.CurrentDomain.BaseDirectory, SessionCacheFilePlaintext, new FileSystemEventHandler(OnChanged)); - updatetimer.Elapsed += HandlePending; - return LoadFromDisk(); - } + private static Cache cache = new(); - /// - /// Reloads cache on external cache file change. - /// - /// Sender - /// Event data - private static void OnChanged(object sender, FileSystemEventArgs e) - { - updatetimer.Stop(); - updatetimer.Start(); - } + private const string SessionCacheFileJson = "SessionCache.json"; - /// - /// Called after timer elapsed. Reads disk cache and adds new/modified sessions back. - /// - /// Sender - /// Event data - private static void HandlePending(object? sender, ElapsedEventArgs e) + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.General) { - updatetimer.Stop(); - LoadFromDisk(); + WriteIndented = true, + AllowTrailingCommas = true, + PropertyNameCaseInsensitive = false, + ReadCommentHandling = JsonCommentHandling.Skip, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + }; - foreach (KeyValuePair pending in pendingadds.ToArray()) - { - Store(pending.Key, pending.Value); - pendingadds.Remove(pending); - } - } - /// - /// Reads cache file and loads SessionTokens into SessionCache. - /// - /// True if data is successfully loaded - private static bool LoadFromDisk() + public static async Task ReadCacheSessionAsync() { - //Grab sessions in the Minecraft directory - if (File.Exists(SessionCacheFileMinecraft)) + if (File.Exists(SessionCacheFileJson)) { if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading, Path.GetFileName(SessionCacheFileMinecraft))); - Json.JSONData mcSession = new(Json.JSONData.DataType.String); - try - { - mcSession = Json.ParseJson(File.ReadAllText(SessionCacheFileMinecraft)); - } - catch (IOException) { /* Failed to read file from disk -- ignoring */ } - if (mcSession.Type == Json.JSONData.DataType.Object - && mcSession.Properties.ContainsKey("clientToken") - && mcSession.Properties.ContainsKey("authenticationDatabase")) - { - string clientID = mcSession.Properties["clientToken"].StringValue.Replace("-", ""); - Dictionary sessionItems = mcSession.Properties["authenticationDatabase"].Properties; - foreach (string key in sessionItems.Keys) - { - if (Guid.TryParseExact(key, "N", out Guid temp)) - { - Dictionary sessionItem = sessionItems[key].Properties; - if (sessionItem.ContainsKey("displayName") - && sessionItem.ContainsKey("accessToken") - && sessionItem.ContainsKey("username") - && sessionItem.ContainsKey("uuid")) - { - string login = Settings.ToLowerIfNeed(sessionItem["username"].StringValue); - try - { - SessionToken session = SessionToken.FromString(String.Join(",", - sessionItem["accessToken"].StringValue, - sessionItem["displayName"].StringValue, - sessionItem["uuid"].StringValue.Replace("-", ""), - clientID - )); - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, login, session.ID)); - sessions[login] = session; - } - catch (InvalidDataException) { /* Not a valid session */ } - } - } - } - } - } + ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_session, SessionCacheFileJson)); - //Serialized session cache file in binary format - if (File.Exists(SessionCacheFileSerialized)) - { - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_converting, SessionCacheFileSerialized)); + FileStream fileStream = File.OpenRead(SessionCacheFileJson); try { - using FileStream fs = new(SessionCacheFileSerialized, FileMode.Open, FileAccess.Read, FileShare.Read); -#pragma warning disable SYSLIB0011 // BinaryFormatter.Deserialize() is obsolete - // Possible risk of information disclosure or remote code execution. The impact of this vulnerability is limited to the user side only. - Dictionary sessionsTemp = (Dictionary)formatter.Deserialize(fs); -#pragma warning restore SYSLIB0011 // BinaryFormatter.Deserialize() is obsolete - foreach (KeyValuePair item in sessionsTemp) + Cache? diskCache = (Cache?)await JsonSerializer.DeserializeAsync(fileStream, typeof(Cache), JsonOptions); + + if (diskCache != null) { + cache = diskCache; + if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, item.Key, item.Value.ID)); - sessions[item.Key] = item.Value; + ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, cache.SessionTokens.Count, cache.ProfileKeys.Count)); } } - catch (IOException ex) + catch (IOException e) { - ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail, ex.Message)); + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message)); } - catch (SerializationException ex2) + catch (JsonException e) { - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_malformed, ex2.Message)); + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message)); } + + await fileStream.DisposeAsync(); } + } - //User-editable session cache file in text format - if (File.Exists(SessionCacheFilePlaintext)) - { - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_session, SessionCacheFilePlaintext)); + /// + /// Retrieve whether SessionCache contains a session for the given login. + /// + /// User login used with Minecraft.net + /// TRUE if session is available + public static bool ContainsSession(string login) + { + return cache.SessionTokens.ContainsKey(login); + } - try - { - foreach (string line in FileMonitor.ReadAllLinesWithRetries(SessionCacheFilePlaintext)) - { - if (!line.Trim().StartsWith("#")) - { - string[] keyValue = line.Split('='); - if (keyValue.Length == 2) - { - try - { - string login = Settings.ToLowerIfNeed(keyValue[0]); - SessionToken session = SessionToken.FromString(keyValue[1]); - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, login, session.ID)); - sessions[login] = session; - } - catch (InvalidDataException e) - { - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string, keyValue[1], e.Message)); - } - } - else if (Config.Logging.DebugMessages) - { - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_line, line)); - } - } - } - } - catch (IOException e) - { - ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message)); - } - } + /// + /// Retrieve a session token for the given login. + /// + /// User login used with Minecraft.net + /// SessionToken for given login + public static Tuple GetSession(string login) + { + cache.SessionTokens.TryGetValue(login, out SessionToken? sessionToken); + cache.ProfileKeys.TryGetValue(login, out PlayerKeyPair? playerKeyPair); + return new(sessionToken, playerKeyPair); + } + + public static Cache.ServerInfo? GetServerInfo(string server) + { + if (cache.ServerKeys.TryGetValue(server, out Cache.ServerInfo? info)) + return info; + else + return null; + } + + /// + /// Store a session and save it to disk if required. + /// + /// User login used with Minecraft.net + /// User session token used with Minecraft.net + public static async Task StoreSessionAsync(string login, SessionToken? sessionToken, PlayerKeyPair? profileKey) + { + if (sessionToken != null) + cache.SessionTokens[login] = sessionToken; + if (profileKey != null) + cache.ProfileKeys[login] = profileKey; - return sessions.Count > 0; + if (Config.Main.Advanced.SessionCache == CacheType.disk) + await SaveToDisk(); + } + + public static void StoreServerInfo(string server, string ServerIDhash, byte[] ServerPublicKey) + { + cache.ServerKeys[server] = new(ServerIDhash, ServerPublicKey); } /// /// Saves SessionToken's from SessionCache into cache file. /// - private static void SaveToDisk() + private static async Task SaveToDisk() { if (Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted("§8" + Translations.cache_saving, acceptnewlines: true); - List sessionCacheLines = new() + foreach ((string login, SessionToken session) in cache.SessionTokens) { - "# Generated by MCC v" + Program.Version + " - Keep it secret & Edit at own risk!", - "# Login=SessionID,PlayerName,UUID,ClientID,RefreshToken,ServerIDhash,ServerPublicKey" - }; - foreach (KeyValuePair entry in sessions) - sessionCacheLines.Add(entry.Key + '=' + entry.Value.ToString()); + if (!GetJwtRegex().IsMatch(session.ID)) + cache.SessionTokens.Remove(login); + else if (!ChatBot.IsValidName(session.PlayerName)) + cache.SessionTokens.Remove(login); + else if (!Guid.TryParseExact(session.PlayerID, "N", out _)) + cache.SessionTokens.Remove(login); + else if (!Guid.TryParseExact(session.ClientID, "N", out _)) + cache.SessionTokens.Remove(login); + // No validation on refresh token because it is custom format token (not Jwt) + } + + foreach ((string login, PlayerKeyPair profileKey) in cache.ProfileKeys) + { + if (profileKey.NeedRefresh()) + cache.ProfileKeys.Remove(login); + } try { - FileMonitor.WriteAllLinesWithRetries(SessionCacheFilePlaintext, sessionCacheLines); + FileStream fileStream = File.Open(SessionCacheFileJson, FileMode.Create); + + await fileStream.WriteAsync(Encoding.UTF8.GetBytes($"/* Generated by MCC v{Program.Version} - Keep it secret & Edit at own risk! */{Environment.NewLine}")); + + await JsonSerializer.SerializeAsync(fileStream, cache, typeof(Cache), JsonOptions); + + await fileStream.FlushAsync(); + fileStream.Close(); + await fileStream.DisposeAsync(); } catch (IOException e) { ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail, e.Message)); } + catch (JsonException e) + { + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail, e.Message)); + } } + + [GeneratedRegex("^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$", RegexOptions.Compiled)] + private static partial Regex GetJwtRegex(); } } diff --git a/MinecraftClient/Protocol/Session/SessionToken.cs b/MinecraftClient/Protocol/Session/SessionToken.cs index c3c0aee921..dac4e6b707 100644 --- a/MinecraftClient/Protocol/Session/SessionToken.cs +++ b/MinecraftClient/Protocol/Session/SessionToken.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Net.Http; +using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Threading.Tasks; using MinecraftClient.Scripting; @@ -9,92 +11,39 @@ namespace MinecraftClient.Protocol.Session [Serializable] public class SessionToken { - private static readonly Regex JwtRegex = new("^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$"); - + [JsonInclude] + [JsonPropertyName("SessionID")] public string ID { get; set; } + + [JsonInclude] + [JsonPropertyName("PlayerName")] public string PlayerName { get; set; } + + [JsonInclude] + [JsonPropertyName("PlayerID")] public string PlayerID { get; set; } - public string ClientID { get; set; } - public string RefreshToken { get; set; } - public string ServerIDhash { get; set; } - public byte[]? ServerPublicKey { get; set; } - public Task? SessionPreCheckTask = null; + [JsonInclude] + [JsonPropertyName("ClientID")] + public string ClientID { get; set; } - public SessionToken() - { - ID = String.Empty; - PlayerName = String.Empty; - PlayerID = String.Empty; - ClientID = String.Empty; - RefreshToken = String.Empty; - ServerIDhash = String.Empty; - ServerPublicKey = null; - } + [JsonInclude] + [JsonPropertyName("RefreshToken")] + public string RefreshToken { get; set; } - public bool SessionPreCheck() - { - if (ID == string.Empty || PlayerID == String.Empty || ServerPublicKey == null) - return false; - Crypto.CryptoHandler.ClientAESPrivateKey ??= Crypto.CryptoHandler.GenerateAESPrivateKey(); - string serverHash = Crypto.CryptoHandler.GetServerHash(ServerIDhash, ServerPublicKey, Crypto.CryptoHandler.ClientAESPrivateKey); - if (ProtocolHandler.SessionCheck(PlayerID, ID, serverHash)) - return true; - return false; - } + [JsonIgnore] + public string? ServerInfoHash = null; - public override string ToString() - { - return String.Join(",", ID, PlayerName, PlayerID, ClientID, RefreshToken, ServerIDhash, - (ServerPublicKey == null) ? String.Empty : Convert.ToBase64String(ServerPublicKey)); - } + [JsonIgnore] + public Task>? SessionPreCheckTask = null; - public static SessionToken FromString(string tokenString) + public SessionToken() { - string[] fields = tokenString.Split(','); - if (fields.Length < 4) - throw new InvalidDataException("Invalid string format"); - - SessionToken session = new() - { - ID = fields[0], - PlayerName = fields[1], - PlayerID = fields[2], - ClientID = fields[3] - }; - // Backward compatible with old session file without refresh token field - if (fields.Length > 4) - session.RefreshToken = fields[4]; - else - session.RefreshToken = String.Empty; - if (fields.Length > 5) - session.ServerIDhash = fields[5]; - else - session.ServerIDhash = String.Empty; - if (fields.Length > 6) - { - try - { - session.ServerPublicKey = Convert.FromBase64String(fields[6]); - } - catch - { - session.ServerPublicKey = null; - } - } - else - session.ServerPublicKey = null; - if (!JwtRegex.IsMatch(session.ID)) - throw new InvalidDataException("Invalid session ID"); - if (!ChatBot.IsValidName(session.PlayerName)) - throw new InvalidDataException("Invalid player name"); - if (!Guid.TryParseExact(session.PlayerID, "N", out _)) - throw new InvalidDataException("Invalid player ID"); - if (!Guid.TryParseExact(session.ClientID, "N", out _)) - throw new InvalidDataException("Invalid client ID"); - // No validation on refresh token because it is custom format token (not Jwt) - - return session; + ID = string.Empty; + PlayerName = string.Empty; + PlayerID = string.Empty; + ClientID = string.Empty; + RefreshToken = string.Empty; } } } diff --git a/MinecraftClient/Protocol/TitlePacket.cs b/MinecraftClient/Protocol/TitlePacket.cs new file mode 100644 index 0000000000..f1fe5d1102 --- /dev/null +++ b/MinecraftClient/Protocol/TitlePacket.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MinecraftClient.Protocol +{ + public record TitlePacket + { + public int Action { init; get; } + + public string TitleText { init; get; } = string.Empty; + + public string SubtitleText { init; get; } = string.Empty; + + public string ActionbarText { init; get; } = string.Empty; + + public int Stay { init; get; } + + public int FadeIn { init; get; } + + public int FadeOut { init; get; } + + public string JsonText { init; get; } = string.Empty; + } +} diff --git a/MinecraftClient/Proxy/ProxyHandler.cs b/MinecraftClient/Proxy/ProxyHandler.cs index 1cd55e403e..df567258e7 100644 --- a/MinecraftClient/Proxy/ProxyHandler.cs +++ b/MinecraftClient/Proxy/ProxyHandler.cs @@ -1,4 +1,7 @@ -using System.Net.Sockets; +using System; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; using Starksoft.Aspen.Proxy; using Tomlet.Attributes; @@ -17,14 +20,17 @@ public static class ProxyHandler [TomlDoNotInlineObject] public class Configs { - [TomlInlineComment("$Proxy.Enabled_Update$")] - public bool Enabled_Update = false; + [NonSerialized] // Compatible with old settings. + public bool? Enabled_Login = false, Enabled_Ingame = false, Enabled_Update = false; - [TomlInlineComment("$Proxy.Enabled_Login$")] - public bool Enabled_Login = false; + [TomlInlineComment("$Proxy.Ingame_Proxy$")] + public ProxyPreferenceType Ingame_Proxy = ProxyPreferenceType.disable; - [TomlInlineComment("$Proxy.Enabled_Ingame$")] - public bool Enabled_Ingame = false; + [TomlInlineComment("$Proxy.Login_Proxy$")] + public ProxyPreferenceType Login_Proxy = ProxyPreferenceType.follow_system; + + [TomlInlineComment("$Proxy.MCC_Update_Proxy$")] + public ProxyPreferenceType MCC_Update_Proxy = ProxyPreferenceType.follow_system; [TomlInlineComment("$Proxy.Server$")] public ProxyInfoConfig Server = new("0.0.0.0", 8080); @@ -33,12 +39,22 @@ public class Configs public ProxyType Proxy_Type = ProxyType.HTTP; [TomlInlineComment("$Proxy.Username$")] - public string Username = ""; + public string Username = string.Empty; [TomlInlineComment("$Proxy.Password$")] - public string Password = ""; + public string Password = string.Empty; - public void OnSettingUpdate() { } + public void OnSettingUpdate() + { + { // Compatible with old settings. + if (Enabled_Login.HasValue && Enabled_Login.Value) + Login_Proxy = ProxyPreferenceType.custom; + if (Enabled_Ingame.HasValue && Enabled_Ingame.Value) + Ingame_Proxy = ProxyPreferenceType.custom; + if (Enabled_Update.HasValue && Enabled_Update.Value) + MCC_Update_Proxy = ProxyPreferenceType.custom; + } + } public struct ProxyInfoConfig { @@ -53,8 +69,12 @@ public ProxyInfoConfig(string host, ushort port) } public enum ProxyType { HTTP, SOCKS4, SOCKS4a, SOCKS5 }; + + public enum ProxyPreferenceType { custom, follow_system, disable }; } + public enum ClientType { Ingame, Login, Update }; + private static readonly ProxyClientFactory factory = new(); private static IProxyClient? proxy; private static bool proxy_ok = false; @@ -66,11 +86,14 @@ public enum ProxyType { HTTP, SOCKS4, SOCKS4a, SOCKS5 }; /// Target port /// True if the purpose is logging in to a Minecraft account - public static TcpClient NewTcpClient(string host, int port, bool login = false) + public static TcpClient NewTcpClient(string host, int port, ClientType clientType) { + if (clientType == ClientType.Update) + throw new NotSupportedException(); try { - if (login ? Config.Enabled_Login : Config.Enabled_Ingame) + Configs.ProxyPreferenceType proxyPreference = clientType == ClientType.Ingame ? Config.Ingame_Proxy : Config.Login_Proxy; + if (proxyPreference == Configs.ProxyPreferenceType.custom) { ProxyType innerProxytype = ProxyType.Http; @@ -95,7 +118,30 @@ public static TcpClient NewTcpClient(string host, int port, bool login = false) return proxy.CreateConnection(host, port); } - else return new TcpClient(host, port); + else if (proxyPreference == Configs.ProxyPreferenceType.follow_system) + { + Uri? webProxy = WebRequest.GetSystemWebProxy().GetProxy(new("http://" + host)); + if (webProxy != null) + { + proxy = factory.CreateProxyClient(ProxyType.Http, webProxy.Host, webProxy.Port); + + if (!proxy_ok) + { + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.proxy_connected, webProxy.Host, webProxy.Port)); + proxy_ok = true; + } + + return proxy.CreateConnection(host, port); + } + else + { + return new TcpClient(host, port); + } + } + else + { + return new TcpClient(host, port); + } } catch (ProxyException e) { @@ -104,5 +150,58 @@ public static TcpClient NewTcpClient(string host, int port, bool login = false) throw new SocketException((int)SocketError.HostUnreachable); } } + + public static HttpClient NewHttpClient(ClientType clientType, HttpClientHandler? httpClientHandler = null) + { + if (clientType == ClientType.Ingame) + throw new NotSupportedException(); + + httpClientHandler ??= new(); + AddProxySettings(clientType, ref httpClientHandler); + return new HttpClient(httpClientHandler); + } + + public static void AddProxySettings(ClientType clientType, ref HttpClientHandler httpClientHandler) + { + if (clientType == ClientType.Ingame) + throw new NotSupportedException(); + + Configs.ProxyPreferenceType proxyPreference = clientType == ClientType.Login ? Config.Login_Proxy : Config.MCC_Update_Proxy; + + if (proxyPreference == Configs.ProxyPreferenceType.custom) + { + httpClientHandler ??= new(); + + string proxyAddress; + if (!string.IsNullOrWhiteSpace(Settings.Config.Proxy.Username) && !string.IsNullOrWhiteSpace(Settings.Config.Proxy.Password)) + proxyAddress = string.Format("{0}://{3}:{4}@{1}:{2}", + Settings.Config.Proxy.Proxy_Type.ToString().ToLower(), + Settings.Config.Proxy.Server.Host, + Settings.Config.Proxy.Server.Port, + Settings.Config.Proxy.Username, + Settings.Config.Proxy.Password); + else + proxyAddress = string.Format("{0}://{1}:{2}", + Settings.Config.Proxy.Proxy_Type.ToString().ToLower(), + Settings.Config.Proxy.Server.Host, Settings.Config.Proxy.Server.Port); + + httpClientHandler.Proxy = new WebProxy(proxyAddress, true); + httpClientHandler.UseProxy = true; + } + else if (proxyPreference == Configs.ProxyPreferenceType.follow_system) + { + httpClientHandler.Proxy = WebRequest.GetSystemWebProxy(); + httpClientHandler.UseProxy = true; + } + else if (proxyPreference == Configs.ProxyPreferenceType.disable) + { + httpClientHandler ??= new(); + httpClientHandler.UseProxy = false; + } + else + { + throw new NotSupportedException(); + } + } } } diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs index 8f358c62c7..7f8b486e56 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs @@ -1699,7 +1699,8 @@ internal static string MCSettings_RenderDistance { /// Looks up a localized string similar to Connect to a server via a proxy instead of connecting directly ///If Mojang session services are blocked on your network, set Enabled_Login=true to login using proxy. ///If the connection to the Minecraft game server is blocked by the firewall, set Enabled_Ingame=true to use a proxy to connect to the game server. - ////!\ Make sure your server rules allow Proxies or VPNs before setting enabled=true, or you may face consequences!. + ////!\ Make sure your server rules allow Proxies or VPNs before setting enabled=true, or you may face consequences! + ///Use "custom", "follow_system" or "disable".. /// internal static string Proxy { get { @@ -1710,27 +1711,27 @@ internal static string Proxy { /// /// Looks up a localized string similar to Whether to connect to the game server through a proxy.. /// - internal static string Proxy_Enabled_Ingame { + internal static string Proxy_Ingame_Proxy { get { - return ResourceManager.GetString("Proxy.Enabled_Ingame", resourceCulture); + return ResourceManager.GetString("Proxy.Ingame_Proxy", resourceCulture); } } /// /// Looks up a localized string similar to Whether to connect to the login server through a proxy.. /// - internal static string Proxy_Enabled_Login { + internal static string Proxy_Login_Proxy { get { - return ResourceManager.GetString("Proxy.Enabled_Login", resourceCulture); + return ResourceManager.GetString("Proxy.Login_Proxy", resourceCulture); } } /// /// Looks up a localized string similar to Whether to download MCC updates via proxy.. /// - internal static string Proxy_Enabled_Update { + internal static string Proxy_MCC_Update_Proxy { get { - return ResourceManager.GetString("Proxy.Enabled_Update", resourceCulture); + return ResourceManager.GetString("Proxy.MCC_Update_Proxy", resourceCulture); } } diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index bd3a27de2b..3a6b172bdc 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -751,15 +751,16 @@ Usage examples: "/tell <mybot> connect Server1", "/connect Server2"Connect to a server via a proxy instead of connecting directly If Mojang session services are blocked on your network, set Enabled_Login=true to login using proxy. If the connection to the Minecraft game server is blocked by the firewall, set Enabled_Ingame=true to use a proxy to connect to the game server. -/!\ Make sure your server rules allow Proxies or VPNs before setting enabled=true, or you may face consequences! +/!\ Make sure your server rules allow Proxies or VPNs before setting enabled=true, or you may face consequences! +Use "custom", "follow_system" or "disable". - + Whether to connect to the game server through a proxy. - + Whether to connect to the login server through a proxy. - + Whether to download MCC updates via proxy. diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index 9c1b8d8461..d06a171650 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -2127,7 +2127,7 @@ internal static string cache_ignore_string_keys { } /// - /// Looks up a localized string similar to Loaded session: {0}:{1}. + /// Looks up a localized string similar to Reads {0} session cache and {1} signature key caches from the disk cache.. /// internal static string cache_loaded { get { @@ -3643,6 +3643,15 @@ internal static string cmd_inventory_not_exist { } } + /// + /// Looks up a localized string similar to Player Inventory. + /// + internal static string cmd_inventory_player_inventory { + get { + return ResourceManager.GetString("cmd.inventory.player_inventory", resourceCulture); + } + } + /// /// Looks up a localized string similar to Right. /// @@ -4131,29 +4140,65 @@ internal static string config_commandsuggestion_illegal_color { } /// - /// Looks up a localized string similar to Settings have been loaded from {0}. + /// Looks up a localized string similar to File {0} does not exist.. /// - internal static string config_load { + internal static string config_file_not_exist { get { - return ResourceManager.GetString("config.load", resourceCulture); + return ResourceManager.GetString("config.file_not_exist", resourceCulture); } } /// - /// Looks up a localized string similar to Failed to load settings:. + /// Looks up a localized string similar to Invalid file extension {0} in {1}, requires {2}.. /// - internal static string config_load_fail { + internal static string config_invaild_file_extension { get { - return ResourceManager.GetString("config.load.fail", resourceCulture); + return ResourceManager.GetString("config.invaild_file_extension", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [Settings] The language code is invalid!. + /// + internal static string config_invaild_language { + get { + return ResourceManager.GetString("config.invaild_language", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [Settings] Only Microsoft accounts support logging in using the browser method.. + /// + internal static string config_invaild_login_method { + get { + return ResourceManager.GetString("config.invaild_login_method", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are using an older color scheme and some colors may not be displayed correctly. It is recommended that you switch to a modern terminal for a better display experience.. + /// + internal static string config_legacy_color { + get { + return ResourceManager.GetString("config.legacy_color", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings have been loaded from {0}. + /// + internal static string config_load { + get { + return ResourceManager.GetString("config.load", resourceCulture); } } /// - /// Looks up a localized string similar to The language code is invalid!. + /// Looks up a localized string similar to Failed to load settings:. /// - internal static string config_Main_Advanced_language_invaild { + internal static string config_load_fail { get { - return ResourceManager.GetString("config.Main.Advanced.language.invaild", resourceCulture); + return ResourceManager.GetString("config.load.fail", resourceCulture); } } @@ -5121,6 +5166,16 @@ internal static string mcc_browser_open { } } + /// + /// Looks up a localized string similar to An exception occurred while processing event {0}: + ///{1}. + /// + internal static string mcc_chatbot_event_exception { + get { + return ResourceManager.GetString("mcc.chatbot_event_exception", resourceCulture); + } + } + /// /// Looks up a localized string similar to Connecting to {0}.... /// @@ -5295,7 +5350,16 @@ internal static string mcc_link { } /// - /// Looks up a localized string similar to Login :. + /// Looks up a localized string similar to Exceptions occur when applying settings from command line parameters ({0}), and some entries may be ignored.. + /// + internal static string mcc_load_from_args_fail { + get { + return ResourceManager.GetString("mcc.load_from_args_fail", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login:. /// internal static string mcc_login { get { @@ -5312,6 +5376,15 @@ internal static string mcc_login_basic_io { } } + /// + /// Looks up a localized string similar to Timed out when requesting {0}.. + /// + internal static string mcc_network_timeout { + get { + return ResourceManager.GetString("mcc.network_timeout", resourceCulture); + } + } + /// /// Looks up a localized string similar to Failed to perform SRV lookup for {0} ///{1}: {2}. @@ -5531,7 +5604,7 @@ internal static string mcc_session_valid { } /// - /// Looks up a localized string similar to Settings file MinecraftClient.ini has been generated.. + /// Looks up a localized string similar to The configuration file is saved as {0}. /// internal static string mcc_settings_generated { get { @@ -5539,6 +5612,15 @@ internal static string mcc_settings_generated { } } + /// + /// Looks up a localized string similar to Error: Unhandled exception:. + /// + internal static string mcc_unhandled_exception { + get { + return ResourceManager.GetString("mcc.unhandled_exception", resourceCulture); + } + } + /// /// Looks up a localized string similar to Unknown or not supported MC version {0}. ///Switching to autodetection mode.. diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index 73e81493fe..5c69f145a6 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -822,7 +822,7 @@ Add the ID of this chat to "Authorized_Chat_Ids" field in the configuration file Ignoring profile key token string '{0}': {1} - Loaded session: {0}:{1} + Reads {0} session cache and {1} signature key caches from the disk cache. Loaded profile key, it will be refresh at {0} @@ -1499,8 +1499,8 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s Failed to load settings: - - The language code is invalid! + + [Settings] The language code is invalid! The current setting is saved as {0} @@ -1884,7 +1884,7 @@ Type '{0}quit' to leave the server. Link: {0} - Login : + Login: Please type the username or email of your choice. @@ -1964,7 +1964,7 @@ MCC is running with default settings. Cached session is still valid for {0}. - Settings file MinecraftClient.ini has been generated. + The configuration file is saved as {0} Unknown or not supported MC version {0}. @@ -2028,4 +2028,32 @@ Logging in... Connected to proxy {0}:{1} + + Exceptions occur when applying settings from command line parameters ({0}), and some entries may be ignored. + + + Error: Unhandled exception: + + + Timed out when requesting {0}. + + + [Settings] Only Microsoft accounts support logging in using the browser method. + + + Invalid file extension {0} in {1}, requires {2}. + + + File {0} does not exist. + + + You are using an older color scheme and some colors may not be displayed correctly. It is recommended that you switch to a modern terminal for a better display experience. + + + Player Inventory + + + An exception occurred while processing event {0}: +{1} + \ No newline at end of file diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index d73f088407..e3cbbb075e 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -4,8 +4,10 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading.Tasks; using Brigadier.NET; using MinecraftClient.CommandHandler; +using MinecraftClient.EntityHandler; using MinecraftClient.Inventory; using MinecraftClient.Mapping; using static MinecraftClient.Settings; @@ -36,14 +38,16 @@ public enum DisconnectReason { InGameKick, LoginRejected, ConnectionLost, UserLo //Handler will be automatically set on bot loading, don't worry about this public void SetHandler(McClient handler) { _handler = handler; } protected void SetMaster(ChatBot master) { this.master = master; } - protected void LoadBot(ChatBot bot) { Handler.BotUnLoad(bot); Handler.BotLoad(bot); } - protected List GetLoadedChatBots() { return Handler.GetLoadedChatBots(); } - protected void UnLoadBot(ChatBot bot) { Handler.BotUnLoad(bot); } + protected void LoadBot(ChatBot bot) { Handler.BotUnLoad(bot).Wait(); Handler.BotLoad(bot).Wait(); } + protected ChatBot[] GetLoadedChatBots() { return Handler.GetLoadedChatBots(); } + protected void UnLoadBot(ChatBot bot) { Handler.BotUnLoad(bot).Wait(); } private McClient? _handler = null; private ChatBot? master = null; private readonly List registeredPluginChannels = new(); + private readonly object delayTasksLock = new(); private readonly List delayedTasks = new(); + protected McClient Handler { get @@ -102,6 +106,12 @@ public void UpdateInternal() /// public virtual void Initialize() { } + /// + /// + /// + /// + public virtual Tuple>[]? InitializeEventCallbacks() { return null; } + /// /// This method is called when the bot is being unloaded, you can use it to free up resources like DB connections /// @@ -117,10 +127,15 @@ public virtual void OnUnload() { } public virtual void AfterGameJoined() { } /// - /// Will be called every ~100ms (10fps) if loaded in MinecraftCom + /// Will be called every ~100ms (10tps) if loaded in MinecraftCom /// public virtual void Update() { } + /// + /// Will be called every ~50ms (20tps) if loaded in MinecraftCom + /// + public virtual async Task OnClientTickAsync() { await Task.CompletedTask; } + /// /// Will be called every player break block in gamemode 0 /// @@ -157,8 +172,8 @@ public virtual void GetText(string text, string? json) { } /// /// Disconnect Reason /// Kick message, if any - /// Return TRUE if the client is about to restart - public virtual bool OnDisconnect(DisconnectReason reason, string message) { return false; } + /// A return value less than zero indicates no reconnection, otherwise it is the number of milliseconds to wait before reconnecting. + public virtual int OnDisconnect(DisconnectReason reason, string message) { return -1; } /// /// Called when a plugin channel message is received. @@ -321,7 +336,7 @@ public virtual void OnEntityEquipment(Entity entity, int slot, Item? item) { } /// effect amplifier /// effect duration /// effect flags - public virtual void OnEntityEffect(Entity entity, Effects effect, int amplifier, int duration, byte flags) { } + public virtual void OnEntityEffect(Entity entity, EffectType effect, int amplifier, int duration, byte flags) { } /// /// Called when a scoreboard objective updated @@ -502,7 +517,7 @@ public virtual void OnBlockChange(Location location, Block block) { } protected bool SendText(string text, bool sendImmediately = false) { LogToConsole("Sending '" + text + "'"); - Handler.SendText(text); + Handler.SendTextAsync(text).Wait(); return true; } @@ -929,7 +944,7 @@ protected void ReconnectToTheServer(int ExtraAttempts = 3, int delaySeconds = 0, ConsoleIO.WriteLogLine(string.Format(Translations.chatbot_reconnect, botName)); } McClient.ReconnectionAttemptsLeft = ExtraAttempts; - Program.Restart(delaySeconds, keepAccountAndServerSettings); + Program.SetRestart(delaySeconds * 10, keepAccountAndServerSettings); } /// @@ -937,7 +952,7 @@ protected void ReconnectToTheServer(int ExtraAttempts = 3, int delaySeconds = 0, /// protected void DisconnectAndExit() { - Program.Exit(); + Program.SetExit(); } /// @@ -945,7 +960,7 @@ protected void DisconnectAndExit() /// protected void UnloadBot() { - Handler.BotUnLoad(this); + Handler.BotUnLoad(this).Wait(); } /// @@ -966,7 +981,7 @@ protected void SendPrivateMessage(string player, string message) /// Local variables for use in the Script protected void RunScript(string filename, string? playername = null, Dictionary? localVars = null) { - Handler.BotLoad(new ChatBots.Script(filename, playername, localVars)); + Handler.BotLoad(new ChatBots.Script(filename, playername, localVars)).Wait(); } /// @@ -975,7 +990,7 @@ protected void RunScript(string filename, string? playername = null, Dictionary< /// ChatBot to load protected void BotLoad(ChatBot chatBot) { - Handler.BotLoad(chatBot); + Handler.BotLoad(chatBot).Wait(); } /// @@ -1021,7 +1036,15 @@ protected bool Sneak(bool on) /// private bool SendEntityAction(Protocol.EntityActionType entityAction) { - return Handler.SendEntityAction(entityAction); + return Handler.SendEntityActionAsync(entityAction).Result; + } + + /// + /// Send Entity Action + /// + private async Task SendEntityActionAsync(Protocol.EntityActionType entityAction) + { + return await Handler.SendEntityActionAsync(entityAction); } /// @@ -1032,15 +1055,15 @@ private bool SendEntityAction(Protocol.EntityActionType entityAction) /// Also look at the block before digging protected bool DigBlock(Location location, bool swingArms = true, bool lookAtBlock = true) { - return Handler.DigBlock(location, swingArms, lookAtBlock); + return Handler.DigBlockAsync(location, swingArms, lookAtBlock).Result; } /// /// SetSlot /// - protected void SetSlot(int slotNum) + protected bool SetSlot(int slotNum) { - Handler.ChangeSlot((short)slotNum); + return Handler.ChangeSlotAsync((short)slotNum).Result; } /// @@ -1092,7 +1115,7 @@ protected Location GetCurrentLocation() /// True if a path has been found protected bool MoveToLocation(Location location, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, TimeSpan? timeout = null) { - return Handler.MoveTo(location, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeout); + return Handler.MoveToAsync(location, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeout).Result; } /// @@ -1273,7 +1296,7 @@ protected Dictionary GetOnlinePlayersWithUUID() protected void RegisterPluginChannel(string channel) { registeredPluginChannels.Add(channel); - Handler.RegisterPluginChannel(channel, this); + Handler.RegisterPluginChannelAsync(channel, this).Wait(); } /// @@ -1283,7 +1306,7 @@ protected void RegisterPluginChannel(string channel) protected void UnregisterPluginChannel(string channel) { registeredPluginChannels.RemoveAll(chan => chan == channel); - Handler.UnregisterPluginChannel(channel, this); + Handler.UnregisterPluginChannelAsync(channel, this).Wait(); } /// @@ -1303,7 +1326,7 @@ protected bool SendPluginChannelMessage(string channel, byte[] data, bool sendEv return false; } } - return Handler.SendPluginChannelMessage(channel, data, sendEvenIfNotRegistered); + return Handler.SendPluginChannelMessageAsync(channel, data, sendEvenIfNotRegistered).Result; } /// @@ -1325,7 +1348,7 @@ protected double GetServerTPS() [Obsolete("Prefer using InteractType enum instead of int for interaction type")] protected bool InteractEntity(int EntityID, int type, Hand hand = Hand.MainHand) { - return Handler.InteractEntity(EntityID, (InteractType)type, hand); + return Handler.InteractEntityAsync(EntityID, (InteractType)type, hand).Result; } /// @@ -1337,7 +1360,7 @@ protected bool InteractEntity(int EntityID, int type, Hand hand = Hand.MainHand) /// TRUE in case of success protected bool InteractEntity(int EntityID, InteractType type, Hand hand = Hand.MainHand) { - return Handler.InteractEntity(EntityID, type, hand); + return Handler.InteractEntityAsync(EntityID, type, hand).Result; } /// @@ -1351,7 +1374,7 @@ protected bool InteractEntity(int EntityID, InteractType type, Hand hand = Hand. /// TRUE if item given successfully protected bool CreativeGive(int slot, ItemType itemType, int count, Dictionary? nbt = null) { - return Handler.DoCreativeGive(slot, itemType, count, nbt); + return Handler.DoCreativeGiveAsync(slot, itemType, count, nbt).Result; } /// @@ -1373,7 +1396,7 @@ protected bool CreativeDelete(int slot) /// TRUE if animation successfully done public bool SendAnimation(Hand hand = Hand.MainHand) { - return Handler.DoAnimation((int)hand); + return Handler.DoAnimationAsync((int)hand).Result; } /// @@ -1382,7 +1405,7 @@ public bool SendAnimation(Hand hand = Hand.MainHand) /// TRUE if successful protected bool UseItemInHand() { - return Handler.UseItemOnHand(); + return Handler.UseItemOnHandAsync().Result; } /// @@ -1391,7 +1414,7 @@ protected bool UseItemInHand() /// TRUE if successful protected bool UseItemInLeftHand() { - return Handler.UseItemOnLeftHand(); + return Handler.UseItemOnOffHandAsync().Result; } /// @@ -1412,7 +1435,7 @@ public bool GetInventoryEnabled() /// TRUE if successfully placed public bool SendPlaceBlock(Location location, Direction blockFace, Hand hand = Hand.MainHand) { - return Handler.PlaceBlock(location, blockFace, hand); + return Handler.PlaceBlockAsync(location, blockFace, hand).Result; } /// @@ -1443,7 +1466,7 @@ public Dictionary GetInventories() /// TRUE in case of success protected bool WindowAction(int inventoryId, int slot, WindowActionType actionType) { - return Handler.DoWindowAction(inventoryId, slot, actionType); + return Handler.DoWindowActionAsync(inventoryId, slot, actionType).Result; } /// @@ -1463,7 +1486,7 @@ protected ItemMovingHelper GetItemMovingHelper(Container container) /// True if success protected bool ChangeSlot(short slot) { - return Handler.ChangeSlot(slot); + return Handler.ChangeSlotAsync(slot).Result; } /// @@ -1494,7 +1517,7 @@ protected bool ClearInventories() /// text1 four protected bool UpdateSign(Location location, string line1, string line2, string line3, string line4) { - return Handler.UpdateSign(location, line1, line2, line3, line4); + return Handler.UpdateSignAsync(location, line1, line2, line3, line4).Result; } /// @@ -1503,7 +1526,7 @@ protected bool UpdateSign(Location location, string line1, string line2, string /// Trade slot to select, starts at 0. protected bool SelectTrade(int selectedSlot) { - return Handler.SelectTrade(selectedSlot); + return Handler.SelectTradeAsync(selectedSlot).Result; } /// @@ -1512,7 +1535,7 @@ protected bool SelectTrade(int selectedSlot) /// player to teleport to protected bool SpectatorTeleport(Entity entity) { - return Handler.Spectate(entity); + return Handler.SpectateAsync(entity).Result; } /// @@ -1521,7 +1544,7 @@ protected bool SpectatorTeleport(Entity entity) /// uuid of entity to teleport to protected bool SpectatorTeleport(Guid UUID) { - return Handler.SpectateByUUID(UUID); + return Handler.SpectateByUuidAsync(UUID).Result; } /// @@ -1533,7 +1556,7 @@ protected bool SpectatorTeleport(Guid UUID) /// command block flags protected bool UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags) { - return Handler.UpdateCommandBlock(location, command, mode, flags); + return Handler.UpdateCommandBlockAsync(location, command, mode, flags).Result; } /// @@ -1543,7 +1566,7 @@ protected bool UpdateCommandBlock(Location location, string command, CommandBloc /// True if success protected bool CloseInventory(int inventoryID) { - return Handler.CloseInventory(inventoryID); + return Handler.CloseInventoryAsync(inventoryID).Result; } /// @@ -1561,7 +1584,7 @@ protected int GetMaxChatMessageLength() protected bool Respawn() { if (Handler.GetHealth() <= 0) - return Handler.SendRespawnPacket(); + return Handler.SendRespawnPacketAsync().Result; else return false; } @@ -1586,32 +1609,6 @@ protected int GetProtocolVersion() return Handler.GetProtocolVersion(); } - /// - /// Invoke a task on the main thread, wait for completion and retrieve return value. - /// - /// Task to run with any type or return value - /// Any result returned from task, result type is inferred from the task - /// bool result = InvokeOnMainThread(methodThatReturnsAbool); - /// bool result = InvokeOnMainThread(() => methodThatReturnsAbool(argument)); - /// int result = InvokeOnMainThread(() => { yourCode(); return 42; }); - /// Type of the return value - protected T InvokeOnMainThread(Func task) - { - return Handler.InvokeOnMainThread(task); - } - - /// - /// Invoke a task on the main thread and wait for completion - /// - /// Task to run without return value - /// InvokeOnMainThread(methodThatReturnsNothing); - /// InvokeOnMainThread(() => methodThatReturnsNothing(argument)); - /// InvokeOnMainThread(() => { yourCode(); }); - protected void InvokeOnMainThread(Action task) - { - Handler.InvokeOnMainThread(task); - } - /// /// Schedule a task to run on the main thread, and do not wait for completion /// diff --git a/MinecraftClient/Scripting/McClientEventType.cs b/MinecraftClient/Scripting/McClientEventType.cs new file mode 100644 index 0000000000..2bee3fd9bc --- /dev/null +++ b/MinecraftClient/Scripting/McClientEventType.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MinecraftClient.Scripting +{ + public enum McClientEventType + { + /* Internal Events */ + Initialize, + ClientDisconnect, /* Tuple(reason, message) */ + ClientTick, + InternalCommand, + NetworkPacket, /* Tuple(packetID, packetData, isLogin, isInbound) */ + ServerTpsUpdate, /* double(tps) */ + + /* General in-game events */ + BlockBreakAnimation, + BlockChange, + Enchantments, /* EnchantmentData(lastEnchantment) */ + Explosion, /* Tuple(location, strength, affectedBlocks) */ + GameJoin, + MapDataReceive, + PluginMessage, + RainLevelChange, + ScoreboardUpdate, + ScoreUpdate, + TextReceive, /* Tuple(messageText, message.content) */ + ThunderLevelChange, + TimeUpdate, /* Tuple(WorldAge, TimeOfDay) */ + TitleReceive, /* TitlePacket(title) */ + TradeListReceive, + + /* Player Events */ + PlayerLatencyUpdate, /* Tuple(player, latency) */ + PlayerJoin, /* PlayerInfo(player) */ + PlayerKilled, /* Tuple(killer, chatMessage) */ + PlayerLeave, /* Tuple(uuid, playerInfo) */ + PlayerPropertyReceive, /* Dictionary(prop) */ + PlayerStatusUpdate, /* byte(status) */ + + /* Player's own events */ + Death, /* null */ + ExperienceChange, /* Tuple(Experiencebar, Level, TotalExperience) */ + GamemodeUpdate, /* Tuple(player, gamemode) */ + HealthUpdate, /* Tuple(health, food) */ + HeldItemChange, /* byte(slot) */ + Respawn, + + /* Inventory-related events */ + InventoryClose, /* int(inventoryID) */ + InventoryOpen, /* int(inventoryID) */ + InventoryProperties, /* Tuple(inventoryID, propertyId, propertyValue) */ + InventoryUpdate, /* int(inventoryID) */ + + /* Entity-related events */ + EntityAnimation, + EntityDespawn, /* Entity(entity) */ + EntityEffect, /* Tuple(entity, effect) */ + EntityEquipment, /* Tuple(entity, slot, item) */ + EntityHealth, + EntityMetadata, + EntityMove, /* Entity(entity) */ + EntitySpawn, /* Entity(entity) */ + }; +} diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 8ddd556b80..d698cf926b 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -8,11 +8,13 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; +using System.Threading.Tasks; using MinecraftClient.Protocol; using MinecraftClient.Proxy; using Tomlet; using Tomlet.Attributes; using Tomlet.Models; +using static MinecraftClient.Protocol.ProtocolHandler; using static MinecraftClient.Settings.AppVarConfigHelper; using static MinecraftClient.Settings.ChatBotConfigHealper; using static MinecraftClient.Settings.ChatFormatConfigHelper; @@ -27,10 +29,9 @@ namespace MinecraftClient { - public static class Settings + public static partial class Settings { private const int CommentsAlignPosition = 45; - private readonly static Regex CommentRegex = new(@"^(.*)\s?#\s\$([\w\.]+)\$\s*$$", RegexOptions.Compiled); // Other Settings public const string TranslationsFile_Version = "1.19.3"; @@ -43,7 +44,7 @@ public static class Settings public static class InternalConfig { - public static string ServerIP = String.Empty; + public static string ServerIP = string.Empty; public static ushort ServerPort = 25565; @@ -128,7 +129,7 @@ public ChatFormatConfig ChatFormat public ChatBotConfig ChatBot { get { return ChatBotConfigHealper.Config; } - set { ChatBotConfigHealper.Config = value; } + set { ChatBotConfigHealper.Config = value; ChatBotConfigHealper.Config.OnSettingUpdate(); } } } @@ -180,7 +181,7 @@ public static Tuple LoadFromFile(string filepath, bool keepAccountAn return new(true, false); } - public static void WriteToFile(string filepath, bool backupOldFile) + public static async Task WriteToFileAsync(string filepath, bool backupOldFile) { Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; string tomlString = TomletMain.TomlStringFrom(Config); @@ -190,7 +191,7 @@ public static void WriteToFile(string filepath, bool backupOldFile) StringBuilder newConfig = new(); foreach (string line in tomlList) { - Match matchComment = CommentRegex.Match(line); + Match matchComment = GetCommentRegex().Match(line); if (matchComment.Success && matchComment.Groups.Count == 3) { string config = matchComment.Groups[1].Value, comment = matchComment.Groups[2].Value; @@ -215,7 +216,7 @@ public static void WriteToFile(string filepath, bool backupOldFile) try { if (new FileInfo(filepath).Length == newConfigByte.Length) - if (File.ReadAllBytes(filepath).SequenceEqual(newConfigByte)) + if ((await File.ReadAllBytesAsync(filepath)).SequenceEqual(newConfigByte)) needUpdate = false; } catch { } @@ -238,7 +239,7 @@ public static void WriteToFile(string filepath, bool backupOldFile) if (backupSuccessed) { - try { File.WriteAllBytes(filepath, newConfigByte); } + try { await File.WriteAllBytesAsync(filepath, newConfigByte); } catch (Exception ex) { ConsoleIO.WriteLineFormatted("§c" + string.Format(Translations.config_write_fail, filepath)); @@ -252,7 +253,7 @@ public static void WriteToFile(string filepath, bool backupOldFile) /// Load settings from the command line /// /// Command-line arguments - /// Thrown on invalid arguments + /// Thrown on invalid arguments public static void LoadArguments(string[] args) { int positionalIndex = 0; @@ -293,7 +294,7 @@ public static void LoadArguments(string[] args) InternalConfig.Account.Password = argument; break; case 2: - Config.Main.SetServerIP(new MainConfig.ServerInfoConfig(argument), true); + Config.Main.SetServerIP(new ServerInfoConfig(argument), true); InternalConfig.KeepServerSettings = true; break; case 3: @@ -326,12 +327,12 @@ public void OnSettingUpdate() } } - public static class MainConfigHealper + public static partial class MainConfigHealper { public static MainConfig Config = new(); [TomlDoNotInlineObject] - public class MainConfig + public partial class MainConfig { public GeneralConfig General = new(); @@ -385,8 +386,12 @@ public bool SetServerIP(ServerInfoConfig serverInfo, bool checkAlias) //Server IP (IP or domain names contains at least a dot) if (sip.Length == 1 && !serverInfo.Port.HasValue && host.Contains('.') && host.Any(c => char.IsLetter(c)) && Settings.Config.Main.Advanced.ResolveSrvRecords != ResolveSrvRecordType.no) + { //Domain name without port may need Minecraft SRV Record lookup - ProtocolHandler.MinecraftServiceLookup(ref host, ref port); + var lookup = MinecraftServiceLookupAsync(host); + if (lookup.Result.Item1) + (host, port) = (lookup.Result.Item2, lookup.Result.Item3); + } InternalConfig.ServerIP = host; InternalConfig.ServerPort = port; return true; @@ -417,6 +422,12 @@ public void OnSettingUpdate() General.Server.Host ??= string.Empty; + if (General.AccountType == GeneralConfig.LoginType.mojang && General.Method == GeneralConfig.LoginMethod.browser) + { + General.Method = GeneralConfig.LoginMethod.mcc; + ConsoleIO.WriteLogLine(Translations.config_invaild_login_method); + } + if (Advanced.MessageCooldown < 0) Advanced.MessageCooldown = 0; @@ -436,12 +447,12 @@ public void OnSettingUpdate() Thread.CurrentThread.CurrentUICulture = culture; } - Advanced.Language = Regex.Replace(Advanced.Language, @"[^-^_^\w^*\d]", string.Empty).Replace('-', '_'); + Advanced.Language = GetLanguageCodeRegex().Replace(Advanced.Language, string.Empty).Replace('-', '_'); Advanced.Language = ToLowerIfNeed(Advanced.Language); if (!AvailableLang.Contains(Advanced.Language)) { Advanced.Language = GetDefaultGameLanguage(); - ConsoleIO.WriteLogLine("[Settings] " + Translations.config_Main_Advanced_language_invaild); + ConsoleIO.WriteLogLine(Translations.config_invaild_language); } if (!InternalConfig.KeepServerSettings) @@ -510,7 +521,7 @@ public class AdvancedConfig public InternalCmdCharType InternalCmdChar = InternalCmdCharType.slash; [TomlInlineComment("$Main.Advanced.message_cooldown$")] - public double MessageCooldown = 1.0; + public double MessageCooldown = 0.4; [TomlInlineComment("$Main.Advanced.bot_owners$")] public List BotOwners = new() { "Player1", "Player2" }; @@ -519,7 +530,7 @@ public class AdvancedConfig public string MinecraftVersion = "auto"; [TomlInlineComment("$Main.Advanced.mc_forge$")] - public ForgeConfigType EnableForge = ForgeConfigType.auto; + public ForgeConfigType EnableForge = ForgeConfigType.no; [TomlInlineComment("$Main.Advanced.brand_info$")] public BrandInfoType BrandInfo = BrandInfoType.mcc; @@ -646,7 +657,7 @@ public struct AccountInfoConfig public AccountInfoConfig(string Login) { this.Login = Login; - this.Password = "-"; + Password = "-"; } public AccountInfoConfig(string Login, string Password) @@ -668,7 +679,7 @@ public ServerInfoConfig(string Host) if (sip.Length > 1) { - try { this.Port = Convert.ToUInt16(sip[1]); } + try { Port = Convert.ToUInt16(sip[1]); } catch (FormatException) { } } } @@ -679,6 +690,9 @@ public ServerInfoConfig(string Host, ushort Port) this.Port = Port; } } + + [GeneratedRegex("[^-^_^\\w^*\\d]")] + private static partial Regex GetLanguageCodeRegex(); } } @@ -783,6 +797,9 @@ public class ConsoleConfig public void OnSettingUpdate() { + if (General.ConsoleColorMode == ConsoleColorModeType.legacy_4bit) + ConsoleIO.WriteLineFormatted("§8" + Translations.config_legacy_color, true); + // Reader ConsoleInteractive.ConsoleReader.DisplayUesrInput = General.Display_Input; @@ -792,7 +809,7 @@ public void OnSettingUpdate() ConsoleInteractive.ConsoleWriter.UseVT100ColorCode = General.ConsoleColorMode != ConsoleColorModeType.legacy_4bit; // Buffer - General.History_Input_Records = + General.History_Input_Records = ConsoleInteractive.ConsoleBuffer.SetBackreadBufferLimit(General.History_Input_Records); // Suggestion @@ -955,7 +972,7 @@ public void OnSettingUpdate() { } /// True if the parameters were valid public bool SetVar(string varName, object varData) { - varName = Settings.ToLowerIfNeed(new string(varName.TakeWhile(char.IsLetterOrDigit).ToArray())); + varName = ToLowerIfNeed(new string(varName.TakeWhile(char.IsLetterOrDigit).ToArray())); if (varName.Length > 0) { bool isString = varData.GetType() == typeof(string); @@ -1063,7 +1080,7 @@ public string ExpandVars(string str, Dictionary? localVars = nul if (varname_ok) { string varname = var_name.ToString(); - string varname_lower = Settings.ToLowerIfNeed(varname); + string varname_lower = ToLowerIfNeed(varname); i = i + varname.Length + 1; switch (varname_lower) @@ -1074,7 +1091,7 @@ public string ExpandVars(string str, Dictionary? localVars = nul case "serverport": result.Append(InternalConfig.ServerPort); break; case "datetime": DateTime time = DateTime.Now; - result.Append(String.Format("{0}-{1}-{2} {3}:{4}:{5}", + result.Append(string.Format("{0}-{1}-{2} {3}:{4}:{5}", time.Year.ToString("0000"), time.Month.ToString("00"), time.Day.ToString("00"), @@ -1162,13 +1179,13 @@ public SkinInfo(bool Cape, bool Hat, bool Jacket, bool Sleeve_Left, bool Sleeve_ public byte GetByte() { return (byte)( - ((Cape ? 1 : 0) << 0) - | ((Jacket ? 1 : 0) << 1) - | ((Sleeve_Left ? 1 : 0) << 2) - | ((Sleeve_Right ? 1 : 0) << 3) - | ((Pants_Left ? 1 : 0) << 4) - | ((Pants_Right ? 1 : 0) << 5) - | ((Hat ? 1 : 0) << 6) + (Cape ? 1 : 0) << 0 + | (Jacket ? 1 : 0) << 1 + | (Sleeve_Left ? 1 : 0) << 2 + | (Sleeve_Right ? 1 : 0) << 3 + | (Pants_Left ? 1 : 0) << 4 + | (Pants_Right ? 1 : 0) << 5 + | (Hat ? 1 : 0) << 6 ); } } @@ -1238,6 +1255,8 @@ public static class ChatBotConfigHealper [TomlDoNotInlineObject] public class ChatBotConfig { + public void OnSettingUpdate() { } + [TomlPrecedingComment("$ChatBot.Alerts$")] public ChatBots.Alerts.Configs Alerts { @@ -1821,9 +1840,9 @@ public static string ToLowerIfNeed(string str) const string lookupStringL = "---------------------------------!-#$%&-()*+,-./0123456789:;<=>?@abcdefghijklmnopqrstuvwxyz[-]^_`abcdefghijklmnopqrstuvwxyz{|}~-"; bool needLower = false; - foreach (Char c in str) + foreach (char c in str) { - if (Char.IsUpper(c)) + if (char.IsUpper(c)) { needLower = true; break; @@ -1849,6 +1868,10 @@ public static int DoubleToTick(double time) time = Math.Min(int.MaxValue / 10, time); return (int)Math.Round(time * 10); } + + + [GeneratedRegex("^(.*)\\s?#\\s\\$([\\w\\.]+)\\$\\s*$$", RegexOptions.Compiled)] + private static partial Regex GetCommentRegex(); } public static class InternalCmdCharTypeExtensions diff --git a/MinecraftClient/UpgradeHelper.cs b/MinecraftClient/UpgradeHelper.cs index cca5b16356..8d71be9878 100644 --- a/MinecraftClient/UpgradeHelper.cs +++ b/MinecraftClient/UpgradeHelper.cs @@ -8,12 +8,15 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using MinecraftClient.Proxy; namespace MinecraftClient { - internal static class UpgradeHelper + internal static partial class UpgradeHelper { - private const string GithubReleaseUrl = "https://github.com/MCCTeam/Minecraft-Console-Client/releases"; + internal const string GithubReleaseUrl = "https://github.com/MCCTeam/Minecraft-Console-Client/releases"; + + // private static HttpClient? httpClient = null; private static int running = 0; // Type: bool; 1 for running; 0 for stopped; private static CancellationTokenSource cancellationTokenSource = new(); @@ -23,29 +26,17 @@ internal static class UpgradeHelper private static DateTime downloadStartTime = DateTime.Now, lastNotifyTime = DateTime.Now; private static TimeSpan minNotifyInterval = TimeSpan.FromMilliseconds(3000); - public static void CheckUpdate(bool forceUpdate = false) + public static async Task CheckUpdate(bool forceUpdate = false) { - bool needPromptUpdate = true; - if (!forceUpdate && CompareVersionInfo(Settings.Config.Head.CurrentVersion, Settings.Config.Head.LatestVersion)) + await DoCheckUpdate(CancellationToken.None); + if (CompareVersionInfo(Settings.Config.Head.CurrentVersion, Settings.Config.Head.LatestVersion)) { - needPromptUpdate = false; ConsoleIO.WriteLineFormatted("§e" + string.Format(Translations.mcc_has_update, GithubReleaseUrl), true); } - Task.Run(() => + else if (forceUpdate) { - DoCheckUpdate(CancellationToken.None); - if (needPromptUpdate) - { - if (CompareVersionInfo(Settings.Config.Head.CurrentVersion, Settings.Config.Head.LatestVersion)) - { - ConsoleIO.WriteLineFormatted("§e" + string.Format(Translations.mcc_has_update, GithubReleaseUrl), true); - } - else if (forceUpdate) - { - ConsoleIO.WriteLine(Translations.mcc_update_already_latest + ' ' + Translations.mcc_update_promote_force_cmd); - } - } - }); + ConsoleIO.WriteLine(Translations.mcc_update_already_latest + ' ' + Translations.mcc_update_promote_force_cmd); + } } public static bool DownloadLatestBuild(bool forceUpdate, bool isCommandLine = false) @@ -71,7 +62,7 @@ public static bool DownloadLatestBuild(bool forceUpdate, bool isCommandLine = fa } else { - string latestVersion = DoCheckUpdate(cancellationToken); + string latestVersion = await DoCheckUpdate(cancellationToken); if (cancellationToken.IsCancellationRequested) { } @@ -89,7 +80,7 @@ public static bool DownloadLatestBuild(bool forceUpdate, bool isCommandLine = fa ConsoleIO.WriteLine(string.Format(Translations.mcc_update_exist_update, latestVersion, OSInfo)); HttpClientHandler httpClientHandler = new() { AllowAutoRedirect = true }; - AddProxySettings(httpClientHandler); + ProxyHandler.AddProxySettings(ProxyHandler.ClientType.Update, ref httpClientHandler); ProgressMessageHandler progressMessageHandler = new(httpClientHandler); progressMessageHandler.HttpReceiveProgress += (_, info) => @@ -183,43 +174,33 @@ public static void HandleBlockingUpdate(bool forceUpgrade) Thread.Sleep(500); } - private static string DoCheckUpdate(CancellationToken cancellationToken) + internal static async Task DoCheckUpdate(CancellationToken cancellationToken) { string latestBuildInfo = string.Empty; + HttpClientHandler httpClientHandler = new() { AllowAutoRedirect = false }; - AddProxySettings(httpClientHandler); - HttpClient httpClient = new(httpClientHandler); - Task? httpWebRequest = null; - try + ProxyHandler.AddProxySettings(ProxyHandler.ClientType.Update, ref httpClientHandler); + using HttpClient httpClient = new(httpClientHandler); + using HttpRequestMessage request = new(HttpMethod.Head, GithubReleaseUrl + "/latest"); + using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); + if (!cancellationToken.IsCancellationRequested) { - httpWebRequest = httpClient.GetAsync(GithubReleaseUrl + "/latest", HttpCompletionOption.ResponseHeadersRead, cancellationToken); - httpWebRequest.Wait(); - if (!cancellationToken.IsCancellationRequested) + if (response.Headers.Location != null) { - HttpResponseMessage res = httpWebRequest.Result; - if (res.Headers.Location != null) + Match match = GetReleaseUrlRegex().Match(response.Headers.Location.ToString()); + if (match.Success && match.Groups.Count == 5) { - Match match = Regex.Match(res.Headers.Location.ToString(), GithubReleaseUrl + @"/tag/(\d{4})(\d{2})(\d{2})-(\d+)"); - if (match.Success && match.Groups.Count == 5) + string year = match.Groups[1].Value, month = match.Groups[2].Value, day = match.Groups[3].Value, run = match.Groups[4].Value; + string latestVersion = string.Format("GitHub build {0}, built on {1}-{2}-{3}", run, year, month, day); + latestBuildInfo = string.Format("{0}{1}{2}-{3}", year, month, day, run); + if (latestVersion != Settings.Config.Head.LatestVersion) { - string year = match.Groups[1].Value, month = match.Groups[2].Value, day = match.Groups[3].Value, run = match.Groups[4].Value; - string latestVersion = string.Format("GitHub build {0}, built on {1}-{2}-{3}", run, year, month, day); - latestBuildInfo = string.Format("{0}{1}{2}-{3}", year, month, day, run); - if (latestVersion != Settings.Config.Head.LatestVersion) - { - Settings.Config.Head.LatestVersion = latestVersion; - Program.WriteBackSettings(false); - } + Settings.Config.Head.LatestVersion = latestVersion; + _ = Program.WriteBackSettings(false); } } - res.Dispose(); } - httpWebRequest.Dispose(); } - catch (Exception) { } - finally { httpWebRequest?.Dispose(); } - httpClient.Dispose(); - httpClientHandler.Dispose(); return latestBuildInfo; } @@ -247,12 +228,12 @@ private static string GetOSIdentifier() return string.Empty; } - private static bool CompareVersionInfo(string? current, string? latest) + internal static bool CompareVersionInfo(string? current, string? latest) { if (current == null || latest == null) return false; - Regex reg = new(@"\w+\sbuild\s(\d+),\sbuilt\son\s(\d{4})[-\/\.\s]?(\d{2})[-\/\.\s]?(\d{2}).*"); - Regex reg2 = new(@"\w+\sbuild\s(\d+),\sbuilt\son\s\w+\s(\d{2})[-\/\.\s]?(\d{2})[-\/\.\s]?(\d{4}).*"); + Regex reg = GetVersionRegex1(); + Regex reg2 = GetVersionRegex2(); DateTime? curTime = null, latestTime = null; @@ -302,24 +283,13 @@ private static bool CompareVersionInfo(string? current, string? latest) return false; } - private static void AddProxySettings(HttpClientHandler httpClientHandler) - { - if (Settings.Config.Proxy.Enabled_Update) - { - string proxyAddress; - if (!string.IsNullOrWhiteSpace(Settings.Config.Proxy.Username) && !string.IsNullOrWhiteSpace(Settings.Config.Proxy.Password)) - proxyAddress = string.Format("{0}://{3}:{4}@{1}:{2}", - Settings.Config.Proxy.Proxy_Type.ToString().ToLower(), - Settings.Config.Proxy.Server.Host, - Settings.Config.Proxy.Server.Port, - Settings.Config.Proxy.Username, - Settings.Config.Proxy.Password); - else - proxyAddress = string.Format("{0}://{1}:{2}", - Settings.Config.Proxy.Proxy_Type.ToString().ToLower(), - Settings.Config.Proxy.Server.Host, Settings.Config.Proxy.Server.Port); - httpClientHandler.Proxy = new WebProxy(proxyAddress, true); - } - } + [GeneratedRegex("https://github.com/MCCTeam/Minecraft-Console-Client/releases/tag/(\\d{4})(\\d{2})(\\d{2})-(\\d+)")] + private static partial Regex GetReleaseUrlRegex(); + + [GeneratedRegex("\\w+\\sbuild\\s(\\d+),\\sbuilt\\son\\s(\\d{4})[-\\/\\.\\s]?(\\d{2})[-\\/\\.\\s]?(\\d{2}).*")] + private static partial Regex GetVersionRegex1(); + + [GeneratedRegex("\\w+\\sbuild\\s(\\d+),\\sbuilt\\son\\s\\w+\\s(\\d{2})[-\\/\\.\\s]?(\\d{2})[-\\/\\.\\s]?(\\d{4}).*")] + private static partial Regex GetVersionRegex2(); } } diff --git a/MinecraftClient/WinAPI/ConsoleIcon.cs b/MinecraftClient/WinAPI/ConsoleIcon.cs index e66eca8985..74e270c29f 100644 --- a/MinecraftClient/WinAPI/ConsoleIcon.cs +++ b/MinecraftClient/WinAPI/ConsoleIcon.cs @@ -30,7 +30,7 @@ public enum WinMessages : uint SETICON = 0x0080, } - private static void SetWindowIcon(System.Drawing.Icon icon) + private static void SetWindowIcon(Icon icon) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -43,55 +43,40 @@ private static void SetWindowIcon(System.Drawing.Icon icon) /// /// Asynchronously download the player's skin and set the head as console icon /// - [SupportedOSPlatform("windows")] - public static void SetPlayerIconAsync(string playerName) + public static async Task SetPlayerIconAsync(HttpClient httpClient, string playerName) { - Thread t = new(new ThreadStart(delegate + if (!OperatingSystem.IsWindows()) + return; + + try { - HttpClient httpClient = new(); + using Stream imageStream = await httpClient.GetStreamAsync($"https://minotar.net/helm/{playerName}/100.png"); try { - Task httpWebRequest = httpClient.GetStreamAsync("https://minotar.net/helm/" + playerName + "/100.png"); - httpWebRequest.Wait(); - Stream imageStream = httpWebRequest.Result; - try - { - Bitmap skin = new(Image.FromStream(imageStream)); //Read skin from network - SetWindowIcon(Icon.FromHandle(skin.GetHicon())); // Windows 10+ (New console) - SetConsoleIcon(skin.GetHicon()); // Windows 8 and lower (Older console) - } - catch (ArgumentException) - { - /* Invalid image in HTTP response */ - } - imageStream.Dispose(); - httpWebRequest.Dispose(); - } - catch (AggregateException ae) - { - bool needRevert = false; - foreach (var ex in ae.InnerExceptions) - { - if (ex is HttpRequestException || ex is TaskCanceledException) //Skin not found? Reset to default icon - needRevert = true; - } - if (needRevert) - RevertToMCCIcon(); + Bitmap skin = new(Image.FromStream(imageStream)); //Read skin from network + SetWindowIcon(Icon.FromHandle(skin.GetHicon())); // Windows 10+ (New console) + SetConsoleIcon(skin.GetHicon()); // Windows 8 and lower (Older console) } - catch (HttpRequestException) //Skin not found? Reset to default icon + catch (ArgumentException) { - RevertToMCCIcon(); + /* Invalid image in HTTP response */ } - finally + } + catch (AggregateException ae) + { + bool needRevert = false; + foreach (var ex in ae.InnerExceptions) { - httpClient.Dispose(); + if (ex is HttpRequestException || ex is TaskCanceledException) //Skin not found? Reset to default icon + needRevert = true; } + if (needRevert) + RevertToMCCIcon(); } - )) + catch (HttpRequestException) //Skin not found? Reset to default icon { - Name = "Player skin icon setter" - }; - t.Start(); + RevertToMCCIcon(); + } } /// diff --git a/MinecraftProtocolLibrary b/MinecraftProtocolLibrary new file mode 160000 index 0000000000..0430d0d23a --- /dev/null +++ b/MinecraftProtocolLibrary @@ -0,0 +1 @@ +Subproject commit 0430d0d23a8a7914e81e7ff317c933bf6e8bc984