diff --git a/MinecraftClient/ChatBots/Mailer.cs b/MinecraftClient/ChatBots/Mailer.cs index 41132d71..49367ba0 100644 --- a/MinecraftClient/ChatBots/Mailer.cs +++ b/MinecraftClient/ChatBots/Mailer.cs @@ -1,470 +1,470 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.IO; -using System.Linq; -using Brigadier.NET; -using Brigadier.NET.Builder; -using MinecraftClient.CommandHandler; -using MinecraftClient.CommandHandler.Patch; -using MinecraftClient.Scripting; -using Tomlet.Attributes; - -namespace MinecraftClient.ChatBots -{ - /// - /// ChatBot for storing and delivering Mails - /// - public class Mailer : ChatBot - { - public const string CommandName = "mailer"; - - public static Configs Config = new(); - - [TomlDoNotInlineObject] - public class Configs - { - [NonSerialized] - private const string BotName = "Mailer"; - - public bool Enabled = false; - - public string DatabaseFile = "MailerDatabase.ini"; - - public string IgnoreListFile = "MailerIgnoreList.ini"; - - public bool PublicInteractions = false; - - public int MaxMailsPerPlayer = 10; - - public int MaxDatabaseSize = 10000; - - public int MailRetentionDays = 30; - - public void OnSettingUpdate() - { - DatabaseFile ??= string.Empty; - IgnoreListFile ??= string.Empty; - - if (!Enabled) return; - - bool checkSuccessed = true; - - if (Config.MaxDatabaseSize <= 0) - { - LogToConsole(BotName, Translations.bot_mailer_init_fail_db_size); - checkSuccessed = false; - } - - if (Config.MaxMailsPerPlayer <= 0) - { - LogToConsole(BotName, Translations.bot_mailer_init_fail_max_mails); - checkSuccessed = false; - } - - if (Config.MailRetentionDays <= 0) - { - LogToConsole(BotName, Translations.bot_mailer_init_fail_mail_retention); - checkSuccessed = false; - } - - if (!checkSuccessed) - { - LogToConsole(BotName, Translations.general_bot_unload); - Enabled = false; - } - } - } - - /// - /// Holds the list of ignored players - /// - private class IgnoreList : HashSet - { - /// - /// Read ignore list from file - /// - /// Path to the ignore list - /// Ignore list - public static IgnoreList FromFile(string filePath) - { - IgnoreList ignoreList = new(); - foreach (string line in FileMonitor.ReadAllLinesWithRetries(filePath)) - { - if (!line.StartsWith("#")) - { - string entry = line.ToLower(); - if (!ignoreList.Contains(entry)) - ignoreList.Add(entry); - } - } - return ignoreList; - } - - /// - /// Save ignore list to file - /// - /// Path to destination file - public void SaveToFile(string filePath) - { - List lines = new(); - lines.Add("#Ignored Players"); - foreach (string player in this) - lines.Add(player); - FileMonitor.WriteAllLinesWithRetries(filePath, lines); - } - } - - /// - /// Holds the Mail database: a collection of Mails sent from a player to another player - /// - private class MailDatabase : List - { - /// - /// Read mail database from file - /// - /// Path to the database - /// Mail database - public static MailDatabase FromFile(string filePath) - { - MailDatabase database = new(); - Dictionary> iniFileDict = INIFile.ParseFile(FileMonitor.ReadAllLinesWithRetries(filePath)); - foreach (KeyValuePair> iniSection in iniFileDict) - { - //iniSection.Key is "mailXX" but we don't need it here - string sender = iniSection.Value["sender"]; - string recipient = iniSection.Value["recipient"]; - string content = iniSection.Value["content"]; - DateTime timestamp = DateTime.Parse(iniSection.Value["timestamp"]); - bool anonymous = INIFile.Str2Bool(iniSection.Value["anonymous"]); - database.Add(new Mail(sender, recipient, content, anonymous, timestamp)); - } - return database; - } - - /// - /// Save mail database to file - /// - /// Path to destination file - public void SaveToFile(string filePath) - { - Dictionary> iniFileDict = new(); - int mailCount = 0; - foreach (Mail mail in this) - { - mailCount++; - Dictionary iniSection = new() - { -#pragma warning disable format // @formatter:off - ["sender"] = mail.Sender, - ["recipient"] = mail.Recipient, - ["content"] = mail.Content, - ["timestamp"] = mail.DateSent.ToString(), - ["anonymous"] = mail.Anonymous.ToString() -#pragma warning restore format // @formatter:on - }; - iniFileDict["mail" + mailCount] = iniSection; - } - FileMonitor.WriteAllLinesWithRetries(filePath, INIFile.Generate(iniFileDict, "Mail Database")); - } - } - - /// - /// Represents a Mail sent from a player to another player - /// - private class Mail - { - private readonly string sender; - private readonly string senderLower; - private readonly string recipient; - private readonly string recipientLower; - private readonly string message; - private readonly DateTime datesent; - private bool delivered; - private readonly bool anonymous; - - public Mail(string sender, string recipient, string message, bool anonymous, DateTime datesent) - { - this.sender = sender; - senderLower = sender.ToLower(); - this.recipient = recipient; - recipientLower = recipient.ToLower(); - this.message = message; - this.datesent = datesent; - delivered = false; - this.anonymous = anonymous; - } - - public string Sender { get { return sender; } } - public string SenderLowercase { get { return senderLower; } } - public string Recipient { get { return recipient; } } - public string RecipientLowercase { get { return recipientLower; } } - public string Content { get { return message; } } - public DateTime DateSent { get { return datesent; } } - public bool Delivered => delivered; - public bool Anonymous { get { return anonymous; } } - public void SetDelivered() { delivered = true; } - - public override string ToString() - { - return String.Format("{0} {1} {2} {3}", Sender, Recipient, Content, DateSent); - } - } - - // Internal variables - private int maxMessageLength = 0; - private DateTime nextMailSend = DateTime.Now; - private MailDatabase mailDatabase = new(); - private IgnoreList ignoreList = new(); - private FileMonitor? mailDbFileMonitor; - private FileMonitor? ignoreListFileMonitor; - private readonly object readWriteLock = new(); - - /// - /// Initialization of the Mailer bot - /// - public override void Initialize() - { - LogDebugToConsole(Translations.bot_mailer_init); - LogDebugToConsole(Translations.bot_mailer_init_db + Config.DatabaseFile); - LogDebugToConsole(Translations.bot_mailer_init_ignore + Config.IgnoreListFile); - LogDebugToConsole(Translations.bot_mailer_init_public + Config.PublicInteractions); - LogDebugToConsole(Translations.bot_mailer_init_max_mails + Config.MaxMailsPerPlayer); - LogDebugToConsole(Translations.bot_mailer_init_db_size + Config.MaxDatabaseSize); - LogDebugToConsole(Translations.bot_mailer_init_mail_retention + Config.MailRetentionDays + " days"); - - if (!File.Exists(Config.DatabaseFile)) - { - LogToConsole(string.Format(Translations.bot_mailer_create_db, Path.GetFullPath(Config.DatabaseFile))); - new MailDatabase().SaveToFile(Config.DatabaseFile); - } - - if (!File.Exists(Config.IgnoreListFile)) - { - LogToConsole(string.Format(Translations.bot_mailer_create_ignore, Path.GetFullPath(Config.IgnoreListFile))); - new IgnoreList().SaveToFile(Config.IgnoreListFile); - } - - lock (readWriteLock) - { - LogDebugToConsole(string.Format(Translations.bot_mailer_load_db, Path.GetFullPath(Config.DatabaseFile))); - mailDatabase = MailDatabase.FromFile(Config.DatabaseFile); - - LogDebugToConsole(string.Format(Translations.bot_mailer_load_ignore, Path.GetFullPath(Config.IgnoreListFile))); - ignoreList = IgnoreList.FromFile(Config.IgnoreListFile); - } - - //Initialize file monitors. In case the bot needs to unload for some reason in the future, do not forget to .Dispose() them - mailDbFileMonitor = new FileMonitor(Path.GetDirectoryName(Config.DatabaseFile)!, Path.GetFileName(Config.DatabaseFile), FileMonitorCallback); - ignoreListFileMonitor = new FileMonitor(Path.GetDirectoryName(Config.IgnoreListFile)!, Path.GetFileName(Config.IgnoreListFile), FileMonitorCallback); - - McClient.dispatcher.Register(l => l.Literal("help") - .Then(l => l.Literal(CommandName) - .Executes(r => OnCommandHelp(r.Source, string.Empty))) - ); - - McClient.dispatcher.Register(l => l.Literal(CommandName) - .Then(l => l.Literal("getmails") - .Executes(r => OnCommandGetMails())) - .Then(l => l.Literal("getignored") - .Executes(r => OnCommandGetIgnored())) - .Then(l => l.Literal("addignored") - .Then(l => l.Argument("username", Arguments.String()) - .Executes(r => OnCommandAddIgnored(Arguments.GetString(r, "username"))))) - .Then(l => l.Literal("removeignored") - .Then(l => l.Argument("username", Arguments.String()) - .Executes(r => OnCommandRemoveIgnored(Arguments.GetString(r, "username"))))) - .Then(l => l.Literal("_help") - .Executes(r => OnCommandHelp(r.Source, string.Empty)) - .Redirect(McClient.dispatcher.GetRoot().GetChild("help").GetChild(CommandName))) - ); - } - - public override void OnUnload() - { - McClient.dispatcher.Unregister(CommandName); - McClient.dispatcher.GetRoot().GetChild("help").RemoveChild(CommandName); - } - - private int OnCommandHelp(CmdResult r, string? cmd) - { - return r.SetAndReturn(cmd switch - { -#pragma warning disable format // @formatter:off - _ => Translations.bot_mailer_cmd_help + ": /mailer " - + '\n' + McClient.dispatcher.GetAllUsageString(CommandName, false), -#pragma warning restore format // @formatter:on - }); - } - - private int OnCommandGetMails() - { - LogToConsole(string.Format(Translations.bot_mailer_cmd_getmails, string.Join("\n", mailDatabase))); - return 1; - } - - private int OnCommandGetIgnored() - { - LogToConsole(string.Format(Translations.bot_mailer_cmd_getignored, string.Join("\n", ignoreList))); - return 1; - } - - private int OnCommandAddIgnored(string username) - { - if (IsValidName(username)) - { - username = username.ToLower(); - lock (readWriteLock) - { - if (!ignoreList.Contains(username)) - { - ignoreList.Add(username); - ignoreList.SaveToFile(Config.IgnoreListFile); - } - } - LogToConsole(string.Format(Translations.bot_mailer_cmd_ignore_added, username)); - return 1; - } - else - { - LogToConsole(string.Format(Translations.bot_mailer_cmd_ignore_invalid, "addignored")); - return 0; - } - } - - private int OnCommandRemoveIgnored(string username) - { - if (IsValidName(username)) - { - username = username.ToLower(); - lock (readWriteLock) - { - if (ignoreList.Contains(username)) - { - ignoreList.Remove(username); - ignoreList.SaveToFile(Config.IgnoreListFile); - } - } - LogToConsole(string.Format(Translations.bot_mailer_cmd_ignore_removed, username)); - return 1; - } - else - { - LogToConsole(string.Format(Translations.bot_mailer_cmd_ignore_invalid, "removeignored")); - return 0; - } - } - - /// - /// Standard settings for the bot. - /// - public override void AfterGameJoined() - { - maxMessageLength = GetMaxChatMessageLength() - - 44 // Deduct length of "/ 16CharPlayerName 16CharPlayerName mailed: " - - Settings.Config.Main.Advanced.PrivateMsgsCmdName.Length; // Deduct length of "tell" command - } - - /// - /// Process chat messages from the server - /// - public override void GetText(string text) - { - string message = ""; - string username = ""; - text = GetVerbatim(text); - - if (IsPrivateMessage(text, ref message, ref username) || (Config.PublicInteractions && IsChatMessage(text, ref message, ref username))) - { - string usernameLower = username.ToLower(); - if (!ignoreList.Contains(usernameLower)) - { - string command = message.Split(' ')[0].ToLower(); - switch (command) - { - case "mail": - case "tellonym": - if (usernameLower != GetUsername().ToLower() - && mailDatabase.Count < Config.MaxDatabaseSize - && mailDatabase.Where(mail => mail.SenderLowercase == usernameLower).Count() < Config.MaxMailsPerPlayer) - { - Queue args = new(Command.GetArgs(message)); - if (args.Count >= 2) - { - bool anonymous = (command == "tellonym"); - string recipient = args.Dequeue(); - message = string.Join(" ", args); - - if (IsValidName(recipient)) - { - if (message.Length <= maxMessageLength) - { - Mail mail = new(username, recipient, message, anonymous, DateTime.Now); - LogToConsole(string.Format(Translations.bot_mailer_saving, mail.ToString())); - lock (readWriteLock) - { - mailDatabase.Add(mail); - mailDatabase.SaveToFile(Config.DatabaseFile); - } - SendPrivateMessage(username, "Message saved!"); - } - else SendPrivateMessage(username, "Your message cannot be longer than " + maxMessageLength + " characters."); - } - else SendPrivateMessage(username, "Recipient '" + recipient + "' is not a valid player name."); - } - else SendPrivateMessage(username, "Usage: " + command + " "); - } - else SendPrivateMessage(username, "Couldn't save Message. Limit reached!"); - break; - } - } - else LogDebugToConsole(string.Format(Translations.bot_mailer_user_ignored, username)); - } - } - - /// - /// Called on each MCC tick, around 10 times per second - /// - public override void Update() - { - DateTime dateNow = DateTime.Now; - if (nextMailSend < dateNow) - { - LogDebugToConsole(string.Format(Translations.bot_mailer_process_mails, DateTime.Now)); - - // Process at most 3 mails at a time to avoid spamming. Other mails will be processed on next mail send - HashSet onlinePlayersLowercase = new(GetOnlinePlayers().Select(name => name.ToLower())); - foreach (Mail mail in mailDatabase.Where(mail => !mail.Delivered && onlinePlayersLowercase.Contains(mail.RecipientLowercase)).Take(3)) - { - string sender = mail.Anonymous ? "Anonymous" : mail.Sender; - SendPrivateMessage(mail.Recipient, sender + " mailed: " + mail.Content); - mail.SetDelivered(); - LogDebugToConsole(string.Format(Translations.bot_mailer_delivered, mail.ToString())); - } - - lock (readWriteLock) - { - mailDatabase.RemoveAll(mail => mail.Delivered); - mailDatabase.RemoveAll(mail => mail.DateSent.AddDays(Config.MailRetentionDays) < DateTime.Now); - mailDatabase.SaveToFile(Config.DatabaseFile); - } - - nextMailSend = dateNow.AddSeconds(10); - } - } - - /// - /// Called when the Mail Database or Ignore list has changed on disk - /// - /// - /// - private void FileMonitorCallback(object sender, FileSystemEventArgs e) - { - lock (readWriteLock) - { - mailDatabase = MailDatabase.FromFile(Config.DatabaseFile); - ignoreList = IgnoreList.FromFile(Config.IgnoreListFile); - } - } - } -} +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using Brigadier.NET; +using Brigadier.NET.Builder; +using MinecraftClient.CommandHandler; +using MinecraftClient.CommandHandler.Patch; +using MinecraftClient.Scripting; +using Tomlet.Attributes; + +namespace MinecraftClient.ChatBots +{ + /// + /// ChatBot for storing and delivering Mails + /// + public class Mailer : ChatBot + { + public const string CommandName = "mailer"; + + public static Configs Config = new(); + + [TomlDoNotInlineObject] + public class Configs + { + [NonSerialized] + private const string BotName = "Mailer"; + + public bool Enabled = false; + + public string DatabaseFile = "MailerDatabase.ini"; + + public string IgnoreListFile = "MailerIgnoreList.ini"; + + public bool PublicInteractions = false; + + public int MaxMailsPerPlayer = 10; + + public int MaxDatabaseSize = 10000; + + public int MailRetentionDays = 30; + + public void OnSettingUpdate() + { + DatabaseFile ??= string.Empty; + IgnoreListFile ??= string.Empty; + + if (!Enabled) return; + + bool checkSuccessed = true; + + if (Config.MaxDatabaseSize <= 0) + { + LogToConsole(BotName, Translations.bot_mailer_init_fail_db_size); + checkSuccessed = false; + } + + if (Config.MaxMailsPerPlayer <= 0) + { + LogToConsole(BotName, Translations.bot_mailer_init_fail_max_mails); + checkSuccessed = false; + } + + if (Config.MailRetentionDays <= 0) + { + LogToConsole(BotName, Translations.bot_mailer_init_fail_mail_retention); + checkSuccessed = false; + } + + if (!checkSuccessed) + { + LogToConsole(BotName, Translations.general_bot_unload); + Enabled = false; + } + } + } + + /// + /// Holds the list of ignored players + /// + private class IgnoreList : HashSet + { + /// + /// Read ignore list from file + /// + /// Path to the ignore list + /// Ignore list + public static IgnoreList FromFile(string filePath) + { + IgnoreList ignoreList = new(); + foreach (string line in FileMonitor.ReadAllLinesWithRetries(filePath)) + { + if (!line.StartsWith("#")) + { + string entry = line.ToLower(); + if (!ignoreList.Contains(entry)) + ignoreList.Add(entry); + } + } + return ignoreList; + } + + /// + /// Save ignore list to file + /// + /// Path to destination file + public void SaveToFile(string filePath) + { + List lines = new(); + lines.Add("#Ignored Players"); + foreach (string player in this) + lines.Add(player); + FileMonitor.WriteAllLinesWithRetries(filePath, lines); + } + } + + /// + /// Holds the Mail database: a collection of Mails sent from a player to another player + /// + private class MailDatabase : List + { + /// + /// Read mail database from file + /// + /// Path to the database + /// Mail database + public static MailDatabase FromFile(string filePath) + { + MailDatabase database = new(); + Dictionary> iniFileDict = INIFile.ParseFile(FileMonitor.ReadAllLinesWithRetries(filePath)); + foreach (KeyValuePair> iniSection in iniFileDict) + { + //iniSection.Key is "mailXX" but we don't need it here + string sender = iniSection.Value["sender"]; + string recipient = iniSection.Value["recipient"]; + string content = iniSection.Value["content"]; + DateTime timestamp = DateTime.Parse(iniSection.Value["timestamp"]); + bool anonymous = INIFile.Str2Bool(iniSection.Value["anonymous"]); + database.Add(new Mail(sender, recipient, content, anonymous, timestamp)); + } + return database; + } + + /// + /// Save mail database to file + /// + /// Path to destination file + public void SaveToFile(string filePath) + { + Dictionary> iniFileDict = new(); + int mailCount = 0; + foreach (Mail mail in this) + { + mailCount++; + Dictionary iniSection = new() + { +#pragma warning disable format // @formatter:off + ["sender"] = mail.Sender, + ["recipient"] = mail.Recipient, + ["content"] = mail.Content, + ["timestamp"] = mail.DateSent.ToString(), + ["anonymous"] = mail.Anonymous.ToString() +#pragma warning restore format // @formatter:on + }; + iniFileDict["mail" + mailCount] = iniSection; + } + FileMonitor.WriteAllLinesWithRetries(filePath, INIFile.Generate(iniFileDict, "Mail Database")); + } + } + + /// + /// Represents a Mail sent from a player to another player + /// + private class Mail + { + private readonly string sender; + private readonly string senderLower; + private readonly string recipient; + private readonly string recipientLower; + private readonly string message; + private readonly DateTime datesent; + private bool delivered; + private readonly bool anonymous; + + public Mail(string sender, string recipient, string message, bool anonymous, DateTime datesent) + { + this.sender = sender; + senderLower = sender.ToLower(); + this.recipient = recipient; + recipientLower = recipient.ToLower(); + this.message = message; + this.datesent = datesent; + delivered = false; + this.anonymous = anonymous; + } + + public string Sender { get { return sender; } } + public string SenderLowercase { get { return senderLower; } } + public string Recipient { get { return recipient; } } + public string RecipientLowercase { get { return recipientLower; } } + public string Content { get { return message; } } + public DateTime DateSent { get { return datesent; } } + public bool Delivered => delivered; + public bool Anonymous { get { return anonymous; } } + public void SetDelivered() { delivered = true; } + + public override string ToString() + { + return String.Format("{0} {1} {2} {3}", Sender, Recipient, Content, DateSent); + } + } + + // Internal variables + private int maxMessageLength = 0; + private DateTime nextMailSend = DateTime.Now; + private MailDatabase mailDatabase = new(); + private IgnoreList ignoreList = new(); + private FileMonitor? mailDbFileMonitor; + private FileMonitor? ignoreListFileMonitor; + private readonly object readWriteLock = new(); + + /// + /// Initialization of the Mailer bot + /// + public override void Initialize() + { + LogDebugToConsole(Translations.bot_mailer_init); + LogDebugToConsole(Translations.bot_mailer_init_db + Config.DatabaseFile); + LogDebugToConsole(Translations.bot_mailer_init_ignore + Config.IgnoreListFile); + LogDebugToConsole(Translations.bot_mailer_init_public + Config.PublicInteractions); + LogDebugToConsole(Translations.bot_mailer_init_max_mails + Config.MaxMailsPerPlayer); + LogDebugToConsole(Translations.bot_mailer_init_db_size + Config.MaxDatabaseSize); + LogDebugToConsole(Translations.bot_mailer_init_mail_retention + Config.MailRetentionDays + " days"); + + if (!File.Exists(Config.DatabaseFile)) + { + LogToConsole(string.Format(Translations.bot_mailer_create_db, Path.GetFullPath(Config.DatabaseFile))); + new MailDatabase().SaveToFile(Config.DatabaseFile); + } + + if (!File.Exists(Config.IgnoreListFile)) + { + LogToConsole(string.Format(Translations.bot_mailer_create_ignore, Path.GetFullPath(Config.IgnoreListFile))); + new IgnoreList().SaveToFile(Config.IgnoreListFile); + } + + lock (readWriteLock) + { + LogDebugToConsole(string.Format(Translations.bot_mailer_load_db, Path.GetFullPath(Config.DatabaseFile))); + mailDatabase = MailDatabase.FromFile(Config.DatabaseFile); + + LogDebugToConsole(string.Format(Translations.bot_mailer_load_ignore, Path.GetFullPath(Config.IgnoreListFile))); + ignoreList = IgnoreList.FromFile(Config.IgnoreListFile); + } + + //Initialize file monitors. In case the bot needs to unload for some reason in the future, do not forget to .Dispose() them + mailDbFileMonitor = new FileMonitor(Path.GetDirectoryName(Config.DatabaseFile)!, Path.GetFileName(Config.DatabaseFile), FileMonitorCallback); + ignoreListFileMonitor = new FileMonitor(Path.GetDirectoryName(Config.IgnoreListFile)!, Path.GetFileName(Config.IgnoreListFile), FileMonitorCallback); + + McClient.dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CommandName) + .Executes(r => OnCommandHelp(r.Source, string.Empty))) + ); + + McClient.dispatcher.Register(l => l.Literal(CommandName) + .Then(l => l.Literal("getmails") + .Executes(r => OnCommandGetMails())) + .Then(l => l.Literal("getignored") + .Executes(r => OnCommandGetIgnored())) + .Then(l => l.Literal("addignored") + .Then(l => l.Argument("username", Arguments.String()) + .Executes(r => OnCommandAddIgnored(Arguments.GetString(r, "username"))))) + .Then(l => l.Literal("removeignored") + .Then(l => l.Argument("username", Arguments.String()) + .Executes(r => OnCommandRemoveIgnored(Arguments.GetString(r, "username"))))) + .Then(l => l.Literal("_help") + .Executes(r => OnCommandHelp(r.Source, string.Empty)) + .Redirect(McClient.dispatcher.GetRoot().GetChild("help").GetChild(CommandName))) + ); + } + + public override void OnUnload() + { + McClient.dispatcher.Unregister(CommandName); + McClient.dispatcher.GetRoot().GetChild("help").RemoveChild(CommandName); + } + + private int OnCommandHelp(CmdResult r, string? cmd) + { + return r.SetAndReturn(cmd switch + { +#pragma warning disable format // @formatter:off + _ => Translations.bot_mailer_cmd_help + ": /mailer " + + '\n' + McClient.dispatcher.GetAllUsageString(CommandName, false), +#pragma warning restore format // @formatter:on + }); + } + + private int OnCommandGetMails() + { + LogToConsole(string.Format(Translations.bot_mailer_cmd_getmails, string.Join("\n", mailDatabase))); + return 1; + } + + private int OnCommandGetIgnored() + { + LogToConsole(string.Format(Translations.bot_mailer_cmd_getignored, string.Join("\n", ignoreList))); + return 1; + } + + private int OnCommandAddIgnored(string username) + { + if (IsValidName(username)) + { + username = username.ToLower(); + lock (readWriteLock) + { + if (!ignoreList.Contains(username)) + { + ignoreList.Add(username); + ignoreList.SaveToFile(Config.IgnoreListFile); + } + } + LogToConsole(string.Format(Translations.bot_mailer_cmd_ignore_added, username)); + return 1; + } + else + { + LogToConsole(string.Format(Translations.bot_mailer_cmd_ignore_invalid, "addignored")); + return 0; + } + } + + private int OnCommandRemoveIgnored(string username) + { + if (IsValidName(username)) + { + username = username.ToLower(); + lock (readWriteLock) + { + if (ignoreList.Contains(username)) + { + ignoreList.Remove(username); + ignoreList.SaveToFile(Config.IgnoreListFile); + } + } + LogToConsole(string.Format(Translations.bot_mailer_cmd_ignore_removed, username)); + return 1; + } + else + { + LogToConsole(string.Format(Translations.bot_mailer_cmd_ignore_invalid, "removeignored")); + return 0; + } + } + + /// + /// Standard settings for the bot. + /// + public override void AfterGameJoined() + { + maxMessageLength = GetMaxChatMessageLength() + - 44 // Deduct length of "/ 16CharPlayerName 16CharPlayerName mailed: " + - Settings.Config.Main.Advanced.PrivateMsgsCmdName.Length; // Deduct length of "tell" command + } + + /// + /// Process chat messages from the server + /// + public override void GetText(string text) + { + string message = ""; + string username = ""; + text = GetVerbatim(text); + + if (IsPrivateMessage(text, ref message, ref username) || (Config.PublicInteractions && IsChatMessage(text, ref message, ref username))) + { + string usernameLower = username.ToLower(); + if (!ignoreList.Contains(usernameLower)) + { + string command = message.Split(' ')[0].ToLower(); + switch (command) + { + case "mail": + case "tellonym": + if (usernameLower != GetUsername().ToLower() + && mailDatabase.Count < Config.MaxDatabaseSize + && mailDatabase.Where(mail => mail.SenderLowercase == usernameLower).Count() < Config.MaxMailsPerPlayer) + { + Queue args = new(Command.GetArgs(message)); + if (args.Count >= 2) + { + bool anonymous = (command == "tellonym"); + string recipient = args.Dequeue(); + message = string.Join(" ", args); + + if (IsValidName(recipient)) + { + if (message.Length <= maxMessageLength) + { + Mail mail = new(username, recipient, message, anonymous, DateTime.Now); + LogToConsole(string.Format(Translations.bot_mailer_saving, mail.ToString())); + lock (readWriteLock) + { + mailDatabase.Add(mail); + mailDatabase.SaveToFile(Config.DatabaseFile); + } + SendPrivateMessage(username, "Message saved!"); + } + else SendPrivateMessage(username, "Your message cannot be longer than " + maxMessageLength + " characters."); + } + else SendPrivateMessage(username, "Recipient '" + recipient + "' is not a valid player name."); + } + else SendPrivateMessage(username, "Usage: " + command + " "); + } + else SendPrivateMessage(username, "Couldn't save Message. Limit reached!"); + break; + } + } + else LogDebugToConsole(string.Format(Translations.bot_mailer_user_ignored, username)); + } + } + + /// + /// Called on each MCC tick, around 10 times per second + /// + public override void Update() + { + DateTime dateNow = DateTime.Now; + if (nextMailSend < dateNow) + { + LogDebugToConsole(string.Format(Translations.bot_mailer_process_mails, DateTime.Now)); + + // Process at most 3 mails at a time to avoid spamming. Other mails will be processed on next mail send + HashSet onlinePlayersLowercase = new(GetOnlinePlayers().Select(name => name.ToLower())); + foreach (Mail mail in mailDatabase.Where(mail => !mail.Delivered && onlinePlayersLowercase.Contains(mail.RecipientLowercase)).Take(3)) + { + string sender = mail.Anonymous ? "Anonymous" : mail.Sender; + SendPrivateMessage(mail.Recipient, sender + " mailed: " + mail.Content); + mail.SetDelivered(); + LogDebugToConsole(string.Format(Translations.bot_mailer_delivered, mail.ToString())); + } + + lock (readWriteLock) + { + mailDatabase.RemoveAll(mail => mail.Delivered); + mailDatabase.RemoveAll(mail => mail.DateSent.AddDays(Config.MailRetentionDays) < DateTime.Now); + mailDatabase.SaveToFile(Config.DatabaseFile); + } + + nextMailSend = dateNow.AddSeconds(10); + } + } + + /// + /// Called when the Mail Database or Ignore list has changed on disk + /// + /// + /// + private void FileMonitorCallback(object sender, FileSystemEventArgs e) + { + lock (readWriteLock) + { + mailDatabase = MailDatabase.FromFile(Config.DatabaseFile); + ignoreList = IgnoreList.FromFile(Config.IgnoreListFile); + } + } + } +} diff --git a/MinecraftClient/Commands/Animation.cs b/MinecraftClient/Commands/Animation.cs index dab51b6f..b996211c 100644 --- a/MinecraftClient/Commands/Animation.cs +++ b/MinecraftClient/Commands/Animation.cs @@ -1,55 +1,55 @@ -using Brigadier.NET; -using Brigadier.NET.Builder; -using MinecraftClient.CommandHandler; - -namespace MinecraftClient.Commands -{ - public class Animation : Command - { - public override string CmdName { get { return "animation"; } } - public override string CmdUsage { get { return "animation "; } } - public override string CmdDesc { get { return Translations.cmd_animation_desc; } } - - public override void RegisterCommand(CommandDispatcher dispatcher) - { - dispatcher.Register(l => l.Literal("help") - .Then(l => l.Literal(CmdName) - .Executes(r => GetUsage(r.Source, string.Empty)) - .Then(l => l.Literal("mainhand") - .Executes(r => GetUsage(r.Source, "mainhand"))) - .Then(l => l.Literal("offhand") - .Executes(r => GetUsage(r.Source, "offhand"))) - ) - ); - - dispatcher.Register(l => l.Literal(CmdName) - .Executes(r => DoAnimation(r.Source, mainhand: true)) - .Then(l => l.Literal("mainhand") - .Executes(r => DoAnimation(r.Source, mainhand: true))) - .Then(l => l.Literal("offhand") - .Executes(r => DoAnimation(r.Source, mainhand: false))) - .Then(l => l.Literal("_help") - .Executes(r => GetUsage(r.Source, string.Empty)) - .Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))) - ); - } - - private int GetUsage(CmdResult r, string? cmd) - { - return r.SetAndReturn(cmd switch - { -#pragma warning disable format // @formatter:off - "mainhand" => GetCmdDescTranslated(), - "offhand" => GetCmdDescTranslated(), - _ => GetCmdDescTranslated(), -#pragma warning restore format // @formatter:on - }); - } - - private static int DoAnimation(CmdResult r, bool mainhand) - { - McClient handler = CmdResult.currentHandler!; - return r.SetAndReturn(handler.DoAnimation(mainhand ? 1 : 0)); - } - } -} +using Brigadier.NET; +using Brigadier.NET.Builder; +using MinecraftClient.CommandHandler; + +namespace MinecraftClient.Commands +{ + public class Animation : Command + { + public override string CmdName { get { return "animation"; } } + public override string CmdUsage { get { return "animation "; } } + public override string CmdDesc { get { return Translations.cmd_animation_desc; } } + + public override void RegisterCommand(CommandDispatcher dispatcher) + { + dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CmdName) + .Executes(r => GetUsage(r.Source, string.Empty)) + .Then(l => l.Literal("mainhand") + .Executes(r => GetUsage(r.Source, "mainhand"))) + .Then(l => l.Literal("offhand") + .Executes(r => GetUsage(r.Source, "offhand"))) + ) + ); + + dispatcher.Register(l => l.Literal(CmdName) + .Executes(r => DoAnimation(r.Source, mainhand: true)) + .Then(l => l.Literal("mainhand") + .Executes(r => DoAnimation(r.Source, mainhand: true))) + .Then(l => l.Literal("offhand") + .Executes(r => DoAnimation(r.Source, mainhand: false))) + .Then(l => l.Literal("_help") + .Executes(r => GetUsage(r.Source, string.Empty)) + .Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))) + ); + } + + private int GetUsage(CmdResult r, string? cmd) + { + return r.SetAndReturn(cmd switch + { +#pragma warning disable format // @formatter:off + "mainhand" => GetCmdDescTranslated(), + "offhand" => GetCmdDescTranslated(), + _ => GetCmdDescTranslated(), +#pragma warning restore format // @formatter:on + }); + } + + private static int DoAnimation(CmdResult r, bool mainhand) + { + McClient handler = CmdResult.currentHandler!; + return r.SetAndReturn(handler.DoAnimation(mainhand ? 1 : 0)); + } + } +} diff --git a/MinecraftClient/Commands/NameItem.cs b/MinecraftClient/Commands/NameItem.cs index 7c07dc38..91f32e61 100644 --- a/MinecraftClient/Commands/NameItem.cs +++ b/MinecraftClient/Commands/NameItem.cs @@ -1,63 +1,63 @@ -using System.Linq; -using Brigadier.NET; -using Brigadier.NET.Builder; -using MinecraftClient.CommandHandler; -using MinecraftClient.Inventory; - -namespace MinecraftClient.Commands -{ - public class NameItem : Command - { - public override string CmdName => "nameitem"; - public override string CmdUsage => "nameitem "; - - public override string CmdDesc => Translations.cmd_nameitem_desc; - - public override void RegisterCommand(CommandDispatcher dispatcher) - { - dispatcher.Register(l => l.Literal("help") - .Then(l => l.Literal(CmdName) - .Executes(r => GetUsage(r.Source, string.Empty)) - ) - ); - - dispatcher.Register(l => l.Literal(CmdName) - .Then(l => l.Argument("any", Arguments.GreedyString()) - .Executes(r => DoSetItemName(r.Source, Arguments.GetString(r, "any")))) - ); - } - - private int GetUsage(CmdResult r, string? cmd) - { - return r.SetAndReturn(cmd switch - { -#pragma warning disable format // @formatter:off - _ => GetCmdDescTranslated(), -#pragma warning restore format // @formatter:on - }); - } - - private int DoSetItemName(CmdResult r, string itemName) - { - var handler = CmdResult.currentHandler!; - - if (itemName.Trim().Length == 0) - return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_nameitem_item_name_empty); - - var currentInventory = handler.GetInventories().Count == 0 - ? null - : handler.GetInventories().Values.ToList().Last(); - - if (currentInventory is not { Type: ContainerType.Anvil }) - return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_nameitem_no_anvil_inventory_open); - - if (currentInventory.Items[0].IsEmpty) - return r.SetAndReturn(CmdResult.Status.Fail, - Translations.cmd_nameitem_first_slot_empty); - - return handler.SendRenameItem(itemName) - ? r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_nameitem_successful) - : r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_nameitem_failed); - } - } +using System.Linq; +using Brigadier.NET; +using Brigadier.NET.Builder; +using MinecraftClient.CommandHandler; +using MinecraftClient.Inventory; + +namespace MinecraftClient.Commands +{ + public class NameItem : Command + { + public override string CmdName => "nameitem"; + public override string CmdUsage => "nameitem "; + + public override string CmdDesc => Translations.cmd_nameitem_desc; + + public override void RegisterCommand(CommandDispatcher dispatcher) + { + dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CmdName) + .Executes(r => GetUsage(r.Source, string.Empty)) + ) + ); + + dispatcher.Register(l => l.Literal(CmdName) + .Then(l => l.Argument("any", Arguments.GreedyString()) + .Executes(r => DoSetItemName(r.Source, Arguments.GetString(r, "any")))) + ); + } + + private int GetUsage(CmdResult r, string? cmd) + { + return r.SetAndReturn(cmd switch + { +#pragma warning disable format // @formatter:off + _ => GetCmdDescTranslated(), +#pragma warning restore format // @formatter:on + }); + } + + private int DoSetItemName(CmdResult r, string itemName) + { + var handler = CmdResult.currentHandler!; + + if (itemName.Trim().Length == 0) + return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_nameitem_item_name_empty); + + var currentInventory = handler.GetInventories().Count == 0 + ? null + : handler.GetInventories().Values.ToList().Last(); + + if (currentInventory is not { Type: ContainerType.Anvil }) + return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_nameitem_no_anvil_inventory_open); + + if (currentInventory.Items[0].IsEmpty) + return r.SetAndReturn(CmdResult.Status.Fail, + Translations.cmd_nameitem_first_slot_empty); + + return handler.SendRenameItem(itemName) + ? r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_nameitem_successful) + : r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_nameitem_failed); + } + } } \ No newline at end of file diff --git a/MinecraftClient/INIFile.cs b/MinecraftClient/INIFile.cs index 2dd8c719..87f8de7e 100644 --- a/MinecraftClient/INIFile.cs +++ b/MinecraftClient/INIFile.cs @@ -1,134 +1,134 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace MinecraftClient -{ - /// - /// INI File tools for parsing and generating user-friendly INI files - /// By ORelio (c) 2014-2020 - CDDL 1.0 - /// - static class INIFile - { - /// - /// Parse a INI file into a dictionary. - /// Values can be accessed like this: dict["section"]["setting"] - /// - /// INI file to parse - /// INI sections and keys will be converted to lowercase unless this parameter is set to false - /// If failed to read the file - /// Parsed data from INI file - public static Dictionary> ParseFile(string iniFile, bool lowerCase = true) - { - return ParseFile(File.ReadAllLines(iniFile, Encoding.UTF8), lowerCase); - } - - /// - /// Parse a INI file into a dictionary. - /// Values can be accessed like this: dict["section"]["setting"] - /// - /// INI file content to parse - /// INI sections and keys will be converted to lowercase unless this parameter is set to false - /// If failed to read the file - /// Parsed data from INI file - public static Dictionary> ParseFile(IEnumerable lines, bool lowerCase = true) - { - var iniContents = new Dictionary>(); - string iniSection = "default"; - foreach (string lineRaw in lines) - { - string line = lineRaw.Split('#')[0].Trim(); - if (line.Length > 0 && line[0] != ';') - { - if (line[0] == '[' && line[^1] == ']') - { - iniSection = line[1..^1]; - if (lowerCase) - iniSection = iniSection.ToLower(); - } - else - { - string argName = line.Split('=')[0]; - if (lowerCase) - argName = argName.ToLower(); - if (line.Length > (argName.Length + 1)) - { - string argValue = line[(argName.Length + 1)..]; - if (!iniContents.ContainsKey(iniSection)) - iniContents[iniSection] = new Dictionary(); - iniContents[iniSection][argName] = argValue; - } - } - } - } - return iniContents; - } - - /// - /// Write given data into an INI file - /// - /// File to write into - /// Data to put into the file - /// INI file description, inserted as a comment on first line of the INI file - /// Automatically change first char of section and keys to uppercase - public static void WriteFile(string iniFile, Dictionary> contents, string? description = null, bool autoCase = true) - { - File.WriteAllLines(iniFile, Generate(contents, description, autoCase), Encoding.UTF8); - } - - /// - /// Generate given data into the INI format - /// - /// Data to put into the INI format - /// INI file description, inserted as a comment on first line of the INI file - /// Automatically change first char of section and keys to uppercase - /// Lines of the INI file - public static string[] Generate(Dictionary> contents, string? description = null, bool autoCase = true) - { - List lines = new(); - if (!String.IsNullOrWhiteSpace(description)) - lines.Add('#' + description); - foreach (var section in contents) - { - if (lines.Count > 0) - lines.Add(""); - if (!String.IsNullOrEmpty(section.Key)) - { - lines.Add("[" + (autoCase ? char.ToUpper(section.Key[0]) + section.Key[1..] : section.Key) + ']'); - foreach (var item in section.Value) - if (!String.IsNullOrEmpty(item.Key)) - lines.Add((autoCase ? char.ToUpper(item.Key[0]) + item.Key[1..] : item.Key) + '=' + item.Value); - } - } - return lines.ToArray(); - } - - /// - /// Convert an integer from a string or return 0 if failed to parse - /// - /// String to parse - /// Int value - public static int Str2Int(string str) - { - try - { - return Convert.ToInt32(str); - } - catch - { - return 0; - } - } - - /// - /// Convert a 0/1 or True/False value to boolean - /// - /// String to parse - /// Boolean value - public static bool Str2Bool(string str) - { - return str.ToLower() == "true" || str == "1"; - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace MinecraftClient +{ + /// + /// INI File tools for parsing and generating user-friendly INI files + /// By ORelio (c) 2014-2020 - CDDL 1.0 + /// + static class INIFile + { + /// + /// Parse a INI file into a dictionary. + /// Values can be accessed like this: dict["section"]["setting"] + /// + /// INI file to parse + /// INI sections and keys will be converted to lowercase unless this parameter is set to false + /// If failed to read the file + /// Parsed data from INI file + public static Dictionary> ParseFile(string iniFile, bool lowerCase = true) + { + return ParseFile(File.ReadAllLines(iniFile, Encoding.UTF8), lowerCase); + } + + /// + /// Parse a INI file into a dictionary. + /// Values can be accessed like this: dict["section"]["setting"] + /// + /// INI file content to parse + /// INI sections and keys will be converted to lowercase unless this parameter is set to false + /// If failed to read the file + /// Parsed data from INI file + public static Dictionary> ParseFile(IEnumerable lines, bool lowerCase = true) + { + var iniContents = new Dictionary>(); + string iniSection = "default"; + foreach (string lineRaw in lines) + { + string line = lineRaw.Split('#')[0].Trim(); + if (line.Length > 0 && line[0] != ';') + { + if (line[0] == '[' && line[^1] == ']') + { + iniSection = line[1..^1]; + if (lowerCase) + iniSection = iniSection.ToLower(); + } + else + { + string argName = line.Split('=')[0]; + if (lowerCase) + argName = argName.ToLower(); + if (line.Length > (argName.Length + 1)) + { + string argValue = line[(argName.Length + 1)..]; + if (!iniContents.ContainsKey(iniSection)) + iniContents[iniSection] = new Dictionary(); + iniContents[iniSection][argName] = argValue; + } + } + } + } + return iniContents; + } + + /// + /// Write given data into an INI file + /// + /// File to write into + /// Data to put into the file + /// INI file description, inserted as a comment on first line of the INI file + /// Automatically change first char of section and keys to uppercase + public static void WriteFile(string iniFile, Dictionary> contents, string? description = null, bool autoCase = true) + { + File.WriteAllLines(iniFile, Generate(contents, description, autoCase), Encoding.UTF8); + } + + /// + /// Generate given data into the INI format + /// + /// Data to put into the INI format + /// INI file description, inserted as a comment on first line of the INI file + /// Automatically change first char of section and keys to uppercase + /// Lines of the INI file + public static string[] Generate(Dictionary> contents, string? description = null, bool autoCase = true) + { + List lines = new(); + if (!String.IsNullOrWhiteSpace(description)) + lines.Add('#' + description); + foreach (var section in contents) + { + if (lines.Count > 0) + lines.Add(""); + if (!String.IsNullOrEmpty(section.Key)) + { + lines.Add("[" + (autoCase ? char.ToUpper(section.Key[0]) + section.Key[1..] : section.Key) + ']'); + foreach (var item in section.Value) + if (!String.IsNullOrEmpty(item.Key)) + lines.Add((autoCase ? char.ToUpper(item.Key[0]) + item.Key[1..] : item.Key) + '=' + item.Value); + } + } + return lines.ToArray(); + } + + /// + /// Convert an integer from a string or return 0 if failed to parse + /// + /// String to parse + /// Int value + public static int Str2Int(string str) + { + try + { + return Convert.ToInt32(str); + } + catch + { + return 0; + } + } + + /// + /// Convert a 0/1 or True/False value to boolean + /// + /// String to parse + /// Boolean value + public static bool Str2Bool(string str) + { + return str.ToLower() == "true" || str == "1"; + } + } +} diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 9c743407..dd3f4c82 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -1,3598 +1,3598 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using Brigadier.NET; -using Brigadier.NET.Exceptions; -using MinecraftClient.ChatBots; -using MinecraftClient.CommandHandler; -using MinecraftClient.CommandHandler.Patch; -using MinecraftClient.Commands; -using MinecraftClient.Inventory; -using MinecraftClient.Logger; -using MinecraftClient.Mapping; -using MinecraftClient.Protocol; -using MinecraftClient.Protocol.Handlers; -using MinecraftClient.Protocol.Handlers.Forge; -using MinecraftClient.Protocol.Message; -using MinecraftClient.Protocol.ProfileKey; -using MinecraftClient.Protocol.Session; -using MinecraftClient.Proxy; -using MinecraftClient.Scripting; -using static MinecraftClient.Settings; - -namespace MinecraftClient -{ - /// - /// The main client class, used to connect to a Minecraft server. - /// - 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 Queue threadTasks = new(); - private readonly object threadTasksLock = new(); - - private readonly List bots = new(); - private static readonly List botsOnHold = new(); - private static readonly Dictionary inventories = new(); - - private readonly Dictionary> registeredBotPluginChannels = new(); - private readonly List registeredServerPluginChannels = new(); - - private bool terrainAndMovementsEnabled; - private bool terrainAndMovementsRequested = false; - private bool inventoryHandlingEnabled; - private bool inventoryHandlingRequested = false; - private bool entityHandlingEnabled; - - private readonly object locationLock = new(); - private bool locationReceived = false; - private readonly World world = new(); - private Queue? steps; - private Queue? path; - private Location location; - private float? _yaw; // Used for calculation ONLY!!! Doesn't reflect the client yaw - private float? _pitch; // Used for calculation ONLY!!! Doesn't reflect the client pitch - private float playerYaw; - private float playerPitch; - private double motionY; - public enum MovementType { Sneak, Walk, Sprint } - private int sequenceId; // User for player block synchronization (Aka. digging, placing blocks, etc..) - private bool CanSendMessage = false; - - private readonly string host; - private readonly int port; - private readonly int protocolversion; - private readonly string username; - private Guid uuid; - private string uuidStr; - private readonly string sessionid; - private readonly PlayerKeyPair? playerKeyPair; - private DateTime lastKeepAlive; - private readonly object lastKeepAliveLock = new(); - private int respawnTicks = 0; - private int gamemode = 0; - private bool isSupportPreviewsChat; - private EnchantmentData? lastEnchantment = null; - - private int playerEntityID; - - private object DigLock = new(); - private Tuple? LastDigPosition; - private int RemainingDiggingTime = 0; - - // player health and hunger - private float playerHealth; - private int playerFoodSaturation; - private int playerLevel; - private int playerTotalExperience; - private byte CurrentSlot = 0; - - // Sneaking - public bool IsSneaking { get; set; } = false; - private bool isUnderSlab = false; - private DateTime nextSneakingUpdate = DateTime.Now; - - // Entity handling - private readonly Dictionary entities = new(); - - // server TPS - private long lastAge = 0; - private DateTime lastTime; - private double serverTPS = 0; - private double averageTPS = 20; - private const int maxSamples = 5; - private readonly List tpsSamples = new(maxSamples); - private double sampleSum = 0; - - // ChatBot OnNetworkPacket event - private bool networkPacketCaptureEnabled = false; - - 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 Location GetCurrentLocation() { return location; } - public float GetYaw() { return playerYaw; } - public int GetSequenceId() { return sequenceId; } - public float GetPitch() { return playerPitch; } - public World GetWorld() { return world; } - public double GetServerTPS() { return averageTPS; } - public bool GetIsSupportPreviewsChat() { return isSupportPreviewsChat; } - public float GetHealth() { return playerHealth; } - public int GetSaturation() { return playerFoodSaturation; } - public int GetLevel() { return playerLevel; } - public int GetTotalExperience() { return playerTotalExperience; } - public byte GetCurrentSlot() { return CurrentSlot; } - public int GetGamemode() { return gamemode; } - public bool GetNetworkPacketCaptureEnabled() { return networkPacketCaptureEnabled; } - public int GetProtocolVersion() { return protocolversion; } - public ILogger GetLogger() { return Log; } - public int GetPlayerEntityID() { return playerEntityID; } - public List GetLoadedChatBots() { return new List(bots); } - - readonly TcpClient client; - readonly IMinecraftCom handler; - CancellationTokenSource? cmdprompt = null; - Tuple? timeoutdetector = null; - - public ILogger Log; - - private static IMinecraftComHandler? instance; - public static IMinecraftComHandler? Instance => instance; - /// - /// 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 - /// 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) - { - CmdResult.currentHandler = this; - instance = 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; - - Log = Settings.Config.Logging.LogToFile - ? new FileLogLogger(Config.AppVar.ExpandVars(Settings.Config.Logging.LogFile), Settings.Config.Logging.PrependTimestamp) - : new FilteredLogger(); - Log.DebugEnabled = Config.Logging.DebugMessages; - Log.InfoEnabled = Config.Logging.InfoMessages; - Log.ChatEnabled = Config.Logging.ChatMessages; - Log.WarnEnabled = Config.Logging.WarningMessages; - Log.ErrorEnabled = Config.Logging.ErrorMessages; - - /* Load commands from Commands namespace */ - LoadCommands(); - - if (botsOnHold.Count == 0) - RegisterBots(); - - 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); - 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); - - 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(); - ConsoleInteractive.ConsoleReader.MessageReceived += ConsoleReaderOnMessageReceived; - ConsoleInteractive.ConsoleReader.OnInputChange += ConsoleIO.AutocompleteHandler; - } - else - { - Log.Error(Translations.error_login_failed); - goto Retry; - } - } - catch (Exception e) - { - Log.Error(e.GetType().Name + ": " + e.Message); - 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(); - } - else if (InternalConfig.InteractiveMode) - { - ConsoleInteractive.ConsoleReader.StopReadThread(); - ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived; - ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler; - Program.HandleFailure(); - } - - throw new Exception("Initialization failed."); - } - - /// - /// 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()); } - if (Config.ChatBot.ItemsCollector.Enabled) { BotLoad(new ItemsCollector()); } - if (Config.ChatBot.WebSocketBot.Enabled) { BotLoad(new WebSocketBot()); } - //Add your ChatBot here by uncommenting and adapting - //BotLoad(new ChatBots.YourBot()); - } - - /// - /// Retrieve messages from the queue and send. - /// Note: requires external locking. - /// - private void TrySendMessageToServer() - { - if (!CanSendMessage) - return; - - while (chatQueue.Count > 0 && nextMessageSendTime < DateTime.Now) - { - string text = chatQueue.Dequeue(); - handler.SendChatMessage(text, playerKeyPair); - nextMessageSendTime = DateTime.Now + TimeSpan.FromSeconds(Config.Main.Advanced.MessageCooldown); - } - } - - /// - /// Called ~10 times per second by the protocol handler - /// - public void OnUpdate() - { - foreach (ChatBot bot in bots.ToArray()) - { - try - { - bot.Update(); - bot.UpdateInternal(); - } - catch (Exception e) - { - if (e is not ThreadAbortException) - Log.Warn("Update: Got error from " + bot.ToString() + ": " + e.ToString()); - else - throw; //ThreadAbortException should not be caught - } - } - - if (nextSneakingUpdate < DateTime.Now) - { - if (world.GetBlock(new Location(location.X, location.Y + 1, location.Z)).IsTopSlab(protocolversion) && !IsSneaking) - { - isUnderSlab = true; - SendEntityAction(EntityActionType.StartSneaking); - } - else - { - if (isUnderSlab && !IsSneaking) - { - isUnderSlab = false; - SendEntityAction(EntityActionType.StopSneaking); - } - } - - nextSneakingUpdate = DateTime.Now.AddMilliseconds(300); - } - - lock (chatQueue) - { - TrySendMessageToServer(); - } - - if (terrainAndMovementsEnabled && locationReceived) - { - lock (locationLock) - { - for (int i = 0; i < Config.Main.Advanced.MovementSpeed; i++) //Needs to run at 20 tps; MCC runs at 10 tps - { - if (_yaw == null || _pitch == null) - { - 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); - - 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; - } - } - - if (Config.Main.Advanced.AutoRespawn && respawnTicks > 0) - { - respawnTicks--; - if (respawnTicks == 0) - SendRespawnPacket(); - } - - lock (threadTasksLock) - { - while (threadTasks.Count > 0) - { - Action taskToRun = threadTasks.Dequeue(); - taskToRun(); - } - } - - lock (DigLock) - { - if (RemainingDiggingTime > 0) - { - if (--RemainingDiggingTime == 0 && LastDigPosition != null) - { - handler.SendPlayerDigging(2, LastDigPosition.Item1, LastDigPosition.Item2, sequenceId++); - Log.Info(string.Format(Translations.cmd_dig_end, LastDigPosition.Item1)); - } - else - { - DoAnimation((int)Hand.MainHand); - } - } - } - } - - #region Connection Lost and Disconnect from Server - - /// - /// 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) - { - UpdateKeepAlive(); - do - { - Thread.Sleep(TimeSpan.FromSeconds(15)); - - if (((CancellationToken)o!).IsCancellationRequested) - return; - - lock (lastKeepAliveLock) - { - 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); - } - - /// - /// Update last keep alive to current time - /// - private void UpdateKeepAlive() - { - lock (lastKeepAliveLock) - { - lastKeepAlive = DateTime.Now; - } - } - - /// - /// Disconnect the client from the server (initiated from MCC) - /// - public void Disconnect() - { - DispatchBotEvent(bot => bot.OnDisconnect(ChatBot.DisconnectReason.UserLogout, "")); - - botsOnHold.Clear(); - botsOnHold.AddRange(bots); - - if (handler != null) - { - handler.Disconnect(); - handler.Dispose(); - } - - if (cmdprompt != null) - { - cmdprompt.Cancel(); - cmdprompt = null; - } - - if (timeoutdetector != null) - { - timeoutdetector.Item2.Cancel(); - timeoutdetector = null; - } - - if (client != null) - client.Close(); - } - - /// - /// When connection has been lost, login was denied or played was kicked from the server - /// - 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: - message = Translations.mcc_disconnect_lost; - Log.Info(message); - break; - - case ChatBot.DisconnectReason.InGameKick: - Log.Info(Translations.mcc_disconnect_server); - Log.Info(message); - break; - - case ChatBot.DisconnectReason.LoginRejected: - Log.Info(Translations.mcc_disconnect_login); - Log.Info(message); - break; - - case ChatBot.DisconnectReason.UserLogout: - 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)); - - foreach (ChatBot bot in onDisconnectBotList) - { - try - { - will_restart |= bot.OnDisconnect(reason, message); - } - catch (Exception e) - { - if (e is not ThreadAbortException) - { - Log.Warn("OnDisconnect: Got error from " + bot.ToString() + ": " + e.ToString()); - } - else throw; //ThreadAbortException should not be caught - } - } - - if (!will_restart) - { - ConsoleInteractive.ConsoleReader.StopReadThread(); - ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived; - ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler; - Program.HandleFailure(); - } - } - - #endregion - - #region Command prompt and internal MCC commands - - private void ConsoleReaderOnMessageReceived(object? sender, string e) - { - - if (client.Client == null) - return; - - if (client.Client.Connected) - { - new Thread(() => - { - InvokeOnMainThread(() => HandleCommandPromptText(e)); - }).Start(); - } - else - return; - } - - /// - /// 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) - { - if (ConsoleIO.BasicIO && text.Length > 0 && text[0] == (char)0x00) - { - //Process a request from the GUI - string[] command = text[1..].Split((char)0x00); - switch (command[0].ToLower()) - { - case "autocomplete": - int id = handler.AutoComplete(command[1]); - while (!ConsoleIO.AutoCompleteDone) { Thread.Sleep(100); } - if (command.Length > 1) { ConsoleIO.WriteLine((char)0x00 + "autocomplete" + (char)0x00 + ConsoleIO.AutoCompleteResult); } - else ConsoleIO.WriteLine((char)0x00 + "autocomplete" + (char)0x00); - break; - } - } - else - { - text = text.Trim(); - - if (text.Length > 1 - && Config.Main.Advanced.InternalCmdChar == MainConfigHealper.MainConfig.AdvancedConfig.InternalCmdCharType.none - && text[0] == '/') - { - SendText(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..]); - } - else if (text.Length > 0) - { - if (Config.Main.Advanced.InternalCmdChar == MainConfigHealper.MainConfig.AdvancedConfig.InternalCmdCharType.none - || text[0] == Config.Main.Advanced.InternalCmdChar.ToChar()) - { - CmdResult result = new(); - 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); - } - else if (result.status != CmdResult.Status.NotRun && (result.status != CmdResult.Status.Done || !string.IsNullOrWhiteSpace(result.result))) - { - Log.Info(result); - } - } - else - { - SendText(text); - } - } - } - } - - /// - /// Perform an internal MCC command (not a server command, use SendText() instead for that!) - /// - /// The command - /// May contain a confirmation or error message after processing the command, or "" otherwise. - /// Local variables passed along with the command - /// TRUE if the command was indeed an internal MCC command - public bool PerformInternalCommand(string command, ref CmdResult result, Dictionary? localVars = null) - { - /* Process the provided command */ - ParseResults parse; - try - { - parse = dispatcher.Parse(command, result); - } - catch (Exception e) - { - Log.Debug("Parse fail: " + e.GetFullMessage()); - return false; - } - - try - { - dispatcher.Execute(parse); - - foreach (ChatBot bot in bots.ToArray()) - { - try - { - bot.OnInternalCommand(command, string.Join(" ", Command.GetArgs(command)), result); - } - catch (Exception e) - { - if (e is not ThreadAbortException) - { - Log.Warn(string.Format(Translations.icmd_error, bot.ToString() ?? string.Empty, e.ToString())); - } - else throw; //ThreadAbortException should not be caught - } - } - - return true; - } - catch (CommandSyntaxException e) - { - if (parse.Context.Nodes.Count == 0) - { - return false; - } - else - { - Log.Info("§e" + e.Message ?? e.StackTrace ?? "Incorrect argument."); - Log.Info(dispatcher.GetAllUsageString(parse.Context.Nodes[0].Node.Name, false)); - return true; - } - } - } - - 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() - { - Program.ReloadSettings(true); - ReloadBots(); - } - - /// - /// Reload loaded bots (Only builtin bots) - /// - public void ReloadBots() - { - UnloadAllBots(); - RegisterBots(true); - - if (client.Client.Connected) - bots.ForEach(bot => bot.AfterGameJoined()); - } - - /// - /// Unload All Bots - /// - public void UnloadAllBots() - { - foreach (ChatBot bot in GetLoadedChatBots()) - BotUnLoad(bot); - } - - #endregion - - #region Thread-Invoke: Cross-thread method calls - - /// - /// 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(); - } - } - - /// - /// Invoke a task on the main thread and wait for completion - /// - /// Task to run without return value - /// InvokeOnMainThread(methodThatReturnsNothing); - /// InvokeOnMainThread(() => methodThatReturnsNothing(argument)); - /// InvokeOnMainThread(() => { yourCode(); }); - public void InvokeOnMainThread(Action task) - { - InvokeOnMainThread(() => { task(); return true; }); - } - - /// - /// Clear all tasks - /// - public void ClearTasks() - { - lock (threadTasksLock) - { - threadTasks.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; - } - } - } - - #endregion - - #region Management: Load/Unload ChatBots and Enable/Disable settings - - /// - /// Load a new bot - /// - public void BotLoad(ChatBot b, bool init = true) - { - if (InvokeRequired) - { - InvokeOnMainThread(() => BotLoad(b, init)); - return; - } - - 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 }); - } - - /// - /// Unload a bot - /// - public void BotUnLoad(ChatBot b) - { - if (InvokeRequired) - { - InvokeOnMainThread(() => BotUnLoad(b)); - return; - } - - b.OnUnload(); - - bots.RemoveAll(item => ReferenceEquals(item, b)); - - // 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(); - foreach (var entry in botRegistrations) - { - UnregisterPluginChannel(entry.Key, b); - } - } - - /// - /// Clear bots - /// - public void BotClear() - { - InvokeOnMainThread(bots.Clear); - } - - /// - /// Get Terrain and Movements status. - /// - public bool GetTerrainEnabled() - { - return terrainAndMovementsEnabled; - } - - /// - /// Get Inventory Handling Mode - /// - public bool GetInventoryEnabled() - { - return inventoryHandlingEnabled; - } - - /// - /// Get entity handling status - /// - /// - /// Entity Handling cannot be enabled in runtime (or after joining server) - public bool GetEntityHandlingEnabled() - { - return entityHandlingEnabled; - } - - /// - /// Enable or disable Terrain and Movements. - /// Please note that Enabling will be deferred until next relog, respawn or world change. - /// - /// Enabled - /// 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) - { - terrainAndMovementsRequested = true; - return false; - } - } - else - { - terrainAndMovementsEnabled = false; - terrainAndMovementsRequested = false; - locationReceived = false; - world.Clear(); - } - return true; - } - - /// - /// Enable or disable Inventories. - /// Please note that Enabling will be deferred until next relog. - /// - /// 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) - { - inventoryHandlingRequested = true; - return false; - } - } - else - { - inventoryHandlingEnabled = false; - inventoryHandlingRequested = false; - inventories.Clear(); - } - return true; - } - - /// - /// Enable or disable Entity handling. - /// Please note that Enabling will be deferred until next relog. - /// - /// 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) - { - entityHandlingEnabled = false; - return true; - } - else - { - return false; - } - } - else - { - // Entity Handling cannot be enabled in runtime (or after joining server) - return false; - } - } - - /// - /// Enable or disable network packet event calling. - /// - /// - /// Enable this may increase memory usage. - /// - /// - public void SetNetworkPacketCaptureEnabled(bool enabled) - { - if (InvokeRequired) - { - InvokeOnMainThread(() => SetNetworkPacketCaptureEnabled(enabled)); - return; - } - - networkPacketCaptureEnabled = enabled; - } - - #endregion - - #region Getters: Retrieve data for use in other methods or ChatBots - - /// - /// Get max length for chat messages - /// - /// Max length, in characters - public int GetMaxChatMessageLength() - { - return handler.GetMaxChatMessageLength(); - } - - /// - /// Get a list of disallowed characters in chat - /// - /// - public static char[] GetDisallowedChatCharacters() - { - return new char[] { (char)167, (char)127 }; // Minecraft color code and ASCII code DEL - } - - /// - /// Get all inventories. ID 0 is the player inventory. - /// - /// All inventories - public Dictionary GetInventories() - { - return inventories; - } - - /// - /// Get all Entities - /// - /// Ladt Enchantments - public EnchantmentData? GetLastEnchantments() - { - return lastEnchantment; - } - - /// - /// Get all Entities - /// - /// All Entities - public Dictionary GetEntities() - { - return entities; - } - - /// - /// Get all players latency - /// - /// All players latency - public Dictionary GetPlayersLatency() - { - Dictionary playersLatency = new(); - foreach (var player in onlinePlayers) - playersLatency.Add(player.Value.Name, player.Value.Ping); - return playersLatency; - } - - /// - /// Get client player's inventory items - /// - /// Window ID of the requested inventory - /// 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 - return null; - } - - /// - /// Get client player's inventory items - /// - /// Item Dictionary indexed by Slot ID (Check wiki.vg for slot ID) - public Container GetPlayerInventory() - { - return GetInventory(0)!; - } - - /// - /// Get a set of online player names - /// - /// Online player names - public string[] GetOnlinePlayers() - { - lock (onlinePlayers) - { - string[] playerNames = new string[onlinePlayers.Count]; - int idx = 0; - foreach (var player in onlinePlayers) - playerNames[idx++] = player.Value.Name; - return playerNames; - } - } - - /// - /// Get a dictionary of online player names and their corresponding UUID - /// - /// Dictionay of online players, key is UUID, value is Player name - public Dictionary GetOnlinePlayersWithUUID() - { - Dictionary uuid2Player = new(); - lock (onlinePlayers) - { - foreach (Guid key in onlinePlayers.Keys) - { - uuid2Player.Add(key.ToString(), onlinePlayers[key].Name); - } - } - return uuid2Player; - } - - /// - /// Get player info from uuid - /// - /// Player's UUID - /// Player info - public PlayerInfo? GetPlayerInfo(Guid uuid) - { - if (onlinePlayers.TryGetValue(uuid, out PlayerInfo? player)) - return player; - else - return null; - } - - public PlayerKeyPair? GetPlayerKeyPair() - { - return playerKeyPair; - } - - #endregion - - #region Action methods: Perform an action on the Server - - /// - /// Move to the specified location - /// - /// Location to reach - /// Allow possible but unsafe locations thay may hurt the player: lava, cactus... - /// Allow non-vanilla direct teleport instead of computing path, but may cause invalid moves and/or trigger anti-cheat plugins - /// If no valid path can be found, also allow locations within specified distance of destination - /// Do not get closer of destination than specified distance - /// 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) - { - lock (locationLock) - { - 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; - } - } - } - - /// - /// Send a chat message or command to the server - /// - /// Text to send to the server - public void SendText(string text) - { - if (String.IsNullOrEmpty(text)) - return; - - int maxLength = handler.GetMaxChatMessageLength(); - - lock (chatQueue) - { - if (text.Length > maxLength) //Message is too long? - { - 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) - { - chatQueue.Enqueue(text[..maxLength]); - text = text[maxLength..]; - } - chatQueue.Enqueue(text); - } - } - else - chatQueue.Enqueue(text); - - TrySendMessageToServer(); - } - } - - /// - /// Allow to respawn after death - /// - /// True if packet successfully sent - public bool SendRespawnPacket() - { - if (InvokeRequired) - return InvokeOnMainThread(SendRespawnPacket); - - return handler.SendRespawnPacket(); - } - - /// - /// Registers the given plugin channel for the given bot. - /// - /// The channel to register. - /// The bot to register the channel for. - public void RegisterPluginChannel(string channel, ChatBot bot) - { - if (InvokeRequired) - { - InvokeOnMainThread(() => RegisterPluginChannel(channel, bot)); - return; - } - - if (registeredBotPluginChannels.ContainsKey(channel)) - { - registeredBotPluginChannels[channel].Add(bot); - } - else - { - List bots = new() - { - bot - }; - registeredBotPluginChannels[channel] = bots; - SendPluginChannelMessage("REGISTER", Encoding.UTF8.GetBytes(channel), true); - } - } - - /// - /// Unregisters the given plugin channel for the given bot. - /// - /// The channel to unregister. - /// The bot to unregister the channel for. - public void UnregisterPluginChannel(string channel, ChatBot bot) - { - if (InvokeRequired) - { - InvokeOnMainThread(() => UnregisterPluginChannel(channel, bot)); - return; - } - - if (registeredBotPluginChannels.ContainsKey(channel)) - { - List registeredBots = registeredBotPluginChannels[channel]; - registeredBots.RemoveAll(item => object.ReferenceEquals(item, bot)); - if (registeredBots.Count == 0) - { - registeredBotPluginChannels.Remove(channel); - SendPluginChannelMessage("UNREGISTER", Encoding.UTF8.GetBytes(channel), true); - } - } - } - - /// - /// Sends a plugin channel packet to the server. See http://wiki.vg/Plugin_channel for more information - /// about plugin channels. - /// - /// The channel to send the packet on. - /// 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) - { - if (InvokeRequired) - return InvokeOnMainThread(() => SendPluginChannelMessage(channel, data, sendEvenIfNotRegistered)); - - if (!sendEvenIfNotRegistered) - { - if (!registeredBotPluginChannels.ContainsKey(channel)) - { - return false; - } - if (!registeredServerPluginChannels.Contains(channel)) - { - return false; - } - } - return 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) - { - return InvokeOnMainThread(() => handler.SendEntityAction(playerEntityID, (int)entityAction)); - } - - /// - /// Use the item currently in the player's hand - /// - /// TRUE if the item was successfully used - public bool UseItemOnHand() - { - return InvokeOnMainThread(() => handler.SendUseItem(0, sequenceId++)); - } - - /// - /// Use the item currently in the player's left hand - /// - /// TRUE if the item was successfully used - public bool UseItemOnLeftHand() - { - return InvokeOnMainThread(() => handler.SendUseItem(1, sequenceId++)); - } - - /// - /// Try to merge a slot - /// - /// The container where the item is located - /// Items to be processed - /// The ID of the slot of the item to be processed - /// The slot that was put down - /// The ID of the slot being put down - /// Record changes - /// Whether to fully merge - private static bool TryMergeSlot(Container inventory, Item item, int slotId, Item curItem, int curId, List> changedSlots) - { - int spaceLeft = curItem.Type.StackCount() - curItem.Count; - if (curItem.Type == item!.Type && spaceLeft > 0) - { - // Put item on that stack - if (item.Count <= spaceLeft) - { - // Can fit into the stack - item.Count = 0; - curItem.Count += item.Count; - - changedSlots.Add(new Tuple((short)curId, curItem)); - changedSlots.Add(new Tuple((short)slotId, null)); - - inventory.Items.Remove(slotId); - return true; - } - else - { - item.Count -= spaceLeft; - curItem.Count += spaceLeft; - - changedSlots.Add(new Tuple((short)curId, curItem)); - } - } - return false; - } - - /// - /// Store items in a new slot - /// - /// The container where the item is located - /// Items to be processed - /// The ID of the slot of the item to be processed - /// ID of the new slot - /// Record changes - private static void StoreInNewSlot(Container inventory, Item item, int slotId, int newSlotId, List> changedSlots) - { - Item newItem = new(item.Type, item.Count, item.NBT); - inventory.Items[newSlotId] = newItem; - inventory.Items.Remove(slotId); - - changedSlots.Add(new Tuple((short)newSlotId, newItem)); - changedSlots.Add(new Tuple((short)slotId, null)); - } - - /// - /// Click a slot in the specified window - /// - /// TRUE if the slot was successfully clicked - public bool DoWindowAction(int windowId, int slotId, WindowActionType action) - { - if (InvokeRequired) - return InvokeOnMainThread(() => DoWindowAction(windowId, slotId, action)); - - 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) - { - 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; - - // Check target slot also have item? - if (inventory.Items.ContainsKey(slotId)) - { - // Check if both item are the same? - if (inventory.Items[slotId].Type == playerInventory.Items[-1].Type) - { - 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 - { - // Swap two items - (inventory.Items[slotId], playerInventory.Items[-1]) = (playerInventory.Items[-1], inventory.Items[slotId]); - } - } - 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; - - // Put target slot item to cursor - playerInventory.Items[-1] = inventory.Items[slotId]; - inventory.Items.Remove(slotId); - - 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)) - { - // Check if both item are the same? - if (inventory.Items[slotId].Type == playerInventory.Items[-1].Type) - { - // Check item stacking - if (inventory.Items[slotId].Count < inventory.Items[slotId].Type.StackCount()) - { - // Drop 1 item count from cursor - playerInventory.Items[-1].Count--; - inventory.Items[slotId].Count++; - } - } - else - { - // Swap two items - (inventory.Items[slotId], playerInventory.Items[-1]) = (playerInventory.Items[-1], inventory.Items[slotId]); - } - } - 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 - { - // 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 */ - - 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) - { - 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 = 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) - 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) - { - 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) - { - 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 = 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) - { - backpack2hotbar = true; - lowerStartSlot = 31; - } - else - { - lower2upper = true; - upperStartSlot = 4; - upperEndSlot = 30; - } - } - 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) - { - 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 = upperEndSlot = 0; - } - else if (item != null && item.Type == ItemType.Map) - { - 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 = 0; - upperEndSlot = 0; - } - 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; - } - - // 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 (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) - { - 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])); - } - } - } - 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) - { - 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; - } - } - - return handler.SendWindowAction(windowId, slotId, action, item, changedSlots, inventories[windowId].StateID); - } - - /// - /// Give Creative Mode items into regular/survival Player Inventory - /// - /// (obviously) requires to be in creative mode - /// Destination inventory slot - /// Item type - /// Item count - /// Item NBT - /// TRUE if item given successfully - public bool DoCreativeGive(int slot, ItemType itemType, int count, Dictionary? nbt = null) - { - return InvokeOnMainThread(() => handler.SendCreativeInventoryAction(slot, itemType, count, nbt)); - } - - /// - /// Plays animation (Player arm swing) - /// - /// 0 for left arm, 1 for right arm - /// TRUE if animation successfully done - public bool DoAnimation(int animation) - { - return InvokeOnMainThread(() => handler.SendAnimation(animation, playerEntityID)); - } - - /// - /// Close the specified inventory window - /// - /// 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) - { - if (InvokeRequired) - return InvokeOnMainThread(() => CloseInventory(windowId)); - - if (inventories.ContainsKey(windowId)) - { - if (windowId != 0) - inventories.Remove(windowId); - bool result = handler.SendCloseWindow(windowId); - DispatchBotEvent(bot => bot.OnInventoryClose(windowId)); - return result; - } - return false; - } - - /// - /// Clean all inventory - /// - /// TRUE if the uccessfully clear - public bool ClearInventories() - { - if (!inventoryHandlingEnabled) - return false; - - if (InvokeRequired) - return InvokeOnMainThread(ClearInventories); - - inventories.Clear(); - inventories[0] = new Container(0, ContainerType.PlayerInventory, "Player Inventory"); - return true; - } - - /// - /// Interact with an entity - /// - /// - /// 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) - { - if (InvokeRequired) - return InvokeOnMainThread(() => InteractEntity(entityID, type, hand)); - - if (entities.ContainsKey(entityID)) - { - switch (type) - { - case InteractType.Interact: - return handler.SendInteractEntity(entityID, (int)type, (int)hand); - - case InteractType.InteractAt: - return handler.SendInteractEntity( - EntityID: entityID, - type: (int)type, - X: (float)entities[entityID].Location.X, - Y: (float)entities[entityID].Location.Y, - Z: (float)entities[entityID].Location.Z, - hand: (int)hand); - - default: - return handler.SendInteractEntity(entityID, (int)type); - } - } - - return false; - } - - /// - /// Place the block at hand in the Minecraft world - /// - /// 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) - { - return InvokeOnMainThread(() => handler.SendPlayerBlockPlacement((int)hand, location, blockFace, sequenceId++)); - } - - /// - /// Attempt to dig a block at the specified location - /// - /// 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, double duration = 0) - { - if (!GetTerrainEnabled()) - return false; - - if (InvokeRequired) - return InvokeOnMainThread(() => DigBlock(location, swingArms, lookAtBlock, duration)); - - // TODO select best face from current player location - Direction blockFace = Direction.Down; - - lock (DigLock) - { - if (RemainingDiggingTime > 0 && LastDigPosition != null) - { - handler.SendPlayerDigging(1, LastDigPosition.Item1, LastDigPosition.Item2, sequenceId++); - Log.Info(string.Format(Translations.cmd_dig_cancel, LastDigPosition.Item1)); - } - - // Look at block before attempting to break it - if (lookAtBlock) - UpdateLocation(GetCurrentLocation(), location); - - // 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 - bool result = handler.SendPlayerDigging(0, location, blockFace, sequenceId++) - && (!swingArms || DoAnimation((int)Hand.MainHand)); - - if (duration <= 0) - result &= handler.SendPlayerDigging(2, location, blockFace, sequenceId++); - else - { - LastDigPosition = new(location, blockFace); - RemainingDiggingTime = Settings.DoubleToTick(duration); - } - - return result; - } - } - - /// - /// Change active slot in the player inventory - /// - /// Slot to activate (0 to 8) - /// TRUE if the slot was changed - public bool ChangeSlot(short slot) - { - if (slot < 0 || slot > 8) - return false; - - if (InvokeRequired) - return InvokeOnMainThread(() => ChangeSlot(slot)); - - CurrentSlot = Convert.ToByte(slot); - return handler.SendHeldItemChange(slot); - } - - /// - /// Update sign text - /// - /// sign location - /// text one - /// text two - /// text three - /// text1 four - public bool UpdateSign(Location location, string line1, string line2, string line3, string line4) - { - // TODO Open sign editor first https://wiki.vg/Protocol#Open_Sign_Editor - return InvokeOnMainThread(() => handler.SendUpdateSign(location, line1, line2, line3, line4)); - } - - /// - /// Select villager trade - /// - /// The slot of the trade, starts at 0. - public bool SelectTrade(int selectedSlot) - { - return InvokeOnMainThread(() => handler.SelectTrade(selectedSlot)); - } - - /// - /// Update command block - /// - /// command block location - /// command - /// command block mode - /// command block flags - public bool UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags) - { - return InvokeOnMainThread(() => handler.UpdateCommandBlock(location, command, mode, flags)); - } - - /// - /// Teleport to player in spectator mode - /// - /// Player to teleport to - /// Teleporting to other entityies is NOT implemented yet - public bool Spectate(Entity entity) - { - if (entity.Type == EntityType.Player) - { - return SpectateByUUID(entity.UUID); - } - else - { - return false; - } - } - - /// - /// Teleport to player/entity in spectator mode - /// - /// UUID of player/entity to teleport to - public bool SpectateByUUID(Guid UUID) - { - if (GetGamemode() == 3) - { - if (InvokeRequired) - return InvokeOnMainThread(() => SpectateByUUID(UUID)); - return handler.SendSpectate(UUID); - } - else - { - return false; - } - } - - /// - /// Send the server a command to type in the item name in the Anvil inventory when it's open. - /// - /// The new item name - public bool SendRenameItem(string itemName) - { - if (inventories.Count == 0) - return false; - - if (inventories.Values.ToList().Last().Type != ContainerType.Anvil) - return false; - - return handler.SendRenameItem(itemName); - } - #endregion - - #region Event handlers: An event occurs on the Server - - /// - /// Dispatch a ChatBot event with automatic exception handling - /// - /// - /// Example for calling SomeEvent() on all bots at once: - /// DispatchBotEvent(bot => bot.SomeEvent()); - /// - /// Action to execute on each bot - /// 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) - { - try - { - action(bot); - } - catch (Exception e) - { - if (e is not ThreadAbortException) - { - //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 - 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 - } - } - } - - /// - /// Called when a network packet received or sent - /// - /// - /// Only called if is set to True - /// - /// Packet ID - /// 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) - { - DispatchBotEvent(bot => bot.OnNetworkPacket(packetID, packetData, isLogin, isInbound)); - } - - /// - /// Called when a server was successfully joined - /// - public void OnGameJoined(bool isOnlineMode) - { - if (protocolversion < Protocol18Handler.MC_1_19_3_Version || playerKeyPair == null || !isOnlineMode) - SetCanSendMessage(true); - else - SetCanSendMessage(false); - - string? bandString = Config.Main.Advanced.BrandInfo.ToBrandString(); - if (!String.IsNullOrWhiteSpace(bandString)) - handler.SendBrandInfo(bandString.Trim()); - - if (Config.MCSettings.Enabled) - handler.SendClientSettings( - Config.MCSettings.Locale, - Config.MCSettings.RenderDistance, - (byte)Config.MCSettings.Difficulty, - (byte)Config.MCSettings.ChatMode, - Config.MCSettings.ChatColors, - Config.MCSettings.Skin.GetByte(), - (byte)Config.MCSettings.MainHand); - - if (protocolversion >= Protocol18Handler.MC_1_19_3_Version - && playerKeyPair != null && isOnlineMode) - handler.SendPlayerSession(playerKeyPair); - - if (inventoryHandlingRequested) - { - inventoryHandlingRequested = false; - inventoryHandlingEnabled = true; - Log.Info(Translations.extra_inventory_enabled); - } - - ClearInventories(); - - DispatchBotEvent(bot => bot.AfterGameJoined()); - - ConsoleIO.InitCommandList(dispatcher); - } - - /// - /// Called when the player respawns, which happens on login, respawn and world change. - /// - public void OnRespawn() - { - ClearTasks(); - - if (terrainAndMovementsRequested) - { - terrainAndMovementsEnabled = true; - terrainAndMovementsRequested = false; - Log.Info(Translations.extra_terrainandmovement_enabled); - } - - if (terrainAndMovementsEnabled) - { - world.Clear(); - } - - entities.Clear(); - ClearInventories(); - DispatchBotEvent(bot => bot.OnRespawn()); - } - - /// - /// Check if the client is currently processing a Movement. - /// - /// true if a movement is currently handled - public bool ClientIsMoving() - { - return terrainAndMovementsEnabled && locationReceived && ((steps != null && steps.Count > 0) || (path != null && path.Count > 0)); - } - - /// - /// Get the current goal - /// - /// Current goal of movement. Location.Zero if not set. - public Location GetCurrentMovementGoal() - { - return (ClientIsMoving() || path == null) ? Location.Zero : path.Last(); - } - - /// - /// Cancels the current movement - /// - /// True if there was an active path - public bool CancelMovement() - { - bool success = ClientIsMoving(); - path = null; - return success; - } - - /// - /// Change the amount of sent movement packets per time - /// - /// Set a new walking type - public void SetMovementSpeed(MovementType newSpeed) - { - switch (newSpeed) - { - case MovementType.Sneak: - // https://minecraft.fandom.com/wiki/Sneaking#Effects - Sneaking 1.31m/s - Config.Main.Advanced.MovementSpeed = 2; - break; - case MovementType.Walk: - // https://minecraft.fandom.com/wiki/Walking#Usage - Walking 4.317 m/s - Config.Main.Advanced.MovementSpeed = 4; - break; - case MovementType.Sprint: - // https://minecraft.fandom.com/wiki/Sprinting#Usage - Sprinting 5.612 m/s - Config.Main.Advanced.MovementSpeed = 5; - break; - } - } - - /// - /// Called when the server sends a new player location, - /// or if a ChatBot whishes to update the player's location. - /// - /// The new location - /// If true, the location is relative to the current location - public void UpdateLocation(Location location, bool relative) - { - lock (locationLock) - { - if (relative) - { - this.location += location; - } - else this.location = location; - locationReceived = true; - } - } - - /// - /// Called when the server sends a new player location, - /// or if a ChatBot whishes to update the player's location. - /// - /// The new location - /// Yaw to look at - /// Pitch to look at - public void UpdateLocation(Location location, float yaw, float pitch) - { - _yaw = yaw; - _pitch = pitch; - UpdateLocation(location, false); - } - - /// - /// Called when the server sends a new player location, - /// or if a ChatBot whishes to update the player's location. - /// - /// The new location - /// Block coordinates to look at - public void UpdateLocation(Location location, Location lookAtLocation) - { - double dx = lookAtLocation.X - (location.X - 0.5); - double dy = lookAtLocation.Y - (location.Y + 1); - double dz = lookAtLocation.Z - (location.Z - 0.5); - - double r = Math.Sqrt(dx * dx + dy * dy + dz * dz); - - float yaw = Convert.ToSingle(-Math.Atan2(dx, dz) / Math.PI * 180); - float pitch = Convert.ToSingle(-Math.Asin(dy / r) / Math.PI * 180); - if (yaw < 0) yaw += 360; - - UpdateLocation(location, yaw, pitch); - } - - /// - /// Called when the server sends a new player location, - /// or if a ChatBot whishes to update the player's location. - /// - /// The new location - /// Direction to look at - public void UpdateLocation(Location location, Direction direction) - { - float yaw = 0; - float pitch = 0; - - switch (direction) - { - case Direction.Up: - pitch = -90; - break; - case Direction.Down: - pitch = 90; - break; - case Direction.East: - yaw = 270; - break; - case Direction.West: - yaw = 90; - break; - case Direction.North: - yaw = 180; - break; - case Direction.South: - break; - default: - throw new ArgumentException(Translations.exception_unknown_direction, nameof(direction)); - } - - UpdateLocation(location, yaw, pitch); - } - - /// - /// Received chat/system message from the server - /// - /// Message received - public void OnTextReceived(ChatMessage message) - { - UpdateKeepAlive(); - - List links = new(); - string messageText; - - // Used for 1.19+ to mark: system message, legal / illegal signature - string color = string.Empty; - - if (message.isSignedChat) - { - if (!Config.Signature.ShowIllegalSignedChat && !message.isSystemChat && !(bool)message.isSignatureLegal!) - return; - messageText = ChatParser.ParseSignedChat(message, links); - - if (message.isSystemChat) - { - if (Config.Signature.MarkSystemMessage) - color = "§7▌§r"; // Background Gray - } - else - { - if ((bool)message.isSignatureLegal!) - { - if (Config.Signature.ShowModifiedChat && message.unsignedContent != null) - { - if (Config.Signature.MarkModifiedMsg) - color = "§6▌§r"; // Background Yellow - } - else - { - if (Config.Signature.MarkLegallySignedMsg) - color = "§2▌§r"; // Background Green - } - } - else - { - if (Config.Signature.MarkIllegallySignedMsg) - color = "§4▌§r"; // Background Red - } - } - } - else - { - if (message.isJson) - messageText = ChatParser.ParseText(message.content, links); - else - messageText = message.content; - } - - Log.Chat(color + messageText); - - if (Config.Main.Advanced.ShowChatLinks) - foreach (string link in links) - Log.Chat(string.Format(Translations.mcc_link, link)); - - DispatchBotEvent(bot => bot.GetText(messageText)); - DispatchBotEvent(bot => bot.GetText(messageText, message.content)); - } - - /// - /// Received a connection keep-alive from the server - /// - public void OnServerKeepAlive() - { - UpdateKeepAlive(); - } - - /// - /// When an inventory is opened - /// - /// The inventory - /// Inventory ID - public void OnInventoryOpen(int inventoryID, Container inventory) - { - inventories[inventoryID] = inventory; - - if (inventoryID != 0) - { - Log.Info(string.Format(Translations.extra_inventory_open, inventoryID, inventory.Title)); - Log.Info(Translations.extra_inventory_interact); - DispatchBotEvent(bot => bot.OnInventoryOpen(inventoryID)); - } - } - - /// - /// When an inventory is close - /// - /// Inventory ID - public void OnInventoryClose(int inventoryID) - { - if (inventories.ContainsKey(inventoryID)) - { - if (inventoryID == 0) - inventories[0].Items.Clear(); // Don't delete player inventory - else - inventories.Remove(inventoryID); - } - - if (inventoryID != 0) - { - Log.Info(string.Format(Translations.extra_inventory_close, inventoryID)); - DispatchBotEvent(bot => bot.OnInventoryClose(inventoryID)); - } - } - - /// - /// When received window properties from server. - /// Used for Frunaces, Enchanting Table, Beacon, Brewing stand, Stone cutter, Loom and Lectern - /// More info about: https://wiki.vg/Protocol#Set_Container_Property - /// - /// Inventory ID - /// Property ID - /// Property Value - public void OnWindowProperties(byte inventoryID, short propertyId, short propertyValue) - { - if (!inventories.ContainsKey(inventoryID)) - return; - - Container inventory = inventories[inventoryID]; - - if (inventory.Properties.ContainsKey(propertyId)) - inventory.Properties.Remove(propertyId); - - inventory.Properties.Add(propertyId, propertyValue); - - DispatchBotEvent(bot => bot.OnInventoryProperties(inventoryID, propertyId, propertyValue)); - - if (inventory.Type == ContainerType.Enchantment) - { - // We got the last property for enchantment - if (propertyId == 9 && propertyValue != -1) - { - short topEnchantmentLevelRequirement = inventory.Properties[0]; - short middleEnchantmentLevelRequirement = inventory.Properties[1]; - short bottomEnchantmentLevelRequirement = inventory.Properties[2]; - - Enchantment topEnchantment = EnchantmentMapping.GetEnchantmentById( - GetProtocolVersion(), - inventory.Properties[4]); - - Enchantment middleEnchantment = EnchantmentMapping.GetEnchantmentById( - GetProtocolVersion(), - inventory.Properties[5]); - - Enchantment bottomEnchantment = EnchantmentMapping.GetEnchantmentById( - GetProtocolVersion(), - inventory.Properties[6]); - - short topEnchantmentLevel = inventory.Properties[7]; - short middleEnchantmentLevel = inventory.Properties[8]; - short bottomEnchantmentLevel = inventory.Properties[9]; - - StringBuilder sb = new(); - - sb.AppendLine(Translations.Enchantment_enchantments_available + ":"); - - sb.AppendLine(Translations.Enchantment_tops_slot + ":\t" - + EnchantmentMapping.GetEnchantmentName(topEnchantment) + " " - + EnchantmentMapping.ConvertLevelToRomanNumbers(topEnchantmentLevel) + " (" - + topEnchantmentLevelRequirement + " " + Translations.Enchantment_levels + ")"); - - sb.AppendLine(Translations.Enchantment_middle_slot + ":\t" - + EnchantmentMapping.GetEnchantmentName(middleEnchantment) + " " - + EnchantmentMapping.ConvertLevelToRomanNumbers(middleEnchantmentLevel) + " (" - + middleEnchantmentLevelRequirement + " " + Translations.Enchantment_levels + ")"); - - sb.AppendLine(Translations.Enchantment_bottom_slot + ":\t" - + EnchantmentMapping.GetEnchantmentName(bottomEnchantment) + " " - + EnchantmentMapping.ConvertLevelToRomanNumbers(bottomEnchantmentLevel) + " (" - + bottomEnchantmentLevelRequirement + " " + Translations.Enchantment_levels + ")"); - - Log.Info(sb.ToString()); - - lastEnchantment = new(); - lastEnchantment.TopEnchantment = topEnchantment; - lastEnchantment.MiddleEnchantment = middleEnchantment; - lastEnchantment.BottomEnchantment = bottomEnchantment; - - lastEnchantment.Seed = inventory.Properties[3]; - - lastEnchantment.TopEnchantmentLevel = topEnchantmentLevel; - lastEnchantment.MiddleEnchantmentLevel = middleEnchantmentLevel; - lastEnchantment.BottomEnchantmentLevel = bottomEnchantmentLevel; - - lastEnchantment.TopEnchantmentLevelRequirement = topEnchantmentLevelRequirement; - lastEnchantment.MiddleEnchantmentLevelRequirement = middleEnchantmentLevelRequirement; - lastEnchantment.BottomEnchantmentLevelRequirement = bottomEnchantmentLevelRequirement; - - - DispatchBotEvent(bot => bot.OnEnchantments( - // Enchantments - topEnchantment, - middleEnchantment, - bottomEnchantment, - - // Enchantment levels - topEnchantmentLevel, - middleEnchantmentLevel, - bottomEnchantmentLevel, - - // Required levels for enchanting - topEnchantmentLevelRequirement, - middleEnchantmentLevelRequirement, - bottomEnchantmentLevelRequirement)); - - DispatchBotEvent(bot => bot.OnEnchantments(lastEnchantment)); - } - } - } - - /// - /// When received window items from server. - /// - /// Inventory ID - /// Item list, key = slot ID, value = Item information - public void OnWindowItems(byte inventoryID, Dictionary itemList, int stateId) - { - if (inventories.ContainsKey(inventoryID)) - { - inventories[inventoryID].Items = itemList; - inventories[inventoryID].StateID = stateId; - DispatchBotEvent(bot => bot.OnInventoryUpdate(inventoryID)); - } - } - - /// - /// When a slot is set inside window items - /// - /// Window ID - /// Slot ID - /// Item (may be null for empty slot) - public void OnSetSlot(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) - { - 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)) - { - if (item == null || item.IsEmpty) - { - if (inventories[inventoryID].Items.ContainsKey(slotID)) - inventories[inventoryID].Items.Remove(slotID); - } - else inventories[inventoryID].Items[slotID] = item; - } - } - DispatchBotEvent(bot => bot.OnInventoryUpdate(inventoryID)); - } - - /// - /// Set client player's ID for later receiving player's own properties - /// - /// Player Entity ID - public void OnReceivePlayerEntityID(int EntityID) - { - playerEntityID = EntityID; - } - - /// - /// Triggered when a new player joins the game - /// - /// player info - public void OnPlayerJoin(PlayerInfo player) - { - //Ignore placeholders eg 0000tab# from TabListPlus - if (Config.Main.Advanced.IgnoreInvalidPlayerName && !ChatBot.IsValidName(player.Name)) - return; - - if (player.Name == username) - { - // 1.19+ offline server is possible to return different uuid - uuid = player.Uuid; - uuidStr = player.Uuid.ToString().Replace("-", string.Empty); - } - - lock (onlinePlayers) - { - onlinePlayers[player.Uuid] = player; - } - - DispatchBotEvent(bot => bot.OnPlayerJoin(player.Uuid, player.Name)); - } - - /// - /// Triggered when a player has left the game - /// - /// UUID of the player - public void OnPlayerLeave(Guid uuid) - { - string? username = null; - - lock (onlinePlayers) - { - if (onlinePlayers.ContainsKey(uuid)) - { - username = onlinePlayers[uuid].Name; - onlinePlayers.Remove(uuid); - } - } - - DispatchBotEvent(bot => bot.OnPlayerLeave(uuid, username)); - } - - // - /// This method is called when a player has been killed by another entity - /// - /// Victim's entity - /// Killer's entity - public void OnPlayerKilled(int killerEntityId, string chatMessage) - { - if (!entities.ContainsKey(killerEntityId)) - return; - - DispatchBotEvent(bot => bot.OnKilled(entities[killerEntityId], chatMessage)); - } - - /// - /// Called when a plugin channel message was sent from the server. - /// - /// The channel the message was sent on - /// The data from the channel - public void OnPluginChannelMessage(string channel, byte[] data) - { - if (channel == "REGISTER") - { - string[] channels = Encoding.UTF8.GetString(data).Split('\0'); - foreach (string chan in channels) - { - if (!registeredServerPluginChannels.Contains(chan)) - { - registeredServerPluginChannels.Add(chan); - } - } - } - if (channel == "UNREGISTER") - { - string[] channels = Encoding.UTF8.GetString(data).Split('\0'); - foreach (string chan in channels) - { - registeredServerPluginChannels.Remove(chan); - } - } - - if (registeredBotPluginChannels.ContainsKey(channel)) - { - DispatchBotEvent(bot => bot.OnPluginMessage(channel, data), registeredBotPluginChannels[channel]); - } - } - - /// - /// Called when an entity spawned - /// - public void 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 }); - - entities.Add(entity.ID, 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) - { - if (entities.ContainsKey(entityid)) - DispatchBotEvent(bot => bot.OnEntityEffect(entities[entityid], effect, amplifier, duration, flags)); - } - - /// - /// 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) - { - Entity playerEntity; - if (onlinePlayers.TryGetValue(uuid, out PlayerInfo? player)) - { - playerEntity = new(entityID, EntityType.Player, location, uuid, player.Name, yaw, pitch); - player.entity = playerEntity; - } - else - playerEntity = new(entityID, EntityType.Player, location, uuid, null, yaw, pitch); - OnSpawnEntity(playerEntity); - } - - /// - /// Called on Entity Equipment - /// - /// 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) - { - if (entities.ContainsKey(entityid)) - { - Entity entity = entities[entityid]; - if (entity.Equipment.ContainsKey(slot)) - entity.Equipment.Remove(slot); - if (item != null) - entity.Equipment[slot] = item; - DispatchBotEvent(bot => bot.OnEntityEquipment(entities[entityid], slot, item)); - } - } - - /// - /// Called when the Game Mode has been updated for a player - /// - /// 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) - { - // Initial gamemode on login - if (uuid == Guid.Empty) - this.gamemode = gamemode; - - // Further regular gamemode change events - if (onlinePlayers.ContainsKey(uuid)) - { - string playerName = onlinePlayers[uuid].Name; - if (playerName == username) - this.gamemode = gamemode; - DispatchBotEvent(bot => bot.OnGamemodeUpdate(playerName, uuid, gamemode)); - } - } - - /// - /// Called when entities dead/despawn. - /// - public void OnDestroyEntities(int[] Entities) - { - foreach (int a in Entities) - { - if (entities.TryGetValue(a, out Entity? entity)) - { - DispatchBotEvent(bot => bot.OnEntityDespawn(entity)); - entities.Remove(a); - } - } - } - - /// - /// Called when an entity's position changed within 8 block of its previous position. - /// - /// - /// - /// - /// - /// - public void OnEntityPosition(int EntityID, Double Dx, Double Dy, Double Dz, bool onGround) - { - if (entities.ContainsKey(EntityID)) - { - Location L = entities[EntityID].Location; - L.X += Dx; - L.Y += Dy; - L.Z += Dz; - entities[EntityID].Location = L; - DispatchBotEvent(bot => bot.OnEntityMove(entities[EntityID])); - } - - } - - /// - /// Called when an entity moved over 8 block. - /// - /// - /// - /// - /// - /// - public void OnEntityTeleport(int EntityID, Double X, Double Y, Double Z, bool onGround) - { - if (entities.ContainsKey(EntityID)) - { - Location location = new(X, Y, Z); - entities[EntityID].Location = location; - DispatchBotEvent(bot => bot.OnEntityMove(entities[EntityID])); - } - } - - /// - /// Called when received entity properties from server. - /// - /// - /// - public void OnEntityProperties(int EntityID, Dictionary prop) - { - if (EntityID == playerEntityID) - { - DispatchBotEvent(bot => bot.OnPlayerProperty(prop)); - } - } - - /// - /// Called when the status of an entity have been changed - /// - /// Entity ID - /// Status ID - public void OnEntityStatus(int entityID, byte status) - { - if (entityID == playerEntityID) - { - DispatchBotEvent(bot => bot.OnPlayerStatus(status)); - } - } - - /// - /// Called when server sent a Time Update packet. - /// - /// - /// - public void OnTimeUpdate(long WorldAge, long TimeOfDay) - { - // TimeUpdate sent every server tick hence used as timeout detect - UpdateKeepAlive(); - // calculate server tps - if (lastAge != 0) - { - DateTime currentTime = DateTime.Now; - long tickDiff = WorldAge - lastAge; - Double tps = tickDiff / (currentTime - lastTime).TotalSeconds; - lastAge = WorldAge; - lastTime = currentTime; - if (tps <= 20 && tps > 0) - { - // calculate average tps - if (tpsSamples.Count >= maxSamples) - { - // full - sampleSum -= tpsSamples[0]; - tpsSamples.RemoveAt(0); - } - tpsSamples.Add(tps); - sampleSum += tps; - averageTPS = sampleSum / tpsSamples.Count; - serverTPS = tps; - DispatchBotEvent(bot => bot.OnServerTpsUpdate(tps)); - } - } - else - { - lastAge = WorldAge; - lastTime = DateTime.Now; - } - DispatchBotEvent(bot => bot.OnTimeUpdate(WorldAge, TimeOfDay)); - } - - /// - /// Called when client player's health changed, e.g. getting attack - /// - /// Player current health - public void OnUpdateHealth(float health, int food) - { - playerHealth = health; - playerFoodSaturation = food; - - if (health <= 0) - { - if (Config.Main.Advanced.AutoRespawn) - { - Log.Info(Translations.mcc_player_dead_respawn); - respawnTicks = 10; - } - else - { - Log.Info(string.Format(Translations.mcc_player_dead, Config.Main.Advanced.InternalCmdChar.ToLogString())); - } - DispatchBotEvent(bot => bot.OnDeath()); - } - - DispatchBotEvent(bot => bot.OnHealthUpdate(health, food)); - } - - /// - /// Called when experience updates - /// - /// Between 0 and 1 - /// Level - /// Total Experience - public void OnSetExperience(float Experiencebar, int Level, int TotalExperience) - { - playerLevel = Level; - playerTotalExperience = TotalExperience; - DispatchBotEvent(bot => bot.OnSetExperience(Experiencebar, Level, TotalExperience)); - } - - /// - /// Called when and explosion occurs on the server - /// - /// Explosion location - /// Explosion strength - /// Amount of affected blocks - public void OnExplosion(Location location, float strength, int affectedBlocks) - { - DispatchBotEvent(bot => bot.OnExplosion(location, strength, affectedBlocks)); - } - - /// - /// Called when Latency is updated - /// - /// player uuid - /// Latency - public void OnLatencyUpdate(Guid uuid, int latency) - { - if (onlinePlayers.ContainsKey(uuid)) - { - PlayerInfo player = onlinePlayers[uuid]; - player.Ping = 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)); - } - } - - /// - /// Called when held item change - /// - /// item slot - public void OnHeldItemChange(byte slot) - { - CurrentSlot = slot; - DispatchBotEvent(bot => bot.OnHeldItemChange(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 - /// Map format and colors: https://minecraft.fandom.com/wiki/Map_item_format - /// - /// Map ID of the map being modified - /// A scale of the Map, from 0 for a fully zoomed-in map (1 block per pixel) to 4 for a fully zoomed-out map (16 blocks per pixel) - /// Specifies whether player and item frame icons are shown - /// True if the map has been locked in a cartography table - /// A list of MapIcon objects of map icons, send only if trackingPosition is true - /// Numbs of columns that were updated (map width) (NOTE: If it is 0, the next fields are not used/are set to default values of 0 and null respectively) - /// Map height - /// 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) - { - DispatchBotEvent(bot => bot.OnMapData(mapid, scale, trackingPosition, locked, icons, columnsUpdated, rowsUpdated, mapCoulmnX, mapCoulmnZ, colors)); - } - - /// - /// Received some Title from the server - /// 0 = set title, 1 = set subtitle, 3 = set action bar, 4 = set times and display, 4 = hide, 5 = reset - /// title text - /// suntitle text - /// action bar text - /// Fade In - /// Stay - /// Fade Out - /// json text - public void OnTitle(int action, string titletext, string subtitletext, string actionbartext, int fadein, int stay, int fadeout, string json) - { - DispatchBotEvent(bot => bot.OnTitle(action, titletext, subtitletext, actionbartext, fadein, stay, fadeout, json)); - } - - /// - /// Called when coreboardObjective - /// - /// objective name - /// 0 to create the scoreboard. 1 to remove the scoreboard. 2 to update the display text. - /// Only if mode is 0 or 2. The text to be displayed for the score - /// Only if mode is 0 or 2. 0 = "integer", 1 = "hearts". - public void OnScoreboardObjective(string objectivename, byte mode, string objectivevalue, int type) - { - string json = objectivevalue; - objectivevalue = ChatParser.ParseText(objectivevalue); - DispatchBotEvent(bot => bot.OnScoreboardObjective(objectivename, mode, objectivevalue, type, json)); - } - - /// - /// Called when DisplayScoreboard - /// - /// The entity whose score this is. For players, this is their username; for other entities, it is their UUID. - /// 0 to create/update an item. 1 to remove an item. - /// The name of the objective the score belongs to - /// he score to be displayed next to the entry. Only sent when Action does not equal 1. - public void OnUpdateScore(string entityname, int action, string objectivename, int value) - { - DispatchBotEvent(bot => bot.OnUpdateScore(entityname, action, objectivename, value)); - } - - /// - /// Called when the health of an entity changed - /// - /// Entity ID - /// The health of the entity - public void OnEntityHealth(int entityID, float health) - { - if (entities.ContainsKey(entityID)) - { - entities[entityID].Health = health; - DispatchBotEvent(bot => bot.OnEntityHealth(entities[entityID], health)); - } - } - - /// - /// Called when the metadata of an entity changed - /// - /// Entity ID - /// The metadata of the entity - public void OnEntityMetadata(int entityID, Dictionary metadata) - { - if (entities.ContainsKey(entityID)) - { - Entity entity = entities[entityID]; - entity.Metadata = metadata; - int itemEntityMetadataFieldIndex = protocolversion < Protocol18Handler.MC_1_17_Version ? 7 : 8; - - if (entity.Type.ContainsItem() && metadata.TryGetValue(itemEntityMetadataFieldIndex, out object? itemObj) && itemObj != null && itemObj.GetType() == typeof(Item)) - { - Item item = (Item)itemObj; - if (item == null) - entity.Item = new Item(ItemType.Air, 0, null); - else entity.Item = item; - } - if (metadata.TryGetValue(6, out object? poseObj) && poseObj != null && poseObj.GetType() == typeof(Int32)) - { - entity.Pose = (EntityPose)poseObj; - } - if (metadata.TryGetValue(2, out object? nameObj) && nameObj != null && nameObj.GetType() == typeof(string)) - { - string name = nameObj.ToString() ?? string.Empty; - entity.CustomNameJson = name; - entity.CustomName = ChatParser.ParseText(name); - } - if (metadata.TryGetValue(3, out object? nameVisableObj) && nameVisableObj != null && nameVisableObj.GetType() == typeof(bool)) - { - entity.IsCustomNameVisible = bool.Parse(nameVisableObj.ToString() ?? string.Empty); - } - DispatchBotEvent(bot => bot.OnEntityMetadata(entity, metadata)); - } - } - - /// - /// Called when tradeList is recieved after interacting with villager - /// - /// Window ID - /// List of trades. - /// Contains Level, Experience, IsRegularVillager and CanRestock . - public void OnTradeList(int windowID, List trades, VillagerInfo villagerInfo) - { - DispatchBotEvent(bot => bot.OnTradeList(windowID, trades, villagerInfo)); - } - - /// - /// Will be called every player break block in gamemode 0 - /// - /// Player ID - /// Block location - /// Destroy stage, maximum 255 - public void OnBlockBreakAnimation(int entityId, Location location, byte stage) - { - if (entities.ContainsKey(entityId)) - { - Entity entity = entities[entityId]; - DispatchBotEvent(bot => bot.OnBlockBreakAnimation(entity, location, stage)); - } - } - - /// - /// Will be called every animations of the hit and place block - /// - /// Player ID - /// 0 = LMB, 1 = RMB (RMB Corrent not work) - public void OnEntityAnimation(int entityID, byte animation) - { - if (entities.ContainsKey(entityID)) - { - Entity entity = entities[entityID]; - DispatchBotEvent(bot => bot.OnEntityAnimation(entity, animation)); - } - } - - /// - /// Will be called when a Synchronization sequence is recevied, this sequence need to be sent when breaking or placing blocks - /// - /// Sequence ID - public void OnBlockChangeAck(int sequenceId) - { - this.sequenceId = sequenceId; - } - - /// - /// This method is called when the protocol handler receives server data - /// - /// Indicates if the server has a motd message - /// Server MOTD message - /// Indicates if the server has a an icon - /// Server icon in Base 64 format - /// Indicates if the server previews chat - public void OnServerDataRecived(bool hasMotd, string motd, bool hasIcon, string iconBase64, bool previewsChat) - { - isSupportPreviewsChat = previewsChat; - } - - /// - /// This method is called when the protocol handler receives "Set Display Chat Preview" packet - /// - /// Indicates if the server previews chat - public void OnChatPreviewSettingUpdate(bool previewsChat) - { - isSupportPreviewsChat = previewsChat; - } - - /// - /// This method is called when the protocol handler receives "Login Success" packet - /// - /// The player's UUID received from the server - /// The player's username received from the server - /// Tuple - public void OnLoginSuccess(Guid uuid, string userName, Tuple[]? playerProperty) - { - //string UUID = uuid.ToString().Replace("-", String.Empty); - //Log.Info("now UUID = " + this.uuid); - //Log.Info("new UUID = " + UUID); - ////handler.SetUserUUID(UUID); - - } - - /// - /// Used for a wide variety of game events, from weather to bed use to gamemode to demo messages. - /// - /// Event type - /// Depends on Reason - public void OnGameEvent(byte reason, float value) - { - switch (reason) - { - case 7: - DispatchBotEvent(bot => bot.OnRainLevelChange(value)); - break; - case 8: - DispatchBotEvent(bot => bot.OnThunderLevelChange(value)); - break; - } - } - - /// - /// Called when a block is changed. - /// - /// The location of the block. - /// The block - public void OnBlockChange(Location location, Block block) - { - world.SetBlock(location, block); - DispatchBotEvent(bot => bot.OnBlockChange(location, block)); - } - - /// - /// Called when "AutoComplete" completes. - /// - /// The number of this result. - /// All commands. - public void OnAutoCompleteDone(int transactionId, string[] result) - { - ConsoleIO.OnAutoCompleteDone(transactionId, result); - } - - public void SetCanSendMessage(bool canSendMessage) - { - CanSendMessage = canSendMessage; - Log.Debug("CanSendMessage = " + canSendMessage); - } - - /// - /// Send a click container button packet to the server. - /// Used for Enchanting table, Lectern, stone cutter and loom - /// - /// Id of the window being clicked - /// Id of the clicked button - /// True if packet was successfully sent - - public bool ClickContainerButton(int windowId, int buttonId) - { - return handler.ClickContainerButton(windowId, buttonId); - } - - #endregion - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using Brigadier.NET; +using Brigadier.NET.Exceptions; +using MinecraftClient.ChatBots; +using MinecraftClient.CommandHandler; +using MinecraftClient.CommandHandler.Patch; +using MinecraftClient.Commands; +using MinecraftClient.Inventory; +using MinecraftClient.Logger; +using MinecraftClient.Mapping; +using MinecraftClient.Protocol; +using MinecraftClient.Protocol.Handlers; +using MinecraftClient.Protocol.Handlers.Forge; +using MinecraftClient.Protocol.Message; +using MinecraftClient.Protocol.ProfileKey; +using MinecraftClient.Protocol.Session; +using MinecraftClient.Proxy; +using MinecraftClient.Scripting; +using static MinecraftClient.Settings; + +namespace MinecraftClient +{ + /// + /// The main client class, used to connect to a Minecraft server. + /// + 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 Queue threadTasks = new(); + private readonly object threadTasksLock = new(); + + private readonly List bots = new(); + private static readonly List botsOnHold = new(); + private static readonly Dictionary inventories = new(); + + private readonly Dictionary> registeredBotPluginChannels = new(); + private readonly List registeredServerPluginChannels = new(); + + private bool terrainAndMovementsEnabled; + private bool terrainAndMovementsRequested = false; + private bool inventoryHandlingEnabled; + private bool inventoryHandlingRequested = false; + private bool entityHandlingEnabled; + + private readonly object locationLock = new(); + private bool locationReceived = false; + private readonly World world = new(); + private Queue? steps; + private Queue? path; + private Location location; + private float? _yaw; // Used for calculation ONLY!!! Doesn't reflect the client yaw + private float? _pitch; // Used for calculation ONLY!!! Doesn't reflect the client pitch + private float playerYaw; + private float playerPitch; + private double motionY; + public enum MovementType { Sneak, Walk, Sprint } + private int sequenceId; // User for player block synchronization (Aka. digging, placing blocks, etc..) + private bool CanSendMessage = false; + + private readonly string host; + private readonly int port; + private readonly int protocolversion; + private readonly string username; + private Guid uuid; + private string uuidStr; + private readonly string sessionid; + private readonly PlayerKeyPair? playerKeyPair; + private DateTime lastKeepAlive; + private readonly object lastKeepAliveLock = new(); + private int respawnTicks = 0; + private int gamemode = 0; + private bool isSupportPreviewsChat; + private EnchantmentData? lastEnchantment = null; + + private int playerEntityID; + + private object DigLock = new(); + private Tuple? LastDigPosition; + private int RemainingDiggingTime = 0; + + // player health and hunger + private float playerHealth; + private int playerFoodSaturation; + private int playerLevel; + private int playerTotalExperience; + private byte CurrentSlot = 0; + + // Sneaking + public bool IsSneaking { get; set; } = false; + private bool isUnderSlab = false; + private DateTime nextSneakingUpdate = DateTime.Now; + + // Entity handling + private readonly Dictionary entities = new(); + + // server TPS + private long lastAge = 0; + private DateTime lastTime; + private double serverTPS = 0; + private double averageTPS = 20; + private const int maxSamples = 5; + private readonly List tpsSamples = new(maxSamples); + private double sampleSum = 0; + + // ChatBot OnNetworkPacket event + private bool networkPacketCaptureEnabled = false; + + 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 Location GetCurrentLocation() { return location; } + public float GetYaw() { return playerYaw; } + public int GetSequenceId() { return sequenceId; } + public float GetPitch() { return playerPitch; } + public World GetWorld() { return world; } + public double GetServerTPS() { return averageTPS; } + public bool GetIsSupportPreviewsChat() { return isSupportPreviewsChat; } + public float GetHealth() { return playerHealth; } + public int GetSaturation() { return playerFoodSaturation; } + public int GetLevel() { return playerLevel; } + public int GetTotalExperience() { return playerTotalExperience; } + public byte GetCurrentSlot() { return CurrentSlot; } + public int GetGamemode() { return gamemode; } + public bool GetNetworkPacketCaptureEnabled() { return networkPacketCaptureEnabled; } + public int GetProtocolVersion() { return protocolversion; } + public ILogger GetLogger() { return Log; } + public int GetPlayerEntityID() { return playerEntityID; } + public List GetLoadedChatBots() { return new List(bots); } + + readonly TcpClient client; + readonly IMinecraftCom handler; + CancellationTokenSource? cmdprompt = null; + Tuple? timeoutdetector = null; + + public ILogger Log; + + private static IMinecraftComHandler? instance; + public static IMinecraftComHandler? Instance => instance; + /// + /// 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 + /// 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) + { + CmdResult.currentHandler = this; + instance = 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; + + Log = Settings.Config.Logging.LogToFile + ? new FileLogLogger(Config.AppVar.ExpandVars(Settings.Config.Logging.LogFile), Settings.Config.Logging.PrependTimestamp) + : new FilteredLogger(); + Log.DebugEnabled = Config.Logging.DebugMessages; + Log.InfoEnabled = Config.Logging.InfoMessages; + Log.ChatEnabled = Config.Logging.ChatMessages; + Log.WarnEnabled = Config.Logging.WarningMessages; + Log.ErrorEnabled = Config.Logging.ErrorMessages; + + /* Load commands from Commands namespace */ + LoadCommands(); + + if (botsOnHold.Count == 0) + RegisterBots(); + + 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); + 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); + + 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(); + ConsoleInteractive.ConsoleReader.MessageReceived += ConsoleReaderOnMessageReceived; + ConsoleInteractive.ConsoleReader.OnInputChange += ConsoleIO.AutocompleteHandler; + } + else + { + Log.Error(Translations.error_login_failed); + goto Retry; + } + } + catch (Exception e) + { + Log.Error(e.GetType().Name + ": " + e.Message); + 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(); + } + else if (InternalConfig.InteractiveMode) + { + ConsoleInteractive.ConsoleReader.StopReadThread(); + ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived; + ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler; + Program.HandleFailure(); + } + + throw new Exception("Initialization failed."); + } + + /// + /// 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()); } + if (Config.ChatBot.ItemsCollector.Enabled) { BotLoad(new ItemsCollector()); } + if (Config.ChatBot.WebSocketBot.Enabled) { BotLoad(new WebSocketBot()); } + //Add your ChatBot here by uncommenting and adapting + //BotLoad(new ChatBots.YourBot()); + } + + /// + /// Retrieve messages from the queue and send. + /// Note: requires external locking. + /// + private void TrySendMessageToServer() + { + if (!CanSendMessage) + return; + + while (chatQueue.Count > 0 && nextMessageSendTime < DateTime.Now) + { + string text = chatQueue.Dequeue(); + handler.SendChatMessage(text, playerKeyPair); + nextMessageSendTime = DateTime.Now + TimeSpan.FromSeconds(Config.Main.Advanced.MessageCooldown); + } + } + + /// + /// Called ~10 times per second by the protocol handler + /// + public void OnUpdate() + { + foreach (ChatBot bot in bots.ToArray()) + { + try + { + bot.Update(); + bot.UpdateInternal(); + } + catch (Exception e) + { + if (e is not ThreadAbortException) + Log.Warn("Update: Got error from " + bot.ToString() + ": " + e.ToString()); + else + throw; //ThreadAbortException should not be caught + } + } + + if (nextSneakingUpdate < DateTime.Now) + { + if (world.GetBlock(new Location(location.X, location.Y + 1, location.Z)).IsTopSlab(protocolversion) && !IsSneaking) + { + isUnderSlab = true; + SendEntityAction(EntityActionType.StartSneaking); + } + else + { + if (isUnderSlab && !IsSneaking) + { + isUnderSlab = false; + SendEntityAction(EntityActionType.StopSneaking); + } + } + + nextSneakingUpdate = DateTime.Now.AddMilliseconds(300); + } + + lock (chatQueue) + { + TrySendMessageToServer(); + } + + if (terrainAndMovementsEnabled && locationReceived) + { + lock (locationLock) + { + for (int i = 0; i < Config.Main.Advanced.MovementSpeed; i++) //Needs to run at 20 tps; MCC runs at 10 tps + { + if (_yaw == null || _pitch == null) + { + 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); + + 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; + } + } + + if (Config.Main.Advanced.AutoRespawn && respawnTicks > 0) + { + respawnTicks--; + if (respawnTicks == 0) + SendRespawnPacket(); + } + + lock (threadTasksLock) + { + while (threadTasks.Count > 0) + { + Action taskToRun = threadTasks.Dequeue(); + taskToRun(); + } + } + + lock (DigLock) + { + if (RemainingDiggingTime > 0) + { + if (--RemainingDiggingTime == 0 && LastDigPosition != null) + { + handler.SendPlayerDigging(2, LastDigPosition.Item1, LastDigPosition.Item2, sequenceId++); + Log.Info(string.Format(Translations.cmd_dig_end, LastDigPosition.Item1)); + } + else + { + DoAnimation((int)Hand.MainHand); + } + } + } + } + + #region Connection Lost and Disconnect from Server + + /// + /// 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) + { + UpdateKeepAlive(); + do + { + Thread.Sleep(TimeSpan.FromSeconds(15)); + + if (((CancellationToken)o!).IsCancellationRequested) + return; + + lock (lastKeepAliveLock) + { + 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); + } + + /// + /// Update last keep alive to current time + /// + private void UpdateKeepAlive() + { + lock (lastKeepAliveLock) + { + lastKeepAlive = DateTime.Now; + } + } + + /// + /// Disconnect the client from the server (initiated from MCC) + /// + public void Disconnect() + { + DispatchBotEvent(bot => bot.OnDisconnect(ChatBot.DisconnectReason.UserLogout, "")); + + botsOnHold.Clear(); + botsOnHold.AddRange(bots); + + if (handler != null) + { + handler.Disconnect(); + handler.Dispose(); + } + + if (cmdprompt != null) + { + cmdprompt.Cancel(); + cmdprompt = null; + } + + if (timeoutdetector != null) + { + timeoutdetector.Item2.Cancel(); + timeoutdetector = null; + } + + if (client != null) + client.Close(); + } + + /// + /// When connection has been lost, login was denied or played was kicked from the server + /// + 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: + message = Translations.mcc_disconnect_lost; + Log.Info(message); + break; + + case ChatBot.DisconnectReason.InGameKick: + Log.Info(Translations.mcc_disconnect_server); + Log.Info(message); + break; + + case ChatBot.DisconnectReason.LoginRejected: + Log.Info(Translations.mcc_disconnect_login); + Log.Info(message); + break; + + case ChatBot.DisconnectReason.UserLogout: + 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)); + + foreach (ChatBot bot in onDisconnectBotList) + { + try + { + will_restart |= bot.OnDisconnect(reason, message); + } + catch (Exception e) + { + if (e is not ThreadAbortException) + { + Log.Warn("OnDisconnect: Got error from " + bot.ToString() + ": " + e.ToString()); + } + else throw; //ThreadAbortException should not be caught + } + } + + if (!will_restart) + { + ConsoleInteractive.ConsoleReader.StopReadThread(); + ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived; + ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler; + Program.HandleFailure(); + } + } + + #endregion + + #region Command prompt and internal MCC commands + + private void ConsoleReaderOnMessageReceived(object? sender, string e) + { + + if (client.Client == null) + return; + + if (client.Client.Connected) + { + new Thread(() => + { + InvokeOnMainThread(() => HandleCommandPromptText(e)); + }).Start(); + } + else + return; + } + + /// + /// 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) + { + if (ConsoleIO.BasicIO && text.Length > 0 && text[0] == (char)0x00) + { + //Process a request from the GUI + string[] command = text[1..].Split((char)0x00); + switch (command[0].ToLower()) + { + case "autocomplete": + int id = handler.AutoComplete(command[1]); + while (!ConsoleIO.AutoCompleteDone) { Thread.Sleep(100); } + if (command.Length > 1) { ConsoleIO.WriteLine((char)0x00 + "autocomplete" + (char)0x00 + ConsoleIO.AutoCompleteResult); } + else ConsoleIO.WriteLine((char)0x00 + "autocomplete" + (char)0x00); + break; + } + } + else + { + text = text.Trim(); + + if (text.Length > 1 + && Config.Main.Advanced.InternalCmdChar == MainConfigHealper.MainConfig.AdvancedConfig.InternalCmdCharType.none + && text[0] == '/') + { + SendText(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..]); + } + else if (text.Length > 0) + { + if (Config.Main.Advanced.InternalCmdChar == MainConfigHealper.MainConfig.AdvancedConfig.InternalCmdCharType.none + || text[0] == Config.Main.Advanced.InternalCmdChar.ToChar()) + { + CmdResult result = new(); + 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); + } + else if (result.status != CmdResult.Status.NotRun && (result.status != CmdResult.Status.Done || !string.IsNullOrWhiteSpace(result.result))) + { + Log.Info(result); + } + } + else + { + SendText(text); + } + } + } + } + + /// + /// Perform an internal MCC command (not a server command, use SendText() instead for that!) + /// + /// The command + /// May contain a confirmation or error message after processing the command, or "" otherwise. + /// Local variables passed along with the command + /// TRUE if the command was indeed an internal MCC command + public bool PerformInternalCommand(string command, ref CmdResult result, Dictionary? localVars = null) + { + /* Process the provided command */ + ParseResults parse; + try + { + parse = dispatcher.Parse(command, result); + } + catch (Exception e) + { + Log.Debug("Parse fail: " + e.GetFullMessage()); + return false; + } + + try + { + dispatcher.Execute(parse); + + foreach (ChatBot bot in bots.ToArray()) + { + try + { + bot.OnInternalCommand(command, string.Join(" ", Command.GetArgs(command)), result); + } + catch (Exception e) + { + if (e is not ThreadAbortException) + { + Log.Warn(string.Format(Translations.icmd_error, bot.ToString() ?? string.Empty, e.ToString())); + } + else throw; //ThreadAbortException should not be caught + } + } + + return true; + } + catch (CommandSyntaxException e) + { + if (parse.Context.Nodes.Count == 0) + { + return false; + } + else + { + Log.Info("§e" + e.Message ?? e.StackTrace ?? "Incorrect argument."); + Log.Info(dispatcher.GetAllUsageString(parse.Context.Nodes[0].Node.Name, false)); + return true; + } + } + } + + 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() + { + Program.ReloadSettings(true); + ReloadBots(); + } + + /// + /// Reload loaded bots (Only builtin bots) + /// + public void ReloadBots() + { + UnloadAllBots(); + RegisterBots(true); + + if (client.Client.Connected) + bots.ForEach(bot => bot.AfterGameJoined()); + } + + /// + /// Unload All Bots + /// + public void UnloadAllBots() + { + foreach (ChatBot bot in GetLoadedChatBots()) + BotUnLoad(bot); + } + + #endregion + + #region Thread-Invoke: Cross-thread method calls + + /// + /// 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(); + } + } + + /// + /// Invoke a task on the main thread and wait for completion + /// + /// Task to run without return value + /// InvokeOnMainThread(methodThatReturnsNothing); + /// InvokeOnMainThread(() => methodThatReturnsNothing(argument)); + /// InvokeOnMainThread(() => { yourCode(); }); + public void InvokeOnMainThread(Action task) + { + InvokeOnMainThread(() => { task(); return true; }); + } + + /// + /// Clear all tasks + /// + public void ClearTasks() + { + lock (threadTasksLock) + { + threadTasks.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; + } + } + } + + #endregion + + #region Management: Load/Unload ChatBots and Enable/Disable settings + + /// + /// Load a new bot + /// + public void BotLoad(ChatBot b, bool init = true) + { + if (InvokeRequired) + { + InvokeOnMainThread(() => BotLoad(b, init)); + return; + } + + 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 }); + } + + /// + /// Unload a bot + /// + public void BotUnLoad(ChatBot b) + { + if (InvokeRequired) + { + InvokeOnMainThread(() => BotUnLoad(b)); + return; + } + + b.OnUnload(); + + bots.RemoveAll(item => ReferenceEquals(item, b)); + + // 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(); + foreach (var entry in botRegistrations) + { + UnregisterPluginChannel(entry.Key, b); + } + } + + /// + /// Clear bots + /// + public void BotClear() + { + InvokeOnMainThread(bots.Clear); + } + + /// + /// Get Terrain and Movements status. + /// + public bool GetTerrainEnabled() + { + return terrainAndMovementsEnabled; + } + + /// + /// Get Inventory Handling Mode + /// + public bool GetInventoryEnabled() + { + return inventoryHandlingEnabled; + } + + /// + /// Get entity handling status + /// + /// + /// Entity Handling cannot be enabled in runtime (or after joining server) + public bool GetEntityHandlingEnabled() + { + return entityHandlingEnabled; + } + + /// + /// Enable or disable Terrain and Movements. + /// Please note that Enabling will be deferred until next relog, respawn or world change. + /// + /// Enabled + /// 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) + { + terrainAndMovementsRequested = true; + return false; + } + } + else + { + terrainAndMovementsEnabled = false; + terrainAndMovementsRequested = false; + locationReceived = false; + world.Clear(); + } + return true; + } + + /// + /// Enable or disable Inventories. + /// Please note that Enabling will be deferred until next relog. + /// + /// 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) + { + inventoryHandlingRequested = true; + return false; + } + } + else + { + inventoryHandlingEnabled = false; + inventoryHandlingRequested = false; + inventories.Clear(); + } + return true; + } + + /// + /// Enable or disable Entity handling. + /// Please note that Enabling will be deferred until next relog. + /// + /// 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) + { + entityHandlingEnabled = false; + return true; + } + else + { + return false; + } + } + else + { + // Entity Handling cannot be enabled in runtime (or after joining server) + return false; + } + } + + /// + /// Enable or disable network packet event calling. + /// + /// + /// Enable this may increase memory usage. + /// + /// + public void SetNetworkPacketCaptureEnabled(bool enabled) + { + if (InvokeRequired) + { + InvokeOnMainThread(() => SetNetworkPacketCaptureEnabled(enabled)); + return; + } + + networkPacketCaptureEnabled = enabled; + } + + #endregion + + #region Getters: Retrieve data for use in other methods or ChatBots + + /// + /// Get max length for chat messages + /// + /// Max length, in characters + public int GetMaxChatMessageLength() + { + return handler.GetMaxChatMessageLength(); + } + + /// + /// Get a list of disallowed characters in chat + /// + /// + public static char[] GetDisallowedChatCharacters() + { + return new char[] { (char)167, (char)127 }; // Minecraft color code and ASCII code DEL + } + + /// + /// Get all inventories. ID 0 is the player inventory. + /// + /// All inventories + public Dictionary GetInventories() + { + return inventories; + } + + /// + /// Get all Entities + /// + /// Ladt Enchantments + public EnchantmentData? GetLastEnchantments() + { + return lastEnchantment; + } + + /// + /// Get all Entities + /// + /// All Entities + public Dictionary GetEntities() + { + return entities; + } + + /// + /// Get all players latency + /// + /// All players latency + public Dictionary GetPlayersLatency() + { + Dictionary playersLatency = new(); + foreach (var player in onlinePlayers) + playersLatency.Add(player.Value.Name, player.Value.Ping); + return playersLatency; + } + + /// + /// Get client player's inventory items + /// + /// Window ID of the requested inventory + /// 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 + return null; + } + + /// + /// Get client player's inventory items + /// + /// Item Dictionary indexed by Slot ID (Check wiki.vg for slot ID) + public Container GetPlayerInventory() + { + return GetInventory(0)!; + } + + /// + /// Get a set of online player names + /// + /// Online player names + public string[] GetOnlinePlayers() + { + lock (onlinePlayers) + { + string[] playerNames = new string[onlinePlayers.Count]; + int idx = 0; + foreach (var player in onlinePlayers) + playerNames[idx++] = player.Value.Name; + return playerNames; + } + } + + /// + /// Get a dictionary of online player names and their corresponding UUID + /// + /// Dictionay of online players, key is UUID, value is Player name + public Dictionary GetOnlinePlayersWithUUID() + { + Dictionary uuid2Player = new(); + lock (onlinePlayers) + { + foreach (Guid key in onlinePlayers.Keys) + { + uuid2Player.Add(key.ToString(), onlinePlayers[key].Name); + } + } + return uuid2Player; + } + + /// + /// Get player info from uuid + /// + /// Player's UUID + /// Player info + public PlayerInfo? GetPlayerInfo(Guid uuid) + { + if (onlinePlayers.TryGetValue(uuid, out PlayerInfo? player)) + return player; + else + return null; + } + + public PlayerKeyPair? GetPlayerKeyPair() + { + return playerKeyPair; + } + + #endregion + + #region Action methods: Perform an action on the Server + + /// + /// Move to the specified location + /// + /// Location to reach + /// Allow possible but unsafe locations thay may hurt the player: lava, cactus... + /// Allow non-vanilla direct teleport instead of computing path, but may cause invalid moves and/or trigger anti-cheat plugins + /// If no valid path can be found, also allow locations within specified distance of destination + /// Do not get closer of destination than specified distance + /// 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) + { + lock (locationLock) + { + 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; + } + } + } + + /// + /// Send a chat message or command to the server + /// + /// Text to send to the server + public void SendText(string text) + { + if (String.IsNullOrEmpty(text)) + return; + + int maxLength = handler.GetMaxChatMessageLength(); + + lock (chatQueue) + { + if (text.Length > maxLength) //Message is too long? + { + 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) + { + chatQueue.Enqueue(text[..maxLength]); + text = text[maxLength..]; + } + chatQueue.Enqueue(text); + } + } + else + chatQueue.Enqueue(text); + + TrySendMessageToServer(); + } + } + + /// + /// Allow to respawn after death + /// + /// True if packet successfully sent + public bool SendRespawnPacket() + { + if (InvokeRequired) + return InvokeOnMainThread(SendRespawnPacket); + + return handler.SendRespawnPacket(); + } + + /// + /// Registers the given plugin channel for the given bot. + /// + /// The channel to register. + /// The bot to register the channel for. + public void RegisterPluginChannel(string channel, ChatBot bot) + { + if (InvokeRequired) + { + InvokeOnMainThread(() => RegisterPluginChannel(channel, bot)); + return; + } + + if (registeredBotPluginChannels.ContainsKey(channel)) + { + registeredBotPluginChannels[channel].Add(bot); + } + else + { + List bots = new() + { + bot + }; + registeredBotPluginChannels[channel] = bots; + SendPluginChannelMessage("REGISTER", Encoding.UTF8.GetBytes(channel), true); + } + } + + /// + /// Unregisters the given plugin channel for the given bot. + /// + /// The channel to unregister. + /// The bot to unregister the channel for. + public void UnregisterPluginChannel(string channel, ChatBot bot) + { + if (InvokeRequired) + { + InvokeOnMainThread(() => UnregisterPluginChannel(channel, bot)); + return; + } + + if (registeredBotPluginChannels.ContainsKey(channel)) + { + List registeredBots = registeredBotPluginChannels[channel]; + registeredBots.RemoveAll(item => object.ReferenceEquals(item, bot)); + if (registeredBots.Count == 0) + { + registeredBotPluginChannels.Remove(channel); + SendPluginChannelMessage("UNREGISTER", Encoding.UTF8.GetBytes(channel), true); + } + } + } + + /// + /// Sends a plugin channel packet to the server. See http://wiki.vg/Plugin_channel for more information + /// about plugin channels. + /// + /// The channel to send the packet on. + /// 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) + { + if (InvokeRequired) + return InvokeOnMainThread(() => SendPluginChannelMessage(channel, data, sendEvenIfNotRegistered)); + + if (!sendEvenIfNotRegistered) + { + if (!registeredBotPluginChannels.ContainsKey(channel)) + { + return false; + } + if (!registeredServerPluginChannels.Contains(channel)) + { + return false; + } + } + return 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) + { + return InvokeOnMainThread(() => handler.SendEntityAction(playerEntityID, (int)entityAction)); + } + + /// + /// Use the item currently in the player's hand + /// + /// TRUE if the item was successfully used + public bool UseItemOnHand() + { + return InvokeOnMainThread(() => handler.SendUseItem(0, sequenceId++)); + } + + /// + /// Use the item currently in the player's left hand + /// + /// TRUE if the item was successfully used + public bool UseItemOnLeftHand() + { + return InvokeOnMainThread(() => handler.SendUseItem(1, sequenceId++)); + } + + /// + /// Try to merge a slot + /// + /// The container where the item is located + /// Items to be processed + /// The ID of the slot of the item to be processed + /// The slot that was put down + /// The ID of the slot being put down + /// Record changes + /// Whether to fully merge + private static bool TryMergeSlot(Container inventory, Item item, int slotId, Item curItem, int curId, List> changedSlots) + { + int spaceLeft = curItem.Type.StackCount() - curItem.Count; + if (curItem.Type == item!.Type && spaceLeft > 0) + { + // Put item on that stack + if (item.Count <= spaceLeft) + { + // Can fit into the stack + item.Count = 0; + curItem.Count += item.Count; + + changedSlots.Add(new Tuple((short)curId, curItem)); + changedSlots.Add(new Tuple((short)slotId, null)); + + inventory.Items.Remove(slotId); + return true; + } + else + { + item.Count -= spaceLeft; + curItem.Count += spaceLeft; + + changedSlots.Add(new Tuple((short)curId, curItem)); + } + } + return false; + } + + /// + /// Store items in a new slot + /// + /// The container where the item is located + /// Items to be processed + /// The ID of the slot of the item to be processed + /// ID of the new slot + /// Record changes + private static void StoreInNewSlot(Container inventory, Item item, int slotId, int newSlotId, List> changedSlots) + { + Item newItem = new(item.Type, item.Count, item.NBT); + inventory.Items[newSlotId] = newItem; + inventory.Items.Remove(slotId); + + changedSlots.Add(new Tuple((short)newSlotId, newItem)); + changedSlots.Add(new Tuple((short)slotId, null)); + } + + /// + /// Click a slot in the specified window + /// + /// TRUE if the slot was successfully clicked + public bool DoWindowAction(int windowId, int slotId, WindowActionType action) + { + if (InvokeRequired) + return InvokeOnMainThread(() => DoWindowAction(windowId, slotId, action)); + + 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) + { + 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; + + // Check target slot also have item? + if (inventory.Items.ContainsKey(slotId)) + { + // Check if both item are the same? + if (inventory.Items[slotId].Type == playerInventory.Items[-1].Type) + { + 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 + { + // Swap two items + (inventory.Items[slotId], playerInventory.Items[-1]) = (playerInventory.Items[-1], inventory.Items[slotId]); + } + } + 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; + + // Put target slot item to cursor + playerInventory.Items[-1] = inventory.Items[slotId]; + inventory.Items.Remove(slotId); + + 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)) + { + // Check if both item are the same? + if (inventory.Items[slotId].Type == playerInventory.Items[-1].Type) + { + // Check item stacking + if (inventory.Items[slotId].Count < inventory.Items[slotId].Type.StackCount()) + { + // Drop 1 item count from cursor + playerInventory.Items[-1].Count--; + inventory.Items[slotId].Count++; + } + } + else + { + // Swap two items + (inventory.Items[slotId], playerInventory.Items[-1]) = (playerInventory.Items[-1], inventory.Items[slotId]); + } + } + 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 + { + // 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 */ + + 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) + { + 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 = 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) + 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) + { + 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) + { + 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 = 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) + { + backpack2hotbar = true; + lowerStartSlot = 31; + } + else + { + lower2upper = true; + upperStartSlot = 4; + upperEndSlot = 30; + } + } + 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) + { + 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 = upperEndSlot = 0; + } + else if (item != null && item.Type == ItemType.Map) + { + 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 = 0; + upperEndSlot = 0; + } + 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; + } + + // 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 (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) + { + 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])); + } + } + } + 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) + { + 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; + } + } + + return handler.SendWindowAction(windowId, slotId, action, item, changedSlots, inventories[windowId].StateID); + } + + /// + /// Give Creative Mode items into regular/survival Player Inventory + /// + /// (obviously) requires to be in creative mode + /// Destination inventory slot + /// Item type + /// Item count + /// Item NBT + /// TRUE if item given successfully + public bool DoCreativeGive(int slot, ItemType itemType, int count, Dictionary? nbt = null) + { + return InvokeOnMainThread(() => handler.SendCreativeInventoryAction(slot, itemType, count, nbt)); + } + + /// + /// Plays animation (Player arm swing) + /// + /// 0 for left arm, 1 for right arm + /// TRUE if animation successfully done + public bool DoAnimation(int animation) + { + return InvokeOnMainThread(() => handler.SendAnimation(animation, playerEntityID)); + } + + /// + /// Close the specified inventory window + /// + /// 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) + { + if (InvokeRequired) + return InvokeOnMainThread(() => CloseInventory(windowId)); + + if (inventories.ContainsKey(windowId)) + { + if (windowId != 0) + inventories.Remove(windowId); + bool result = handler.SendCloseWindow(windowId); + DispatchBotEvent(bot => bot.OnInventoryClose(windowId)); + return result; + } + return false; + } + + /// + /// Clean all inventory + /// + /// TRUE if the uccessfully clear + public bool ClearInventories() + { + if (!inventoryHandlingEnabled) + return false; + + if (InvokeRequired) + return InvokeOnMainThread(ClearInventories); + + inventories.Clear(); + inventories[0] = new Container(0, ContainerType.PlayerInventory, "Player Inventory"); + return true; + } + + /// + /// Interact with an entity + /// + /// + /// 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) + { + if (InvokeRequired) + return InvokeOnMainThread(() => InteractEntity(entityID, type, hand)); + + if (entities.ContainsKey(entityID)) + { + switch (type) + { + case InteractType.Interact: + return handler.SendInteractEntity(entityID, (int)type, (int)hand); + + case InteractType.InteractAt: + return handler.SendInteractEntity( + EntityID: entityID, + type: (int)type, + X: (float)entities[entityID].Location.X, + Y: (float)entities[entityID].Location.Y, + Z: (float)entities[entityID].Location.Z, + hand: (int)hand); + + default: + return handler.SendInteractEntity(entityID, (int)type); + } + } + + return false; + } + + /// + /// Place the block at hand in the Minecraft world + /// + /// 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) + { + return InvokeOnMainThread(() => handler.SendPlayerBlockPlacement((int)hand, location, blockFace, sequenceId++)); + } + + /// + /// Attempt to dig a block at the specified location + /// + /// 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, double duration = 0) + { + if (!GetTerrainEnabled()) + return false; + + if (InvokeRequired) + return InvokeOnMainThread(() => DigBlock(location, swingArms, lookAtBlock, duration)); + + // TODO select best face from current player location + Direction blockFace = Direction.Down; + + lock (DigLock) + { + if (RemainingDiggingTime > 0 && LastDigPosition != null) + { + handler.SendPlayerDigging(1, LastDigPosition.Item1, LastDigPosition.Item2, sequenceId++); + Log.Info(string.Format(Translations.cmd_dig_cancel, LastDigPosition.Item1)); + } + + // Look at block before attempting to break it + if (lookAtBlock) + UpdateLocation(GetCurrentLocation(), location); + + // 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 + bool result = handler.SendPlayerDigging(0, location, blockFace, sequenceId++) + && (!swingArms || DoAnimation((int)Hand.MainHand)); + + if (duration <= 0) + result &= handler.SendPlayerDigging(2, location, blockFace, sequenceId++); + else + { + LastDigPosition = new(location, blockFace); + RemainingDiggingTime = Settings.DoubleToTick(duration); + } + + return result; + } + } + + /// + /// Change active slot in the player inventory + /// + /// Slot to activate (0 to 8) + /// TRUE if the slot was changed + public bool ChangeSlot(short slot) + { + if (slot < 0 || slot > 8) + return false; + + if (InvokeRequired) + return InvokeOnMainThread(() => ChangeSlot(slot)); + + CurrentSlot = Convert.ToByte(slot); + return handler.SendHeldItemChange(slot); + } + + /// + /// Update sign text + /// + /// sign location + /// text one + /// text two + /// text three + /// text1 four + public bool UpdateSign(Location location, string line1, string line2, string line3, string line4) + { + // TODO Open sign editor first https://wiki.vg/Protocol#Open_Sign_Editor + return InvokeOnMainThread(() => handler.SendUpdateSign(location, line1, line2, line3, line4)); + } + + /// + /// Select villager trade + /// + /// The slot of the trade, starts at 0. + public bool SelectTrade(int selectedSlot) + { + return InvokeOnMainThread(() => handler.SelectTrade(selectedSlot)); + } + + /// + /// Update command block + /// + /// command block location + /// command + /// command block mode + /// command block flags + public bool UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags) + { + return InvokeOnMainThread(() => handler.UpdateCommandBlock(location, command, mode, flags)); + } + + /// + /// Teleport to player in spectator mode + /// + /// Player to teleport to + /// Teleporting to other entityies is NOT implemented yet + public bool Spectate(Entity entity) + { + if (entity.Type == EntityType.Player) + { + return SpectateByUUID(entity.UUID); + } + else + { + return false; + } + } + + /// + /// Teleport to player/entity in spectator mode + /// + /// UUID of player/entity to teleport to + public bool SpectateByUUID(Guid UUID) + { + if (GetGamemode() == 3) + { + if (InvokeRequired) + return InvokeOnMainThread(() => SpectateByUUID(UUID)); + return handler.SendSpectate(UUID); + } + else + { + return false; + } + } + + /// + /// Send the server a command to type in the item name in the Anvil inventory when it's open. + /// + /// The new item name + public bool SendRenameItem(string itemName) + { + if (inventories.Count == 0) + return false; + + if (inventories.Values.ToList().Last().Type != ContainerType.Anvil) + return false; + + return handler.SendRenameItem(itemName); + } + #endregion + + #region Event handlers: An event occurs on the Server + + /// + /// Dispatch a ChatBot event with automatic exception handling + /// + /// + /// Example for calling SomeEvent() on all bots at once: + /// DispatchBotEvent(bot => bot.SomeEvent()); + /// + /// Action to execute on each bot + /// 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) + { + try + { + action(bot); + } + catch (Exception e) + { + if (e is not ThreadAbortException) + { + //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 + 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 + } + } + } + + /// + /// Called when a network packet received or sent + /// + /// + /// Only called if is set to True + /// + /// Packet ID + /// 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) + { + DispatchBotEvent(bot => bot.OnNetworkPacket(packetID, packetData, isLogin, isInbound)); + } + + /// + /// Called when a server was successfully joined + /// + public void OnGameJoined(bool isOnlineMode) + { + if (protocolversion < Protocol18Handler.MC_1_19_3_Version || playerKeyPair == null || !isOnlineMode) + SetCanSendMessage(true); + else + SetCanSendMessage(false); + + string? bandString = Config.Main.Advanced.BrandInfo.ToBrandString(); + if (!String.IsNullOrWhiteSpace(bandString)) + handler.SendBrandInfo(bandString.Trim()); + + if (Config.MCSettings.Enabled) + handler.SendClientSettings( + Config.MCSettings.Locale, + Config.MCSettings.RenderDistance, + (byte)Config.MCSettings.Difficulty, + (byte)Config.MCSettings.ChatMode, + Config.MCSettings.ChatColors, + Config.MCSettings.Skin.GetByte(), + (byte)Config.MCSettings.MainHand); + + if (protocolversion >= Protocol18Handler.MC_1_19_3_Version + && playerKeyPair != null && isOnlineMode) + handler.SendPlayerSession(playerKeyPair); + + if (inventoryHandlingRequested) + { + inventoryHandlingRequested = false; + inventoryHandlingEnabled = true; + Log.Info(Translations.extra_inventory_enabled); + } + + ClearInventories(); + + DispatchBotEvent(bot => bot.AfterGameJoined()); + + ConsoleIO.InitCommandList(dispatcher); + } + + /// + /// Called when the player respawns, which happens on login, respawn and world change. + /// + public void OnRespawn() + { + ClearTasks(); + + if (terrainAndMovementsRequested) + { + terrainAndMovementsEnabled = true; + terrainAndMovementsRequested = false; + Log.Info(Translations.extra_terrainandmovement_enabled); + } + + if (terrainAndMovementsEnabled) + { + world.Clear(); + } + + entities.Clear(); + ClearInventories(); + DispatchBotEvent(bot => bot.OnRespawn()); + } + + /// + /// Check if the client is currently processing a Movement. + /// + /// true if a movement is currently handled + public bool ClientIsMoving() + { + return terrainAndMovementsEnabled && locationReceived && ((steps != null && steps.Count > 0) || (path != null && path.Count > 0)); + } + + /// + /// Get the current goal + /// + /// Current goal of movement. Location.Zero if not set. + public Location GetCurrentMovementGoal() + { + return (ClientIsMoving() || path == null) ? Location.Zero : path.Last(); + } + + /// + /// Cancels the current movement + /// + /// True if there was an active path + public bool CancelMovement() + { + bool success = ClientIsMoving(); + path = null; + return success; + } + + /// + /// Change the amount of sent movement packets per time + /// + /// Set a new walking type + public void SetMovementSpeed(MovementType newSpeed) + { + switch (newSpeed) + { + case MovementType.Sneak: + // https://minecraft.fandom.com/wiki/Sneaking#Effects - Sneaking 1.31m/s + Config.Main.Advanced.MovementSpeed = 2; + break; + case MovementType.Walk: + // https://minecraft.fandom.com/wiki/Walking#Usage - Walking 4.317 m/s + Config.Main.Advanced.MovementSpeed = 4; + break; + case MovementType.Sprint: + // https://minecraft.fandom.com/wiki/Sprinting#Usage - Sprinting 5.612 m/s + Config.Main.Advanced.MovementSpeed = 5; + break; + } + } + + /// + /// Called when the server sends a new player location, + /// or if a ChatBot whishes to update the player's location. + /// + /// The new location + /// If true, the location is relative to the current location + public void UpdateLocation(Location location, bool relative) + { + lock (locationLock) + { + if (relative) + { + this.location += location; + } + else this.location = location; + locationReceived = true; + } + } + + /// + /// Called when the server sends a new player location, + /// or if a ChatBot whishes to update the player's location. + /// + /// The new location + /// Yaw to look at + /// Pitch to look at + public void UpdateLocation(Location location, float yaw, float pitch) + { + _yaw = yaw; + _pitch = pitch; + UpdateLocation(location, false); + } + + /// + /// Called when the server sends a new player location, + /// or if a ChatBot whishes to update the player's location. + /// + /// The new location + /// Block coordinates to look at + public void UpdateLocation(Location location, Location lookAtLocation) + { + double dx = lookAtLocation.X - (location.X - 0.5); + double dy = lookAtLocation.Y - (location.Y + 1); + double dz = lookAtLocation.Z - (location.Z - 0.5); + + double r = Math.Sqrt(dx * dx + dy * dy + dz * dz); + + float yaw = Convert.ToSingle(-Math.Atan2(dx, dz) / Math.PI * 180); + float pitch = Convert.ToSingle(-Math.Asin(dy / r) / Math.PI * 180); + if (yaw < 0) yaw += 360; + + UpdateLocation(location, yaw, pitch); + } + + /// + /// Called when the server sends a new player location, + /// or if a ChatBot whishes to update the player's location. + /// + /// The new location + /// Direction to look at + public void UpdateLocation(Location location, Direction direction) + { + float yaw = 0; + float pitch = 0; + + switch (direction) + { + case Direction.Up: + pitch = -90; + break; + case Direction.Down: + pitch = 90; + break; + case Direction.East: + yaw = 270; + break; + case Direction.West: + yaw = 90; + break; + case Direction.North: + yaw = 180; + break; + case Direction.South: + break; + default: + throw new ArgumentException(Translations.exception_unknown_direction, nameof(direction)); + } + + UpdateLocation(location, yaw, pitch); + } + + /// + /// Received chat/system message from the server + /// + /// Message received + public void OnTextReceived(ChatMessage message) + { + UpdateKeepAlive(); + + List links = new(); + string messageText; + + // Used for 1.19+ to mark: system message, legal / illegal signature + string color = string.Empty; + + if (message.isSignedChat) + { + if (!Config.Signature.ShowIllegalSignedChat && !message.isSystemChat && !(bool)message.isSignatureLegal!) + return; + messageText = ChatParser.ParseSignedChat(message, links); + + if (message.isSystemChat) + { + if (Config.Signature.MarkSystemMessage) + color = "§7▌§r"; // Background Gray + } + else + { + if ((bool)message.isSignatureLegal!) + { + if (Config.Signature.ShowModifiedChat && message.unsignedContent != null) + { + if (Config.Signature.MarkModifiedMsg) + color = "§6▌§r"; // Background Yellow + } + else + { + if (Config.Signature.MarkLegallySignedMsg) + color = "§2▌§r"; // Background Green + } + } + else + { + if (Config.Signature.MarkIllegallySignedMsg) + color = "§4▌§r"; // Background Red + } + } + } + else + { + if (message.isJson) + messageText = ChatParser.ParseText(message.content, links); + else + messageText = message.content; + } + + Log.Chat(color + messageText); + + if (Config.Main.Advanced.ShowChatLinks) + foreach (string link in links) + Log.Chat(string.Format(Translations.mcc_link, link)); + + DispatchBotEvent(bot => bot.GetText(messageText)); + DispatchBotEvent(bot => bot.GetText(messageText, message.content)); + } + + /// + /// Received a connection keep-alive from the server + /// + public void OnServerKeepAlive() + { + UpdateKeepAlive(); + } + + /// + /// When an inventory is opened + /// + /// The inventory + /// Inventory ID + public void OnInventoryOpen(int inventoryID, Container inventory) + { + inventories[inventoryID] = inventory; + + if (inventoryID != 0) + { + Log.Info(string.Format(Translations.extra_inventory_open, inventoryID, inventory.Title)); + Log.Info(Translations.extra_inventory_interact); + DispatchBotEvent(bot => bot.OnInventoryOpen(inventoryID)); + } + } + + /// + /// When an inventory is close + /// + /// Inventory ID + public void OnInventoryClose(int inventoryID) + { + if (inventories.ContainsKey(inventoryID)) + { + if (inventoryID == 0) + inventories[0].Items.Clear(); // Don't delete player inventory + else + inventories.Remove(inventoryID); + } + + if (inventoryID != 0) + { + Log.Info(string.Format(Translations.extra_inventory_close, inventoryID)); + DispatchBotEvent(bot => bot.OnInventoryClose(inventoryID)); + } + } + + /// + /// When received window properties from server. + /// Used for Frunaces, Enchanting Table, Beacon, Brewing stand, Stone cutter, Loom and Lectern + /// More info about: https://wiki.vg/Protocol#Set_Container_Property + /// + /// Inventory ID + /// Property ID + /// Property Value + public void OnWindowProperties(byte inventoryID, short propertyId, short propertyValue) + { + if (!inventories.ContainsKey(inventoryID)) + return; + + Container inventory = inventories[inventoryID]; + + if (inventory.Properties.ContainsKey(propertyId)) + inventory.Properties.Remove(propertyId); + + inventory.Properties.Add(propertyId, propertyValue); + + DispatchBotEvent(bot => bot.OnInventoryProperties(inventoryID, propertyId, propertyValue)); + + if (inventory.Type == ContainerType.Enchantment) + { + // We got the last property for enchantment + if (propertyId == 9 && propertyValue != -1) + { + short topEnchantmentLevelRequirement = inventory.Properties[0]; + short middleEnchantmentLevelRequirement = inventory.Properties[1]; + short bottomEnchantmentLevelRequirement = inventory.Properties[2]; + + Enchantment topEnchantment = EnchantmentMapping.GetEnchantmentById( + GetProtocolVersion(), + inventory.Properties[4]); + + Enchantment middleEnchantment = EnchantmentMapping.GetEnchantmentById( + GetProtocolVersion(), + inventory.Properties[5]); + + Enchantment bottomEnchantment = EnchantmentMapping.GetEnchantmentById( + GetProtocolVersion(), + inventory.Properties[6]); + + short topEnchantmentLevel = inventory.Properties[7]; + short middleEnchantmentLevel = inventory.Properties[8]; + short bottomEnchantmentLevel = inventory.Properties[9]; + + StringBuilder sb = new(); + + sb.AppendLine(Translations.Enchantment_enchantments_available + ":"); + + sb.AppendLine(Translations.Enchantment_tops_slot + ":\t" + + EnchantmentMapping.GetEnchantmentName(topEnchantment) + " " + + EnchantmentMapping.ConvertLevelToRomanNumbers(topEnchantmentLevel) + " (" + + topEnchantmentLevelRequirement + " " + Translations.Enchantment_levels + ")"); + + sb.AppendLine(Translations.Enchantment_middle_slot + ":\t" + + EnchantmentMapping.GetEnchantmentName(middleEnchantment) + " " + + EnchantmentMapping.ConvertLevelToRomanNumbers(middleEnchantmentLevel) + " (" + + middleEnchantmentLevelRequirement + " " + Translations.Enchantment_levels + ")"); + + sb.AppendLine(Translations.Enchantment_bottom_slot + ":\t" + + EnchantmentMapping.GetEnchantmentName(bottomEnchantment) + " " + + EnchantmentMapping.ConvertLevelToRomanNumbers(bottomEnchantmentLevel) + " (" + + bottomEnchantmentLevelRequirement + " " + Translations.Enchantment_levels + ")"); + + Log.Info(sb.ToString()); + + lastEnchantment = new(); + lastEnchantment.TopEnchantment = topEnchantment; + lastEnchantment.MiddleEnchantment = middleEnchantment; + lastEnchantment.BottomEnchantment = bottomEnchantment; + + lastEnchantment.Seed = inventory.Properties[3]; + + lastEnchantment.TopEnchantmentLevel = topEnchantmentLevel; + lastEnchantment.MiddleEnchantmentLevel = middleEnchantmentLevel; + lastEnchantment.BottomEnchantmentLevel = bottomEnchantmentLevel; + + lastEnchantment.TopEnchantmentLevelRequirement = topEnchantmentLevelRequirement; + lastEnchantment.MiddleEnchantmentLevelRequirement = middleEnchantmentLevelRequirement; + lastEnchantment.BottomEnchantmentLevelRequirement = bottomEnchantmentLevelRequirement; + + + DispatchBotEvent(bot => bot.OnEnchantments( + // Enchantments + topEnchantment, + middleEnchantment, + bottomEnchantment, + + // Enchantment levels + topEnchantmentLevel, + middleEnchantmentLevel, + bottomEnchantmentLevel, + + // Required levels for enchanting + topEnchantmentLevelRequirement, + middleEnchantmentLevelRequirement, + bottomEnchantmentLevelRequirement)); + + DispatchBotEvent(bot => bot.OnEnchantments(lastEnchantment)); + } + } + } + + /// + /// When received window items from server. + /// + /// Inventory ID + /// Item list, key = slot ID, value = Item information + public void OnWindowItems(byte inventoryID, Dictionary itemList, int stateId) + { + if (inventories.ContainsKey(inventoryID)) + { + inventories[inventoryID].Items = itemList; + inventories[inventoryID].StateID = stateId; + DispatchBotEvent(bot => bot.OnInventoryUpdate(inventoryID)); + } + } + + /// + /// When a slot is set inside window items + /// + /// Window ID + /// Slot ID + /// Item (may be null for empty slot) + public void OnSetSlot(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) + { + 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)) + { + if (item == null || item.IsEmpty) + { + if (inventories[inventoryID].Items.ContainsKey(slotID)) + inventories[inventoryID].Items.Remove(slotID); + } + else inventories[inventoryID].Items[slotID] = item; + } + } + DispatchBotEvent(bot => bot.OnInventoryUpdate(inventoryID)); + } + + /// + /// Set client player's ID for later receiving player's own properties + /// + /// Player Entity ID + public void OnReceivePlayerEntityID(int EntityID) + { + playerEntityID = EntityID; + } + + /// + /// Triggered when a new player joins the game + /// + /// player info + public void OnPlayerJoin(PlayerInfo player) + { + //Ignore placeholders eg 0000tab# from TabListPlus + if (Config.Main.Advanced.IgnoreInvalidPlayerName && !ChatBot.IsValidName(player.Name)) + return; + + if (player.Name == username) + { + // 1.19+ offline server is possible to return different uuid + uuid = player.Uuid; + uuidStr = player.Uuid.ToString().Replace("-", string.Empty); + } + + lock (onlinePlayers) + { + onlinePlayers[player.Uuid] = player; + } + + DispatchBotEvent(bot => bot.OnPlayerJoin(player.Uuid, player.Name)); + } + + /// + /// Triggered when a player has left the game + /// + /// UUID of the player + public void OnPlayerLeave(Guid uuid) + { + string? username = null; + + lock (onlinePlayers) + { + if (onlinePlayers.ContainsKey(uuid)) + { + username = onlinePlayers[uuid].Name; + onlinePlayers.Remove(uuid); + } + } + + DispatchBotEvent(bot => bot.OnPlayerLeave(uuid, username)); + } + + // + /// This method is called when a player has been killed by another entity + /// + /// Victim's entity + /// Killer's entity + public void OnPlayerKilled(int killerEntityId, string chatMessage) + { + if (!entities.ContainsKey(killerEntityId)) + return; + + DispatchBotEvent(bot => bot.OnKilled(entities[killerEntityId], chatMessage)); + } + + /// + /// Called when a plugin channel message was sent from the server. + /// + /// The channel the message was sent on + /// The data from the channel + public void OnPluginChannelMessage(string channel, byte[] data) + { + if (channel == "REGISTER") + { + string[] channels = Encoding.UTF8.GetString(data).Split('\0'); + foreach (string chan in channels) + { + if (!registeredServerPluginChannels.Contains(chan)) + { + registeredServerPluginChannels.Add(chan); + } + } + } + if (channel == "UNREGISTER") + { + string[] channels = Encoding.UTF8.GetString(data).Split('\0'); + foreach (string chan in channels) + { + registeredServerPluginChannels.Remove(chan); + } + } + + if (registeredBotPluginChannels.ContainsKey(channel)) + { + DispatchBotEvent(bot => bot.OnPluginMessage(channel, data), registeredBotPluginChannels[channel]); + } + } + + /// + /// Called when an entity spawned + /// + public void 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 }); + + entities.Add(entity.ID, 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) + { + if (entities.ContainsKey(entityid)) + DispatchBotEvent(bot => bot.OnEntityEffect(entities[entityid], effect, amplifier, duration, flags)); + } + + /// + /// 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) + { + Entity playerEntity; + if (onlinePlayers.TryGetValue(uuid, out PlayerInfo? player)) + { + playerEntity = new(entityID, EntityType.Player, location, uuid, player.Name, yaw, pitch); + player.entity = playerEntity; + } + else + playerEntity = new(entityID, EntityType.Player, location, uuid, null, yaw, pitch); + OnSpawnEntity(playerEntity); + } + + /// + /// Called on Entity Equipment + /// + /// 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) + { + if (entities.ContainsKey(entityid)) + { + Entity entity = entities[entityid]; + if (entity.Equipment.ContainsKey(slot)) + entity.Equipment.Remove(slot); + if (item != null) + entity.Equipment[slot] = item; + DispatchBotEvent(bot => bot.OnEntityEquipment(entities[entityid], slot, item)); + } + } + + /// + /// Called when the Game Mode has been updated for a player + /// + /// 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) + { + // Initial gamemode on login + if (uuid == Guid.Empty) + this.gamemode = gamemode; + + // Further regular gamemode change events + if (onlinePlayers.ContainsKey(uuid)) + { + string playerName = onlinePlayers[uuid].Name; + if (playerName == username) + this.gamemode = gamemode; + DispatchBotEvent(bot => bot.OnGamemodeUpdate(playerName, uuid, gamemode)); + } + } + + /// + /// Called when entities dead/despawn. + /// + public void OnDestroyEntities(int[] Entities) + { + foreach (int a in Entities) + { + if (entities.TryGetValue(a, out Entity? entity)) + { + DispatchBotEvent(bot => bot.OnEntityDespawn(entity)); + entities.Remove(a); + } + } + } + + /// + /// Called when an entity's position changed within 8 block of its previous position. + /// + /// + /// + /// + /// + /// + public void OnEntityPosition(int EntityID, Double Dx, Double Dy, Double Dz, bool onGround) + { + if (entities.ContainsKey(EntityID)) + { + Location L = entities[EntityID].Location; + L.X += Dx; + L.Y += Dy; + L.Z += Dz; + entities[EntityID].Location = L; + DispatchBotEvent(bot => bot.OnEntityMove(entities[EntityID])); + } + + } + + /// + /// Called when an entity moved over 8 block. + /// + /// + /// + /// + /// + /// + public void OnEntityTeleport(int EntityID, Double X, Double Y, Double Z, bool onGround) + { + if (entities.ContainsKey(EntityID)) + { + Location location = new(X, Y, Z); + entities[EntityID].Location = location; + DispatchBotEvent(bot => bot.OnEntityMove(entities[EntityID])); + } + } + + /// + /// Called when received entity properties from server. + /// + /// + /// + public void OnEntityProperties(int EntityID, Dictionary prop) + { + if (EntityID == playerEntityID) + { + DispatchBotEvent(bot => bot.OnPlayerProperty(prop)); + } + } + + /// + /// Called when the status of an entity have been changed + /// + /// Entity ID + /// Status ID + public void OnEntityStatus(int entityID, byte status) + { + if (entityID == playerEntityID) + { + DispatchBotEvent(bot => bot.OnPlayerStatus(status)); + } + } + + /// + /// Called when server sent a Time Update packet. + /// + /// + /// + public void OnTimeUpdate(long WorldAge, long TimeOfDay) + { + // TimeUpdate sent every server tick hence used as timeout detect + UpdateKeepAlive(); + // calculate server tps + if (lastAge != 0) + { + DateTime currentTime = DateTime.Now; + long tickDiff = WorldAge - lastAge; + Double tps = tickDiff / (currentTime - lastTime).TotalSeconds; + lastAge = WorldAge; + lastTime = currentTime; + if (tps <= 20 && tps > 0) + { + // calculate average tps + if (tpsSamples.Count >= maxSamples) + { + // full + sampleSum -= tpsSamples[0]; + tpsSamples.RemoveAt(0); + } + tpsSamples.Add(tps); + sampleSum += tps; + averageTPS = sampleSum / tpsSamples.Count; + serverTPS = tps; + DispatchBotEvent(bot => bot.OnServerTpsUpdate(tps)); + } + } + else + { + lastAge = WorldAge; + lastTime = DateTime.Now; + } + DispatchBotEvent(bot => bot.OnTimeUpdate(WorldAge, TimeOfDay)); + } + + /// + /// Called when client player's health changed, e.g. getting attack + /// + /// Player current health + public void OnUpdateHealth(float health, int food) + { + playerHealth = health; + playerFoodSaturation = food; + + if (health <= 0) + { + if (Config.Main.Advanced.AutoRespawn) + { + Log.Info(Translations.mcc_player_dead_respawn); + respawnTicks = 10; + } + else + { + Log.Info(string.Format(Translations.mcc_player_dead, Config.Main.Advanced.InternalCmdChar.ToLogString())); + } + DispatchBotEvent(bot => bot.OnDeath()); + } + + DispatchBotEvent(bot => bot.OnHealthUpdate(health, food)); + } + + /// + /// Called when experience updates + /// + /// Between 0 and 1 + /// Level + /// Total Experience + public void OnSetExperience(float Experiencebar, int Level, int TotalExperience) + { + playerLevel = Level; + playerTotalExperience = TotalExperience; + DispatchBotEvent(bot => bot.OnSetExperience(Experiencebar, Level, TotalExperience)); + } + + /// + /// Called when and explosion occurs on the server + /// + /// Explosion location + /// Explosion strength + /// Amount of affected blocks + public void OnExplosion(Location location, float strength, int affectedBlocks) + { + DispatchBotEvent(bot => bot.OnExplosion(location, strength, affectedBlocks)); + } + + /// + /// Called when Latency is updated + /// + /// player uuid + /// Latency + public void OnLatencyUpdate(Guid uuid, int latency) + { + if (onlinePlayers.ContainsKey(uuid)) + { + PlayerInfo player = onlinePlayers[uuid]; + player.Ping = 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)); + } + } + + /// + /// Called when held item change + /// + /// item slot + public void OnHeldItemChange(byte slot) + { + CurrentSlot = slot; + DispatchBotEvent(bot => bot.OnHeldItemChange(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 + /// Map format and colors: https://minecraft.fandom.com/wiki/Map_item_format + /// + /// Map ID of the map being modified + /// A scale of the Map, from 0 for a fully zoomed-in map (1 block per pixel) to 4 for a fully zoomed-out map (16 blocks per pixel) + /// Specifies whether player and item frame icons are shown + /// True if the map has been locked in a cartography table + /// A list of MapIcon objects of map icons, send only if trackingPosition is true + /// Numbs of columns that were updated (map width) (NOTE: If it is 0, the next fields are not used/are set to default values of 0 and null respectively) + /// Map height + /// 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) + { + DispatchBotEvent(bot => bot.OnMapData(mapid, scale, trackingPosition, locked, icons, columnsUpdated, rowsUpdated, mapCoulmnX, mapCoulmnZ, colors)); + } + + /// + /// Received some Title from the server + /// 0 = set title, 1 = set subtitle, 3 = set action bar, 4 = set times and display, 4 = hide, 5 = reset + /// title text + /// suntitle text + /// action bar text + /// Fade In + /// Stay + /// Fade Out + /// json text + public void OnTitle(int action, string titletext, string subtitletext, string actionbartext, int fadein, int stay, int fadeout, string json) + { + DispatchBotEvent(bot => bot.OnTitle(action, titletext, subtitletext, actionbartext, fadein, stay, fadeout, json)); + } + + /// + /// Called when coreboardObjective + /// + /// objective name + /// 0 to create the scoreboard. 1 to remove the scoreboard. 2 to update the display text. + /// Only if mode is 0 or 2. The text to be displayed for the score + /// Only if mode is 0 or 2. 0 = "integer", 1 = "hearts". + public void OnScoreboardObjective(string objectivename, byte mode, string objectivevalue, int type) + { + string json = objectivevalue; + objectivevalue = ChatParser.ParseText(objectivevalue); + DispatchBotEvent(bot => bot.OnScoreboardObjective(objectivename, mode, objectivevalue, type, json)); + } + + /// + /// Called when DisplayScoreboard + /// + /// The entity whose score this is. For players, this is their username; for other entities, it is their UUID. + /// 0 to create/update an item. 1 to remove an item. + /// The name of the objective the score belongs to + /// he score to be displayed next to the entry. Only sent when Action does not equal 1. + public void OnUpdateScore(string entityname, int action, string objectivename, int value) + { + DispatchBotEvent(bot => bot.OnUpdateScore(entityname, action, objectivename, value)); + } + + /// + /// Called when the health of an entity changed + /// + /// Entity ID + /// The health of the entity + public void OnEntityHealth(int entityID, float health) + { + if (entities.ContainsKey(entityID)) + { + entities[entityID].Health = health; + DispatchBotEvent(bot => bot.OnEntityHealth(entities[entityID], health)); + } + } + + /// + /// Called when the metadata of an entity changed + /// + /// Entity ID + /// The metadata of the entity + public void OnEntityMetadata(int entityID, Dictionary metadata) + { + if (entities.ContainsKey(entityID)) + { + Entity entity = entities[entityID]; + entity.Metadata = metadata; + int itemEntityMetadataFieldIndex = protocolversion < Protocol18Handler.MC_1_17_Version ? 7 : 8; + + if (entity.Type.ContainsItem() && metadata.TryGetValue(itemEntityMetadataFieldIndex, out object? itemObj) && itemObj != null && itemObj.GetType() == typeof(Item)) + { + Item item = (Item)itemObj; + if (item == null) + entity.Item = new Item(ItemType.Air, 0, null); + else entity.Item = item; + } + if (metadata.TryGetValue(6, out object? poseObj) && poseObj != null && poseObj.GetType() == typeof(Int32)) + { + entity.Pose = (EntityPose)poseObj; + } + if (metadata.TryGetValue(2, out object? nameObj) && nameObj != null && nameObj.GetType() == typeof(string)) + { + string name = nameObj.ToString() ?? string.Empty; + entity.CustomNameJson = name; + entity.CustomName = ChatParser.ParseText(name); + } + if (metadata.TryGetValue(3, out object? nameVisableObj) && nameVisableObj != null && nameVisableObj.GetType() == typeof(bool)) + { + entity.IsCustomNameVisible = bool.Parse(nameVisableObj.ToString() ?? string.Empty); + } + DispatchBotEvent(bot => bot.OnEntityMetadata(entity, metadata)); + } + } + + /// + /// Called when tradeList is recieved after interacting with villager + /// + /// Window ID + /// List of trades. + /// Contains Level, Experience, IsRegularVillager and CanRestock . + public void OnTradeList(int windowID, List trades, VillagerInfo villagerInfo) + { + DispatchBotEvent(bot => bot.OnTradeList(windowID, trades, villagerInfo)); + } + + /// + /// Will be called every player break block in gamemode 0 + /// + /// Player ID + /// Block location + /// Destroy stage, maximum 255 + public void OnBlockBreakAnimation(int entityId, Location location, byte stage) + { + if (entities.ContainsKey(entityId)) + { + Entity entity = entities[entityId]; + DispatchBotEvent(bot => bot.OnBlockBreakAnimation(entity, location, stage)); + } + } + + /// + /// Will be called every animations of the hit and place block + /// + /// Player ID + /// 0 = LMB, 1 = RMB (RMB Corrent not work) + public void OnEntityAnimation(int entityID, byte animation) + { + if (entities.ContainsKey(entityID)) + { + Entity entity = entities[entityID]; + DispatchBotEvent(bot => bot.OnEntityAnimation(entity, animation)); + } + } + + /// + /// Will be called when a Synchronization sequence is recevied, this sequence need to be sent when breaking or placing blocks + /// + /// Sequence ID + public void OnBlockChangeAck(int sequenceId) + { + this.sequenceId = sequenceId; + } + + /// + /// This method is called when the protocol handler receives server data + /// + /// Indicates if the server has a motd message + /// Server MOTD message + /// Indicates if the server has a an icon + /// Server icon in Base 64 format + /// Indicates if the server previews chat + public void OnServerDataRecived(bool hasMotd, string motd, bool hasIcon, string iconBase64, bool previewsChat) + { + isSupportPreviewsChat = previewsChat; + } + + /// + /// This method is called when the protocol handler receives "Set Display Chat Preview" packet + /// + /// Indicates if the server previews chat + public void OnChatPreviewSettingUpdate(bool previewsChat) + { + isSupportPreviewsChat = previewsChat; + } + + /// + /// This method is called when the protocol handler receives "Login Success" packet + /// + /// The player's UUID received from the server + /// The player's username received from the server + /// Tuple + public void OnLoginSuccess(Guid uuid, string userName, Tuple[]? playerProperty) + { + //string UUID = uuid.ToString().Replace("-", String.Empty); + //Log.Info("now UUID = " + this.uuid); + //Log.Info("new UUID = " + UUID); + ////handler.SetUserUUID(UUID); + + } + + /// + /// Used for a wide variety of game events, from weather to bed use to gamemode to demo messages. + /// + /// Event type + /// Depends on Reason + public void OnGameEvent(byte reason, float value) + { + switch (reason) + { + case 7: + DispatchBotEvent(bot => bot.OnRainLevelChange(value)); + break; + case 8: + DispatchBotEvent(bot => bot.OnThunderLevelChange(value)); + break; + } + } + + /// + /// Called when a block is changed. + /// + /// The location of the block. + /// The block + public void OnBlockChange(Location location, Block block) + { + world.SetBlock(location, block); + DispatchBotEvent(bot => bot.OnBlockChange(location, block)); + } + + /// + /// Called when "AutoComplete" completes. + /// + /// The number of this result. + /// All commands. + public void OnAutoCompleteDone(int transactionId, string[] result) + { + ConsoleIO.OnAutoCompleteDone(transactionId, result); + } + + public void SetCanSendMessage(bool canSendMessage) + { + CanSendMessage = canSendMessage; + Log.Debug("CanSendMessage = " + canSendMessage); + } + + /// + /// Send a click container button packet to the server. + /// Used for Enchanting table, Lectern, stone cutter and loom + /// + /// Id of the window being clicked + /// Id of the clicked button + /// True if packet was successfully sent + + public bool ClickContainerButton(int windowId, int buttonId) + { + return handler.ClickContainerButton(windowId, buttonId); + } + + #endregion + } +} diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 3072446e..a3919751 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -1,878 +1,878 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MinecraftClient.Inventory.ItemPalettes; -using MinecraftClient.Mapping.BlockPalettes; -using MinecraftClient.Mapping.EntityPalettes; -using MinecraftClient.Protocol; -using MinecraftClient.Protocol.Handlers.Forge; -using MinecraftClient.Protocol.ProfileKey; -using MinecraftClient.Protocol.Session; -using MinecraftClient.Scripting; -using MinecraftClient.WinAPI; -using Tomlet; -using static MinecraftClient.Settings; -using static MinecraftClient.Settings.ConsoleConfigHealper.ConsoleConfig; -using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig; -using static MinecraftClient.Settings.MainConfigHealper.MainConfig.GeneralConfig; - -namespace MinecraftClient -{ - /// - /// Minecraft Console Client by the MCC Team (c) 2012-2022. - /// Allows to connect to any Minecraft server, send and receive text, automated scripts. - /// This source code is released under the CDDL 1.0 License. - /// - /// - /// Typical steps to update MCC for a new Minecraft version - /// - Implement protocol changes (see Protocol18.cs) - /// - Handle new block types and states (see Material.cs) - /// - Add support for new entity types (see EntityType.cs) - /// - Add new item types for inventories (see ItemType.cs) - /// - Mark new version as handled (see ProtocolHandler.cs) - /// - Update MCHighestVersion field below (for versionning) - /// - static class Program - { - private static McClient? client; - public static string[]? startupargs; - public static CultureInfo ActualCulture = CultureInfo.CurrentCulture; - - public const string Version = MCHighestVersion; - public const string MCLowestVersion = "1.4.6"; - public const string MCHighestVersion = "1.20.1"; - public static readonly string? BuildInfo = null; - - private static Tuple? offlinePrompt = null; - private static bool useMcVersionOnce = false; - private static string settingsIniPath = "MinecraftClient.ini"; - - /// - /// The main entry point of Minecraft Console Client - /// - static void 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; - } - - // Fix issue #2119 - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - }); - - //Setup ConsoleIO - ConsoleIO.LogPrefix = "§8[MCC] "; - if (args.Length >= 1 && args[^1] == "BasicIO" || args.Length >= 1 && args[^1] == "BasicIO-NoColor") - { - if (args.Length >= 1 && args[^1] == "BasicIO-NoColor") - { - ConsoleIO.BasicIO_NoColor = true; - } - ConsoleIO.BasicIO = true; - args = args.Where(o => !Object.ReferenceEquals(o, args[^1])).ToArray(); - } - - if (!ConsoleIO.BasicIO) - ConsoleInteractive.ConsoleWriter.Init(); - - ConsoleIO.WriteLine($"Minecraft Console Client v{Version} - for MC {MCLowestVersion} to {MCHighestVersion} - Github.com/MCCTeam"); - - //Build information to facilitate processing of bug reports - if (BuildInfo != null) - ConsoleIO.WriteLineFormatted("§8" + BuildInfo); - - //Debug input ? - if (args.Length == 1 && args[0] == "--keyboard-debug") - { - ConsoleIO.WriteLine("Keyboard debug mode: Press any key to display info"); - ConsoleIO.DebugReadInput(); - } - - //Process ini configuration file - { - bool loadSucceed, needWriteDefaultSetting, newlyGenerated = false; - if (args.Length >= 1 && File.Exists(args[0]) && Settings.ToLowerIfNeed(Path.GetExtension(args[0])) == ".ini") - { - (loadSucceed, needWriteDefaultSetting) = Settings.LoadFromFile(args[0]); - settingsIniPath = args[0]; - - //remove ini configuration file from arguments array - List args_tmp = args.ToList(); - args_tmp.RemoveAt(0); - args = args_tmp.ToArray(); - } - else if (File.Exists("MinecraftClient.ini")) - { - (loadSucceed, needWriteDefaultSetting) = Settings.LoadFromFile("MinecraftClient.ini"); - } - else - { - loadSucceed = true; - needWriteDefaultSetting = true; - newlyGenerated = true; - } - - if (needWriteDefaultSetting) - { - Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage(); - WriteBackSettings(false); - if (newlyGenerated) - ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_settings_generated); - ConsoleIO.WriteLine(Translations.mcc_run_with_default_settings); - } - else if (!loadSucceed) - { - ConsoleInteractive.ConsoleReader.StopReadThread(); - string command = " "; - while (command.Length > 0) - { - 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; - } - } - return; - } - else - { - //Load external translation file. Should be called AFTER settings loaded - if (!Config.Main.Advanced.Language.StartsWith("en")) - ConsoleIO.WriteLine(string.Format(Translations.mcc_help_us_translate, Settings.TranslationProjectUrl)); - WriteBackSettings(true); // format - } - } - - //Other command-line arguments - if (args.Length >= 1) - { - if (args.Contains("--help")) - { - Console.WriteLine("Command-Line Help:"); - Console.WriteLine("MinecraftClient.exe "); - Console.WriteLine("MinecraftClient.exe \"/mycommand\""); - Console.WriteLine("MinecraftClient.exe --setting=value [--other settings]"); - Console.WriteLine("MinecraftClient.exe --section.setting=value [--other settings]"); - Console.WriteLine("MinecraftClient.exe [--other settings]"); - return; - } - - if (args.Contains("--upgrade")) - { - UpgradeHelper.HandleBlockingUpdate(forceUpgrade: false); - return; - } - - if (args.Contains("--force-upgrade")) - { - UpgradeHelper.HandleBlockingUpdate(forceUpgrade: true); - return; - } - - if (args.Contains("--generate")) - { - string dataGenerator = ""; - string dataPath = ""; - - foreach (string argument in args) - { - if (argument.StartsWith("--") && !argument.Contains("--generate")) - { - if (!argument.Contains('=')) - throw new ArgumentException(string.Format(Translations.error_setting_argument_syntax, argument)); - - string[] argParts = argument[2..].Split('='); - string argName = argParts[0].Trim(); - string argValue = argParts[1].Replace("\"", "").Trim(); - - if (argName == "data-path") - { - Console.WriteLine(dataPath); - dataPath = argValue; - } - - if (argName == "data-generator") - { - dataGenerator = argValue; - } - } - } - - if (string.IsNullOrEmpty(dataGenerator) || !(Settings.ToLowerIfNeed(dataGenerator).Equals("entity") || Settings.ToLowerIfNeed(dataGenerator).Equals("item") || Settings.ToLowerIfNeed(dataGenerator).Equals("block"))) - { - Console.WriteLine(Translations.error_generator_invalid); - Console.WriteLine(Translations.error_usage + " MinecraftClient.exe --data-generator= --data-path=\"\""); - return; - } - - 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; - } - - if (!File.Exists(dataPath)) - { - Console.WriteLine(string.Format(Translations.error_generator_path, dataPath)); - return; - } - - if (!dataPath.EndsWith(".json")) - { - Console.WriteLine(string.Format(Translations.error_generator_json, dataPath)); - return; - } - - Console.WriteLine(string.Format(Translations.mcc_generator_generating, dataGenerator, dataPath)); - - switch (dataGenerator) - { - case "entity": - EntityPaletteGenerator.GenerateEntityTypes(dataPath); - break; - - case "item": - ItemPaletteGenerator.GenerateItemType(dataPath); - break; - - case "block": - BlockPaletteGenerator.GenerateBlockPalette(dataPath); - break; - } - - Console.WriteLine(string.Format(Translations.mcc_generator_done, dataGenerator, dataPath)); - return; - } - } - - if (OperatingSystem.IsWindows() && !string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle)) - { - InternalConfig.Username = "New Window"; - Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle); - } - - // Check for updates - UpgradeHelper.CheckUpdate(); - - // Load command-line arguments - if (args.Length >= 1) - { - try - { - Settings.LoadArguments(args); - } - catch (ArgumentException e) - { - InternalConfig.InteractiveMode = false; - HandleFailure(e.Message); - return; - } - } - - //Test line to troubleshoot invisible colors - if (Config.Logging.DebugMessages) - { - ConsoleIO.WriteLineFormatted(string.Format(Translations.debug_color_test, "[0123456789ABCDEF]: (4bit)[§00§11§22§33§44§55§66§77§88§99§aA§bB§cC§dD§eE§fF§r]")); - Random random = new(); - { // Test 8 bit color - StringBuilder sb = new(); - sb.Append("[0123456789]: (vt100 8bit)["); - for (int i = 0; i < 10; ++i) - { - sb.Append(ColorHelper.GetColorEscapeCode((byte)random.Next(255), - (byte)random.Next(255), - (byte)random.Next(255), - true, - ConsoleColorModeType.vt100_8bit)).Append(i); - } - sb.Append(ColorHelper.GetResetEscapeCode()).Append(']'); - ConsoleIO.WriteLine(string.Format(Translations.debug_color_test, sb.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())); - } - } - - //Load cached sessions from disk if necessary - if (Config.Main.Advanced.SessionCache == CacheType.disk) - { - bool cacheLoaded = SessionCache.InitializeDiskCache(); - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted("§8" + (cacheLoaded ? Translations.debug_session_cache_ok : Translations.debug_session_cache_fail), acceptnewlines: true); - } - - // Setup exit cleaning code - ExitCleanUp.Add(() => { DoExit(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) - { - ConsoleIO.WriteLine(ConsoleIO.BasicIO ? Translations.mcc_login_basic_io : Translations.mcc_login); - InternalConfig.Account.Login = ConsoleIO.ReadLine().Trim(); - if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login)) - { - HandleFailure(Translations.error_login_blocked, false, ChatBot.DisconnectReason.LoginRejected); - return; - } - } - InternalConfig.Username = InternalConfig.Account.Login; - if (string.IsNullOrWhiteSpace(InternalConfig.Account.Password) && !useBrowser && - (Config.Main.Advanced.SessionCache == CacheType.none || !SessionCache.Contains(ToLowerIfNeed(InternalConfig.Account.Login)))) - { - RequestPassword(); - } - - startupargs = args; - InitializeClient(); - } - - /// - /// Reduest user to submit password. - /// - private static void RequestPassword() - { - ConsoleIO.WriteLine(ConsoleIO.BasicIO ? string.Format(Translations.mcc_password_basic_io, InternalConfig.Account.Login) + "\n" : Translations.mcc_password_hidden); - string? password = ConsoleIO.BasicIO ? Console.ReadLine() : ConsoleIO.ReadPassword(); - if (string.IsNullOrWhiteSpace(password)) - InternalConfig.Account.Password = "-"; - else - InternalConfig.Account.Password = password; - } - - /// - /// Start a new Client - /// - private static void InitializeClient() - { - InternalConfig.MinecraftVersion = Config.Main.Advanced.MinecraftVersion; - - SessionToken session = new(); - PlayerKeyPair? playerKeyPair = null; - - ProtocolHandler.LoginResult result = ProtocolHandler.LoginResult.LoginRequired; - - string loginLower = ToLowerIfNeed(InternalConfig.Account.Login); - if (InternalConfig.Account.Password == "-") - { - ConsoleIO.WriteLineFormatted("§8" + Translations.mcc_offline, acceptnewlines: true); - result = ProtocolHandler.LoginResult.Success; - session.PlayerID = "0"; - session.PlayerName = InternalConfig.Username; - } - else - { - // Validate cached session or login new session. - if (Config.Main.Advanced.SessionCache != CacheType.none && SessionCache.Contains(loginLower)) - { - session = SessionCache.Get(loginLower); - result = ProtocolHandler.GetTokenValidation(session); - if (result != ProtocolHandler.LoginResult.Success) - { - ConsoleIO.WriteLineFormatted("§8" + Translations.mcc_session_invalid, acceptnewlines: true); - // Try to refresh access token - if (!string.IsNullOrWhiteSpace(session.RefreshToken)) - { - try - { - result = ProtocolHandler.MicrosoftLoginRefresh(session.RefreshToken, out session); - } - catch (Exception ex) - { - ConsoleIO.WriteLine("Refresh access token fail: " + ex.Message); - result = ProtocolHandler.LoginResult.InvalidResponse; - } - } - - if (result != ProtocolHandler.LoginResult.Success - && string.IsNullOrWhiteSpace(InternalConfig.Account.Password) - && !(Config.Main.General.AccountType == LoginType.microsoft && Config.Main.General.Method == LoginMethod.browser)) - RequestPassword(); - } - else ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_session_valid, session.PlayerName)); - } - - if (result != ProtocolHandler.LoginResult.Success) - { - ConsoleIO.WriteLine(string.Format(Translations.mcc_connecting, Config.Main.General.AccountType == LoginType.mojang ? "Minecraft.net" : "Microsoft")); - result = ProtocolHandler.GetLogin(InternalConfig.Account.Login, InternalConfig.Account.Password, Config.Main.General.AccountType, out session); - } - - if (result == ProtocolHandler.LoginResult.Success && Config.Main.Advanced.SessionCache != CacheType.none) - SessionCache.Store(loginLower, session); - - if (result == ProtocolHandler.LoginResult.Success) - session.SessionPreCheckTask = Task.Factory.StartNew(() => session.SessionPreCheck()); - } - - if (result == ProtocolHandler.LoginResult.Success) - { - InternalConfig.Username = session.PlayerName; - bool isRealms = false; - - if (OperatingSystem.IsWindows() && !string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle)) - Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle); - - if (Config.Main.Advanced.PlayerHeadAsIcon && OperatingSystem.IsWindows()) - ConsoleIcon.SetPlayerIconAsync(InternalConfig.Username); - - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLine(string.Format(Translations.debug_session_id, session.ID)); - - List availableWorlds = new(); - if (Config.Main.Advanced.MinecraftRealms && !String.IsNullOrEmpty(session.ID)) - availableWorlds = ProtocolHandler.RealmsListWorlds(InternalConfig.Username, session.PlayerID, session.ID); - - if (InternalConfig.ServerIP == string.Empty) - { - ConsoleIO.WriteLine(Translations.mcc_ip); - string addressInput = ConsoleIO.ReadLine(); - if (addressInput.StartsWith("realms:")) - { - if (Config.Main.Advanced.MinecraftRealms) - { - if (availableWorlds.Count == 0) - { - HandleFailure(Translations.error_realms_access_denied, false, ChatBot.DisconnectReason.LoginRejected); - return; - } - string worldId = addressInput.Split(':')[1]; - if (!availableWorlds.Contains(worldId) && int.TryParse(worldId, NumberStyles.Any, CultureInfo.CurrentCulture, out int worldIndex) && worldIndex < availableWorlds.Count) - worldId = availableWorlds[worldIndex]; - if (availableWorlds.Contains(worldId)) - { - string RealmsAddress = ProtocolHandler.GetRealmsWorldServerAddress(worldId, InternalConfig.Username, session.PlayerID, session.ID); - if (RealmsAddress != "") - { - addressInput = RealmsAddress; - isRealms = true; - InternalConfig.MinecraftVersion = MCHighestVersion; - } - else - { - HandleFailure(Translations.error_realms_server_unavailable, false, ChatBot.DisconnectReason.LoginRejected); - return; - } - } - else - { - HandleFailure(Translations.error_realms_server_id, false, ChatBot.DisconnectReason.LoginRejected); - return; - } - } - else - { - HandleFailure(Translations.error_realms_disabled, false, null); - return; - } - } - Config.Main.SetServerIP(new MainConfigHealper.MainConfig.ServerInfoConfig(addressInput), true); - } - - //Get server version - int protocolversion = 0; - ForgeInfo? forgeInfo = null; - - if (InternalConfig.MinecraftVersion != "" && Settings.ToLowerIfNeed(InternalConfig.MinecraftVersion) != "auto") - { - protocolversion = Protocol.ProtocolHandler.MCVer2ProtocolVersion(InternalConfig.MinecraftVersion); - - if (protocolversion != 0) - ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_version, InternalConfig.MinecraftVersion, protocolversion)); - else - ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_unknown_version, InternalConfig.MinecraftVersion)); - - if (useMcVersionOnce) - { - useMcVersionOnce = false; - InternalConfig.MinecraftVersion = ""; - } - } - - //Retrieve server info if version is not manually set OR if need to retrieve Forge information - if (!isRealms && (protocolversion == 0 || (Config.Main.Advanced.EnableForge == ForgeConfigType.auto) || - ((Config.Main.Advanced.EnableForge == ForgeConfigType.force) && !ProtocolHandler.ProtocolMayForceForge(protocolversion)))) - { - if (protocolversion != 0) - ConsoleIO.WriteLine(Translations.mcc_forge); - else - ConsoleIO.WriteLine(Translations.mcc_retrieve); - if (!ProtocolHandler.GetServerInfo(InternalConfig.ServerIP, InternalConfig.ServerPort, ref protocolversion, ref forgeInfo)) - { - HandleFailure(Translations.error_ping, true, ChatBots.AutoRelog.DisconnectReason.ConnectionLost); - return; - } - } - - 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 */) - { - // Load cached profile key from disk if necessary - if (Config.Main.Advanced.ProfileKeyCache == CacheType.disk) - { - bool cacheKeyLoaded = KeysCache.InitializeDiskCache(); - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted("§8" + (cacheKeyLoaded ? Translations.debug_keys_cache_ok : Translations.debug_keys_cache_fail), acceptnewlines: true); - } - - if (Config.Main.Advanced.ProfileKeyCache != CacheType.none && KeysCache.Contains(loginLower)) - { - playerKeyPair = KeysCache.Get(loginLower); - if (playerKeyPair.NeedRefresh()) - ConsoleIO.WriteLineFormatted("§8" + Translations.mcc_profile_key_invalid, acceptnewlines: true); - else - ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_profile_key_valid, session.PlayerName)); - } - - if (playerKeyPair == 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); - } - } - } - - //Force-enable Forge support? - if (!isRealms && (Config.Main.Advanced.EnableForge == ForgeConfigType.force) && forgeInfo == null) - { - if (ProtocolHandler.ProtocolMayForceForge(protocolversion)) - { - ConsoleIO.WriteLine(Translations.mcc_forgeforce); - forgeInfo = ProtocolHandler.ProtocolForceForge(protocolversion); - } - else - { - HandleFailure(Translations.error_forgeforce, true, ChatBots.AutoRelog.DisconnectReason.ConnectionLost); - return; - } - } - - //Proceed to server login - if (protocolversion != 0) - { - try - { - //Start the main TCP client - client = new McClient(session, playerKeyPair, InternalConfig.ServerIP, InternalConfig.ServerPort, protocolversion, forgeInfo); - - //Update console title - if (OperatingSystem.IsWindows() && !string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle)) - Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle); - } - catch (NotSupportedException) - { - HandleFailure(Translations.error_unsupported, true); - } - catch (NotImplementedException) - { - throw; - } - catch (Exception e) - { - ConsoleIO.WriteLine(e.Message); - ConsoleIO.WriteLine(e.StackTrace ?? ""); - HandleFailure(); // Other error - } - } - else HandleFailure(Translations.error_determine, true); - } - else - { - string failureMessage = Translations.error_login; - string failureReason = string.Empty; - failureReason = result switch - { -#pragma warning disable format // @formatter:off - ProtocolHandler.LoginResult.AccountMigrated => Translations.error_login_migrated, - ProtocolHandler.LoginResult.ServiceUnavailable => Translations.error_login_server, - ProtocolHandler.LoginResult.WrongPassword => Translations.error_login_blocked, - ProtocolHandler.LoginResult.InvalidResponse => Translations.error_login_response, - ProtocolHandler.LoginResult.NotPremium => Translations.error_login_premium, - ProtocolHandler.LoginResult.OtherError => Translations.error_login_network, - ProtocolHandler.LoginResult.SSLError => Translations.error_login_ssl, - ProtocolHandler.LoginResult.UserCancel => Translations.error_login_cancel, - _ => Translations.error_login_unknown, -#pragma warning restore format // @formatter:on - }; - failureMessage += failureReason; - HandleFailure(failureMessage, false, ChatBot.DisconnectReason.LoginRejected); - } - } - - /// - /// Reloads settings - /// - public static void ReloadSettings(bool keepAccountAndServerSettings = false) - { - if (Settings.LoadFromFile(settingsIniPath, keepAccountAndServerSettings).Item1) - ConsoleIO.WriteLine(string.Format(Translations.config_load, settingsIniPath)); - } - - /// - /// Write-back settings - /// - public static void WriteBackSettings(bool enableBackup = true) - { - Settings.WriteToFile(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) - { - 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(); - } - - public static void DoExit(int exitcode = 0) - { - WriteBackSettings(true); - ConsoleInteractive.ConsoleSuggestion.ClearSuggestions(); - 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 (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(); - } - - /// - /// Handle fatal errors such as ping failure, login failure, server disconnection, and so on. - /// Allows AutoRelog to perform on fatal errors, prompt for server version, and offline commands. - /// - /// 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) - { - if (!String.IsNullOrEmpty(errorMessage)) - { - ConsoleIO.Reset(); - while (Console.KeyAvailable) - Console.ReadKey(true); - ConsoleIO.WriteLine(errorMessage); - - if (disconnectReason.HasValue) - { - if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage)) - return; //AutoRelog is triggering a restart of the client - } - } - - if (InternalConfig.InteractiveMode) - { - if (versionError) - { - ConsoleIO.WriteLine(Translations.mcc_server_version); - InternalConfig.MinecraftVersion = ConsoleInteractive.ConsoleReader.RequestImmediateInput(); - if (InternalConfig.MinecraftVersion != "") - { - useMcVersionOnce = true; - Restart(); - return; - } - } - - if (offlinePrompt == null) - { - ConsoleInteractive.ConsoleReader.StopReadThread(); - - var cancellationTokenSource = new CancellationTokenSource(); - offlinePrompt = new(new Thread(new ThreadStart(delegate - { - bool exitThread = false; - string command = " "; - ConsoleIO.WriteLine(string.Empty); - ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_disconnected, Config.Main.Advanced.InternalCmdChar.ToLogString())); - ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true); - - while (!cancellationTokenSource.IsCancellationRequested) - { - if (exitThread) - return; - - while (command.Length > 0) - { - 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)); - } - } - - if (exitThread) - return; - } - })), cancellationTokenSource); - offlinePrompt.Item1.Start(); - } - } - else - { - // Not in interactive mode, just exit and let the calling script handle the failure - if (disconnectReason.HasValue) - { - // Return distinct exit codes for known failures. - if (disconnectReason.Value == ChatBot.DisconnectReason.UserLogout) Exit(1); - if (disconnectReason.Value == ChatBot.DisconnectReason.InGameKick) Exit(2); - if (disconnectReason.Value == ChatBot.DisconnectReason.ConnectionLost) Exit(3); - if (disconnectReason.Value == ChatBot.DisconnectReason.LoginRejected) Exit(4); - } - Exit(); - } - - } - - /// - /// Enumerate types in namespace through reflection - /// - /// Namespace to process - /// Assembly to use. Default is Assembly.GetExecutingAssembly() - /// - 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(); - } - - /// - /// Static initialization of build information, read from assembly information - /// - static Program() - { - if (typeof(Program) - .Assembly - .GetCustomAttributes(typeof(AssemblyConfigurationAttribute), false) - .FirstOrDefault() is AssemblyConfigurationAttribute attribute) - BuildInfo = attribute.Configuration; - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MinecraftClient.Inventory.ItemPalettes; +using MinecraftClient.Mapping.BlockPalettes; +using MinecraftClient.Mapping.EntityPalettes; +using MinecraftClient.Protocol; +using MinecraftClient.Protocol.Handlers.Forge; +using MinecraftClient.Protocol.ProfileKey; +using MinecraftClient.Protocol.Session; +using MinecraftClient.Scripting; +using MinecraftClient.WinAPI; +using Tomlet; +using static MinecraftClient.Settings; +using static MinecraftClient.Settings.ConsoleConfigHealper.ConsoleConfig; +using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig; +using static MinecraftClient.Settings.MainConfigHealper.MainConfig.GeneralConfig; + +namespace MinecraftClient +{ + /// + /// Minecraft Console Client by the MCC Team (c) 2012-2022. + /// Allows to connect to any Minecraft server, send and receive text, automated scripts. + /// This source code is released under the CDDL 1.0 License. + /// + /// + /// Typical steps to update MCC for a new Minecraft version + /// - Implement protocol changes (see Protocol18.cs) + /// - Handle new block types and states (see Material.cs) + /// - Add support for new entity types (see EntityType.cs) + /// - Add new item types for inventories (see ItemType.cs) + /// - Mark new version as handled (see ProtocolHandler.cs) + /// - Update MCHighestVersion field below (for versionning) + /// + static class Program + { + private static McClient? client; + public static string[]? startupargs; + public static CultureInfo ActualCulture = CultureInfo.CurrentCulture; + + public const string Version = MCHighestVersion; + public const string MCLowestVersion = "1.4.6"; + public const string MCHighestVersion = "1.20.1"; + public static readonly string? BuildInfo = null; + + private static Tuple? offlinePrompt = null; + private static bool useMcVersionOnce = false; + private static string settingsIniPath = "MinecraftClient.ini"; + + /// + /// The main entry point of Minecraft Console Client + /// + static void 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; + } + + // Fix issue #2119 + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + }); + + //Setup ConsoleIO + ConsoleIO.LogPrefix = "§8[MCC] "; + if (args.Length >= 1 && args[^1] == "BasicIO" || args.Length >= 1 && args[^1] == "BasicIO-NoColor") + { + if (args.Length >= 1 && args[^1] == "BasicIO-NoColor") + { + ConsoleIO.BasicIO_NoColor = true; + } + ConsoleIO.BasicIO = true; + args = args.Where(o => !Object.ReferenceEquals(o, args[^1])).ToArray(); + } + + if (!ConsoleIO.BasicIO) + ConsoleInteractive.ConsoleWriter.Init(); + + ConsoleIO.WriteLine($"Minecraft Console Client v{Version} - for MC {MCLowestVersion} to {MCHighestVersion} - Github.com/MCCTeam"); + + //Build information to facilitate processing of bug reports + if (BuildInfo != null) + ConsoleIO.WriteLineFormatted("§8" + BuildInfo); + + //Debug input ? + if (args.Length == 1 && args[0] == "--keyboard-debug") + { + ConsoleIO.WriteLine("Keyboard debug mode: Press any key to display info"); + ConsoleIO.DebugReadInput(); + } + + //Process ini configuration file + { + bool loadSucceed, needWriteDefaultSetting, newlyGenerated = false; + if (args.Length >= 1 && File.Exists(args[0]) && Settings.ToLowerIfNeed(Path.GetExtension(args[0])) == ".ini") + { + (loadSucceed, needWriteDefaultSetting) = Settings.LoadFromFile(args[0]); + settingsIniPath = args[0]; + + //remove ini configuration file from arguments array + List args_tmp = args.ToList(); + args_tmp.RemoveAt(0); + args = args_tmp.ToArray(); + } + else if (File.Exists("MinecraftClient.ini")) + { + (loadSucceed, needWriteDefaultSetting) = Settings.LoadFromFile("MinecraftClient.ini"); + } + else + { + loadSucceed = true; + needWriteDefaultSetting = true; + newlyGenerated = true; + } + + if (needWriteDefaultSetting) + { + Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage(); + WriteBackSettings(false); + if (newlyGenerated) + ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_settings_generated); + ConsoleIO.WriteLine(Translations.mcc_run_with_default_settings); + } + else if (!loadSucceed) + { + ConsoleInteractive.ConsoleReader.StopReadThread(); + string command = " "; + while (command.Length > 0) + { + 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; + } + } + return; + } + else + { + //Load external translation file. Should be called AFTER settings loaded + if (!Config.Main.Advanced.Language.StartsWith("en")) + ConsoleIO.WriteLine(string.Format(Translations.mcc_help_us_translate, Settings.TranslationProjectUrl)); + WriteBackSettings(true); // format + } + } + + //Other command-line arguments + if (args.Length >= 1) + { + if (args.Contains("--help")) + { + Console.WriteLine("Command-Line Help:"); + Console.WriteLine("MinecraftClient.exe "); + Console.WriteLine("MinecraftClient.exe \"/mycommand\""); + Console.WriteLine("MinecraftClient.exe --setting=value [--other settings]"); + Console.WriteLine("MinecraftClient.exe --section.setting=value [--other settings]"); + Console.WriteLine("MinecraftClient.exe [--other settings]"); + return; + } + + if (args.Contains("--upgrade")) + { + UpgradeHelper.HandleBlockingUpdate(forceUpgrade: false); + return; + } + + if (args.Contains("--force-upgrade")) + { + UpgradeHelper.HandleBlockingUpdate(forceUpgrade: true); + return; + } + + if (args.Contains("--generate")) + { + string dataGenerator = ""; + string dataPath = ""; + + foreach (string argument in args) + { + if (argument.StartsWith("--") && !argument.Contains("--generate")) + { + if (!argument.Contains('=')) + throw new ArgumentException(string.Format(Translations.error_setting_argument_syntax, argument)); + + string[] argParts = argument[2..].Split('='); + string argName = argParts[0].Trim(); + string argValue = argParts[1].Replace("\"", "").Trim(); + + if (argName == "data-path") + { + Console.WriteLine(dataPath); + dataPath = argValue; + } + + if (argName == "data-generator") + { + dataGenerator = argValue; + } + } + } + + if (string.IsNullOrEmpty(dataGenerator) || !(Settings.ToLowerIfNeed(dataGenerator).Equals("entity") || Settings.ToLowerIfNeed(dataGenerator).Equals("item") || Settings.ToLowerIfNeed(dataGenerator).Equals("block"))) + { + Console.WriteLine(Translations.error_generator_invalid); + Console.WriteLine(Translations.error_usage + " MinecraftClient.exe --data-generator= --data-path=\"\""); + return; + } + + 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; + } + + if (!File.Exists(dataPath)) + { + Console.WriteLine(string.Format(Translations.error_generator_path, dataPath)); + return; + } + + if (!dataPath.EndsWith(".json")) + { + Console.WriteLine(string.Format(Translations.error_generator_json, dataPath)); + return; + } + + Console.WriteLine(string.Format(Translations.mcc_generator_generating, dataGenerator, dataPath)); + + switch (dataGenerator) + { + case "entity": + EntityPaletteGenerator.GenerateEntityTypes(dataPath); + break; + + case "item": + ItemPaletteGenerator.GenerateItemType(dataPath); + break; + + case "block": + BlockPaletteGenerator.GenerateBlockPalette(dataPath); + break; + } + + Console.WriteLine(string.Format(Translations.mcc_generator_done, dataGenerator, dataPath)); + return; + } + } + + if (OperatingSystem.IsWindows() && !string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle)) + { + InternalConfig.Username = "New Window"; + Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle); + } + + // Check for updates + UpgradeHelper.CheckUpdate(); + + // Load command-line arguments + if (args.Length >= 1) + { + try + { + Settings.LoadArguments(args); + } + catch (ArgumentException e) + { + InternalConfig.InteractiveMode = false; + HandleFailure(e.Message); + return; + } + } + + //Test line to troubleshoot invisible colors + if (Config.Logging.DebugMessages) + { + ConsoleIO.WriteLineFormatted(string.Format(Translations.debug_color_test, "[0123456789ABCDEF]: (4bit)[§00§11§22§33§44§55§66§77§88§99§aA§bB§cC§dD§eE§fF§r]")); + Random random = new(); + { // Test 8 bit color + StringBuilder sb = new(); + sb.Append("[0123456789]: (vt100 8bit)["); + for (int i = 0; i < 10; ++i) + { + sb.Append(ColorHelper.GetColorEscapeCode((byte)random.Next(255), + (byte)random.Next(255), + (byte)random.Next(255), + true, + ConsoleColorModeType.vt100_8bit)).Append(i); + } + sb.Append(ColorHelper.GetResetEscapeCode()).Append(']'); + ConsoleIO.WriteLine(string.Format(Translations.debug_color_test, sb.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())); + } + } + + //Load cached sessions from disk if necessary + if (Config.Main.Advanced.SessionCache == CacheType.disk) + { + bool cacheLoaded = SessionCache.InitializeDiskCache(); + if (Config.Logging.DebugMessages) + ConsoleIO.WriteLineFormatted("§8" + (cacheLoaded ? Translations.debug_session_cache_ok : Translations.debug_session_cache_fail), acceptnewlines: true); + } + + // Setup exit cleaning code + ExitCleanUp.Add(() => { DoExit(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) + { + ConsoleIO.WriteLine(ConsoleIO.BasicIO ? Translations.mcc_login_basic_io : Translations.mcc_login); + InternalConfig.Account.Login = ConsoleIO.ReadLine().Trim(); + if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login)) + { + HandleFailure(Translations.error_login_blocked, false, ChatBot.DisconnectReason.LoginRejected); + return; + } + } + InternalConfig.Username = InternalConfig.Account.Login; + if (string.IsNullOrWhiteSpace(InternalConfig.Account.Password) && !useBrowser && + (Config.Main.Advanced.SessionCache == CacheType.none || !SessionCache.Contains(ToLowerIfNeed(InternalConfig.Account.Login)))) + { + RequestPassword(); + } + + startupargs = args; + InitializeClient(); + } + + /// + /// Reduest user to submit password. + /// + private static void RequestPassword() + { + ConsoleIO.WriteLine(ConsoleIO.BasicIO ? string.Format(Translations.mcc_password_basic_io, InternalConfig.Account.Login) + "\n" : Translations.mcc_password_hidden); + string? password = ConsoleIO.BasicIO ? Console.ReadLine() : ConsoleIO.ReadPassword(); + if (string.IsNullOrWhiteSpace(password)) + InternalConfig.Account.Password = "-"; + else + InternalConfig.Account.Password = password; + } + + /// + /// Start a new Client + /// + private static void InitializeClient() + { + InternalConfig.MinecraftVersion = Config.Main.Advanced.MinecraftVersion; + + SessionToken session = new(); + PlayerKeyPair? playerKeyPair = null; + + ProtocolHandler.LoginResult result = ProtocolHandler.LoginResult.LoginRequired; + + string loginLower = ToLowerIfNeed(InternalConfig.Account.Login); + if (InternalConfig.Account.Password == "-") + { + ConsoleIO.WriteLineFormatted("§8" + Translations.mcc_offline, acceptnewlines: true); + result = ProtocolHandler.LoginResult.Success; + session.PlayerID = "0"; + session.PlayerName = InternalConfig.Username; + } + else + { + // Validate cached session or login new session. + if (Config.Main.Advanced.SessionCache != CacheType.none && SessionCache.Contains(loginLower)) + { + session = SessionCache.Get(loginLower); + result = ProtocolHandler.GetTokenValidation(session); + if (result != ProtocolHandler.LoginResult.Success) + { + ConsoleIO.WriteLineFormatted("§8" + Translations.mcc_session_invalid, acceptnewlines: true); + // Try to refresh access token + if (!string.IsNullOrWhiteSpace(session.RefreshToken)) + { + try + { + result = ProtocolHandler.MicrosoftLoginRefresh(session.RefreshToken, out session); + } + catch (Exception ex) + { + ConsoleIO.WriteLine("Refresh access token fail: " + ex.Message); + result = ProtocolHandler.LoginResult.InvalidResponse; + } + } + + if (result != ProtocolHandler.LoginResult.Success + && string.IsNullOrWhiteSpace(InternalConfig.Account.Password) + && !(Config.Main.General.AccountType == LoginType.microsoft && Config.Main.General.Method == LoginMethod.browser)) + RequestPassword(); + } + else ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_session_valid, session.PlayerName)); + } + + if (result != ProtocolHandler.LoginResult.Success) + { + ConsoleIO.WriteLine(string.Format(Translations.mcc_connecting, Config.Main.General.AccountType == LoginType.mojang ? "Minecraft.net" : "Microsoft")); + result = ProtocolHandler.GetLogin(InternalConfig.Account.Login, InternalConfig.Account.Password, Config.Main.General.AccountType, out session); + } + + if (result == ProtocolHandler.LoginResult.Success && Config.Main.Advanced.SessionCache != CacheType.none) + SessionCache.Store(loginLower, session); + + if (result == ProtocolHandler.LoginResult.Success) + session.SessionPreCheckTask = Task.Factory.StartNew(() => session.SessionPreCheck()); + } + + if (result == ProtocolHandler.LoginResult.Success) + { + InternalConfig.Username = session.PlayerName; + bool isRealms = false; + + if (OperatingSystem.IsWindows() && !string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle)) + Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle); + + if (Config.Main.Advanced.PlayerHeadAsIcon && OperatingSystem.IsWindows()) + ConsoleIcon.SetPlayerIconAsync(InternalConfig.Username); + + if (Config.Logging.DebugMessages) + ConsoleIO.WriteLine(string.Format(Translations.debug_session_id, session.ID)); + + List availableWorlds = new(); + if (Config.Main.Advanced.MinecraftRealms && !String.IsNullOrEmpty(session.ID)) + availableWorlds = ProtocolHandler.RealmsListWorlds(InternalConfig.Username, session.PlayerID, session.ID); + + if (InternalConfig.ServerIP == string.Empty) + { + ConsoleIO.WriteLine(Translations.mcc_ip); + string addressInput = ConsoleIO.ReadLine(); + if (addressInput.StartsWith("realms:")) + { + if (Config.Main.Advanced.MinecraftRealms) + { + if (availableWorlds.Count == 0) + { + HandleFailure(Translations.error_realms_access_denied, false, ChatBot.DisconnectReason.LoginRejected); + return; + } + string worldId = addressInput.Split(':')[1]; + if (!availableWorlds.Contains(worldId) && int.TryParse(worldId, NumberStyles.Any, CultureInfo.CurrentCulture, out int worldIndex) && worldIndex < availableWorlds.Count) + worldId = availableWorlds[worldIndex]; + if (availableWorlds.Contains(worldId)) + { + string RealmsAddress = ProtocolHandler.GetRealmsWorldServerAddress(worldId, InternalConfig.Username, session.PlayerID, session.ID); + if (RealmsAddress != "") + { + addressInput = RealmsAddress; + isRealms = true; + InternalConfig.MinecraftVersion = MCHighestVersion; + } + else + { + HandleFailure(Translations.error_realms_server_unavailable, false, ChatBot.DisconnectReason.LoginRejected); + return; + } + } + else + { + HandleFailure(Translations.error_realms_server_id, false, ChatBot.DisconnectReason.LoginRejected); + return; + } + } + else + { + HandleFailure(Translations.error_realms_disabled, false, null); + return; + } + } + Config.Main.SetServerIP(new MainConfigHealper.MainConfig.ServerInfoConfig(addressInput), true); + } + + //Get server version + int protocolversion = 0; + ForgeInfo? forgeInfo = null; + + if (InternalConfig.MinecraftVersion != "" && Settings.ToLowerIfNeed(InternalConfig.MinecraftVersion) != "auto") + { + protocolversion = Protocol.ProtocolHandler.MCVer2ProtocolVersion(InternalConfig.MinecraftVersion); + + if (protocolversion != 0) + ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_version, InternalConfig.MinecraftVersion, protocolversion)); + else + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_unknown_version, InternalConfig.MinecraftVersion)); + + if (useMcVersionOnce) + { + useMcVersionOnce = false; + InternalConfig.MinecraftVersion = ""; + } + } + + //Retrieve server info if version is not manually set OR if need to retrieve Forge information + if (!isRealms && (protocolversion == 0 || (Config.Main.Advanced.EnableForge == ForgeConfigType.auto) || + ((Config.Main.Advanced.EnableForge == ForgeConfigType.force) && !ProtocolHandler.ProtocolMayForceForge(protocolversion)))) + { + if (protocolversion != 0) + ConsoleIO.WriteLine(Translations.mcc_forge); + else + ConsoleIO.WriteLine(Translations.mcc_retrieve); + if (!ProtocolHandler.GetServerInfo(InternalConfig.ServerIP, InternalConfig.ServerPort, ref protocolversion, ref forgeInfo)) + { + HandleFailure(Translations.error_ping, true, ChatBots.AutoRelog.DisconnectReason.ConnectionLost); + return; + } + } + + 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 */) + { + // Load cached profile key from disk if necessary + if (Config.Main.Advanced.ProfileKeyCache == CacheType.disk) + { + bool cacheKeyLoaded = KeysCache.InitializeDiskCache(); + if (Config.Logging.DebugMessages) + ConsoleIO.WriteLineFormatted("§8" + (cacheKeyLoaded ? Translations.debug_keys_cache_ok : Translations.debug_keys_cache_fail), acceptnewlines: true); + } + + if (Config.Main.Advanced.ProfileKeyCache != CacheType.none && KeysCache.Contains(loginLower)) + { + playerKeyPair = KeysCache.Get(loginLower); + if (playerKeyPair.NeedRefresh()) + ConsoleIO.WriteLineFormatted("§8" + Translations.mcc_profile_key_invalid, acceptnewlines: true); + else + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_profile_key_valid, session.PlayerName)); + } + + if (playerKeyPair == 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); + } + } + } + + //Force-enable Forge support? + if (!isRealms && (Config.Main.Advanced.EnableForge == ForgeConfigType.force) && forgeInfo == null) + { + if (ProtocolHandler.ProtocolMayForceForge(protocolversion)) + { + ConsoleIO.WriteLine(Translations.mcc_forgeforce); + forgeInfo = ProtocolHandler.ProtocolForceForge(protocolversion); + } + else + { + HandleFailure(Translations.error_forgeforce, true, ChatBots.AutoRelog.DisconnectReason.ConnectionLost); + return; + } + } + + //Proceed to server login + if (protocolversion != 0) + { + try + { + //Start the main TCP client + client = new McClient(session, playerKeyPair, InternalConfig.ServerIP, InternalConfig.ServerPort, protocolversion, forgeInfo); + + //Update console title + if (OperatingSystem.IsWindows() && !string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle)) + Console.Title = Config.AppVar.ExpandVars(Config.Main.Advanced.ConsoleTitle); + } + catch (NotSupportedException) + { + HandleFailure(Translations.error_unsupported, true); + } + catch (NotImplementedException) + { + throw; + } + catch (Exception e) + { + ConsoleIO.WriteLine(e.Message); + ConsoleIO.WriteLine(e.StackTrace ?? ""); + HandleFailure(); // Other error + } + } + else HandleFailure(Translations.error_determine, true); + } + else + { + string failureMessage = Translations.error_login; + string failureReason = string.Empty; + failureReason = result switch + { +#pragma warning disable format // @formatter:off + ProtocolHandler.LoginResult.AccountMigrated => Translations.error_login_migrated, + ProtocolHandler.LoginResult.ServiceUnavailable => Translations.error_login_server, + ProtocolHandler.LoginResult.WrongPassword => Translations.error_login_blocked, + ProtocolHandler.LoginResult.InvalidResponse => Translations.error_login_response, + ProtocolHandler.LoginResult.NotPremium => Translations.error_login_premium, + ProtocolHandler.LoginResult.OtherError => Translations.error_login_network, + ProtocolHandler.LoginResult.SSLError => Translations.error_login_ssl, + ProtocolHandler.LoginResult.UserCancel => Translations.error_login_cancel, + _ => Translations.error_login_unknown, +#pragma warning restore format // @formatter:on + }; + failureMessage += failureReason; + HandleFailure(failureMessage, false, ChatBot.DisconnectReason.LoginRejected); + } + } + + /// + /// Reloads settings + /// + public static void ReloadSettings(bool keepAccountAndServerSettings = false) + { + if (Settings.LoadFromFile(settingsIniPath, keepAccountAndServerSettings).Item1) + ConsoleIO.WriteLine(string.Format(Translations.config_load, settingsIniPath)); + } + + /// + /// Write-back settings + /// + public static void WriteBackSettings(bool enableBackup = true) + { + Settings.WriteToFile(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) + { + 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(); + } + + public static void DoExit(int exitcode = 0) + { + WriteBackSettings(true); + ConsoleInteractive.ConsoleSuggestion.ClearSuggestions(); + 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 (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(); + } + + /// + /// Handle fatal errors such as ping failure, login failure, server disconnection, and so on. + /// Allows AutoRelog to perform on fatal errors, prompt for server version, and offline commands. + /// + /// 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) + { + if (!String.IsNullOrEmpty(errorMessage)) + { + ConsoleIO.Reset(); + while (Console.KeyAvailable) + Console.ReadKey(true); + ConsoleIO.WriteLine(errorMessage); + + if (disconnectReason.HasValue) + { + if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage)) + return; //AutoRelog is triggering a restart of the client + } + } + + if (InternalConfig.InteractiveMode) + { + if (versionError) + { + ConsoleIO.WriteLine(Translations.mcc_server_version); + InternalConfig.MinecraftVersion = ConsoleInteractive.ConsoleReader.RequestImmediateInput(); + if (InternalConfig.MinecraftVersion != "") + { + useMcVersionOnce = true; + Restart(); + return; + } + } + + if (offlinePrompt == null) + { + ConsoleInteractive.ConsoleReader.StopReadThread(); + + var cancellationTokenSource = new CancellationTokenSource(); + offlinePrompt = new(new Thread(new ThreadStart(delegate + { + bool exitThread = false; + string command = " "; + ConsoleIO.WriteLine(string.Empty); + ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_disconnected, Config.Main.Advanced.InternalCmdChar.ToLogString())); + ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true); + + while (!cancellationTokenSource.IsCancellationRequested) + { + if (exitThread) + return; + + while (command.Length > 0) + { + 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)); + } + } + + if (exitThread) + return; + } + })), cancellationTokenSource); + offlinePrompt.Item1.Start(); + } + } + else + { + // Not in interactive mode, just exit and let the calling script handle the failure + if (disconnectReason.HasValue) + { + // Return distinct exit codes for known failures. + if (disconnectReason.Value == ChatBot.DisconnectReason.UserLogout) Exit(1); + if (disconnectReason.Value == ChatBot.DisconnectReason.InGameKick) Exit(2); + if (disconnectReason.Value == ChatBot.DisconnectReason.ConnectionLost) Exit(3); + if (disconnectReason.Value == ChatBot.DisconnectReason.LoginRejected) Exit(4); + } + Exit(); + } + + } + + /// + /// Enumerate types in namespace through reflection + /// + /// Namespace to process + /// Assembly to use. Default is Assembly.GetExecutingAssembly() + /// + 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(); + } + + /// + /// Static initialization of build information, read from assembly information + /// + static Program() + { + if (typeof(Program) + .Assembly + .GetCustomAttributes(typeof(AssemblyConfigurationAttribute), false) + .FirstOrDefault() is AssemblyConfigurationAttribute attribute) + BuildInfo = attribute.Configuration; + } + } +} diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index e2b12328..2c005a04 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -1,4202 +1,4202 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Net.Sockets; -using System.Security.Cryptography; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using MinecraftClient.Crypto; -using MinecraftClient.Inventory; -using MinecraftClient.Inventory.ItemPalettes; -using MinecraftClient.Logger; -using MinecraftClient.Mapping; -using MinecraftClient.Mapping.BlockPalettes; -using MinecraftClient.Mapping.EntityPalettes; -using MinecraftClient.Protocol.Handlers.Forge; -using MinecraftClient.Protocol.Handlers.packet.s2c; -using MinecraftClient.Protocol.Handlers.PacketPalettes; -using MinecraftClient.Protocol.Message; -using MinecraftClient.Protocol.ProfileKey; -using MinecraftClient.Protocol.Session; -using MinecraftClient.Proxy; -using MinecraftClient.Scripting; -using Newtonsoft.Json; -using static MinecraftClient.Settings; - -namespace MinecraftClient.Protocol.Handlers -{ - /// - /// Implementation for Minecraft 1.8.X+ Protocols - /// - /// - /// Typical update steps for implementing protocol changes for a new Minecraft version: - /// - Perform a diff between latest supported version in MCC and new stable version to support on https://wiki.vg/Protocol - /// - If there are any changes in packets implemented by MCC, add MCXXXVersion field below and implement new packet layouts - /// - 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 - { - internal const int MC_1_8_Version = 47; - internal const int MC_1_9_Version = 107; - internal const int MC_1_9_1_Version = 108; - internal const int MC_1_10_Version = 210; - internal const int MC_1_11_Version = 315; - internal const int MC_1_11_2_Version = 316; - internal const int MC_1_12_Version = 335; - internal const int MC_1_12_2_Version = 340; - internal const int MC_1_13_Version = 393; - internal const int MC_1_13_2_Version = 404; - internal const int MC_1_14_Version = 477; - internal const int MC_1_15_Version = 573; - internal const int MC_1_15_2_Version = 578; - internal const int MC_1_16_Version = 735; - internal const int MC_1_16_1_Version = 736; - internal const int MC_1_16_2_Version = 751; - internal const int MC_1_16_3_Version = 753; - internal const int MC_1_16_5_Version = 754; - internal const int MC_1_17_Version = 755; - internal const int MC_1_17_1_Version = 756; - internal const int MC_1_18_1_Version = 757; - internal const int MC_1_18_2_Version = 758; - internal const int MC_1_19_Version = 759; - internal const int MC_1_19_2_Version = 760; - internal const int MC_1_19_3_Version = 761; - internal const int MC_1_19_4_Version = 762; - internal const int MC_1_20_Version = 763; - - private int compression_treshold = 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 bool receiveDeclareCommands = false, receivePlayerInfo = false; - private object MessageSigningLock = new(); - private Guid chatUuid = Guid.NewGuid(); - private int pendingAcknowledgments = 0, messageIndex = 0; - private LastSeenMessagesCollector lastSeenMessagesCollector; - private LastSeenMessageList.AcknowledgedMessage? lastReceivedMessage = null; - readonly Protocol18Forge pForge; - readonly Protocol18Terrain pTerrain; - readonly IMinecraftComHandler handler; - readonly EntityPalette entityPalette; - readonly EntityMetadataPalette entityMetadataPalette; - readonly ItemPalette itemPalette; - 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) - { - ConsoleIO.SetAutoCompleteEngine(this); - ChatParser.InitTranslations(); - socketWrapper = new SocketWrapper(Client); - dataTypes = new DataTypes(protocolVersion); - this.protocolVersion = protocolVersion; - this.handler = handler; - pForge = new Protocol18Forge(forgeInfo, protocolVersion, dataTypes, this, handler); - pTerrain = new Protocol18Terrain(protocolVersion, dataTypes, handler); - packetPalette = new PacketTypeHandler(protocolVersion, forgeInfo != null).GetTypeHandler(); - log = handler.GetLogger(); - randomGen = RandomNumberGenerator.Create(); - lastSeenMessagesCollector = protocolVersion >= MC_1_19_3_Version ? new(20) : new(5); - - if (handler.GetTerrainEnabled() && protocolVersion > MC_1_20_Version) - { - log.Error("§c" + Translations.extra_terrainandmovement_disabled); - handler.SetTerrainEnabled(false); - } - - if (handler.GetInventoryEnabled() && - protocolVersion is < MC_1_9_Version or > MC_1_20_Version) - { - log.Error("§c" + Translations.extra_inventory_disabled); - handler.SetInventoryEnabled(false); - } - - if (handler.GetEntityHandlingEnabled() && - protocolVersion is < MC_1_8_Version or > MC_1_20_Version) - { - log.Error("§c" + Translations.extra_entity_disabled); - handler.SetEntityHandlingEnabled(false); - } - - Block.Palette = protocolVersion switch - { - // Block palette - > MC_1_20_Version when handler.GetTerrainEnabled() => - throw new NotImplementedException(Translations.exception_palette_block), - MC_1_20_Version => new Palette120(), - MC_1_19_4_Version => new Palette1194(), - MC_1_19_3_Version => new Palette1193(), - >= MC_1_19_Version => new Palette119(), - >= MC_1_17_Version => new Palette117(), - >= MC_1_16_Version => new Palette116(), - >= MC_1_15_Version => new Palette115(), - >= MC_1_14_Version => new Palette114(), - >= MC_1_13_Version => new Palette113(), - _ => new Palette112() - }; - - entityPalette = protocolVersion switch - { - // Entity palette - > MC_1_20_Version when handler.GetEntityHandlingEnabled() => - throw new NotImplementedException(Translations.exception_palette_entity), - MC_1_20_Version => new EntityPalette120(), - MC_1_19_4_Version => new EntityPalette1194(), - MC_1_19_3_Version => new EntityPalette1193(), - >= MC_1_19_Version => new EntityPalette119(), - >= MC_1_17_Version => new EntityPalette117(), - >= MC_1_16_2_Version => new EntityPalette1162(), - >= MC_1_16_Version => new EntityPalette1161(), - >= MC_1_15_Version => new EntityPalette115(), - >= MC_1_14_Version => new EntityPalette114(), - >= MC_1_13_Version => new EntityPalette113(), - >= MC_1_12_Version => new EntityPalette112(), - _ => new EntityPalette18() - }; - - entityMetadataPalette = EntityMetadataPalette.GetPalette(protocolVersion); - - itemPalette = protocolVersion switch - { - // Item palette - > MC_1_20_Version when handler.GetInventoryEnabled() => - throw new NotImplementedException(Translations.exception_palette_item), - MC_1_20_Version => new ItemPalette120(), - MC_1_19_4_Version => new ItemPalette1194(), - MC_1_19_3_Version => new ItemPalette1193(), - >= MC_1_19_Version => new ItemPalette119(), - >= MC_1_18_1_Version => new ItemPalette118(), - >= MC_1_17_Version => new ItemPalette117(), - >= MC_1_16_2_Version => new ItemPalette1162(), - >= MC_1_16_1_Version => new ItemPalette1161(), - _ => new ItemPalette115() - }; - - // MessageType - // You can find it in https://wiki.vg/Protocol#Player_Chat_Message or /net/minecraft/network/message/MessageType.java - if (this.protocolVersion >= MC_1_19_2_Version) - ChatParser.ChatId2Type = new() - { - { 0, ChatParser.MessageType.CHAT }, - { 1, ChatParser.MessageType.SAY_COMMAND }, - { 2, ChatParser.MessageType.MSG_COMMAND_INCOMING }, - { 3, ChatParser.MessageType.MSG_COMMAND_OUTGOING }, - { 4, ChatParser.MessageType.TEAM_MSG_COMMAND_INCOMING }, - { 5, ChatParser.MessageType.TEAM_MSG_COMMAND_OUTGOING }, - { 6, ChatParser.MessageType.EMOTE_COMMAND }, - }; - else if (this.protocolVersion == MC_1_19_Version) - ChatParser.ChatId2Type = new() - { - { 0, ChatParser.MessageType.CHAT }, - { 1, ChatParser.MessageType.RAW_MSG }, - { 2, ChatParser.MessageType.RAW_MSG }, - { 3, ChatParser.MessageType.SAY_COMMAND }, - { 4, ChatParser.MessageType.MSG_COMMAND_INCOMING }, - { 5, ChatParser.MessageType.TEAM_MSG_COMMAND_INCOMING }, - { 6, ChatParser.MessageType.EMOTE_COMMAND }, - { 7, ChatParser.MessageType.RAW_MSG }, - }; - } - - /// - /// Separate thread. Network reading loop. - /// - private void Updater(object? o) - { - CancellationToken cancelToken = (CancellationToken)o!; - - if (cancelToken.IsCancellationRequested) - return; - - try - { - Stopwatch stopWatch = new(); - while (!packetQueue.IsAddingCompleted) - { - cancelToken.ThrowIfCancellationRequested(); - - handler.OnUpdate(); - stopWatch.Restart(); - - while (packetQueue.TryTake(out Tuple>? packetInfo)) - { - (int packetID, Queue packetData) = packetInfo; - HandlePacket(packetID, packetData); - - if (stopWatch.Elapsed.Milliseconds >= 100) - { - handler.OnUpdate(); - stopWatch.Restart(); - } - } - - int sleepLength = 100 - stopWatch.Elapsed.Milliseconds; - if (sleepLength > 0) - Thread.Sleep(sleepLength); - } - } - catch (ObjectDisposedException) - { - } - catch (OperationCanceledException) - { - } - catch (NullReferenceException) - { - } - - if (cancelToken.IsCancellationRequested) - return; - - handler.OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, ""); - } - - /// - /// Read and decompress packets. - /// - internal void PacketReader(object? o) - { - CancellationToken cancelToken = (CancellationToken)o!; - while (socketWrapper.IsConnected() && !cancelToken.IsCancellationRequested) - { - try - { - while (socketWrapper.HasDataAvailable()) - { - packetQueue.Add(ReadNextPacket()); - - if (cancelToken.IsCancellationRequested) - break; - } - } - catch (System.IO.IOException) - { - break; - } - catch (SocketException) - { - 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 - { - byte[] toDecompress = packetData.ToArray(); - byte[] uncompressed = ZlibUtils.Decompress(toDecompress, sizeUncompressed); - packetData = new(uncompressed); - } - } - - int packetID = dataTypes.ReadNextVarInt(packetData); //Packet ID - - if (handler.GetNetworkPacketCaptureEnabled()) - { - List clone = packetData.ToList(); - handler.OnNetworkPacket(packetID, clone, login_phase, true); - } - - return new(packetID, packetData); - } - - /// - /// Handle the given packet - /// - /// Packet ID - /// Packet contents - /// TRUE if the packet was processed, FALSE if ignored or unknown - internal bool HandlePacket(int packetID, Queue packetData) - { - try - { - if (login_phase) - { - switch (packetID) //Packet IDs are different while logging in - { - case 0x03: - if (protocolVersion >= MC_1_8_Version) - compression_treshold = dataTypes.ReadNextVarInt(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()); - return understood; - default: - return false; //Ignored packet - } - } - // Regular in-game packets - else - switch (packetPalette.GetIncommingTypeById(packetID)) - { - case PacketTypesIn.KeepAlive: - SendPacket(PacketTypesOut.KeepAlive, packetData); - handler.OnServerKeepAlive(); - break; - case PacketTypesIn.Ping: - SendPacket(PacketTypesOut.Pong, packetData); - break; - case PacketTypesIn.JoinGame: - { - // Temporary fix - log.Debug("Receive JoinGame"); - - receiveDeclareCommands = receivePlayerInfo = false; - - messageIndex = 0; - pendingAcknowledgments = 0; - - lastReceivedMessage = null; - lastSeenMessagesCollector = protocolVersion >= MC_1_19_3_Version ? new(20) : new(5); - } - handler.OnGameJoined(isOnlineMode); - - int playerEntityID = dataTypes.ReadNextInt(packetData); - handler.OnReceivePlayerEntityID(playerEntityID); - - if (protocolVersion >= MC_1_16_2_Version) - dataTypes.ReadNextBool(packetData); // Is hardcore - 1.16.2 and above - - handler.OnGamemodeUpdate(Guid.Empty, dataTypes.ReadNextByte(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 - 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 - if (protocolVersion >= MC_1_19_Version) - ChatParser.ReadChatType(registryCodec); - if (handler.GetTerrainEnabled()) - World.StoreDimensionList(registryCodec); - } - - // Current dimension - // String: 1.19 and above - // NBT Tag Compound: [1.16.2 to 1.18.2] - // String identifier: 1.16 and 1.16.1 - // varInt: [1.9.1 to 1.15.2] - // byte: below 1.9.1 - string? dimensionTypeName = null; - Dictionary? dimensionType = null; - if (protocolVersion >= MC_1_16_Version) - { - if (protocolVersion >= MC_1_19_Version) - dimensionTypeName = - dataTypes.ReadNextString(packetData); // Dimension Type: Identifier - else if (protocolVersion >= MC_1_16_2_Version) - dimensionType = - dataTypes.ReadNextNbt(packetData); // Dimension Type: NBT Tag Compound - else - dataTypes.ReadNextString(packetData); - currentDimension = 0; - } - else if (protocolVersion >= MC_1_9_1_Version) - currentDimension = dataTypes.ReadNextInt(packetData); - else - currentDimension = (sbyte)dataTypes.ReadNextByte(packetData); - - if (protocolVersion < MC_1_14_Version) - dataTypes.ReadNextByte(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 - if (handler.GetTerrainEnabled()) - { - if (protocolVersion >= MC_1_16_2_Version && protocolVersion <= MC_1_18_2_Version) - { - World.StoreOneDimension(dimensionName, dimensionType!); - World.SetDimension(dimensionName); - } - else if (protocolVersion >= MC_1_19_Version) - { - World.SetDimension(dimensionTypeName!); - } - } - } - - if (protocolVersion >= MC_1_15_Version) - dataTypes.ReadNextLong(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 - else - dataTypes.ReadNextByte(packetData); // Max Players - 1.16.1 and below - if (protocolVersion < MC_1_16_Version) - dataTypes.SkipNextString(packetData); // Level Type - 1.15 and below - if (protocolVersion >= MC_1_14_Version) - dataTypes.ReadNextVarInt(packetData); // View distance - 1.14 and above - if (protocolVersion >= MC_1_18_1_Version) - dataTypes.ReadNextVarInt(packetData); // Simulation Distance - 1.18 and above - if (protocolVersion >= MC_1_8_Version) - dataTypes.ReadNextBool(packetData); // Reduced debug info - 1.8 and above - if (protocolVersion >= MC_1_15_Version) - dataTypes.ReadNextBool(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 - } - - if (protocolVersion >= MC_1_19_Version) - { - bool hasDeathLocation = dataTypes.ReadNextBool(packetData); // Has death location - if (hasDeathLocation) - { - dataTypes.SkipNextString(packetData); // Death dimension name: Identifier - dataTypes.ReadNextLocation(packetData); // Death location - } - } - - if (protocolVersion >= MC_1_20_Version) - dataTypes.ReadNextVarInt(packetData); // Portal Cooldown - 1.20 and above - - break; - case PacketTypesIn.SpawnPainting: // Just skip, no need for this - return true; - case PacketTypesIn.DeclareCommands: - if (protocolVersion >= MC_1_19_Version) - { - log.Debug("Receive DeclareCommands"); - DeclareCommands.Read(dataTypes, packetData, protocolVersion); - receiveDeclareCommands = true; - if (receivePlayerInfo) - handler.SetCanSendMessage(true); - } - - break; - case PacketTypesIn.ChatMessage: - int messageType = 0; - - if (protocolVersion <= MC_1_18_2_Version) // 1.18 and bellow - { - string message = dataTypes.ReadNextString(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)) - break; - - if (protocolVersion >= MC_1_16_5_Version) - senderUUID = dataTypes.ReadNextUUID(packetData); - else senderUUID = Guid.Empty; - } - else - senderUUID = Guid.Empty; - - handler.OnTextReceived(new(message, null, true, messageType, senderUUID)); - } - else if (protocolVersion == MC_1_19_Version) // 1.19 - { - string signedChat = dataTypes.ReadNextString(packetData); - - bool hasUnsignedChatContent = dataTypes.ReadNextBool(packetData); - string? unsignedChatContent = - hasUnsignedChatContent ? dataTypes.ReadNextString(packetData) : null; - - messageType = dataTypes.ReadNextVarInt(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)); - - bool hasSenderTeamName = dataTypes.ReadNextBool(packetData); - string? senderTeamName = hasSenderTeamName - ? ChatParser.ParseText(dataTypes.ReadNextString(packetData)) - : null; - - long timestamp = dataTypes.ReadNextLong(packetData); - - long salt = dataTypes.ReadNextLong(packetData); - - byte[] messageSignature = dataTypes.ReadNextByteArray(packetData); - - bool verifyResult; - if (!isOnlineMode) - verifyResult = false; - else if (senderUUID == handler.GetUserUuid()) - verifyResult = true; - else - { - PlayerInfo? player = handler.GetPlayerInfo(senderUUID); - verifyResult = player != null && player.VerifyMessage(signedChat, timestamp, salt, - ref messageSignature); - } - - ChatMessage chat = new(signedChat, true, messageType, senderUUID, unsignedChatContent, - senderDisplayName, senderTeamName, timestamp, messageSignature, verifyResult); - handler.OnTextReceived(chat); - } - else if (protocolVersion == MC_1_19_2_Version) - { - // 1.19.1 - 1.19.2 - byte[]? precedingSignature = dataTypes.ReadNextBool(packetData) - ? dataTypes.ReadNextByteArray(packetData) - : null; - Guid senderUUID = dataTypes.ReadNextUUID(packetData); - byte[] headerSignature = dataTypes.ReadNextByteArray(packetData); - - string signedChat = dataTypes.ReadNextString(packetData); - string? decorated = dataTypes.ReadNextBool(packetData) - ? dataTypes.ReadNextString(packetData) - : null; - - long timestamp = dataTypes.ReadNextLong(packetData); - long salt = dataTypes.ReadNextLong(packetData); - - int lastSeenMessageListLen = dataTypes.ReadNextVarInt(packetData); - LastSeenMessageList.AcknowledgedMessage[] lastSeenMessageList = - new LastSeenMessageList.AcknowledgedMessage[lastSeenMessageListLen]; - for (int i = 0; i < lastSeenMessageListLen; ++i) - { - Guid user = dataTypes.ReadNextUUID(packetData); - byte[] lastSignature = dataTypes.ReadNextByteArray(packetData); - lastSeenMessageList[i] = new(user, lastSignature, true); - } - - LastSeenMessageList lastSeenMessages = new(lastSeenMessageList); - - string? unsignedChatContent = dataTypes.ReadNextBool(packetData) - ? dataTypes.ReadNextString(packetData) - : null; - - MessageFilterType filterEnum = (MessageFilterType)dataTypes.ReadNextVarInt(packetData); - if (filterEnum == MessageFilterType.PartiallyFiltered) - dataTypes.ReadNextULongArray(packetData); - - int chatTypeId = dataTypes.ReadNextVarInt(packetData); - string chatName = dataTypes.ReadNextString(packetData); - string? targetName = dataTypes.ReadNextBool(packetData) - ? dataTypes.ReadNextString(packetData) - : null; - - Dictionary chatInfo = Json.ParseJson(chatName).Properties; - string senderDisplayName = - (chatInfo.ContainsKey("insertion") ? chatInfo["insertion"] : chatInfo["text"]) - .StringValue; - string? senderTeamName = null; - ChatParser.MessageType messageTypeEnum = - ChatParser.ChatId2Type!.GetValueOrDefault(chatTypeId, ChatParser.MessageType.CHAT); - if (targetName != null && - (messageTypeEnum == ChatParser.MessageType.TEAM_MSG_COMMAND_INCOMING || - messageTypeEnum == ChatParser.MessageType.TEAM_MSG_COMMAND_OUTGOING)) - senderTeamName = Json.ParseJson(targetName).Properties["with"].DataArray[0] - .Properties["text"].StringValue; - - if (string.IsNullOrWhiteSpace(senderDisplayName)) - { - PlayerInfo? player = handler.GetPlayerInfo(senderUUID); - if (player != null && (player.DisplayName != null || player.Name != null) && - string.IsNullOrWhiteSpace(senderDisplayName)) - { - senderDisplayName = ChatParser.ParseText(player.DisplayName ?? player.Name); - if (string.IsNullOrWhiteSpace(senderDisplayName)) - senderDisplayName = player.DisplayName ?? player.Name; - else - senderDisplayName += "§r"; - } - } - - bool verifyResult; - if (!isOnlineMode) - verifyResult = false; - else if (senderUUID == handler.GetUserUuid()) - verifyResult = true; - else - { - PlayerInfo? player = handler.GetPlayerInfo(senderUUID); - if (player == null || !player.IsMessageChainLegal()) - verifyResult = false; - else - { - bool lastVerifyResult = player.IsMessageChainLegal(); - verifyResult = player.VerifyMessage(signedChat, timestamp, salt, - ref headerSignature, ref precedingSignature, lastSeenMessages); - if (lastVerifyResult && !verifyResult) - log.Warn(string.Format(Translations.chat_message_chain_broken, - senderDisplayName)); - } - } - - ChatMessage chat = new(signedChat, false, chatTypeId, senderUUID, unsignedChatContent, - senderDisplayName, senderTeamName, timestamp, headerSignature, verifyResult); - if (isOnlineMode && !chat.LacksSender()) - Acknowledge(chat); - handler.OnTextReceived(chat); - } - else if (protocolVersion >= MC_1_19_3_Version) - { - // 1.19.3+ - // Header section - // net.minecraft.network.packet.s2c.play.ChatMessageS2CPacket#write - Guid senderUUID = dataTypes.ReadNextUUID(packetData); - int index = dataTypes.ReadNextVarInt(packetData); - // Signature is fixed size of 256 bytes - byte[]? messageSignature = dataTypes.ReadNextBool(packetData) - ? dataTypes.ReadNextByteArray(packetData, 256) - : null; - - // Body - // net.minecraft.network.message.MessageBody.Serialized#write - string message = dataTypes.ReadNextString(packetData); - long timestamp = dataTypes.ReadNextLong(packetData); - long salt = dataTypes.ReadNextLong(packetData); - - // Previous Messages - // net.minecraft.network.message.LastSeenMessageList.Indexed#write - // net.minecraft.network.message.MessageSignatureData.Indexed#write - int totalPreviousMessages = dataTypes.ReadNextVarInt(packetData); - Tuple[] previousMessageSignatures = - new Tuple[totalPreviousMessages]; - for (int i = 0; i < totalPreviousMessages; i++) - { - // net.minecraft.network.message.MessageSignatureData.Indexed#fromBuf - int messageId = dataTypes.ReadNextVarInt(packetData) - 1; - if (messageId == -1) - previousMessageSignatures[i] = new Tuple(messageId, - dataTypes.ReadNextByteArray(packetData, 256)); - else - previousMessageSignatures[i] = new Tuple(messageId, null); - } - - // Other - string? unsignedChatContent = dataTypes.ReadNextBool(packetData) - ? dataTypes.ReadNextString(packetData) - : null; - - MessageFilterType filterType = (MessageFilterType)dataTypes.ReadNextVarInt(packetData); - - if (filterType == MessageFilterType.PartiallyFiltered) - dataTypes.ReadNextULongArray(packetData); - - // Network Target - // net.minecraft.network.message.MessageType.Serialized#write - int chatTypeId = dataTypes.ReadNextVarInt(packetData); - string chatName = dataTypes.ReadNextString(packetData); - string? targetName = dataTypes.ReadNextBool(packetData) - ? dataTypes.ReadNextString(packetData) - : null; - - ChatParser.MessageType messageTypeEnum = - ChatParser.ChatId2Type!.GetValueOrDefault(chatTypeId, ChatParser.MessageType.CHAT); - - Dictionary chatInfo = - Json.ParseJson(targetName ?? chatName).Properties; - string senderDisplayName = - (chatInfo.ContainsKey("insertion") ? chatInfo["insertion"] : chatInfo["text"]) - .StringValue; - string? senderTeamName = null; - if (targetName != null && - (messageTypeEnum == ChatParser.MessageType.TEAM_MSG_COMMAND_INCOMING || - messageTypeEnum == ChatParser.MessageType.TEAM_MSG_COMMAND_OUTGOING)) - senderTeamName = Json.ParseJson(targetName).Properties["with"].DataArray[0] - .Properties["text"].StringValue; - - if (string.IsNullOrWhiteSpace(senderDisplayName)) - { - PlayerInfo? player = handler.GetPlayerInfo(senderUUID); - if (player != null && (player.DisplayName != null || player.Name != null) && - string.IsNullOrWhiteSpace(senderDisplayName)) - { - senderDisplayName = ChatParser.ParseText(player.DisplayName ?? player.Name); - if (string.IsNullOrWhiteSpace(senderDisplayName)) - senderDisplayName = player.DisplayName ?? player.Name; - else - senderDisplayName += "§r"; - } - } - - bool verifyResult; - if (!isOnlineMode || messageSignature == null) - verifyResult = false; - else - { - if (senderUUID == handler.GetUserUuid()) - verifyResult = true; - else - { - PlayerInfo? player = handler.GetPlayerInfo(senderUUID); - if (player == null || !player.IsMessageChainLegal()) - verifyResult = false; - else - { - verifyResult = false; - verifyResult = player.VerifyMessage(message, senderUUID, player.ChatUuid, - index, timestamp, salt, ref messageSignature, - previousMessageSignatures); - } - } - } - - ChatMessage chat = new(message, false, chatTypeId, senderUUID, unsignedChatContent, - senderDisplayName, senderTeamName, timestamp, messageSignature, verifyResult); - lock (MessageSigningLock) - Acknowledge(chat); - handler.OnTextReceived(chat); - } - - break; - case PacketTypesIn.HideMessage: - byte[] hideMessageSignature = dataTypes.ReadNextByteArray(packetData); - ConsoleIO.WriteLine( - $"HideMessage was not processed! (SigLen={hideMessageSignature.Length})"); - break; - case PacketTypesIn.SystemChat: - string systemMessage = dataTypes.ReadNextString(packetData); - if (protocolVersion >= MC_1_19_3_Version) - { - bool isOverlay = dataTypes.ReadNextBool(packetData); - if (isOverlay) - { - if (!Config.Main.Advanced.ShowXPBarMessages) - break; - } - else - { - if (!Config.Main.Advanced.ShowSystemMessages) - break; - } - - handler.OnTextReceived(new(systemMessage, null, true, -1, Guid.Empty, true)); - } - else - { - int msgType = dataTypes.ReadNextVarInt(packetData); - if ((msgType == 1 && !Config.Main.Advanced.ShowSystemMessages)) - break; - handler.OnTextReceived(new(systemMessage, null, true, msgType, Guid.Empty, true)); - } - - break; - case PacketTypesIn.ProfilelessChatMessage: - string message_ = dataTypes.ReadNextString(packetData); - int messageType_ = dataTypes.ReadNextVarInt(packetData); - string messageName = dataTypes.ReadNextString(packetData); - string? targetName_ = dataTypes.ReadNextBool(packetData) - ? dataTypes.ReadNextString(packetData) - : null; - ChatMessage profilelessChat = new(message_, targetName_ ?? messageName, true, messageType_, - Guid.Empty, true); - profilelessChat.isSenderJson = true; - handler.OnTextReceived(profilelessChat); - 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); - - if (eventType == CombatEventType.EntityDead) - { - dataTypes.SkipNextVarInt(packetData); - - handler.OnPlayerKilled( - dataTypes.ReadNextInt(packetData), - ChatParser.ParseText(dataTypes.ReadNextString(packetData)) - ); - } - } - - break; - case PacketTypesIn.DeathCombatEvent: - dataTypes.SkipNextVarInt(packetData); - - handler.OnPlayerKilled( - protocolVersion >= MC_1_20_Version ? -1 : dataTypes.ReadNextInt(packetData), - ChatParser.ParseText(dataTypes.ReadNextString(packetData)) - ); - - break; - case PacketTypesIn.DamageEvent: // 1.19.4 - if (handler.GetEntityHandlingEnabled() && protocolVersion >= MC_1_19_4_Version) - { - var entityId = dataTypes.ReadNextVarInt(packetData); - var sourceTypeId = dataTypes.ReadNextVarInt(packetData); - var sourceCauseId = dataTypes.ReadNextVarInt(packetData); - var sourceDirectId = dataTypes.ReadNextVarInt(packetData); - - Location? sourcePos; - if (dataTypes.ReadNextBool(packetData)) - { - sourcePos = new Location() - { - X = dataTypes.ReadNextDouble(packetData), - Y = dataTypes.ReadNextDouble(packetData), - Z = dataTypes.ReadNextDouble(packetData) - }; - } - - // TODO: Write a function to use this data ? But seems not too useful - } - - break; - case PacketTypesIn.MessageHeader: // 1.19.2 only - 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); - - bool verifyResult; - - if (!isOnlineMode) - verifyResult = false; - else if (senderUUID == handler.GetUserUuid()) - verifyResult = true; - else - { - PlayerInfo? player = handler.GetPlayerInfo(senderUUID); - - if (player == null || !player.IsMessageChainLegal()) - verifyResult = false; - else - { - bool lastVerifyResult = player.IsMessageChainLegal(); - verifyResult = player.VerifyMessageHead(ref precedingSignature, - ref headerSignature, ref bodyDigest); - if (lastVerifyResult && !verifyResult) - log.Warn(string.Format(Translations.chat_message_chain_broken, - player.Name)); - } - } - } - - break; - case PacketTypesIn.Respawn: - string? dimensionTypeNameRespawn = null; - Dictionary? dimensionTypeRespawn = null; - if (protocolVersion >= MC_1_16_Version) - { - if (protocolVersion >= MC_1_19_Version) - dimensionTypeNameRespawn = - dataTypes.ReadNextString(packetData); // Dimension Type: Identifier - else if (protocolVersion >= MC_1_16_2_Version) - dimensionTypeRespawn = - dataTypes.ReadNextNbt(packetData); // Dimension Type: NBT Tag Compound - else - dataTypes.ReadNextString(packetData); - currentDimension = 0; - } - else - { - // 1.15 and below - currentDimension = dataTypes.ReadNextInt(packetData); - } - - if (protocolVersion >= MC_1_16_Version) - { - string dimensionName = - dataTypes.ReadNextString( - 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) - { - World.StoreOneDimension(dimensionName, dimensionTypeRespawn!); - World.SetDimension(dimensionName); - } - else if (protocolVersion >= MC_1_19_Version) - { - World.SetDimension(dimensionTypeNameRespawn!); - } - } - } - - if (protocolVersion < MC_1_14_Version) - dataTypes.ReadNextByte(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 - if (protocolVersion >= MC_1_16_Version) - dataTypes.ReadNextByte(packetData); // Previous Game mode - 1.16 and above - if (protocolVersion < MC_1_16_Version) - dataTypes.SkipNextString(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 - } - - if (protocolVersion >= MC_1_19_Version) - { - bool hasDeathLocation = dataTypes.ReadNextBool(packetData); // Has death location - if (hasDeathLocation) - { - dataTypes.ReadNextString(packetData); // Death dimension name: Identifier - dataTypes.ReadNextLocation(packetData); // Death location - } - } - - if (protocolVersion >= MC_1_20_Version) - dataTypes.ReadNextVarInt(packetData); // Portal Cooldown - - handler.OnRespawn(); - 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); - Location location = new(x, y, z); - float yaw = dataTypes.ReadNextFloat(packetData); - float pitch = dataTypes.ReadNextFloat(packetData); - byte locMask = dataTypes.ReadNextByte(packetData); - - // entity handling require player pos for distance calculating - if (handler.GetTerrainEnabled() || handler.GetEntityHandlingEnabled()) - { - if (protocolVersion >= MC_1_8_Version) - { - Location current = handler.GetCurrentLocation(); - location.X = (locMask & 1 << 0) != 0 ? current.X + x : x; - location.Y = (locMask & 1 << 1) != 0 ? current.Y + y : y; - location.Z = (locMask & 1 << 2) != 0 ? current.Z + z : z; - } - } - - if (protocolVersion >= MC_1_9_Version) - { - int teleportID = dataTypes.ReadNextVarInt(packetData); - - if (teleportID < 0) - { - yaw = LastYaw; - pitch = LastPitch; - } - else - { - LastYaw = yaw; - LastPitch = pitch; - } - - handler.UpdateLocation(location, yaw, pitch); - - // Teleport confirm packet - SendPacket(PacketTypesOut.TeleportConfirm, DataTypes.GetVarInt(teleportID)); - if (Config.Main.Advanced.TemporaryFixBadpacket) - { - SendLocationUpdate(location, true, yaw, pitch, true); - if (teleportID == 1) - SendLocationUpdate(location, true, yaw, pitch, true); - } - } - else - { - handler.UpdateLocation(location, yaw, pitch); - LastYaw = yaw; - LastPitch = pitch; - } - - if (protocolVersion >= MC_1_17_Version && protocolVersion < MC_1_19_4_Version) - dataTypes.ReadNextBool(packetData); // Dismount Vehicle - 1.17 to 1.19.3 - } - break; - case PacketTypesIn.ChunkData: - if (handler.GetTerrainEnabled()) - { - Interlocked.Increment(ref handler.GetWorld().chunkCnt); - Interlocked.Increment(ref handler.GetWorld().chunkLoadNotCompleted); - - int chunkX = dataTypes.ReadNextInt(packetData); - int chunkZ = dataTypes.ReadNextInt(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 - - dataTypes.ReadNextNbt(packetData); // Heightmaps - - if (protocolVersion == MC_1_17_Version || protocolVersion == MC_1_17_1_Version) - { - int biomesLength = dataTypes.ReadNextVarInt(packetData); // Biomes length - for (int i = 0; i < biomesLength; i++) - dataTypes.SkipNextVarInt(packetData); // Biomes - } - - int dataSize = dataTypes.ReadNextVarInt(packetData); // Size - - pTerrain.ProcessChunkColumnData(chunkX, chunkZ, verticalStripBitmask, packetData); - Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); - - // Block Entity data: ignored - // Trust edges: ignored (Removed in 1.20) - // Light data: ignored - } - else - { - bool chunksContinuous = dataTypes.ReadNextBool(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 - ushort chunkMask = protocolVersion >= MC_1_9_Version - ? (ushort)dataTypes.ReadNextVarInt(packetData) - : dataTypes.ReadNextUShort(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)); - Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); - } - else - { - if (protocolVersion >= MC_1_14_Version) - dataTypes.ReadNextNbt(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 - if (protocolVersion >= MC_1_15_Version && chunksContinuous) - { - if (protocolVersion >= MC_1_16_2_Version) - { - for (int i = 0; i < biomesLength; i++) - { - // Biomes - 1.16.2 and above - // Don't use ReadNextVarInt because it cost too much time - dataTypes.SkipNextVarInt(packetData); - } - } - else dataTypes.DropData(1024 * 4, packetData); // Biomes - 1.15 and above - } - - int dataSize = dataTypes.ReadNextVarInt(packetData); - - pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, 0, false, - chunksContinuous, currentDimension, packetData); - Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); - } - } - } - - break; - case PacketTypesIn.ChunksBiomes: // 1.19.4 - // Biomes are not handled by MCC - break; - case PacketTypesIn.MapData: - if (protocolVersion < MC_1_8_Version) - break; - - int mapid = dataTypes.ReadNextVarInt(packetData); - byte scale = dataTypes.ReadNextByte(packetData); - - - // 1.9 + - bool trackingPosition = true; - - // 1.14+ - bool locked = false; - - // 1.17+ (locked and trackingPosition switched places) - if (protocolVersion >= MC_1_17_Version) - { - if (protocolVersion >= MC_1_14_Version) - locked = dataTypes.ReadNextBool(packetData); - - if (protocolVersion >= MC_1_9_Version) - trackingPosition = dataTypes.ReadNextBool(packetData); - } - else - { - if (protocolVersion >= MC_1_9_Version) - trackingPosition = dataTypes.ReadNextBool(packetData); - - if (protocolVersion >= MC_1_14_Version) - locked = dataTypes.ReadNextBool(packetData); - } - - int iconcount = 0; - List icons = new(); - - // 1,9 + = needs tracking position to be true to get the icons - if (protocolVersion <= MC_1_16_5_Version || trackingPosition) - { - iconcount = dataTypes.ReadNextVarInt(packetData); - - for (int i = 0; i < iconcount; i++) - { - MapIcon mapIcon = new(); - - // 1.8 - 1.13 - if (protocolVersion < MC_1_13_2_Version) - { - byte directionAndtype = dataTypes.ReadNextByte(packetData); - byte direction, type; - - // 1.12.2+ - if (protocolVersion >= MC_1_12_2_Version) - { - direction = (byte)(directionAndtype & 0xF); - type = (byte)((directionAndtype >> 4) & 0xF); - } - else // 1.8 - 1.12 - { - direction = (byte)((directionAndtype >> 4) & 0xF); - type = (byte)(directionAndtype & 0xF); - } - - mapIcon.Type = (MapIconType)type; - mapIcon.Direction = direction; - } - - // 1.13.2+ - if (protocolVersion >= MC_1_13_2_Version) - mapIcon.Type = (MapIconType)dataTypes.ReadNextVarInt(packetData); - - mapIcon.X = dataTypes.ReadNextByte(packetData); - mapIcon.Z = dataTypes.ReadNextByte(packetData); - - // 1.13.2+ - if (protocolVersion >= MC_1_13_2_Version) - { - mapIcon.Direction = dataTypes.ReadNextByte(packetData); - - if (dataTypes.ReadNextBool(packetData)) // Has Display Name? - mapIcon.DisplayName = - ChatParser.ParseText(dataTypes.ReadNextString(packetData)); - } - - icons.Add(mapIcon); - } - } - - byte columnsUpdated = dataTypes.ReadNextByte(packetData); // width - byte rowsUpdated = 0; // height - byte mapCoulmnX = 0; - byte mapRowZ = 0; - byte[]? colors = null; - - if (columnsUpdated > 0) - { - rowsUpdated = dataTypes.ReadNextByte(packetData); // height - mapCoulmnX = dataTypes.ReadNextByte(packetData); - mapRowZ = dataTypes.ReadNextByte(packetData); - colors = dataTypes.ReadNextByteArray(packetData); - } - - handler.OnMapData(mapid, scale, trackingPosition, locked, icons, columnsUpdated, - rowsUpdated, mapCoulmnX, mapRowZ, colors); - break; - case PacketTypesIn.TradeList: - if ((protocolVersion >= MC_1_14_Version) && (handler.GetInventoryEnabled())) - { - // MC 1.14 or greater - int windowID = dataTypes.ReadNextVarInt(packetData); - int size = dataTypes.ReadNextByte(packetData); - List trades = new(); - for (int tradeId = 0; tradeId < size; tradeId++) - { - VillagerTrade trade = dataTypes.ReadNextTrade(packetData, itemPalette); - trades.Add(trade); - } - - VillagerInfo villagerInfo = new() - { - Level = dataTypes.ReadNextVarInt(packetData), - Experience = dataTypes.ReadNextVarInt(packetData), - IsRegularVillager = dataTypes.ReadNextBool(packetData), - CanRestock = dataTypes.ReadNextBool(packetData) - }; - handler.OnTradeList(windowID, trades, villagerInfo); - } - - break; - 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 fadein = -1; - int stay = -1; - int fadeout = -1; - if (protocolVersion >= MC_1_10_Version) - { - if (action2 == 0) - { - json = titletext; - titletext = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); - } - else if (action2 == 1) - { - json = subtitletext; - subtitletext = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); - } - else if (action2 == 2) - { - json = actionbartext; - actionbartext = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); - } - else if (action2 == 3) - { - fadein = dataTypes.ReadNextInt(packetData); - stay = dataTypes.ReadNextInt(packetData); - fadeout = dataTypes.ReadNextInt(packetData); - } - } - else - { - if (action2 == 0) - { - json = titletext; - titletext = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); - } - else if (action2 == 1) - { - json = subtitletext; - subtitletext = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); - } - else if (action2 == 2) - { - fadein = dataTypes.ReadNextInt(packetData); - stay = dataTypes.ReadNextInt(packetData); - fadeout = dataTypes.ReadNextInt(packetData); - } - } - - handler.OnTitle(action2, titletext, subtitletext, actionbartext, fadein, stay, fadeout, - json); - } - - break; - case PacketTypesIn.MultiBlockChange: - if (handler.GetTerrainEnabled()) - { - if (protocolVersion >= MC_1_16_2_Version) - { - long chunkSection = dataTypes.ReadNextLong(packetData); - int sectionX = (int)(chunkSection >> 42); - int sectionY = (int)((chunkSection << 44) >> 44); - int sectionZ = (int)((chunkSection << 22) >> 42); - - if(protocolVersion < MC_1_20_Version) - dataTypes.ReadNextBool(packetData); // Useless boolean (Related to light update) - - int blocksSize = dataTypes.ReadNextVarInt(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 localY = (int)(chunkSectionPosition & 0x0F); - - Block block = new((ushort)blockId); - int blockX = (sectionX * 16) + localX; - int blockY = (sectionY * 16) + localY; - int blockZ = (sectionZ * 16) + localZ; - - Location location = new(blockX, blockY, blockZ); - - handler.OnBlockChange(location, block); - } - } - else - { - int chunkX = dataTypes.ReadNextInt(packetData); - int chunkZ = dataTypes.ReadNextInt(packetData); - int recordCount = protocolVersion < MC_1_8_Version - ? (int)dataTypes.ReadNextShort(packetData) - : dataTypes.ReadNextVarInt(packetData); - - for (int i = 0; i < recordCount; i++) - { - byte locationXZ; - ushort blockIdMeta; - int blockY; - - if (protocolVersion < MC_1_8_Version) - { - blockIdMeta = dataTypes.ReadNextUShort(packetData); - blockY = (ushort)dataTypes.ReadNextByte(packetData); - locationXZ = dataTypes.ReadNextByte(packetData); - } - else - { - locationXZ = dataTypes.ReadNextByte(packetData); - blockY = (ushort)dataTypes.ReadNextByte(packetData); - blockIdMeta = (ushort)dataTypes.ReadNextVarInt(packetData); - } - - int blockX = locationXZ >> 4; - int blockZ = locationXZ & 0x0F; - - Location location = new(chunkX, chunkZ, blockX, blockY, blockZ); - Block block = new(blockIdMeta); - handler.OnBlockChange(location, block); - } - } - } - - break; - case PacketTypesIn.ServerData: - string motd = "-"; - - bool hasMotd = false; - if (protocolVersion < MC_1_19_4_Version) - { - hasMotd = dataTypes.ReadNextBool(packetData); - - if (hasMotd) - motd = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); - } - else - { - hasMotd = true; - motd = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); - } - - string iconBase64 = "-"; - bool hasIcon = dataTypes.ReadNextBool(packetData); - if (hasIcon) - iconBase64 = dataTypes.ReadNextString(packetData); - - bool previewsChat = false; - if (protocolVersion < MC_1_19_3_Version) - previewsChat = dataTypes.ReadNextBool(packetData); - - handler.OnServerDataRecived(hasMotd, motd, hasIcon, iconBase64, previewsChat); - break; - case PacketTypesIn.BlockChange: - if (handler.GetTerrainEnabled()) - { - 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); - - Location location = new(blockX, blockY, blockZ); - Block block = new(blockId, blockMeta); - handler.OnBlockChange(location, block); - } - else - { - Location location = dataTypes.ReadNextLocation(packetData); - Block block = new((ushort)dataTypes.ReadNextVarInt(packetData)); - handler.OnBlockChange(location, block); - } - } - - break; - case PacketTypesIn.SetDisplayChatPreview: - bool previewsChatSetting = dataTypes.ReadNextBool(packetData); - handler.OnChatPreviewSettingUpdate(previewsChatSetting); - break; - case PacketTypesIn.ChatSuggestions: - break; - case PacketTypesIn.MapChunkBulk: - if (protocolVersion < MC_1_9_Version && handler.GetTerrainEnabled()) - { - int chunkCount; - bool hasSkyLight; - Queue chunkData = 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); - } - else - { - hasSkyLight = dataTypes.ReadNextBool(packetData); - chunkCount = dataTypes.ReadNextVarInt(packetData); - } - - //Read chunk records - int[] chunkXs = new int[chunkCount]; - int[] chunkZs = new int[chunkCount]; - ushort[] chunkMasks = new ushort[chunkCount]; - 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); - addBitmaps[chunkColumnNo] = protocolVersion < MC_1_8_Version - ? dataTypes.ReadNextUShort(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); - Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); - } - } - - break; - case PacketTypesIn.UnloadChunk: - if (protocolVersion >= MC_1_9_Version && handler.GetTerrainEnabled()) - { - int chunkX = dataTypes.ReadNextInt(packetData); - int chunkZ = dataTypes.ReadNextInt(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 - // in loading chunks that should be unloaded and inaccurate statistics. - if (handler.GetWorld()[chunkX, chunkZ] != null) - Interlocked.Decrement(ref handler.GetWorld().chunkCnt); - - handler.GetWorld()[chunkX, chunkZ] = null; - } - - break; - case PacketTypesIn.ChangeGameState: - if (protocolVersion >= MC_1_15_2_Version) - { - byte reason = dataTypes.ReadNextByte(packetData); - float state = dataTypes.ReadNextFloat(packetData); - - handler.OnGameEvent(reason, state); - } - - break; - case PacketTypesIn.PlayerInfo: - if (protocolVersion >= MC_1_19_3_Version) - { - byte actionBitset = dataTypes.ReadNextByte(packetData); - int numberOfActions = dataTypes.ReadNextVarInt(packetData); - for (int i = 0; i < numberOfActions; i++) - { - Guid playerUuid = dataTypes.ReadNextUUID(packetData); - - PlayerInfo player; - if ((actionBitset & (1 << 0)) > 0) // Actions bit 0: add player - { - string name = dataTypes.ReadNextString(packetData); - int numberOfProperties = dataTypes.ReadNextVarInt(packetData); - for (int j = 0; j < numberOfProperties; ++j) - { - dataTypes.SkipNextString(packetData); - dataTypes.SkipNextString(packetData); - if (dataTypes.ReadNextBool(packetData)) - dataTypes.SkipNextString(packetData); - } - - player = new(name, playerUuid); - handler.OnPlayerJoin(player); - } - else - { - PlayerInfo? playerGet = handler.GetPlayerInfo(playerUuid); - if (playerGet == null) - { - player = new(string.Empty, playerUuid); - handler.OnPlayerJoin(player); - } - else - { - player = playerGet; - } - } - - if ((actionBitset & (1 << 1)) > 0) // Actions bit 1: initialize chat - { - bool hasSignatureData = dataTypes.ReadNextBool(packetData); - if (hasSignatureData) - { - Guid chatUuid = dataTypes.ReadNextUUID(packetData); - long publicKeyExpiryTime = dataTypes.ReadNextLong(packetData); - byte[] encodedPublicKey = dataTypes.ReadNextByteArray(packetData); - byte[] publicKeySignature = dataTypes.ReadNextByteArray(packetData); - player.SetPublicKey(chatUuid, publicKeyExpiryTime, encodedPublicKey, - publicKeySignature); - - if (playerUuid == handler.GetUserUuid()) - { - log.Debug("Receive ChatUuid = " + chatUuid); - this.chatUuid = chatUuid; - } - } - else - { - player.ClearPublicKey(); - - if (playerUuid == handler.GetUserUuid()) - { - log.Debug("Receive ChatUuid = Empty"); - } - } - - if (playerUuid == handler.GetUserUuid()) - { - receivePlayerInfo = true; - if (receiveDeclareCommands) - handler.SetCanSendMessage(true); - } - } - - if ((actionBitset & 1 << 2) > 0) // Actions bit 2: update gamemode - { - handler.OnGamemodeUpdate(playerUuid, dataTypes.ReadNextVarInt(packetData)); - } - - if ((actionBitset & (1 << 3)) > 0) // Actions bit 3: update listed - { - player.Listed = dataTypes.ReadNextBool(packetData); - } - - if ((actionBitset & (1 << 4)) > 0) // Actions bit 4: update latency - { - int latency = dataTypes.ReadNextVarInt(packetData); - handler.OnLatencyUpdate(playerUuid, latency); //Update latency; - } - - if ((actionBitset & (1 << 5)) > 0) // Actions bit 5: update display name - { - if (dataTypes.ReadNextBool(packetData)) - player.DisplayName = dataTypes.ReadNextString(packetData); - else - player.DisplayName = null; - } - } - } - else if (protocolVersion >= MC_1_8_Version) - { - int action = dataTypes.ReadNextVarInt(packetData); // Action Name - int numberOfPlayers = dataTypes.ReadNextVarInt(packetData); // Number Of Players - - for (int i = 0; i < numberOfPlayers; i++) - { - Guid uuid = dataTypes.ReadNextUUID(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 - - // Property: Tuple[]? properties = - useProperty ? 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? propertySignature = null; - if (dataTypes.ReadNextBool(packetData)) // Is Signed - propertySignature = - dataTypes.ReadNextString( - 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 ping = dataTypes.ReadNextVarInt(packetData); // Ping - - string? displayName = null; - if (dataTypes.ReadNextBool(packetData)) // Has display name - displayName = dataTypes.ReadNextString(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) - { - keyExpiration = dataTypes.ReadNextLong(packetData); // Timestamp - - int publicKeyLength = - dataTypes.ReadNextVarInt(packetData); // Public Key Length - if (publicKeyLength > 0) - publicKey = dataTypes.ReadData(publicKeyLength, - packetData); // Public key - - int signatureLength = - dataTypes.ReadNextVarInt(packetData); // Signature Length - if (signatureLength > 0) - signature = dataTypes.ReadData(signatureLength, - packetData); // Public key - } - } - - handler.OnPlayerJoin(new PlayerInfo(uuid, name, properties, gameMode, ping, - displayName, keyExpiration, publicKey, signature)); - break; - case 0x01: //Update gamemode - handler.OnGamemodeUpdate(uuid, dataTypes.ReadNextVarInt(packetData)); - break; - case 0x02: //Update latency - int latency = dataTypes.ReadNextVarInt(packetData); - handler.OnLatencyUpdate(uuid, latency); //Update latency; - break; - case 0x03: //Update display name - if (dataTypes.ReadNextBool(packetData)) - { - PlayerInfo? player = handler.GetPlayerInfo(uuid); - if (player != null) - player.DisplayName = dataTypes.ReadNextString(packetData); - else - dataTypes.SkipNextString(packetData); - } - - break; - case 0x04: //Player Leave - handler.OnPlayerLeave(uuid); - break; - default: - //Unknown player list item type - break; - } - } - } - 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()); - if (online) - handler.OnPlayerJoin(new PlayerInfo(name, FakeUUID)); - else handler.OnPlayerLeave(FakeUUID); - } - - break; - case PacketTypesIn.PlayerRemove: - int numberOfLeavePlayers = dataTypes.ReadNextVarInt(packetData); - for (int i = 0; i < numberOfLeavePlayers; ++i) - { - Guid playerUuid = dataTypes.ReadNextUUID(packetData); - handler.OnPlayerLeave(playerUuid); - } - - 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 - } - - int autocomplete_count = dataTypes.ReadNextVarInt(packetData); - - string[] autocomplete_result = new string[autocomplete_count]; - for (int i = 0; i < autocomplete_count; i++) - { - autocomplete_result[i] = dataTypes.ReadNextString(packetData); - if (protocolVersion >= MC_1_13_Version) - { - // Skip optional tooltip for each tab-complete resul`t - if (dataTypes.ReadNextBool(packetData)) - dataTypes.SkipNextString(packetData); - } - } - - handler.OnAutoCompleteDone(old_transaction_id, autocomplete_result); - break; - case PacketTypesIn.PluginMessage: - String channel = dataTypes.ReadNextString(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); - case PacketTypesIn.Disconnect: - handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick, - ChatParser.ParseText(dataTypes.ReadNextString(packetData))); - return false; - case PacketTypesIn.SetCompression: - if (protocolVersion >= MC_1_8_Version && protocolVersion < MC_1_9_Version) - compression_treshold = dataTypes.ReadNextVarInt(packetData); - break; - case PacketTypesIn.OpenWindow: - if (handler.GetInventoryEnabled()) - { - if (protocolVersion < MC_1_14_Version) - { - // MC 1.13 or lower - byte windowID = dataTypes.ReadNextByte(packetData); - string type = dataTypes.ReadNextString(packetData).Replace("minecraft:", "") - .ToUpper(); - ContainerTypeOld inventoryType = - (ContainerTypeOld)Enum.Parse(typeof(ContainerTypeOld), type); - string title = dataTypes.ReadNextString(packetData); - byte slots = dataTypes.ReadNextByte(packetData); - Container inventory = new(windowID, inventoryType, ChatParser.ParseText(title)); - handler.OnInventoryOpen(windowID, inventory); - } - else - { - // MC 1.14 or greater - int windowID = dataTypes.ReadNextVarInt(packetData); - int windowType = dataTypes.ReadNextVarInt(packetData); - string title = dataTypes.ReadNextString(packetData); - Container inventory = new(windowID, windowType, ChatParser.ParseText(title)); - handler.OnInventoryOpen(windowID, inventory); - } - } - - break; - case PacketTypesIn.CloseWindow: - if (handler.GetInventoryEnabled()) - { - byte windowID = dataTypes.ReadNextByte(packetData); - lock (window_actions) - { - window_actions[windowID] = 0; - } - - handler.OnInventoryClose(windowID); - } - - break; - case PacketTypesIn.WindowItems: - if (handler.GetInventoryEnabled()) - { - byte windowId = dataTypes.ReadNextByte(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); - } - else - { - // Elements as Short - 1.17.0 and below - dataTypes.ReadNextShort(packetData); - } - - Dictionary inventorySlots = new(); - for (int slotId = 0; slotId < elements; slotId++) - { - Item? item = dataTypes.ReadNextItemSlot(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); - - handler.OnWindowItems(windowId, inventorySlots, stateId); - } - - break; - case PacketTypesIn.WindowProperty: - byte containerId = dataTypes.ReadNextByte(packetData); - short propertyId = dataTypes.ReadNextShort(packetData); - short propertyValue = dataTypes.ReadNextShort(packetData); - - handler.OnWindowProperties(containerId, propertyId, propertyValue); - break; - case PacketTypesIn.SetSlot: - if (handler.GetInventoryEnabled()) - { - byte windowID = dataTypes.ReadNextByte(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); - } - - break; - case PacketTypesIn.WindowConfirmation: - if (handler.GetInventoryEnabled()) - { - byte windowID = dataTypes.ReadNextByte(packetData); - short actionID = dataTypes.ReadNextShort(packetData); - bool accepted = dataTypes.ReadNextBool(packetData); - if (!accepted) - SendWindowConfirmation(windowID, actionID, true); - } - - break; - case PacketTypesIn.ResourcePackSend: - string url = dataTypes.ReadNextString(packetData); - string hash = dataTypes.ReadNextString(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 - if (hasPromptMessage) - dataTypes.SkipNextString( - 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 - break; - //Send back "accepted" and "successfully loaded" responses for plugins or server config making use of resource pack mandatory - 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 - break; - case PacketTypesIn.SpawnEntity: - if (handler.GetEntityHandlingEnabled()) - { - Entity entity = dataTypes.ReadNextEntity(packetData, entityPalette, false); - handler.OnSpawnEntity(entity); - } - - break; - case PacketTypesIn.EntityEquipment: - if (handler.GetEntityHandlingEnabled()) - { - int entityid = dataTypes.ReadNextVarInt(packetData); - if (protocolVersion >= MC_1_16_Version) - { - bool hasNext; - do - { - byte bitsData = dataTypes.ReadNextByte(packetData); - // Top bit set if another entry follows, and otherwise unset if this is the last item in the array - hasNext = (bitsData >> 7) == 1; - int slot2 = bitsData >> 1; - Item? item = dataTypes.ReadNextItemSlot(packetData, itemPalette); - handler.OnEntityEquipment(entityid, slot2, item); - } while (hasNext); - } - else - { - int slot2 = protocolVersion < MC_1_9_Version - ? dataTypes.ReadNextShort(packetData) - : dataTypes.ReadNextVarInt(packetData); - - Item? item = dataTypes.ReadNextItemSlot(packetData, itemPalette); - handler.OnEntityEquipment(entityid, slot2, item); - } - } - - break; - case PacketTypesIn.SpawnLivingEntity: - if (handler.GetEntityHandlingEnabled()) - { - Entity entity = 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); - } - - break; - case PacketTypesIn.SpawnPlayer: - if (handler.GetEntityHandlingEnabled()) - { - int EntityID = dataTypes.ReadNextVarInt(packetData); - Guid UUID = dataTypes.ReadNextUUID(packetData); - - double x, y, z; - - if (protocolVersion < MC_1_9_Version) - { - x = dataTypes.ReadNextInt(packetData) / 32.0D; - y = dataTypes.ReadNextInt(packetData) / 32.0D; - z = dataTypes.ReadNextInt(packetData) / 32.0D; - } - else - { - x = dataTypes.ReadNextDouble(packetData); - y = dataTypes.ReadNextDouble(packetData); - z = dataTypes.ReadNextDouble(packetData); - } - - byte Yaw = dataTypes.ReadNextByte(packetData); - byte Pitch = dataTypes.ReadNextByte(packetData); - - Location EntityLocation = new(x, y, z); - - 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 effectId = protocolVersion >= MC_1_18_2_Version - ? dataTypes.ReadNextVarInt(packetData) - : dataTypes.ReadNextByte(packetData); - if (Enum.TryParse(effectId.ToString(), out effect)) - { - int amplifier = dataTypes.ReadNextByte(packetData); - int duration = dataTypes.ReadNextVarInt(packetData); - byte flags = dataTypes.ReadNextByte(packetData); - - bool hasFactorData = false; - Dictionary? factorCodec = null; - - if (protocolVersion >= MC_1_19_Version) - { - hasFactorData = dataTypes.ReadNextBool(packetData); - if (hasFactorData) - factorCodec = dataTypes.ReadNextNbt(packetData); - } - - handler.OnEntityEffect(entityid, effect, amplifier, duration, flags, hasFactorData, - factorCodec); - } - } - - break; - case PacketTypesIn.DestroyEntities: - if (handler.GetEntityHandlingEnabled()) - { - 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 - int[] entityList = new int[entityCount]; - for (int i = 0; i < entityCount; i++) - { - entityList[i] = dataTypes.ReadNextVarInt(packetData); - } - - handler.OnDestroyEntities(entityList); - } - - break; - case PacketTypesIn.EntityPosition: - if (handler.GetEntityHandlingEnabled()) - { - int EntityID = dataTypes.ReadNextVarInt(packetData); - - Double DeltaX, DeltaY, DeltaZ; - - if (protocolVersion < MC_1_9_Version) - { - DeltaX = Convert.ToDouble(dataTypes.ReadNextByte(packetData)); - DeltaY = Convert.ToDouble(dataTypes.ReadNextByte(packetData)); - DeltaZ = Convert.ToDouble(dataTypes.ReadNextByte(packetData)); - } - else - { - DeltaX = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); - DeltaY = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); - DeltaZ = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); - } - - bool OnGround = dataTypes.ReadNextBool(packetData); - DeltaX = DeltaX / (128 * 32); - DeltaY = DeltaY / (128 * 32); - DeltaZ = DeltaZ / (128 * 32); - - handler.OnEntityPosition(EntityID, DeltaX, DeltaY, DeltaZ, OnGround); - } - - break; - case PacketTypesIn.EntityPositionAndRotation: - if (handler.GetEntityHandlingEnabled()) - { - int EntityID = dataTypes.ReadNextVarInt(packetData); - - Double DeltaX, DeltaY, DeltaZ; - - if (protocolVersion < MC_1_9_Version) - { - DeltaX = dataTypes.ReadNextByte(packetData) / 32.0D; - DeltaY = dataTypes.ReadNextByte(packetData) / 32.0D; - DeltaZ = dataTypes.ReadNextByte(packetData) / 32.0D; - } - else - { - DeltaX = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); - DeltaY = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); - DeltaZ = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); - } - - - byte _yaw = dataTypes.ReadNextByte(packetData); - byte _pitch = dataTypes.ReadNextByte(packetData); - bool OnGround = dataTypes.ReadNextBool(packetData); - DeltaX = DeltaX / (128 * 32); - DeltaY = DeltaY / (128 * 32); - DeltaZ = DeltaZ / (128 * 32); - - 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(); - for (int i = 0; i < NumberOfProperties; i++) - { - string _key = dataTypes.ReadNextString(packetData); - Double _value = dataTypes.ReadNextDouble(packetData); - - List op0 = new(); - List op1 = new(); - List op2 = new(); - int NumberOfModifiers = dataTypes.ReadNextVarInt(packetData); - for (int j = 0; j < NumberOfModifiers; j++) - { - dataTypes.ReadNextUUID(packetData); - Double amount = dataTypes.ReadNextDouble(packetData); - byte operation = dataTypes.ReadNextByte(packetData); - switch (operation) - { - case 0: - op0.Add(amount); - break; - case 1: - op1.Add(amount); - break; - case 2: - op2.Add(amount + 1); - break; - } - } - - if (op0.Count > 0) _value += op0.Sum(); - if (op1.Count > 0) _value *= 1 + op1.Sum(); - if (op2.Count > 0) _value *= op2.Aggregate((a, _x) => a * _x); - keys.Add(_key, _value); - } - - handler.OnEntityProperties(EntityID, keys); - } - - break; - case PacketTypesIn.EntityMetadata: - if (handler.GetEntityHandlingEnabled()) - { - int EntityID = dataTypes.ReadNextVarInt(packetData); - Dictionary metadata = - dataTypes.ReadNextMetadata(packetData, itemPalette, entityMetadataPalette); - - // Also make a palette for field? Will be a lot of work - int healthField = protocolVersion switch - { - > MC_1_20_Version => throw new NotImplementedException(Translations - .exception_palette_healthfield), - // 1.17 and above - >= MC_1_17_Version => 9, - // 1.14 and above - >= MC_1_14_Version => 8, - // 1.10 and above - >= MC_1_10_Version => 7, - // 1.8 and above - >= MC_1_8_Version => 6, - _ => throw new NotImplementedException(Translations.exception_palette_healthfield) - }; - - if (metadata.TryGetValue(healthField, out var healthObj) && healthObj != null && - healthObj is float) - handler.OnEntityHealth(EntityID, (float)healthObj); - - handler.OnEntityMetadata(EntityID, metadata); - } - - break; - case PacketTypesIn.EntityStatus: - if (handler.GetEntityHandlingEnabled()) - { - int entityId = dataTypes.ReadNextInt(packetData); - byte status = dataTypes.ReadNextByte(packetData); - handler.OnEntityStatus(entityId, status); - } - - break; - case PacketTypesIn.TimeUpdate: - long WorldAge = dataTypes.ReadNextLong(packetData); - long TimeOfday = dataTypes.ReadNextLong(packetData); - handler.OnTimeUpdate(WorldAge, TimeOfday); - break; - case PacketTypesIn.EntityTeleport: - if (handler.GetEntityHandlingEnabled()) - { - int EntityID = dataTypes.ReadNextVarInt(packetData); - - double x, y, z; - - if (protocolVersion < MC_1_9_Version) - { - x = dataTypes.ReadNextInt(packetData) / 32.0D; - y = dataTypes.ReadNextInt(packetData) / 32.0D; - z = dataTypes.ReadNextInt(packetData) / 32.0D; - } - else - { - x = dataTypes.ReadNextDouble(packetData); - y = dataTypes.ReadNextDouble(packetData); - 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); - } - - break; - case PacketTypesIn.UpdateHealth: - float health = dataTypes.ReadNextFloat(packetData); - int food; - if (protocolVersion >= MC_1_8_Version) - food = dataTypes.ReadNextVarInt(packetData); - else - food = dataTypes.ReadNextShort(packetData); - dataTypes.ReadNextFloat(packetData); // Food Saturation - handler.OnUpdateHealth(health, food); - break; - case PacketTypesIn.SetExperience: - float experiencebar = dataTypes.ReadNextFloat(packetData); - int totalexperience, level; - level = dataTypes.ReadNextVarInt(packetData); - totalexperience = dataTypes.ReadNextVarInt(packetData); - handler.OnSetExperience(experiencebar, level, totalexperience); - break; - case PacketTypesIn.Explosion: - Location explosionLocation; - if (protocolVersion >= MC_1_19_3_Version) - explosionLocation = new(dataTypes.ReadNextDouble(packetData), - dataTypes.ReadNextDouble(packetData), dataTypes.ReadNextDouble(packetData)); - else - explosionLocation = new(dataTypes.ReadNextFloat(packetData), - dataTypes.ReadNextFloat(packetData), dataTypes.ReadNextFloat(packetData)); - - float explosionStrength = dataTypes.ReadNextFloat(packetData); - int explosionBlockCount = protocolVersion >= MC_1_17_Version - ? dataTypes.ReadNextVarInt(packetData) - : dataTypes.ReadNextInt(packetData); - - for (int i = 0; i < explosionBlockCount; i++) - dataTypes.ReadData(3, packetData); - - float playerVelocityX = dataTypes.ReadNextFloat(packetData); - float playerVelocityY = dataTypes.ReadNextFloat(packetData); - float playerVelocityZ = dataTypes.ReadNextFloat(packetData); - - handler.OnExplosion(explosionLocation, explosionStrength, explosionBlockCount); - break; - case PacketTypesIn.HeldItemChange: - byte slot = dataTypes.ReadNextByte(packetData); - handler.OnHeldItemChange(slot); - break; - case PacketTypesIn.ScoreboardObjective: - string objectivename = dataTypes.ReadNextString(packetData); - byte mode = dataTypes.ReadNextByte(packetData); - string objectivevalue = String.Empty; - int type2 = -1; - if (mode == 0 || mode == 2) - { - objectivevalue = dataTypes.ReadNextString(packetData); - type2 = dataTypes.ReadNextVarInt(packetData); - } - - handler.OnScoreboardObjective(objectivename, mode, objectivevalue, type2); - break; - case PacketTypesIn.UpdateScore: - string entityname = dataTypes.ReadNextString(packetData); - int action3 = protocolVersion >= MC_1_18_2_Version - ? dataTypes.ReadNextVarInt(packetData) - : dataTypes.ReadNextByte(packetData); - string objectivename2 = string.Empty; - int value = -1; - if (action3 != 1 || protocolVersion >= MC_1_8_Version) - objectivename2 = dataTypes.ReadNextString(packetData); - if (action3 != 1) - value = dataTypes.ReadNextVarInt(packetData); - handler.OnUpdateScore(entityname, action3, objectivename2, value); - break; - case PacketTypesIn.BlockChangedAck: - handler.OnBlockChangeAck(dataTypes.ReadNextVarInt(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); - handler.OnBlockBreakAnimation(playerId, blockLocation, stage); - } - - break; - case PacketTypesIn.EntityAnimation: - if (handler.GetEntityHandlingEnabled()) - { - int playerId2 = dataTypes.ReadNextVarInt(packetData); - byte animation = dataTypes.ReadNextByte(packetData); - handler.OnEntityAnimation(playerId2, animation); - } - - break; - - case PacketTypesIn.OpenSignEditor: - var signLocation = dataTypes.ReadNextLocation(packetData); - var isFrontText = true; - - if (protocolVersion >= MC_1_20_Version) - isFrontText = dataTypes.ReadNextBool(packetData); - - // TODO: Use - break; - - // Temporarily disabled until I find a fix - /*case PacketTypesIn.BlockEntityData: - var location_ = dataTypes.ReadNextLocation(packetData); - var type_ = dataTypes.ReadNextInt(packetData); - var nbt = dataTypes.ReadNextNbt(packetData); - var nbtJson = JsonConvert.SerializeObject(nbt["messages"]); - - //log.Info($"BLOCK ENTITY DATA -> {location_.ToString()} [{type_}] -> NBT: {nbtJson}"); - - break;*/ - - default: - return false; //Ignored packet - } - - return true; //Packet processed - } - catch (Exception innerException) - { - 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( - string.Format(Translations.exception_packet_process, - packetPalette.GetIncommingTypeById(packetID), - packetID, - protocolVersion, - login_phase, - innerException.GetType()), - innerException); - } - } - - /// - /// Start the updating thread. Should be called after login success. - /// - private void StartUpdating() - { - 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); - } - - /// - /// Get net read thread (main thread) ID - /// - /// Net read thread ID - public int GetNetMainThreadId() - { - return netMain != null ? netMain.Item1.ManagedThreadId : -1; - } - - /// - /// Disconnect from the server, cancel network reading. - /// - public void Dispose() - { - try - { - if (netMain != null) - { - netMain.Item2.Cancel(); - } - - if (netReader != null) - { - netReader.Item2.Cancel(); - socketWrapper.Disconnect(); - } - } - catch - { - } - } - - /// - /// Send a packet to the server. Packet ID, compression, and encryption will be handled automatically. - /// - /// packet type - /// packet Data - private void SendPacket(PacketTypesOut packet, IEnumerable packetData) - { - SendPacket(packetPalette.GetOutgoingIdByType(packet), packetData); - } - - /// - /// Send a packet to the server. Compression and encryption will be handled automatically. - /// - /// packet ID - /// packet Data - private void SendPacket(int packetID, IEnumerable packetData) - { - if (handler.GetNetworkPacketCaptureEnabled()) - { - List clone = packetData.ToList(); - handler.OnNetworkPacket(packetID, clone, 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 (the_packet.Length >= compression_treshold) //Packet long enough for compressing? - { - byte[] compressed_packet = ZlibUtils.Compress(the_packet); - the_packet = dataTypes.ConcatBytes(DataTypes.GetVarInt(the_packet.Length), compressed_packet); - } - else - { - byte[] uncompressed_length = DataTypes.GetVarInt(0); //Not compressed (short packet) - the_packet = dataTypes.ConcatBytes(uncompressed_length, the_packet); - } - } - - //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)); - } - - /// - /// Do the Minecraft login. - /// - /// True if login successful - public bool Login(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); - - List fullLoginPacket = new(); - fullLoginPacket.AddRange(dataTypes.GetString(handler.GetUsername())); // Username - - // 1.19 - 1.19.2 - if (protocolVersion >= MC_1_19_Version && protocolVersion < MC_1_19_3_Version) - { - if (playerKeyPair == null) - fullLoginPacket.AddRange(dataTypes.GetBool(false)); // Has Sig Data - else - { - fullLoginPacket.AddRange(dataTypes.GetBool(true)); // Has Sig Data - fullLoginPacket.AddRange( - DataTypes.GetLong(playerKeyPair.GetExpirationMilliseconds())); // Expiration time - fullLoginPacket.AddRange( - dataTypes.GetArray(playerKeyPair.PublicKey.Key)); // Public key received from Microsoft API - if (protocolVersion >= MC_1_19_2_Version) - fullLoginPacket.AddRange( - dataTypes.GetArray(playerKeyPair.PublicKey - .SignatureV2!)); // Public key signature received from Microsoft API - else - fullLoginPacket.AddRange( - dataTypes.GetArray(playerKeyPair.PublicKey - .Signature!)); // Public key signature received from Microsoft API - } - } - - if (protocolVersion >= MC_1_19_2_Version) - { - Guid uuid = handler.GetUserUuid(); - - if (uuid == Guid.Empty) - fullLoginPacket.AddRange(dataTypes.GetBool(false)); // Has UUID - else - { - fullLoginPacket.AddRange(dataTypes.GetBool(true)); // Has UUID - fullLoginPacket.AddRange(DataTypes.GetUUID(uuid)); // UUID - } - } - - SendPacket(0x00, fullLoginPacket); - - while (true) - { - (int packetID, Queue packetData) = ReadNextPacket(); - if (packetID == 0x00) //Login rejected - { - handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, - ChatParser.ParseText(dataTypes.ReadNextString(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); - } - else if (packetID == 0x02) //Login successful - { - log.Info("§8" + Translations.mcc_server_offline); - login_phase = false; - - if (!pForge.CompleteForgeHandshake()) - { - log.Error("§8" + Translations.error_forge); - return false; - } - - StartUpdating(); - return true; //No need to check session or start encryption - } - else HandlePacket(packetID, packetData); - } - } - - /// - /// 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) - { - RSACryptoServiceProvider RSAService = CryptoHandler.DecodeRSAPublicKey(serverPublicKey)!; - byte[] secretKey = CryptoHandler.ClientAESPrivateKey ?? CryptoHandler.GenerateAESPrivateKey(); - - log.Debug("§8" + Translations.debug_crypto); - - if (serverIDhash != "-") - { - 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 (ProtocolHandler.SessionCheck(uuid, sessionID, serverHash)) - { - session.ServerIDhash = serverIDhash; - session.ServerPublicKey = serverPublicKey; - SessionCache.Store(InternalConfig.Account.Login.ToLower(), session); - } - else - { - handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, Translations.mcc_session_fail); - return false; - } - } - } - - // Encryption Response packet - List encryptionResponse = new(); - encryptionResponse.AddRange(dataTypes.GetArray(RSAService.Encrypt(secretKey, false))); // Shared Secret - - // 1.19 - 1.19.2 - if (protocolVersion >= MC_1_19_Version && protocolVersion < MC_1_19_3_Version) - { - if (playerKeyPair == null) - { - encryptionResponse.AddRange(dataTypes.GetBool(true)); // Has Verify Token - encryptionResponse.AddRange(dataTypes.GetArray(RSAService.Encrypt(token, false))); // Verify Token - } - else - { - byte[] salt = GenerateSalt(); - byte[] messageSignature = playerKeyPair.PrivateKey.SignData(dataTypes.ConcatBytes(token, salt)); - - encryptionResponse.AddRange(dataTypes.GetBool(false)); // Has Verify Token - encryptionResponse.AddRange(salt); // Salt - encryptionResponse.AddRange(dataTypes.GetArray(messageSignature)); // Message Signature - } - } - else - { - encryptionResponse.AddRange(dataTypes.GetArray(RSAService.Encrypt(token, false))); // Verify Token - } - - SendPacket(0x01, encryptionResponse); - - //Start client-side encryption - socketWrapper.SwitchToEncrypted(secretKey); // pre switch - - //Process the next packet - int loopPrevention = UInt16.MaxValue; - while (true) - { - (int packetID, Queue packetData) = ReadNextPacket(); - 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); - return false; - } - else if (packetID == 0x00) //Login rejected - { - handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, - ChatParser.ParseText(dataTypes.ReadNextString(packetData))); - return false; - } - else if (packetID == 0x02) //Login successful - { - Guid uuidReceived; - if (protocolVersion >= Protocol18Handler.MC_1_16_Version) - uuidReceived = dataTypes.ReadNextUUID(packetData); - else - uuidReceived = Guid.Parse(dataTypes.ReadNextString(packetData)); - string userName = dataTypes.ReadNextString(packetData); - Tuple[]? playerProperty = null; - if (protocolVersion >= Protocol18Handler.MC_1_19_Version) - { - int count = dataTypes.ReadNextVarInt(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; - playerProperty[i] = new Tuple(name, value, signature); - } - } - - handler.OnLoginSuccess(uuidReceived, userName, playerProperty); - - login_phase = false; - - if (!pForge.CompleteForgeHandshake()) - { - log.Error("§8" + Translations.error_forge_encrypt); - return false; - } - - StartUpdating(); - return true; - } - else HandlePacket(packetID, packetData); - } - } - - /// - /// Disconnect from the server - /// - public void Disconnect() - { - socketWrapper.Disconnect(); - } - - /// - /// Autocomplete text while typing username or command - /// - /// Text behind cursor - /// Completed text - int IAutoComplete.AutoComplete(string BehindCursor) - { - if (string.IsNullOrEmpty(BehindCursor)) - return -1; - - byte[] transaction_id = DataTypes.GetVarInt(autocomplete_transaction_id); - byte[] assume_command = new byte[] { 0x00 }; - byte[] has_position = new byte[] { 0x00 }; - - byte[] tabcomplete_packet = Array.Empty(); - - if (protocolVersion >= MC_1_8_Version) - { - if (protocolVersion >= MC_1_13_Version) - { - tabcomplete_packet = dataTypes.ConcatBytes(tabcomplete_packet, transaction_id); - tabcomplete_packet = dataTypes.ConcatBytes(tabcomplete_packet, - dataTypes.GetString(BehindCursor.Replace(' ', (char)0x00))); - } - else - { - tabcomplete_packet = dataTypes.ConcatBytes(tabcomplete_packet, dataTypes.GetString(BehindCursor)); - - if (protocolVersion >= MC_1_9_Version) - tabcomplete_packet = dataTypes.ConcatBytes(tabcomplete_packet, assume_command); - - tabcomplete_packet = dataTypes.ConcatBytes(tabcomplete_packet, has_position); - } - } - else - { - tabcomplete_packet = dataTypes.ConcatBytes(dataTypes.GetString(BehindCursor)); - } - - ConsoleIO.AutoCompleteDone = false; - SendPacket(PacketTypesOut.TabComplete, tabcomplete_packet); - return autocomplete_transaction_id; - } - - /// - /// 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) - { - string version = ""; - TcpClient tcp = ProxyHandler.NewTcpClient(host, port); - tcp.ReceiveTimeout = 30000; // 30 seconds - tcp.ReceiveBufferSize = 1024 * 1024; - SocketWrapper socketWrapper = new(tcp); - DataTypes dataTypes = new(MC_1_8_Version); - - byte[] packet_id = DataTypes.GetVarInt(0); - byte[] protocol_version = DataTypes.GetVarInt(-1); - byte[] server_port = BitConverter.GetBytes((ushort)port); - Array.Reverse(server_port); - byte[] next_state = DataTypes.GetVarInt(1); - 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); - - byte[] status_request = DataTypes.GetVarInt(0); - byte[] request_packet = dataTypes.ConcatBytes(DataTypes.GetVarInt(status_request.Length), status_request); - - socketWrapper.SendDataRAW(request_packet); - - int packetLength = dataTypes.ReadNextVarIntRAW(socketWrapper); - if (packetLength > 0) //Read Response length - { - Queue packetData = new(socketWrapper.ReadDataRAW(packetLength)); - if (dataTypes.ReadNextVarInt(packetData) == 0x00) //Read Packet ID - { - string result = dataTypes.ReadNextString(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 (!String.IsNullOrEmpty(result) && result.StartsWith("{") && result.EndsWith("}")) - { - Json.JSONData jsonData = Json.ParseJson(result); - if (jsonData.Type == Json.JSONData.DataType.Object && - jsonData.Properties.ContainsKey("version")) - { - Json.JSONData versionData = jsonData.Properties["version"]; - - //Retrieve display name of the Minecraft version - if (versionData.Properties.ContainsKey("name")) - version = versionData.Properties["name"].StringValue; - - //Retrieve protocol version number for handling this server - if (versionData.Properties.ContainsKey("protocol")) - protocolVersion = int.Parse(versionData.Properties["protocol"].StringValue, - NumberStyles.Any, CultureInfo.CurrentCulture); - - // Check for forge on the server. - Protocol18Forge.ServerInfoCheckForge(jsonData, ref forgeInfo); - - ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_server_protocol, version, - protocolVersion + (forgeInfo != null ? Translations.mcc_with_forge : ""))); - - return true; - } - } - } - } - - return false; - } - - /// - /// Get max length for chat messages - /// - /// Max length, in characters - public int GetMaxChatMessageLength() - { - return protocolVersion > MC_1_10_Version - ? 256 - : 100; - } - - /// - /// Get the current protocol version. - /// - /// - /// Version-specific operations should be handled inside the Protocol handled whenever possible. - /// - /// Minecraft Protocol version number - public int GetProtocolVersion() - { - return protocolVersion; - } - - /// - /// Send MessageAcknowledgment packet - /// - /// Message acknowledgment - /// True if properly sent - public bool SendMessageAcknowledgment(LastSeenMessageList.Acknowledgment acknowledgment) - { - try - { - byte[] fields = dataTypes.GetAcknowledgment(acknowledgment, - isOnlineMode && Config.Signature.LoginWithSecureProfile); - - SendPacket(PacketTypesOut.MessageAcknowledgment, fields); - - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - /// - /// Send MessageAcknowledgment packet - /// - /// Message acknowledgment - /// True if properly sent - public bool SendMessageAcknowledgment(int messageCount) - { - try - { - byte[] fields = DataTypes.GetVarInt(messageCount); - - SendPacket(PacketTypesOut.MessageAcknowledgment, fields); - - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - public LastSeenMessageList.Acknowledgment ConsumeAcknowledgment() - { - pendingAcknowledgments = 0; - return new LastSeenMessageList.Acknowledgment(lastSeenMessagesCollector.GetLastSeenMessages(), - lastReceivedMessage); - } - - public void Acknowledge(ChatMessage message) - { - LastSeenMessageList.AcknowledgedMessage? entry = message.ToLastSeenMessageEntry(); - - if (entry != null) - { - if (protocolVersion >= MC_1_19_3_Version) - { - if (lastSeenMessagesCollector.Add_1_19_3(entry, true)) - { - if (lastSeenMessagesCollector.messageCount > 64) - { - int messageCount = lastSeenMessagesCollector.ResetMessageCount(); - if (messageCount > 0) - SendMessageAcknowledgment(messageCount); - } - } - } - else - { - lastSeenMessagesCollector.Add_1_19_2(entry); - lastReceivedMessage = null; - if (pendingAcknowledgments++ > 64) - SendMessageAcknowledgment(ConsumeAcknowledgment()); - } - } - } - - /// - /// Send a chat command to the server - 1.19 and above - /// - /// Command - /// PlayerKeyPair - /// True if properly sent - public bool SendChatCommand(string command, PlayerKeyPair? playerKeyPair) - { - if (String.IsNullOrEmpty(command)) - return true; - - command = Regex.Replace(command, @"\s+", " "); - command = Regex.Replace(command, @"\s$", string.Empty); - - log.Debug("chat command = " + command); - - try - { - List>? needSigned = null; // List< Argument Name, Argument Value > - if (playerKeyPair != null && isOnlineMode && protocolVersion >= MC_1_19_Version - && Config.Signature.LoginWithSecureProfile && Config.Signature.SignMessageInCommand) - needSigned = DeclareCommands.CollectSignArguments(command); - - lock (MessageSigningLock) - { - LastSeenMessageList.Acknowledgment? acknowledgment_1_19_2 = - (protocolVersion == MC_1_19_2_Version) ? ConsumeAcknowledgment() : null; - - (LastSeenMessageList.AcknowledgedMessage[] acknowledgment_1_19_3, byte[] bitset_1_19_3, - int messageCount_1_19_3) = - (protocolVersion >= MC_1_19_3_Version) - ? lastSeenMessagesCollector.Collect_1_19_3() - : new(Array.Empty(), Array.Empty(), 0); - - List fields = new(); - - // Command: String - fields.AddRange(dataTypes.GetString(command)); - - // Timestamp: Instant(Long) - DateTimeOffset timeNow = DateTimeOffset.UtcNow; - fields.AddRange(DataTypes.GetLong(timeNow.ToUnixTimeMilliseconds())); - - if (needSigned == null || needSigned!.Count == 0) - { - fields.AddRange(DataTypes.GetLong(0)); // Salt: Long - fields.AddRange(DataTypes.GetVarInt(0)); // Signature Length: VarInt - } - else - { - Guid uuid = handler.GetUserUuid(); - byte[] salt = GenerateSalt(); - fields.AddRange(salt); // Salt: Long - fields.AddRange(DataTypes.GetVarInt(needSigned.Count)); // Signature Length: VarInt - foreach ((string argName, string message) in needSigned) - { - fields.AddRange(dataTypes.GetString(argName)); // Argument name: String - - byte[] sign; - if (protocolVersion == MC_1_19_Version) - sign = playerKeyPair!.PrivateKey.SignMessage(message, uuid, timeNow, ref salt); - else if (protocolVersion == MC_1_19_2_Version) - sign = playerKeyPair!.PrivateKey.SignMessage(message, uuid, timeNow, ref salt, - acknowledgment_1_19_2!.lastSeen); - else // protocolVersion >= MC_1_19_3_Version - sign = playerKeyPair!.PrivateKey.SignMessage(message, uuid, chatUuid, messageIndex++, - timeNow, ref salt, acknowledgment_1_19_3); - - if (protocolVersion <= MC_1_19_2_Version) - fields.AddRange(DataTypes.GetVarInt(sign.Length)); // Signature length: VarInt - - fields.AddRange(sign); // Signature: Byte Array - } - } - - if (protocolVersion <= MC_1_19_2_Version) - fields.AddRange(dataTypes.GetBool(false)); // Signed Preview: Boolean - - if (protocolVersion == MC_1_19_2_Version) - { - // Message Acknowledgment (1.19.2) - fields.AddRange(dataTypes.GetAcknowledgment(acknowledgment_1_19_2!, - isOnlineMode && Config.Signature.LoginWithSecureProfile)); - } - else if (protocolVersion >= MC_1_19_3_Version) - { - // message count - fields.AddRange(DataTypes.GetVarInt(messageCount_1_19_3)); - - // Acknowledged: BitSet - fields.AddRange(bitset_1_19_3); - } - - SendPacket(PacketTypesOut.ChatCommand, fields); - } - - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - /// - /// Send a chat message to the server - /// - /// Message - /// PlayerKeyPair - /// True if properly sent - public bool SendChatMessage(string message, PlayerKeyPair? playerKeyPair) - { - 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); - - try - { - List fields = new(); - - // Message: String (up to 256 chars) - fields.AddRange(dataTypes.GetString(message)); - - if (protocolVersion >= MC_1_19_Version) - { - lock (MessageSigningLock) - { - LastSeenMessageList.Acknowledgment? acknowledgment_1_19_2 = - (protocolVersion == MC_1_19_2_Version) ? ConsumeAcknowledgment() : null; - - (LastSeenMessageList.AcknowledgedMessage[] acknowledgment_1_19_3, byte[] bitset_1_19_3, - int messageCount_1_19_3) = - (protocolVersion >= MC_1_19_3_Version) - ? lastSeenMessagesCollector.Collect_1_19_3() - : new(Array.Empty(), Array.Empty(), 0); - - // Timestamp: Instant(Long) - DateTimeOffset timeNow = DateTimeOffset.UtcNow; - fields.AddRange(DataTypes.GetLong(timeNow.ToUnixTimeMilliseconds())); - - if (!isOnlineMode || playerKeyPair == null || !Config.Signature.LoginWithSecureProfile || - !Config.Signature.SignChat) - { - fields.AddRange(DataTypes.GetLong(0)); // Salt: Long - if (protocolVersion < MC_1_19_3_Version) - fields.AddRange(DataTypes.GetVarInt(0)); // Signature Length: VarInt (1.19 - 1.19.2) - else - fields.AddRange(dataTypes.GetBool(false)); // Has signature: bool (1.19.3) - } - else - { - // Salt: Long - byte[] salt = GenerateSalt(); - fields.AddRange(salt); - - // Signature Length & Signature: (VarInt) and Byte Array - Guid playerUuid = handler.GetUserUuid(); - byte[] sign; - if (protocolVersion == MC_1_19_Version) // 1.19.1 or lower - sign = playerKeyPair.PrivateKey.SignMessage(message, playerUuid, timeNow, ref salt); - else if (protocolVersion == MC_1_19_2_Version) // 1.19.2 - sign = playerKeyPair.PrivateKey.SignMessage(message, playerUuid, timeNow, ref salt, - acknowledgment_1_19_2!.lastSeen); - else // protocolVersion >= MC_1_19_3_Version - sign = playerKeyPair.PrivateKey.SignMessage(message, playerUuid, chatUuid, - messageIndex++, timeNow, ref salt, acknowledgment_1_19_3); - - if (protocolVersion >= MC_1_19_3_Version) - fields.AddRange(dataTypes.GetBool(true)); - else - fields.AddRange(DataTypes.GetVarInt(sign.Length)); - fields.AddRange(sign); - } - - if (protocolVersion <= MC_1_19_2_Version) - fields.AddRange(dataTypes.GetBool(false)); // Signed Preview: Boolean - - if (protocolVersion >= MC_1_19_3_Version) - { - // message count - fields.AddRange(DataTypes.GetVarInt(messageCount_1_19_3)); - - // Acknowledged: BitSet - fields.AddRange(bitset_1_19_3); - } - else if (protocolVersion == MC_1_19_2_Version) - { - // Message Acknowledgment - fields.AddRange(dataTypes.GetAcknowledgment(acknowledgment_1_19_2!, - isOnlineMode && Config.Signature.LoginWithSecureProfile)); - } - } - } - - SendPacket(PacketTypesOut.ChatMessage, fields); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - public bool SendEntityAction(int PlayerEntityID, int ActionID) - { - try - { - List fields = new(); - fields.AddRange(DataTypes.GetVarInt(PlayerEntityID)); - fields.AddRange(DataTypes.GetVarInt(ActionID)); - fields.AddRange(DataTypes.GetVarInt(0)); - SendPacket(PacketTypesOut.EntityAction, fields); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - /// - /// Send a respawn packet to the server - /// - /// True if properly sent - public bool SendRespawnPacket() - { - try - { - SendPacket(PacketTypesOut.ClientStatus, new byte[] { 0 }); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - /// - /// Tell the server what client is being used to connect to the server - /// - /// Client string describing the client - /// True if brand info was successfully sent - public bool SendBrandInfo(string 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)); - } - else - { - return SendPluginChannelPacket("MC|Brand", dataTypes.GetString(brandInfo)); - } - } - - /// - /// Inform the server of the client's Minecraft settings - /// - /// Client language eg en_US - /// View distance, in chunks - /// Game difficulty (client-side...) - /// Chat mode (allows muting yourself) - /// Show chat colors - /// 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) - { - try - { - List fields = new(); - fields.AddRange(dataTypes.GetString(language)); - fields.Add(viewDistance); - - if (protocolVersion >= MC_1_9_Version) - fields.AddRange(DataTypes.GetVarInt(chatMode)); - else - fields.AddRange(new byte[] { chatMode }); - - fields.Add(chatColors ? (byte)1 : (byte)0); - if (protocolVersion < MC_1_8_Version) - { - fields.Add(difficulty); - fields.Add((byte)(skinParts & 0x1)); //show cape - } - else fields.Add(skinParts); - - if (protocolVersion >= MC_1_9_Version) - fields.AddRange(DataTypes.GetVarInt(mainHand)); - if (protocolVersion >= MC_1_17_Version) - { - if (protocolVersion >= MC_1_18_1_Version) - fields.Add(0); // 1.18 and above - Enable text filtering. (Always false) - else - fields.Add(1); // 1.17 and 1.17.1 - Disable text filtering. (Always true) - } - - if (protocolVersion >= MC_1_18_1_Version) - fields.Add(1); // 1.18 and above - Allow server listings - SendPacket(PacketTypesOut.ClientSettings, fields); - } - catch (SocketException) - { - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - - return false; - } - - - /// - /// Send a location update to the server - /// - /// The new location of the player - /// True if the player is on the ground - /// 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) - { - return SendLocationUpdate(location, onGround, yaw, pitch, true); - } - - public bool SendLocationUpdate(Location location, bool onGround, float? yaw = null, float? pitch = null, - bool forceUpdate = false) - { - if (handler.GetTerrainEnabled()) - { - byte[] yawpitch = Array.Empty(); - PacketTypesOut packetType = PacketTypesOut.PlayerPosition; - - if (Config.Main.Advanced.TemporaryFixBadpacket) - { - if (yaw.HasValue && pitch.HasValue && - (forceUpdate || yaw.Value != LastYaw || pitch.Value != LastPitch)) - { - yawpitch = dataTypes.ConcatBytes(dataTypes.GetFloat(yaw.Value), - dataTypes.GetFloat(pitch.Value)); - packetType = PacketTypesOut.PlayerPositionAndRotation; - - LastYaw = yaw.Value; - LastPitch = pitch.Value; - } - } - else - { - if (yaw.HasValue && pitch.HasValue) - { - yawpitch = dataTypes.ConcatBytes(dataTypes.GetFloat(yaw.Value), - dataTypes.GetFloat(pitch.Value)); - packetType = PacketTypesOut.PlayerPositionAndRotation; - - LastYaw = yaw.Value; - LastPitch = pitch.Value; - } - } - - try - { - SendPacket(packetType, dataTypes.ConcatBytes( - dataTypes.GetDouble(location.X), - dataTypes.GetDouble(location.Y), - protocolVersion < MC_1_8_Version - ? dataTypes.GetDouble(location.Y + 1.62) - : Array.Empty(), - dataTypes.GetDouble(location.Z), - yawpitch, - new byte[] { onGround ? (byte)1 : (byte)0 })); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - else return false; - } - - /// - /// Send a plugin channel packet (0x17) to the server, compression and encryption will be handled automatically - /// - /// Channel to send packet on - /// packet Data - public bool SendPluginChannelPacket(string channel, byte[] data) - { - try - { - // In 1.7, length needs to be included. - // In 1.8, it must not be. - if (protocolVersion < MC_1_8_Version) - { - byte[] length = BitConverter.GetBytes((short)data.Length); - Array.Reverse(length); - - SendPacket(PacketTypesOut.PluginMessage, - dataTypes.ConcatBytes(dataTypes.GetString(channel), length, data)); - } - else - { - SendPacket(PacketTypesOut.PluginMessage, dataTypes.ConcatBytes(dataTypes.GetString(channel), data)); - } - - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - /// - /// Send a Login Plugin Response packet (0x02) - /// - /// Login Plugin Request message Id - /// TRUE if the request was understood - /// Response to the request - /// TRUE if successfully sent - public bool SendLoginPluginResponse(int messageId, bool understood, byte[] data) - { - try - { - SendPacket(0x02, - dataTypes.ConcatBytes(DataTypes.GetVarInt(messageId), dataTypes.GetBool(understood), data)); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - /// - /// Send an Interact Entity Packet to server - /// - /// - /// - /// - public bool SendInteractEntity(int EntityID, int type) - { - try - { - List fields = new(); - fields.AddRange(DataTypes.GetVarInt(EntityID)); - fields.AddRange(DataTypes.GetVarInt(type)); - - // Is player Sneaking (Only 1.16 and above) - // Currently hardcoded to false - // TODO: Update to reflect the real player state - if (protocolVersion >= MC_1_16_Version) - fields.AddRange(dataTypes.GetBool(false)); - - SendPacket(PacketTypesOut.InteractEntity, fields); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.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) - { - try - { - List fields = new(); - fields.AddRange(DataTypes.GetVarInt(EntityID)); - fields.AddRange(DataTypes.GetVarInt(type)); - fields.AddRange(dataTypes.GetFloat(X)); - fields.AddRange(dataTypes.GetFloat(Y)); - fields.AddRange(dataTypes.GetFloat(Z)); - fields.AddRange(DataTypes.GetVarInt(hand)); - // Is player Sneaking (Only 1.16 and above) - // Currently hardcoded to false - // TODO: Update to reflect the real player state - if (protocolVersion >= MC_1_16_Version) - fields.AddRange(dataTypes.GetBool(false)); - SendPacket(PacketTypesOut.InteractEntity, fields); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - public bool SendInteractEntity(int EntityID, int type, int hand) - { - try - { - List fields = new(); - fields.AddRange(DataTypes.GetVarInt(EntityID)); - fields.AddRange(DataTypes.GetVarInt(type)); - fields.AddRange(DataTypes.GetVarInt(hand)); - // Is player Sneaking (Only 1.16 and above) - // Currently hardcoded to false - // TODO: Update to reflect the real player state - if (protocolVersion >= MC_1_16_Version) - fields.AddRange(dataTypes.GetBool(false)); - SendPacket(PacketTypesOut.InteractEntity, fields); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - public bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z) - { - return false; - } - - public bool SendUseItem(int hand, int sequenceId) - { - if (protocolVersion < MC_1_9_Version) - return false; // Packet does not exist prior to MC 1.9 - // According to https://wiki.vg/index.php?title=Protocol&oldid=5486#Player_Block_Placement - // MC 1.7 does this using Player Block Placement with special values - // TODO once Player Block Placement is implemented for older versions - try - { - List packet = new(); - packet.AddRange(DataTypes.GetVarInt(hand)); - if (protocolVersion >= MC_1_19_Version) - packet.AddRange(DataTypes.GetVarInt(sequenceId)); - SendPacket(PacketTypesOut.UseItem, packet); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - public bool SendPlayerDigging(int status, Location location, Direction face, int sequenceId) - { - try - { - List packet = new(); - packet.AddRange(DataTypes.GetVarInt(status)); - packet.AddRange(dataTypes.GetLocation(location)); - packet.AddRange(DataTypes.GetVarInt(dataTypes.GetBlockFace(face))); - if (protocolVersion >= MC_1_19_Version) - packet.AddRange(DataTypes.GetVarInt(sequenceId)); - SendPacket(PacketTypesOut.PlayerDigging, packet); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - public bool SendPlayerBlockPlacement(int hand, Location location, Direction face, int sequenceId) - { - if (protocolVersion < MC_1_14_Version) - { - Container? playerInventory = handler.GetInventory(0); - - if (playerInventory == null) - return false; - - List packet = new List(); - - packet.AddRange(dataTypes.GetLocation(location)); - packet.Add(dataTypes.GetBlockFace(face)); - - Item item = playerInventory.Items[((McClient)handler).GetCurrentSlot()]; - packet.AddRange(dataTypes.GetItemSlot(item, itemPalette)); - - packet.Add((byte)0); // cursorX - packet.Add((byte)0); // cursorY - packet.Add((byte)0); // cursorZ - - SendPacket(PacketTypesOut.PlayerBlockPlacement, packet); - return true; - } - - try - { - List packet = new List(); - packet.AddRange(DataTypes.GetVarInt(hand)); - packet.AddRange(dataTypes.GetLocation(location)); - packet.AddRange(DataTypes.GetVarInt(dataTypes.GetBlockFace(face))); - packet.AddRange(dataTypes.GetFloat(0.5f)); // cursorX - packet.AddRange(dataTypes.GetFloat(0.5f)); // cursorY - packet.AddRange(dataTypes.GetFloat(0.5f)); // cursorZ - packet.Add(0); // insideBlock = false; - if (protocolVersion >= MC_1_19_Version) - packet.AddRange(DataTypes.GetVarInt(sequenceId)); - SendPacket(PacketTypesOut.PlayerBlockPlacement, packet); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - public bool SendHeldItemChange(short slot) - { - try - { - List packet = new(); - packet.AddRange(dataTypes.GetShort(slot)); - SendPacket(PacketTypesOut.HeldItemChange, packet); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - public bool SendWindowAction(int windowId, int slotId, WindowActionType action, Item? item, - List> changedSlots, int stateId) - { - try - { - short actionNumber; - lock (window_actions) - { - if (!window_actions.ContainsKey(windowId)) - window_actions[windowId] = 0; - actionNumber = (short)(window_actions[windowId] + 1); - window_actions[windowId] = actionNumber; - } - - byte button = 0; - byte mode = 0; - - switch (action) - { - case WindowActionType.LeftClick: - button = 0; - break; - case WindowActionType.RightClick: - button = 1; - break; - case WindowActionType.MiddleClick: - button = 2; - mode = 3; - break; - case WindowActionType.ShiftClick: - button = 0; - mode = 1; - item = new Item(ItemType.Null, 0, null); - break; - case WindowActionType.DropItem: - button = 0; - mode = 4; - item = new Item(ItemType.Null, 0, null); - break; - case WindowActionType.DropItemStack: - button = 1; - mode = 4; - item = new Item(ItemType.Null, 0, null); - break; - case WindowActionType.StartDragLeft: - button = 0; - mode = 5; - item = new Item(ItemType.Null, 0, null); - slotId = -999; - break; - case WindowActionType.StartDragRight: - button = 4; - mode = 5; - item = new Item(ItemType.Null, 0, null); - slotId = -999; - break; - case WindowActionType.StartDragMiddle: - button = 8; - mode = 5; - item = new Item(ItemType.Null, 0, null); - slotId = -999; - break; - case WindowActionType.EndDragLeft: - button = 2; - mode = 5; - item = new Item(ItemType.Null, 0, null); - slotId = -999; - break; - case WindowActionType.EndDragRight: - button = 6; - mode = 5; - item = new Item(ItemType.Null, 0, null); - slotId = -999; - break; - case WindowActionType.EndDragMiddle: - button = 10; - mode = 5; - item = new Item(ItemType.Null, 0, null); - slotId = -999; - break; - case WindowActionType.AddDragLeft: - button = 1; - mode = 5; - item = new Item(ItemType.Null, 0, null); - break; - case WindowActionType.AddDragRight: - button = 5; - mode = 5; - item = new Item(ItemType.Null, 0, null); - break; - case WindowActionType.AddDragMiddle: - button = 9; - mode = 5; - item = new Item(ItemType.Null, 0, null); - break; - } - - List packet = new() - { - (byte)windowId // Window ID - }; - - // 1.18+ - if (protocolVersion >= MC_1_18_1_Version) - { - packet.AddRange(DataTypes.GetVarInt(stateId)); // State ID - packet.AddRange(dataTypes.GetShort((short)slotId)); // Slot ID - } - // 1.17.1 - else if (protocolVersion == MC_1_17_1_Version) - { - packet.AddRange(dataTypes.GetShort((short)slotId)); // Slot ID - packet.AddRange(DataTypes.GetVarInt(stateId)); // State ID - } - // Older - else - { - packet.AddRange(dataTypes.GetShort((short)slotId)); // Slot ID - } - - packet.Add(button); // Button - - if (protocolVersion < MC_1_17_Version) - packet.AddRange(dataTypes.GetShort(actionNumber)); - - if (protocolVersion >= MC_1_9_Version) - packet.AddRange(DataTypes.GetVarInt(mode)); // Mode - else packet.Add(mode); - - // 1.17+ Array of changed slots - if (protocolVersion >= MC_1_17_Version) - { - packet.AddRange(DataTypes.GetVarInt(changedSlots.Count)); // Length of the array - foreach (var slot in changedSlots) - { - packet.AddRange(dataTypes.GetShort(slot.Item1)); // slot ID - packet.AddRange(dataTypes.GetItemSlot(slot.Item2, itemPalette)); // slot Data - } - } - - packet.AddRange(dataTypes.GetItemSlot(item, itemPalette)); // Carried item (Clicked item) - - SendPacket(PacketTypesOut.ClickWindow, packet); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - public bool 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); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - public bool ClickContainerButton(int windowId, int buttonId) - { - try - { - List packet = new(); - packet.Add((byte)windowId); - packet.Add((byte)buttonId); - SendPacket(PacketTypesOut.ClickWindowButton, packet); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - public bool SendAnimation(int animation, int playerid) - { - try - { - if (animation == 0 || animation == 1) - { - List packet = new(); - - if (protocolVersion < MC_1_8_Version) - { - packet.AddRange(DataTypes.GetInt(playerid)); - packet.Add((byte)1); // Swing arm - } - else if (protocolVersion < MC_1_9_Version) - { - // No fields in 1.8.X - } - else // MC 1.9+ - { - packet.AddRange(DataTypes.GetVarInt(animation)); - } - - SendPacket(PacketTypesOut.Animation, packet); - return true; - } - else - { - return false; - } - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - public bool SendCloseWindow(int windowId) - { - try - { - lock (window_actions) - { - if (window_actions.ContainsKey(windowId)) - window_actions[windowId] = 0; - } - - SendPacket(PacketTypesOut.CloseWindow, new[] { (byte)windowId }); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - public bool SendUpdateSign(Location sign, string line1, string line2, string line3, string line4, bool isFrontText = true) - { - try - { - if (line1.Length > 23) - line1 = line1[..23]; - if (line2.Length > 23) - line2 = line1[..23]; - if (line3.Length > 23) - line3 = line1[..23]; - if (line4.Length > 23) - line4 = line1[..23]; - - List packet = new(); - packet.AddRange(dataTypes.GetLocation(sign)); - if(protocolVersion >= MC_1_20_Version) - packet.AddRange(dataTypes.GetBool((isFrontText))); - packet.AddRange(dataTypes.GetString(line1)); - packet.AddRange(dataTypes.GetString(line2)); - packet.AddRange(dataTypes.GetString(line3)); - packet.AddRange(dataTypes.GetString(line4)); - SendPacket(PacketTypesOut.UpdateSign, packet); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - public bool UpdateCommandBlock(Location location, string command, CommandBlockMode mode, - CommandBlockFlags flags) - { - if (protocolVersion <= MC_1_13_Version) - { - try - { - List packet = new(); - packet.AddRange(dataTypes.GetLocation(location)); - packet.AddRange(dataTypes.GetString(command)); - packet.AddRange(DataTypes.GetVarInt((int)mode)); - packet.Add((byte)flags); - SendPacket(PacketTypesOut.UpdateSign, packet); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - else - { - return false; - } - } - - public bool SendWindowConfirmation(byte windowID, short actionID, bool accepted) - { - try - { - List packet = new(); - packet.Add(windowID); - packet.AddRange(dataTypes.GetShort(actionID)); - packet.Add(accepted ? (byte)1 : (byte)0); - SendPacket(PacketTypesOut.WindowConfirmation, packet); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - public bool SelectTrade(int selectedSlot) - { - // MC 1.13 or greater - if (protocolVersion >= MC_1_13_Version) - { - try - { - List packet = new(); - packet.AddRange(DataTypes.GetVarInt(selectedSlot)); - SendPacket(PacketTypesOut.SelectTrade, packet); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - else - { - return false; - } - } - - public bool SendSpectate(Guid UUID) - { - // MC 1.8 or greater - if (protocolVersion >= MC_1_8_Version) - { - try - { - List packet = new(); - packet.AddRange(DataTypes.GetUUID(UUID)); - SendPacket(PacketTypesOut.Spectate, packet); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - else - { - return false; - } - } - - public bool SendPlayerSession(PlayerKeyPair? playerKeyPair) - { - if (playerKeyPair == null || !isOnlineMode) - return false; - - if (protocolVersion >= MC_1_19_3_Version) - { - try - { - List packet = new(); - - packet.AddRange(DataTypes.GetUUID(chatUuid)); - packet.AddRange(DataTypes.GetLong(playerKeyPair.GetExpirationMilliseconds())); - packet.AddRange(DataTypes.GetVarInt(playerKeyPair.PublicKey.Key.Length)); - packet.AddRange(playerKeyPair.PublicKey.Key); - packet.AddRange(DataTypes.GetVarInt(playerKeyPair.PublicKey.SignatureV2!.Length)); - packet.AddRange(playerKeyPair.PublicKey.SignatureV2); - - log.Debug( - $"SendPlayerSession MessageUUID = {chatUuid.ToString()}, len(PublicKey) = {playerKeyPair.PublicKey.Key.Length}, len(SignatureV2) = {playerKeyPair.PublicKey.SignatureV2!.Length}"); - - SendPacket(PacketTypesOut.PlayerSession, packet); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - else - { - return false; - } - } - - public bool SendRenameItem(string itemName) - { - try - { - List packet = new(); - packet.AddRange(dataTypes.GetString(itemName.Length > 50 ? itemName[..50] : itemName)); - SendPacket(PacketTypesOut.NameItem, packet); - return true; - } - catch (SocketException) - { - return false; - } - catch (System.IO.IOException) - { - return false; - } - catch (ObjectDisposedException) - { - return false; - } - } - - private byte[] GenerateSalt() - { - byte[] salt = new byte[8]; - randomGen.GetNonZeroBytes(salt); - return salt; - } - } +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using MinecraftClient.Crypto; +using MinecraftClient.Inventory; +using MinecraftClient.Inventory.ItemPalettes; +using MinecraftClient.Logger; +using MinecraftClient.Mapping; +using MinecraftClient.Mapping.BlockPalettes; +using MinecraftClient.Mapping.EntityPalettes; +using MinecraftClient.Protocol.Handlers.Forge; +using MinecraftClient.Protocol.Handlers.packet.s2c; +using MinecraftClient.Protocol.Handlers.PacketPalettes; +using MinecraftClient.Protocol.Message; +using MinecraftClient.Protocol.ProfileKey; +using MinecraftClient.Protocol.Session; +using MinecraftClient.Proxy; +using MinecraftClient.Scripting; +using Newtonsoft.Json; +using static MinecraftClient.Settings; + +namespace MinecraftClient.Protocol.Handlers +{ + /// + /// Implementation for Minecraft 1.8.X+ Protocols + /// + /// + /// Typical update steps for implementing protocol changes for a new Minecraft version: + /// - Perform a diff between latest supported version in MCC and new stable version to support on https://wiki.vg/Protocol + /// - If there are any changes in packets implemented by MCC, add MCXXXVersion field below and implement new packet layouts + /// - 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 + { + internal const int MC_1_8_Version = 47; + internal const int MC_1_9_Version = 107; + internal const int MC_1_9_1_Version = 108; + internal const int MC_1_10_Version = 210; + internal const int MC_1_11_Version = 315; + internal const int MC_1_11_2_Version = 316; + internal const int MC_1_12_Version = 335; + internal const int MC_1_12_2_Version = 340; + internal const int MC_1_13_Version = 393; + internal const int MC_1_13_2_Version = 404; + internal const int MC_1_14_Version = 477; + internal const int MC_1_15_Version = 573; + internal const int MC_1_15_2_Version = 578; + internal const int MC_1_16_Version = 735; + internal const int MC_1_16_1_Version = 736; + internal const int MC_1_16_2_Version = 751; + internal const int MC_1_16_3_Version = 753; + internal const int MC_1_16_5_Version = 754; + internal const int MC_1_17_Version = 755; + internal const int MC_1_17_1_Version = 756; + internal const int MC_1_18_1_Version = 757; + internal const int MC_1_18_2_Version = 758; + internal const int MC_1_19_Version = 759; + internal const int MC_1_19_2_Version = 760; + internal const int MC_1_19_3_Version = 761; + internal const int MC_1_19_4_Version = 762; + internal const int MC_1_20_Version = 763; + + private int compression_treshold = 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 bool receiveDeclareCommands = false, receivePlayerInfo = false; + private object MessageSigningLock = new(); + private Guid chatUuid = Guid.NewGuid(); + private int pendingAcknowledgments = 0, messageIndex = 0; + private LastSeenMessagesCollector lastSeenMessagesCollector; + private LastSeenMessageList.AcknowledgedMessage? lastReceivedMessage = null; + readonly Protocol18Forge pForge; + readonly Protocol18Terrain pTerrain; + readonly IMinecraftComHandler handler; + readonly EntityPalette entityPalette; + readonly EntityMetadataPalette entityMetadataPalette; + readonly ItemPalette itemPalette; + 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) + { + ConsoleIO.SetAutoCompleteEngine(this); + ChatParser.InitTranslations(); + socketWrapper = new SocketWrapper(Client); + dataTypes = new DataTypes(protocolVersion); + this.protocolVersion = protocolVersion; + this.handler = handler; + pForge = new Protocol18Forge(forgeInfo, protocolVersion, dataTypes, this, handler); + pTerrain = new Protocol18Terrain(protocolVersion, dataTypes, handler); + packetPalette = new PacketTypeHandler(protocolVersion, forgeInfo != null).GetTypeHandler(); + log = handler.GetLogger(); + randomGen = RandomNumberGenerator.Create(); + lastSeenMessagesCollector = protocolVersion >= MC_1_19_3_Version ? new(20) : new(5); + + if (handler.GetTerrainEnabled() && protocolVersion > MC_1_20_Version) + { + log.Error("§c" + Translations.extra_terrainandmovement_disabled); + handler.SetTerrainEnabled(false); + } + + if (handler.GetInventoryEnabled() && + protocolVersion is < MC_1_9_Version or > MC_1_20_Version) + { + log.Error("§c" + Translations.extra_inventory_disabled); + handler.SetInventoryEnabled(false); + } + + if (handler.GetEntityHandlingEnabled() && + protocolVersion is < MC_1_8_Version or > MC_1_20_Version) + { + log.Error("§c" + Translations.extra_entity_disabled); + handler.SetEntityHandlingEnabled(false); + } + + Block.Palette = protocolVersion switch + { + // Block palette + > MC_1_20_Version when handler.GetTerrainEnabled() => + throw new NotImplementedException(Translations.exception_palette_block), + MC_1_20_Version => new Palette120(), + MC_1_19_4_Version => new Palette1194(), + MC_1_19_3_Version => new Palette1193(), + >= MC_1_19_Version => new Palette119(), + >= MC_1_17_Version => new Palette117(), + >= MC_1_16_Version => new Palette116(), + >= MC_1_15_Version => new Palette115(), + >= MC_1_14_Version => new Palette114(), + >= MC_1_13_Version => new Palette113(), + _ => new Palette112() + }; + + entityPalette = protocolVersion switch + { + // Entity palette + > MC_1_20_Version when handler.GetEntityHandlingEnabled() => + throw new NotImplementedException(Translations.exception_palette_entity), + MC_1_20_Version => new EntityPalette120(), + MC_1_19_4_Version => new EntityPalette1194(), + MC_1_19_3_Version => new EntityPalette1193(), + >= MC_1_19_Version => new EntityPalette119(), + >= MC_1_17_Version => new EntityPalette117(), + >= MC_1_16_2_Version => new EntityPalette1162(), + >= MC_1_16_Version => new EntityPalette1161(), + >= MC_1_15_Version => new EntityPalette115(), + >= MC_1_14_Version => new EntityPalette114(), + >= MC_1_13_Version => new EntityPalette113(), + >= MC_1_12_Version => new EntityPalette112(), + _ => new EntityPalette18() + }; + + entityMetadataPalette = EntityMetadataPalette.GetPalette(protocolVersion); + + itemPalette = protocolVersion switch + { + // Item palette + > MC_1_20_Version when handler.GetInventoryEnabled() => + throw new NotImplementedException(Translations.exception_palette_item), + MC_1_20_Version => new ItemPalette120(), + MC_1_19_4_Version => new ItemPalette1194(), + MC_1_19_3_Version => new ItemPalette1193(), + >= MC_1_19_Version => new ItemPalette119(), + >= MC_1_18_1_Version => new ItemPalette118(), + >= MC_1_17_Version => new ItemPalette117(), + >= MC_1_16_2_Version => new ItemPalette1162(), + >= MC_1_16_1_Version => new ItemPalette1161(), + _ => new ItemPalette115() + }; + + // MessageType + // You can find it in https://wiki.vg/Protocol#Player_Chat_Message or /net/minecraft/network/message/MessageType.java + if (this.protocolVersion >= MC_1_19_2_Version) + ChatParser.ChatId2Type = new() + { + { 0, ChatParser.MessageType.CHAT }, + { 1, ChatParser.MessageType.SAY_COMMAND }, + { 2, ChatParser.MessageType.MSG_COMMAND_INCOMING }, + { 3, ChatParser.MessageType.MSG_COMMAND_OUTGOING }, + { 4, ChatParser.MessageType.TEAM_MSG_COMMAND_INCOMING }, + { 5, ChatParser.MessageType.TEAM_MSG_COMMAND_OUTGOING }, + { 6, ChatParser.MessageType.EMOTE_COMMAND }, + }; + else if (this.protocolVersion == MC_1_19_Version) + ChatParser.ChatId2Type = new() + { + { 0, ChatParser.MessageType.CHAT }, + { 1, ChatParser.MessageType.RAW_MSG }, + { 2, ChatParser.MessageType.RAW_MSG }, + { 3, ChatParser.MessageType.SAY_COMMAND }, + { 4, ChatParser.MessageType.MSG_COMMAND_INCOMING }, + { 5, ChatParser.MessageType.TEAM_MSG_COMMAND_INCOMING }, + { 6, ChatParser.MessageType.EMOTE_COMMAND }, + { 7, ChatParser.MessageType.RAW_MSG }, + }; + } + + /// + /// Separate thread. Network reading loop. + /// + private void Updater(object? o) + { + CancellationToken cancelToken = (CancellationToken)o!; + + if (cancelToken.IsCancellationRequested) + return; + + try + { + Stopwatch stopWatch = new(); + while (!packetQueue.IsAddingCompleted) + { + cancelToken.ThrowIfCancellationRequested(); + + handler.OnUpdate(); + stopWatch.Restart(); + + while (packetQueue.TryTake(out Tuple>? packetInfo)) + { + (int packetID, Queue packetData) = packetInfo; + HandlePacket(packetID, packetData); + + if (stopWatch.Elapsed.Milliseconds >= 100) + { + handler.OnUpdate(); + stopWatch.Restart(); + } + } + + int sleepLength = 100 - stopWatch.Elapsed.Milliseconds; + if (sleepLength > 0) + Thread.Sleep(sleepLength); + } + } + catch (ObjectDisposedException) + { + } + catch (OperationCanceledException) + { + } + catch (NullReferenceException) + { + } + + if (cancelToken.IsCancellationRequested) + return; + + handler.OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, ""); + } + + /// + /// Read and decompress packets. + /// + internal void PacketReader(object? o) + { + CancellationToken cancelToken = (CancellationToken)o!; + while (socketWrapper.IsConnected() && !cancelToken.IsCancellationRequested) + { + try + { + while (socketWrapper.HasDataAvailable()) + { + packetQueue.Add(ReadNextPacket()); + + if (cancelToken.IsCancellationRequested) + break; + } + } + catch (System.IO.IOException) + { + break; + } + catch (SocketException) + { + 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 + { + byte[] toDecompress = packetData.ToArray(); + byte[] uncompressed = ZlibUtils.Decompress(toDecompress, sizeUncompressed); + packetData = new(uncompressed); + } + } + + int packetID = dataTypes.ReadNextVarInt(packetData); //Packet ID + + if (handler.GetNetworkPacketCaptureEnabled()) + { + List clone = packetData.ToList(); + handler.OnNetworkPacket(packetID, clone, login_phase, true); + } + + return new(packetID, packetData); + } + + /// + /// Handle the given packet + /// + /// Packet ID + /// Packet contents + /// TRUE if the packet was processed, FALSE if ignored or unknown + internal bool HandlePacket(int packetID, Queue packetData) + { + try + { + if (login_phase) + { + switch (packetID) //Packet IDs are different while logging in + { + case 0x03: + if (protocolVersion >= MC_1_8_Version) + compression_treshold = dataTypes.ReadNextVarInt(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()); + return understood; + default: + return false; //Ignored packet + } + } + // Regular in-game packets + else + switch (packetPalette.GetIncommingTypeById(packetID)) + { + case PacketTypesIn.KeepAlive: + SendPacket(PacketTypesOut.KeepAlive, packetData); + handler.OnServerKeepAlive(); + break; + case PacketTypesIn.Ping: + SendPacket(PacketTypesOut.Pong, packetData); + break; + case PacketTypesIn.JoinGame: + { + // Temporary fix + log.Debug("Receive JoinGame"); + + receiveDeclareCommands = receivePlayerInfo = false; + + messageIndex = 0; + pendingAcknowledgments = 0; + + lastReceivedMessage = null; + lastSeenMessagesCollector = protocolVersion >= MC_1_19_3_Version ? new(20) : new(5); + } + handler.OnGameJoined(isOnlineMode); + + int playerEntityID = dataTypes.ReadNextInt(packetData); + handler.OnReceivePlayerEntityID(playerEntityID); + + if (protocolVersion >= MC_1_16_2_Version) + dataTypes.ReadNextBool(packetData); // Is hardcore - 1.16.2 and above + + handler.OnGamemodeUpdate(Guid.Empty, dataTypes.ReadNextByte(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 + 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 + if (protocolVersion >= MC_1_19_Version) + ChatParser.ReadChatType(registryCodec); + if (handler.GetTerrainEnabled()) + World.StoreDimensionList(registryCodec); + } + + // Current dimension + // String: 1.19 and above + // NBT Tag Compound: [1.16.2 to 1.18.2] + // String identifier: 1.16 and 1.16.1 + // varInt: [1.9.1 to 1.15.2] + // byte: below 1.9.1 + string? dimensionTypeName = null; + Dictionary? dimensionType = null; + if (protocolVersion >= MC_1_16_Version) + { + if (protocolVersion >= MC_1_19_Version) + dimensionTypeName = + dataTypes.ReadNextString(packetData); // Dimension Type: Identifier + else if (protocolVersion >= MC_1_16_2_Version) + dimensionType = + dataTypes.ReadNextNbt(packetData); // Dimension Type: NBT Tag Compound + else + dataTypes.ReadNextString(packetData); + currentDimension = 0; + } + else if (protocolVersion >= MC_1_9_1_Version) + currentDimension = dataTypes.ReadNextInt(packetData); + else + currentDimension = (sbyte)dataTypes.ReadNextByte(packetData); + + if (protocolVersion < MC_1_14_Version) + dataTypes.ReadNextByte(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 + if (handler.GetTerrainEnabled()) + { + if (protocolVersion >= MC_1_16_2_Version && protocolVersion <= MC_1_18_2_Version) + { + World.StoreOneDimension(dimensionName, dimensionType!); + World.SetDimension(dimensionName); + } + else if (protocolVersion >= MC_1_19_Version) + { + World.SetDimension(dimensionTypeName!); + } + } + } + + if (protocolVersion >= MC_1_15_Version) + dataTypes.ReadNextLong(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 + else + dataTypes.ReadNextByte(packetData); // Max Players - 1.16.1 and below + if (protocolVersion < MC_1_16_Version) + dataTypes.SkipNextString(packetData); // Level Type - 1.15 and below + if (protocolVersion >= MC_1_14_Version) + dataTypes.ReadNextVarInt(packetData); // View distance - 1.14 and above + if (protocolVersion >= MC_1_18_1_Version) + dataTypes.ReadNextVarInt(packetData); // Simulation Distance - 1.18 and above + if (protocolVersion >= MC_1_8_Version) + dataTypes.ReadNextBool(packetData); // Reduced debug info - 1.8 and above + if (protocolVersion >= MC_1_15_Version) + dataTypes.ReadNextBool(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 + } + + if (protocolVersion >= MC_1_19_Version) + { + bool hasDeathLocation = dataTypes.ReadNextBool(packetData); // Has death location + if (hasDeathLocation) + { + dataTypes.SkipNextString(packetData); // Death dimension name: Identifier + dataTypes.ReadNextLocation(packetData); // Death location + } + } + + if (protocolVersion >= MC_1_20_Version) + dataTypes.ReadNextVarInt(packetData); // Portal Cooldown - 1.20 and above + + break; + case PacketTypesIn.SpawnPainting: // Just skip, no need for this + return true; + case PacketTypesIn.DeclareCommands: + if (protocolVersion >= MC_1_19_Version) + { + log.Debug("Receive DeclareCommands"); + DeclareCommands.Read(dataTypes, packetData, protocolVersion); + receiveDeclareCommands = true; + if (receivePlayerInfo) + handler.SetCanSendMessage(true); + } + + break; + case PacketTypesIn.ChatMessage: + int messageType = 0; + + if (protocolVersion <= MC_1_18_2_Version) // 1.18 and bellow + { + string message = dataTypes.ReadNextString(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)) + break; + + if (protocolVersion >= MC_1_16_5_Version) + senderUUID = dataTypes.ReadNextUUID(packetData); + else senderUUID = Guid.Empty; + } + else + senderUUID = Guid.Empty; + + handler.OnTextReceived(new(message, null, true, messageType, senderUUID)); + } + else if (protocolVersion == MC_1_19_Version) // 1.19 + { + string signedChat = dataTypes.ReadNextString(packetData); + + bool hasUnsignedChatContent = dataTypes.ReadNextBool(packetData); + string? unsignedChatContent = + hasUnsignedChatContent ? dataTypes.ReadNextString(packetData) : null; + + messageType = dataTypes.ReadNextVarInt(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)); + + bool hasSenderTeamName = dataTypes.ReadNextBool(packetData); + string? senderTeamName = hasSenderTeamName + ? ChatParser.ParseText(dataTypes.ReadNextString(packetData)) + : null; + + long timestamp = dataTypes.ReadNextLong(packetData); + + long salt = dataTypes.ReadNextLong(packetData); + + byte[] messageSignature = dataTypes.ReadNextByteArray(packetData); + + bool verifyResult; + if (!isOnlineMode) + verifyResult = false; + else if (senderUUID == handler.GetUserUuid()) + verifyResult = true; + else + { + PlayerInfo? player = handler.GetPlayerInfo(senderUUID); + verifyResult = player != null && player.VerifyMessage(signedChat, timestamp, salt, + ref messageSignature); + } + + ChatMessage chat = new(signedChat, true, messageType, senderUUID, unsignedChatContent, + senderDisplayName, senderTeamName, timestamp, messageSignature, verifyResult); + handler.OnTextReceived(chat); + } + else if (protocolVersion == MC_1_19_2_Version) + { + // 1.19.1 - 1.19.2 + byte[]? precedingSignature = dataTypes.ReadNextBool(packetData) + ? dataTypes.ReadNextByteArray(packetData) + : null; + Guid senderUUID = dataTypes.ReadNextUUID(packetData); + byte[] headerSignature = dataTypes.ReadNextByteArray(packetData); + + string signedChat = dataTypes.ReadNextString(packetData); + string? decorated = dataTypes.ReadNextBool(packetData) + ? dataTypes.ReadNextString(packetData) + : null; + + long timestamp = dataTypes.ReadNextLong(packetData); + long salt = dataTypes.ReadNextLong(packetData); + + int lastSeenMessageListLen = dataTypes.ReadNextVarInt(packetData); + LastSeenMessageList.AcknowledgedMessage[] lastSeenMessageList = + new LastSeenMessageList.AcknowledgedMessage[lastSeenMessageListLen]; + for (int i = 0; i < lastSeenMessageListLen; ++i) + { + Guid user = dataTypes.ReadNextUUID(packetData); + byte[] lastSignature = dataTypes.ReadNextByteArray(packetData); + lastSeenMessageList[i] = new(user, lastSignature, true); + } + + LastSeenMessageList lastSeenMessages = new(lastSeenMessageList); + + string? unsignedChatContent = dataTypes.ReadNextBool(packetData) + ? dataTypes.ReadNextString(packetData) + : null; + + MessageFilterType filterEnum = (MessageFilterType)dataTypes.ReadNextVarInt(packetData); + if (filterEnum == MessageFilterType.PartiallyFiltered) + dataTypes.ReadNextULongArray(packetData); + + int chatTypeId = dataTypes.ReadNextVarInt(packetData); + string chatName = dataTypes.ReadNextString(packetData); + string? targetName = dataTypes.ReadNextBool(packetData) + ? dataTypes.ReadNextString(packetData) + : null; + + Dictionary chatInfo = Json.ParseJson(chatName).Properties; + string senderDisplayName = + (chatInfo.ContainsKey("insertion") ? chatInfo["insertion"] : chatInfo["text"]) + .StringValue; + string? senderTeamName = null; + ChatParser.MessageType messageTypeEnum = + ChatParser.ChatId2Type!.GetValueOrDefault(chatTypeId, ChatParser.MessageType.CHAT); + if (targetName != null && + (messageTypeEnum == ChatParser.MessageType.TEAM_MSG_COMMAND_INCOMING || + messageTypeEnum == ChatParser.MessageType.TEAM_MSG_COMMAND_OUTGOING)) + senderTeamName = Json.ParseJson(targetName).Properties["with"].DataArray[0] + .Properties["text"].StringValue; + + if (string.IsNullOrWhiteSpace(senderDisplayName)) + { + PlayerInfo? player = handler.GetPlayerInfo(senderUUID); + if (player != null && (player.DisplayName != null || player.Name != null) && + string.IsNullOrWhiteSpace(senderDisplayName)) + { + senderDisplayName = ChatParser.ParseText(player.DisplayName ?? player.Name); + if (string.IsNullOrWhiteSpace(senderDisplayName)) + senderDisplayName = player.DisplayName ?? player.Name; + else + senderDisplayName += "§r"; + } + } + + bool verifyResult; + if (!isOnlineMode) + verifyResult = false; + else if (senderUUID == handler.GetUserUuid()) + verifyResult = true; + else + { + PlayerInfo? player = handler.GetPlayerInfo(senderUUID); + if (player == null || !player.IsMessageChainLegal()) + verifyResult = false; + else + { + bool lastVerifyResult = player.IsMessageChainLegal(); + verifyResult = player.VerifyMessage(signedChat, timestamp, salt, + ref headerSignature, ref precedingSignature, lastSeenMessages); + if (lastVerifyResult && !verifyResult) + log.Warn(string.Format(Translations.chat_message_chain_broken, + senderDisplayName)); + } + } + + ChatMessage chat = new(signedChat, false, chatTypeId, senderUUID, unsignedChatContent, + senderDisplayName, senderTeamName, timestamp, headerSignature, verifyResult); + if (isOnlineMode && !chat.LacksSender()) + Acknowledge(chat); + handler.OnTextReceived(chat); + } + else if (protocolVersion >= MC_1_19_3_Version) + { + // 1.19.3+ + // Header section + // net.minecraft.network.packet.s2c.play.ChatMessageS2CPacket#write + Guid senderUUID = dataTypes.ReadNextUUID(packetData); + int index = dataTypes.ReadNextVarInt(packetData); + // Signature is fixed size of 256 bytes + byte[]? messageSignature = dataTypes.ReadNextBool(packetData) + ? dataTypes.ReadNextByteArray(packetData, 256) + : null; + + // Body + // net.minecraft.network.message.MessageBody.Serialized#write + string message = dataTypes.ReadNextString(packetData); + long timestamp = dataTypes.ReadNextLong(packetData); + long salt = dataTypes.ReadNextLong(packetData); + + // Previous Messages + // net.minecraft.network.message.LastSeenMessageList.Indexed#write + // net.minecraft.network.message.MessageSignatureData.Indexed#write + int totalPreviousMessages = dataTypes.ReadNextVarInt(packetData); + Tuple[] previousMessageSignatures = + new Tuple[totalPreviousMessages]; + for (int i = 0; i < totalPreviousMessages; i++) + { + // net.minecraft.network.message.MessageSignatureData.Indexed#fromBuf + int messageId = dataTypes.ReadNextVarInt(packetData) - 1; + if (messageId == -1) + previousMessageSignatures[i] = new Tuple(messageId, + dataTypes.ReadNextByteArray(packetData, 256)); + else + previousMessageSignatures[i] = new Tuple(messageId, null); + } + + // Other + string? unsignedChatContent = dataTypes.ReadNextBool(packetData) + ? dataTypes.ReadNextString(packetData) + : null; + + MessageFilterType filterType = (MessageFilterType)dataTypes.ReadNextVarInt(packetData); + + if (filterType == MessageFilterType.PartiallyFiltered) + dataTypes.ReadNextULongArray(packetData); + + // Network Target + // net.minecraft.network.message.MessageType.Serialized#write + int chatTypeId = dataTypes.ReadNextVarInt(packetData); + string chatName = dataTypes.ReadNextString(packetData); + string? targetName = dataTypes.ReadNextBool(packetData) + ? dataTypes.ReadNextString(packetData) + : null; + + ChatParser.MessageType messageTypeEnum = + ChatParser.ChatId2Type!.GetValueOrDefault(chatTypeId, ChatParser.MessageType.CHAT); + + Dictionary chatInfo = + Json.ParseJson(targetName ?? chatName).Properties; + string senderDisplayName = + (chatInfo.ContainsKey("insertion") ? chatInfo["insertion"] : chatInfo["text"]) + .StringValue; + string? senderTeamName = null; + if (targetName != null && + (messageTypeEnum == ChatParser.MessageType.TEAM_MSG_COMMAND_INCOMING || + messageTypeEnum == ChatParser.MessageType.TEAM_MSG_COMMAND_OUTGOING)) + senderTeamName = Json.ParseJson(targetName).Properties["with"].DataArray[0] + .Properties["text"].StringValue; + + if (string.IsNullOrWhiteSpace(senderDisplayName)) + { + PlayerInfo? player = handler.GetPlayerInfo(senderUUID); + if (player != null && (player.DisplayName != null || player.Name != null) && + string.IsNullOrWhiteSpace(senderDisplayName)) + { + senderDisplayName = ChatParser.ParseText(player.DisplayName ?? player.Name); + if (string.IsNullOrWhiteSpace(senderDisplayName)) + senderDisplayName = player.DisplayName ?? player.Name; + else + senderDisplayName += "§r"; + } + } + + bool verifyResult; + if (!isOnlineMode || messageSignature == null) + verifyResult = false; + else + { + if (senderUUID == handler.GetUserUuid()) + verifyResult = true; + else + { + PlayerInfo? player = handler.GetPlayerInfo(senderUUID); + if (player == null || !player.IsMessageChainLegal()) + verifyResult = false; + else + { + verifyResult = false; + verifyResult = player.VerifyMessage(message, senderUUID, player.ChatUuid, + index, timestamp, salt, ref messageSignature, + previousMessageSignatures); + } + } + } + + ChatMessage chat = new(message, false, chatTypeId, senderUUID, unsignedChatContent, + senderDisplayName, senderTeamName, timestamp, messageSignature, verifyResult); + lock (MessageSigningLock) + Acknowledge(chat); + handler.OnTextReceived(chat); + } + + break; + case PacketTypesIn.HideMessage: + byte[] hideMessageSignature = dataTypes.ReadNextByteArray(packetData); + ConsoleIO.WriteLine( + $"HideMessage was not processed! (SigLen={hideMessageSignature.Length})"); + break; + case PacketTypesIn.SystemChat: + string systemMessage = dataTypes.ReadNextString(packetData); + if (protocolVersion >= MC_1_19_3_Version) + { + bool isOverlay = dataTypes.ReadNextBool(packetData); + if (isOverlay) + { + if (!Config.Main.Advanced.ShowXPBarMessages) + break; + } + else + { + if (!Config.Main.Advanced.ShowSystemMessages) + break; + } + + handler.OnTextReceived(new(systemMessage, null, true, -1, Guid.Empty, true)); + } + else + { + int msgType = dataTypes.ReadNextVarInt(packetData); + if ((msgType == 1 && !Config.Main.Advanced.ShowSystemMessages)) + break; + handler.OnTextReceived(new(systemMessage, null, true, msgType, Guid.Empty, true)); + } + + break; + case PacketTypesIn.ProfilelessChatMessage: + string message_ = dataTypes.ReadNextString(packetData); + int messageType_ = dataTypes.ReadNextVarInt(packetData); + string messageName = dataTypes.ReadNextString(packetData); + string? targetName_ = dataTypes.ReadNextBool(packetData) + ? dataTypes.ReadNextString(packetData) + : null; + ChatMessage profilelessChat = new(message_, targetName_ ?? messageName, true, messageType_, + Guid.Empty, true); + profilelessChat.isSenderJson = true; + handler.OnTextReceived(profilelessChat); + 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); + + if (eventType == CombatEventType.EntityDead) + { + dataTypes.SkipNextVarInt(packetData); + + handler.OnPlayerKilled( + dataTypes.ReadNextInt(packetData), + ChatParser.ParseText(dataTypes.ReadNextString(packetData)) + ); + } + } + + break; + case PacketTypesIn.DeathCombatEvent: + dataTypes.SkipNextVarInt(packetData); + + handler.OnPlayerKilled( + protocolVersion >= MC_1_20_Version ? -1 : dataTypes.ReadNextInt(packetData), + ChatParser.ParseText(dataTypes.ReadNextString(packetData)) + ); + + break; + case PacketTypesIn.DamageEvent: // 1.19.4 + if (handler.GetEntityHandlingEnabled() && protocolVersion >= MC_1_19_4_Version) + { + var entityId = dataTypes.ReadNextVarInt(packetData); + var sourceTypeId = dataTypes.ReadNextVarInt(packetData); + var sourceCauseId = dataTypes.ReadNextVarInt(packetData); + var sourceDirectId = dataTypes.ReadNextVarInt(packetData); + + Location? sourcePos; + if (dataTypes.ReadNextBool(packetData)) + { + sourcePos = new Location() + { + X = dataTypes.ReadNextDouble(packetData), + Y = dataTypes.ReadNextDouble(packetData), + Z = dataTypes.ReadNextDouble(packetData) + }; + } + + // TODO: Write a function to use this data ? But seems not too useful + } + + break; + case PacketTypesIn.MessageHeader: // 1.19.2 only + 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); + + bool verifyResult; + + if (!isOnlineMode) + verifyResult = false; + else if (senderUUID == handler.GetUserUuid()) + verifyResult = true; + else + { + PlayerInfo? player = handler.GetPlayerInfo(senderUUID); + + if (player == null || !player.IsMessageChainLegal()) + verifyResult = false; + else + { + bool lastVerifyResult = player.IsMessageChainLegal(); + verifyResult = player.VerifyMessageHead(ref precedingSignature, + ref headerSignature, ref bodyDigest); + if (lastVerifyResult && !verifyResult) + log.Warn(string.Format(Translations.chat_message_chain_broken, + player.Name)); + } + } + } + + break; + case PacketTypesIn.Respawn: + string? dimensionTypeNameRespawn = null; + Dictionary? dimensionTypeRespawn = null; + if (protocolVersion >= MC_1_16_Version) + { + if (protocolVersion >= MC_1_19_Version) + dimensionTypeNameRespawn = + dataTypes.ReadNextString(packetData); // Dimension Type: Identifier + else if (protocolVersion >= MC_1_16_2_Version) + dimensionTypeRespawn = + dataTypes.ReadNextNbt(packetData); // Dimension Type: NBT Tag Compound + else + dataTypes.ReadNextString(packetData); + currentDimension = 0; + } + else + { + // 1.15 and below + currentDimension = dataTypes.ReadNextInt(packetData); + } + + if (protocolVersion >= MC_1_16_Version) + { + string dimensionName = + dataTypes.ReadNextString( + 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) + { + World.StoreOneDimension(dimensionName, dimensionTypeRespawn!); + World.SetDimension(dimensionName); + } + else if (protocolVersion >= MC_1_19_Version) + { + World.SetDimension(dimensionTypeNameRespawn!); + } + } + } + + if (protocolVersion < MC_1_14_Version) + dataTypes.ReadNextByte(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 + if (protocolVersion >= MC_1_16_Version) + dataTypes.ReadNextByte(packetData); // Previous Game mode - 1.16 and above + if (protocolVersion < MC_1_16_Version) + dataTypes.SkipNextString(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 + } + + if (protocolVersion >= MC_1_19_Version) + { + bool hasDeathLocation = dataTypes.ReadNextBool(packetData); // Has death location + if (hasDeathLocation) + { + dataTypes.ReadNextString(packetData); // Death dimension name: Identifier + dataTypes.ReadNextLocation(packetData); // Death location + } + } + + if (protocolVersion >= MC_1_20_Version) + dataTypes.ReadNextVarInt(packetData); // Portal Cooldown + + handler.OnRespawn(); + 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); + Location location = new(x, y, z); + float yaw = dataTypes.ReadNextFloat(packetData); + float pitch = dataTypes.ReadNextFloat(packetData); + byte locMask = dataTypes.ReadNextByte(packetData); + + // entity handling require player pos for distance calculating + if (handler.GetTerrainEnabled() || handler.GetEntityHandlingEnabled()) + { + if (protocolVersion >= MC_1_8_Version) + { + Location current = handler.GetCurrentLocation(); + location.X = (locMask & 1 << 0) != 0 ? current.X + x : x; + location.Y = (locMask & 1 << 1) != 0 ? current.Y + y : y; + location.Z = (locMask & 1 << 2) != 0 ? current.Z + z : z; + } + } + + if (protocolVersion >= MC_1_9_Version) + { + int teleportID = dataTypes.ReadNextVarInt(packetData); + + if (teleportID < 0) + { + yaw = LastYaw; + pitch = LastPitch; + } + else + { + LastYaw = yaw; + LastPitch = pitch; + } + + handler.UpdateLocation(location, yaw, pitch); + + // Teleport confirm packet + SendPacket(PacketTypesOut.TeleportConfirm, DataTypes.GetVarInt(teleportID)); + if (Config.Main.Advanced.TemporaryFixBadpacket) + { + SendLocationUpdate(location, true, yaw, pitch, true); + if (teleportID == 1) + SendLocationUpdate(location, true, yaw, pitch, true); + } + } + else + { + handler.UpdateLocation(location, yaw, pitch); + LastYaw = yaw; + LastPitch = pitch; + } + + if (protocolVersion >= MC_1_17_Version && protocolVersion < MC_1_19_4_Version) + dataTypes.ReadNextBool(packetData); // Dismount Vehicle - 1.17 to 1.19.3 + } + break; + case PacketTypesIn.ChunkData: + if (handler.GetTerrainEnabled()) + { + Interlocked.Increment(ref handler.GetWorld().chunkCnt); + Interlocked.Increment(ref handler.GetWorld().chunkLoadNotCompleted); + + int chunkX = dataTypes.ReadNextInt(packetData); + int chunkZ = dataTypes.ReadNextInt(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 + + dataTypes.ReadNextNbt(packetData); // Heightmaps + + if (protocolVersion == MC_1_17_Version || protocolVersion == MC_1_17_1_Version) + { + int biomesLength = dataTypes.ReadNextVarInt(packetData); // Biomes length + for (int i = 0; i < biomesLength; i++) + dataTypes.SkipNextVarInt(packetData); // Biomes + } + + int dataSize = dataTypes.ReadNextVarInt(packetData); // Size + + pTerrain.ProcessChunkColumnData(chunkX, chunkZ, verticalStripBitmask, packetData); + Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); + + // Block Entity data: ignored + // Trust edges: ignored (Removed in 1.20) + // Light data: ignored + } + else + { + bool chunksContinuous = dataTypes.ReadNextBool(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 + ushort chunkMask = protocolVersion >= MC_1_9_Version + ? (ushort)dataTypes.ReadNextVarInt(packetData) + : dataTypes.ReadNextUShort(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)); + Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); + } + else + { + if (protocolVersion >= MC_1_14_Version) + dataTypes.ReadNextNbt(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 + if (protocolVersion >= MC_1_15_Version && chunksContinuous) + { + if (protocolVersion >= MC_1_16_2_Version) + { + for (int i = 0; i < biomesLength; i++) + { + // Biomes - 1.16.2 and above + // Don't use ReadNextVarInt because it cost too much time + dataTypes.SkipNextVarInt(packetData); + } + } + else dataTypes.DropData(1024 * 4, packetData); // Biomes - 1.15 and above + } + + int dataSize = dataTypes.ReadNextVarInt(packetData); + + pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, 0, false, + chunksContinuous, currentDimension, packetData); + Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); + } + } + } + + break; + case PacketTypesIn.ChunksBiomes: // 1.19.4 + // Biomes are not handled by MCC + break; + case PacketTypesIn.MapData: + if (protocolVersion < MC_1_8_Version) + break; + + int mapid = dataTypes.ReadNextVarInt(packetData); + byte scale = dataTypes.ReadNextByte(packetData); + + + // 1.9 + + bool trackingPosition = true; + + // 1.14+ + bool locked = false; + + // 1.17+ (locked and trackingPosition switched places) + if (protocolVersion >= MC_1_17_Version) + { + if (protocolVersion >= MC_1_14_Version) + locked = dataTypes.ReadNextBool(packetData); + + if (protocolVersion >= MC_1_9_Version) + trackingPosition = dataTypes.ReadNextBool(packetData); + } + else + { + if (protocolVersion >= MC_1_9_Version) + trackingPosition = dataTypes.ReadNextBool(packetData); + + if (protocolVersion >= MC_1_14_Version) + locked = dataTypes.ReadNextBool(packetData); + } + + int iconcount = 0; + List icons = new(); + + // 1,9 + = needs tracking position to be true to get the icons + if (protocolVersion <= MC_1_16_5_Version || trackingPosition) + { + iconcount = dataTypes.ReadNextVarInt(packetData); + + for (int i = 0; i < iconcount; i++) + { + MapIcon mapIcon = new(); + + // 1.8 - 1.13 + if (protocolVersion < MC_1_13_2_Version) + { + byte directionAndtype = dataTypes.ReadNextByte(packetData); + byte direction, type; + + // 1.12.2+ + if (protocolVersion >= MC_1_12_2_Version) + { + direction = (byte)(directionAndtype & 0xF); + type = (byte)((directionAndtype >> 4) & 0xF); + } + else // 1.8 - 1.12 + { + direction = (byte)((directionAndtype >> 4) & 0xF); + type = (byte)(directionAndtype & 0xF); + } + + mapIcon.Type = (MapIconType)type; + mapIcon.Direction = direction; + } + + // 1.13.2+ + if (protocolVersion >= MC_1_13_2_Version) + mapIcon.Type = (MapIconType)dataTypes.ReadNextVarInt(packetData); + + mapIcon.X = dataTypes.ReadNextByte(packetData); + mapIcon.Z = dataTypes.ReadNextByte(packetData); + + // 1.13.2+ + if (protocolVersion >= MC_1_13_2_Version) + { + mapIcon.Direction = dataTypes.ReadNextByte(packetData); + + if (dataTypes.ReadNextBool(packetData)) // Has Display Name? + mapIcon.DisplayName = + ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + } + + icons.Add(mapIcon); + } + } + + byte columnsUpdated = dataTypes.ReadNextByte(packetData); // width + byte rowsUpdated = 0; // height + byte mapCoulmnX = 0; + byte mapRowZ = 0; + byte[]? colors = null; + + if (columnsUpdated > 0) + { + rowsUpdated = dataTypes.ReadNextByte(packetData); // height + mapCoulmnX = dataTypes.ReadNextByte(packetData); + mapRowZ = dataTypes.ReadNextByte(packetData); + colors = dataTypes.ReadNextByteArray(packetData); + } + + handler.OnMapData(mapid, scale, trackingPosition, locked, icons, columnsUpdated, + rowsUpdated, mapCoulmnX, mapRowZ, colors); + break; + case PacketTypesIn.TradeList: + if ((protocolVersion >= MC_1_14_Version) && (handler.GetInventoryEnabled())) + { + // MC 1.14 or greater + int windowID = dataTypes.ReadNextVarInt(packetData); + int size = dataTypes.ReadNextByte(packetData); + List trades = new(); + for (int tradeId = 0; tradeId < size; tradeId++) + { + VillagerTrade trade = dataTypes.ReadNextTrade(packetData, itemPalette); + trades.Add(trade); + } + + VillagerInfo villagerInfo = new() + { + Level = dataTypes.ReadNextVarInt(packetData), + Experience = dataTypes.ReadNextVarInt(packetData), + IsRegularVillager = dataTypes.ReadNextBool(packetData), + CanRestock = dataTypes.ReadNextBool(packetData) + }; + handler.OnTradeList(windowID, trades, villagerInfo); + } + + break; + 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 fadein = -1; + int stay = -1; + int fadeout = -1; + if (protocolVersion >= MC_1_10_Version) + { + if (action2 == 0) + { + json = titletext; + titletext = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + } + else if (action2 == 1) + { + json = subtitletext; + subtitletext = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + } + else if (action2 == 2) + { + json = actionbartext; + actionbartext = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + } + else if (action2 == 3) + { + fadein = dataTypes.ReadNextInt(packetData); + stay = dataTypes.ReadNextInt(packetData); + fadeout = dataTypes.ReadNextInt(packetData); + } + } + else + { + if (action2 == 0) + { + json = titletext; + titletext = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + } + else if (action2 == 1) + { + json = subtitletext; + subtitletext = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + } + else if (action2 == 2) + { + fadein = dataTypes.ReadNextInt(packetData); + stay = dataTypes.ReadNextInt(packetData); + fadeout = dataTypes.ReadNextInt(packetData); + } + } + + handler.OnTitle(action2, titletext, subtitletext, actionbartext, fadein, stay, fadeout, + json); + } + + break; + case PacketTypesIn.MultiBlockChange: + if (handler.GetTerrainEnabled()) + { + if (protocolVersion >= MC_1_16_2_Version) + { + long chunkSection = dataTypes.ReadNextLong(packetData); + int sectionX = (int)(chunkSection >> 42); + int sectionY = (int)((chunkSection << 44) >> 44); + int sectionZ = (int)((chunkSection << 22) >> 42); + + if(protocolVersion < MC_1_20_Version) + dataTypes.ReadNextBool(packetData); // Useless boolean (Related to light update) + + int blocksSize = dataTypes.ReadNextVarInt(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 localY = (int)(chunkSectionPosition & 0x0F); + + Block block = new((ushort)blockId); + int blockX = (sectionX * 16) + localX; + int blockY = (sectionY * 16) + localY; + int blockZ = (sectionZ * 16) + localZ; + + Location location = new(blockX, blockY, blockZ); + + handler.OnBlockChange(location, block); + } + } + else + { + int chunkX = dataTypes.ReadNextInt(packetData); + int chunkZ = dataTypes.ReadNextInt(packetData); + int recordCount = protocolVersion < MC_1_8_Version + ? (int)dataTypes.ReadNextShort(packetData) + : dataTypes.ReadNextVarInt(packetData); + + for (int i = 0; i < recordCount; i++) + { + byte locationXZ; + ushort blockIdMeta; + int blockY; + + if (protocolVersion < MC_1_8_Version) + { + blockIdMeta = dataTypes.ReadNextUShort(packetData); + blockY = (ushort)dataTypes.ReadNextByte(packetData); + locationXZ = dataTypes.ReadNextByte(packetData); + } + else + { + locationXZ = dataTypes.ReadNextByte(packetData); + blockY = (ushort)dataTypes.ReadNextByte(packetData); + blockIdMeta = (ushort)dataTypes.ReadNextVarInt(packetData); + } + + int blockX = locationXZ >> 4; + int blockZ = locationXZ & 0x0F; + + Location location = new(chunkX, chunkZ, blockX, blockY, blockZ); + Block block = new(blockIdMeta); + handler.OnBlockChange(location, block); + } + } + } + + break; + case PacketTypesIn.ServerData: + string motd = "-"; + + bool hasMotd = false; + if (protocolVersion < MC_1_19_4_Version) + { + hasMotd = dataTypes.ReadNextBool(packetData); + + if (hasMotd) + motd = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + } + else + { + hasMotd = true; + motd = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + } + + string iconBase64 = "-"; + bool hasIcon = dataTypes.ReadNextBool(packetData); + if (hasIcon) + iconBase64 = dataTypes.ReadNextString(packetData); + + bool previewsChat = false; + if (protocolVersion < MC_1_19_3_Version) + previewsChat = dataTypes.ReadNextBool(packetData); + + handler.OnServerDataRecived(hasMotd, motd, hasIcon, iconBase64, previewsChat); + break; + case PacketTypesIn.BlockChange: + if (handler.GetTerrainEnabled()) + { + 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); + + Location location = new(blockX, blockY, blockZ); + Block block = new(blockId, blockMeta); + handler.OnBlockChange(location, block); + } + else + { + Location location = dataTypes.ReadNextLocation(packetData); + Block block = new((ushort)dataTypes.ReadNextVarInt(packetData)); + handler.OnBlockChange(location, block); + } + } + + break; + case PacketTypesIn.SetDisplayChatPreview: + bool previewsChatSetting = dataTypes.ReadNextBool(packetData); + handler.OnChatPreviewSettingUpdate(previewsChatSetting); + break; + case PacketTypesIn.ChatSuggestions: + break; + case PacketTypesIn.MapChunkBulk: + if (protocolVersion < MC_1_9_Version && handler.GetTerrainEnabled()) + { + int chunkCount; + bool hasSkyLight; + Queue chunkData = 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); + } + else + { + hasSkyLight = dataTypes.ReadNextBool(packetData); + chunkCount = dataTypes.ReadNextVarInt(packetData); + } + + //Read chunk records + int[] chunkXs = new int[chunkCount]; + int[] chunkZs = new int[chunkCount]; + ushort[] chunkMasks = new ushort[chunkCount]; + 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); + addBitmaps[chunkColumnNo] = protocolVersion < MC_1_8_Version + ? dataTypes.ReadNextUShort(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); + Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); + } + } + + break; + case PacketTypesIn.UnloadChunk: + if (protocolVersion >= MC_1_9_Version && handler.GetTerrainEnabled()) + { + int chunkX = dataTypes.ReadNextInt(packetData); + int chunkZ = dataTypes.ReadNextInt(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 + // in loading chunks that should be unloaded and inaccurate statistics. + if (handler.GetWorld()[chunkX, chunkZ] != null) + Interlocked.Decrement(ref handler.GetWorld().chunkCnt); + + handler.GetWorld()[chunkX, chunkZ] = null; + } + + break; + case PacketTypesIn.ChangeGameState: + if (protocolVersion >= MC_1_15_2_Version) + { + byte reason = dataTypes.ReadNextByte(packetData); + float state = dataTypes.ReadNextFloat(packetData); + + handler.OnGameEvent(reason, state); + } + + break; + case PacketTypesIn.PlayerInfo: + if (protocolVersion >= MC_1_19_3_Version) + { + byte actionBitset = dataTypes.ReadNextByte(packetData); + int numberOfActions = dataTypes.ReadNextVarInt(packetData); + for (int i = 0; i < numberOfActions; i++) + { + Guid playerUuid = dataTypes.ReadNextUUID(packetData); + + PlayerInfo player; + if ((actionBitset & (1 << 0)) > 0) // Actions bit 0: add player + { + string name = dataTypes.ReadNextString(packetData); + int numberOfProperties = dataTypes.ReadNextVarInt(packetData); + for (int j = 0; j < numberOfProperties; ++j) + { + dataTypes.SkipNextString(packetData); + dataTypes.SkipNextString(packetData); + if (dataTypes.ReadNextBool(packetData)) + dataTypes.SkipNextString(packetData); + } + + player = new(name, playerUuid); + handler.OnPlayerJoin(player); + } + else + { + PlayerInfo? playerGet = handler.GetPlayerInfo(playerUuid); + if (playerGet == null) + { + player = new(string.Empty, playerUuid); + handler.OnPlayerJoin(player); + } + else + { + player = playerGet; + } + } + + if ((actionBitset & (1 << 1)) > 0) // Actions bit 1: initialize chat + { + bool hasSignatureData = dataTypes.ReadNextBool(packetData); + if (hasSignatureData) + { + Guid chatUuid = dataTypes.ReadNextUUID(packetData); + long publicKeyExpiryTime = dataTypes.ReadNextLong(packetData); + byte[] encodedPublicKey = dataTypes.ReadNextByteArray(packetData); + byte[] publicKeySignature = dataTypes.ReadNextByteArray(packetData); + player.SetPublicKey(chatUuid, publicKeyExpiryTime, encodedPublicKey, + publicKeySignature); + + if (playerUuid == handler.GetUserUuid()) + { + log.Debug("Receive ChatUuid = " + chatUuid); + this.chatUuid = chatUuid; + } + } + else + { + player.ClearPublicKey(); + + if (playerUuid == handler.GetUserUuid()) + { + log.Debug("Receive ChatUuid = Empty"); + } + } + + if (playerUuid == handler.GetUserUuid()) + { + receivePlayerInfo = true; + if (receiveDeclareCommands) + handler.SetCanSendMessage(true); + } + } + + if ((actionBitset & 1 << 2) > 0) // Actions bit 2: update gamemode + { + handler.OnGamemodeUpdate(playerUuid, dataTypes.ReadNextVarInt(packetData)); + } + + if ((actionBitset & (1 << 3)) > 0) // Actions bit 3: update listed + { + player.Listed = dataTypes.ReadNextBool(packetData); + } + + if ((actionBitset & (1 << 4)) > 0) // Actions bit 4: update latency + { + int latency = dataTypes.ReadNextVarInt(packetData); + handler.OnLatencyUpdate(playerUuid, latency); //Update latency; + } + + if ((actionBitset & (1 << 5)) > 0) // Actions bit 5: update display name + { + if (dataTypes.ReadNextBool(packetData)) + player.DisplayName = dataTypes.ReadNextString(packetData); + else + player.DisplayName = null; + } + } + } + else if (protocolVersion >= MC_1_8_Version) + { + int action = dataTypes.ReadNextVarInt(packetData); // Action Name + int numberOfPlayers = dataTypes.ReadNextVarInt(packetData); // Number Of Players + + for (int i = 0; i < numberOfPlayers; i++) + { + Guid uuid = dataTypes.ReadNextUUID(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 + + // Property: Tuple[]? properties = + useProperty ? 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? propertySignature = null; + if (dataTypes.ReadNextBool(packetData)) // Is Signed + propertySignature = + dataTypes.ReadNextString( + 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 ping = dataTypes.ReadNextVarInt(packetData); // Ping + + string? displayName = null; + if (dataTypes.ReadNextBool(packetData)) // Has display name + displayName = dataTypes.ReadNextString(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) + { + keyExpiration = dataTypes.ReadNextLong(packetData); // Timestamp + + int publicKeyLength = + dataTypes.ReadNextVarInt(packetData); // Public Key Length + if (publicKeyLength > 0) + publicKey = dataTypes.ReadData(publicKeyLength, + packetData); // Public key + + int signatureLength = + dataTypes.ReadNextVarInt(packetData); // Signature Length + if (signatureLength > 0) + signature = dataTypes.ReadData(signatureLength, + packetData); // Public key + } + } + + handler.OnPlayerJoin(new PlayerInfo(uuid, name, properties, gameMode, ping, + displayName, keyExpiration, publicKey, signature)); + break; + case 0x01: //Update gamemode + handler.OnGamemodeUpdate(uuid, dataTypes.ReadNextVarInt(packetData)); + break; + case 0x02: //Update latency + int latency = dataTypes.ReadNextVarInt(packetData); + handler.OnLatencyUpdate(uuid, latency); //Update latency; + break; + case 0x03: //Update display name + if (dataTypes.ReadNextBool(packetData)) + { + PlayerInfo? player = handler.GetPlayerInfo(uuid); + if (player != null) + player.DisplayName = dataTypes.ReadNextString(packetData); + else + dataTypes.SkipNextString(packetData); + } + + break; + case 0x04: //Player Leave + handler.OnPlayerLeave(uuid); + break; + default: + //Unknown player list item type + break; + } + } + } + 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()); + if (online) + handler.OnPlayerJoin(new PlayerInfo(name, FakeUUID)); + else handler.OnPlayerLeave(FakeUUID); + } + + break; + case PacketTypesIn.PlayerRemove: + int numberOfLeavePlayers = dataTypes.ReadNextVarInt(packetData); + for (int i = 0; i < numberOfLeavePlayers; ++i) + { + Guid playerUuid = dataTypes.ReadNextUUID(packetData); + handler.OnPlayerLeave(playerUuid); + } + + 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 + } + + int autocomplete_count = dataTypes.ReadNextVarInt(packetData); + + string[] autocomplete_result = new string[autocomplete_count]; + for (int i = 0; i < autocomplete_count; i++) + { + autocomplete_result[i] = dataTypes.ReadNextString(packetData); + if (protocolVersion >= MC_1_13_Version) + { + // Skip optional tooltip for each tab-complete resul`t + if (dataTypes.ReadNextBool(packetData)) + dataTypes.SkipNextString(packetData); + } + } + + handler.OnAutoCompleteDone(old_transaction_id, autocomplete_result); + break; + case PacketTypesIn.PluginMessage: + String channel = dataTypes.ReadNextString(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); + case PacketTypesIn.Disconnect: + handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick, + ChatParser.ParseText(dataTypes.ReadNextString(packetData))); + return false; + case PacketTypesIn.SetCompression: + if (protocolVersion >= MC_1_8_Version && protocolVersion < MC_1_9_Version) + compression_treshold = dataTypes.ReadNextVarInt(packetData); + break; + case PacketTypesIn.OpenWindow: + if (handler.GetInventoryEnabled()) + { + if (protocolVersion < MC_1_14_Version) + { + // MC 1.13 or lower + byte windowID = dataTypes.ReadNextByte(packetData); + string type = dataTypes.ReadNextString(packetData).Replace("minecraft:", "") + .ToUpper(); + ContainerTypeOld inventoryType = + (ContainerTypeOld)Enum.Parse(typeof(ContainerTypeOld), type); + string title = dataTypes.ReadNextString(packetData); + byte slots = dataTypes.ReadNextByte(packetData); + Container inventory = new(windowID, inventoryType, ChatParser.ParseText(title)); + handler.OnInventoryOpen(windowID, inventory); + } + else + { + // MC 1.14 or greater + int windowID = dataTypes.ReadNextVarInt(packetData); + int windowType = dataTypes.ReadNextVarInt(packetData); + string title = dataTypes.ReadNextString(packetData); + Container inventory = new(windowID, windowType, ChatParser.ParseText(title)); + handler.OnInventoryOpen(windowID, inventory); + } + } + + break; + case PacketTypesIn.CloseWindow: + if (handler.GetInventoryEnabled()) + { + byte windowID = dataTypes.ReadNextByte(packetData); + lock (window_actions) + { + window_actions[windowID] = 0; + } + + handler.OnInventoryClose(windowID); + } + + break; + case PacketTypesIn.WindowItems: + if (handler.GetInventoryEnabled()) + { + byte windowId = dataTypes.ReadNextByte(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); + } + else + { + // Elements as Short - 1.17.0 and below + dataTypes.ReadNextShort(packetData); + } + + Dictionary inventorySlots = new(); + for (int slotId = 0; slotId < elements; slotId++) + { + Item? item = dataTypes.ReadNextItemSlot(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); + + handler.OnWindowItems(windowId, inventorySlots, stateId); + } + + break; + case PacketTypesIn.WindowProperty: + byte containerId = dataTypes.ReadNextByte(packetData); + short propertyId = dataTypes.ReadNextShort(packetData); + short propertyValue = dataTypes.ReadNextShort(packetData); + + handler.OnWindowProperties(containerId, propertyId, propertyValue); + break; + case PacketTypesIn.SetSlot: + if (handler.GetInventoryEnabled()) + { + byte windowID = dataTypes.ReadNextByte(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); + } + + break; + case PacketTypesIn.WindowConfirmation: + if (handler.GetInventoryEnabled()) + { + byte windowID = dataTypes.ReadNextByte(packetData); + short actionID = dataTypes.ReadNextShort(packetData); + bool accepted = dataTypes.ReadNextBool(packetData); + if (!accepted) + SendWindowConfirmation(windowID, actionID, true); + } + + break; + case PacketTypesIn.ResourcePackSend: + string url = dataTypes.ReadNextString(packetData); + string hash = dataTypes.ReadNextString(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 + if (hasPromptMessage) + dataTypes.SkipNextString( + 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 + break; + //Send back "accepted" and "successfully loaded" responses for plugins or server config making use of resource pack mandatory + 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 + break; + case PacketTypesIn.SpawnEntity: + if (handler.GetEntityHandlingEnabled()) + { + Entity entity = dataTypes.ReadNextEntity(packetData, entityPalette, false); + handler.OnSpawnEntity(entity); + } + + break; + case PacketTypesIn.EntityEquipment: + if (handler.GetEntityHandlingEnabled()) + { + int entityid = dataTypes.ReadNextVarInt(packetData); + if (protocolVersion >= MC_1_16_Version) + { + bool hasNext; + do + { + byte bitsData = dataTypes.ReadNextByte(packetData); + // Top bit set if another entry follows, and otherwise unset if this is the last item in the array + hasNext = (bitsData >> 7) == 1; + int slot2 = bitsData >> 1; + Item? item = dataTypes.ReadNextItemSlot(packetData, itemPalette); + handler.OnEntityEquipment(entityid, slot2, item); + } while (hasNext); + } + else + { + int slot2 = protocolVersion < MC_1_9_Version + ? dataTypes.ReadNextShort(packetData) + : dataTypes.ReadNextVarInt(packetData); + + Item? item = dataTypes.ReadNextItemSlot(packetData, itemPalette); + handler.OnEntityEquipment(entityid, slot2, item); + } + } + + break; + case PacketTypesIn.SpawnLivingEntity: + if (handler.GetEntityHandlingEnabled()) + { + Entity entity = 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); + } + + break; + case PacketTypesIn.SpawnPlayer: + if (handler.GetEntityHandlingEnabled()) + { + int EntityID = dataTypes.ReadNextVarInt(packetData); + Guid UUID = dataTypes.ReadNextUUID(packetData); + + double x, y, z; + + if (protocolVersion < MC_1_9_Version) + { + x = dataTypes.ReadNextInt(packetData) / 32.0D; + y = dataTypes.ReadNextInt(packetData) / 32.0D; + z = dataTypes.ReadNextInt(packetData) / 32.0D; + } + else + { + x = dataTypes.ReadNextDouble(packetData); + y = dataTypes.ReadNextDouble(packetData); + z = dataTypes.ReadNextDouble(packetData); + } + + byte Yaw = dataTypes.ReadNextByte(packetData); + byte Pitch = dataTypes.ReadNextByte(packetData); + + Location EntityLocation = new(x, y, z); + + 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 effectId = protocolVersion >= MC_1_18_2_Version + ? dataTypes.ReadNextVarInt(packetData) + : dataTypes.ReadNextByte(packetData); + if (Enum.TryParse(effectId.ToString(), out effect)) + { + int amplifier = dataTypes.ReadNextByte(packetData); + int duration = dataTypes.ReadNextVarInt(packetData); + byte flags = dataTypes.ReadNextByte(packetData); + + bool hasFactorData = false; + Dictionary? factorCodec = null; + + if (protocolVersion >= MC_1_19_Version) + { + hasFactorData = dataTypes.ReadNextBool(packetData); + if (hasFactorData) + factorCodec = dataTypes.ReadNextNbt(packetData); + } + + handler.OnEntityEffect(entityid, effect, amplifier, duration, flags, hasFactorData, + factorCodec); + } + } + + break; + case PacketTypesIn.DestroyEntities: + if (handler.GetEntityHandlingEnabled()) + { + 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 + int[] entityList = new int[entityCount]; + for (int i = 0; i < entityCount; i++) + { + entityList[i] = dataTypes.ReadNextVarInt(packetData); + } + + handler.OnDestroyEntities(entityList); + } + + break; + case PacketTypesIn.EntityPosition: + if (handler.GetEntityHandlingEnabled()) + { + int EntityID = dataTypes.ReadNextVarInt(packetData); + + Double DeltaX, DeltaY, DeltaZ; + + if (protocolVersion < MC_1_9_Version) + { + DeltaX = Convert.ToDouble(dataTypes.ReadNextByte(packetData)); + DeltaY = Convert.ToDouble(dataTypes.ReadNextByte(packetData)); + DeltaZ = Convert.ToDouble(dataTypes.ReadNextByte(packetData)); + } + else + { + DeltaX = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); + DeltaY = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); + DeltaZ = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); + } + + bool OnGround = dataTypes.ReadNextBool(packetData); + DeltaX = DeltaX / (128 * 32); + DeltaY = DeltaY / (128 * 32); + DeltaZ = DeltaZ / (128 * 32); + + handler.OnEntityPosition(EntityID, DeltaX, DeltaY, DeltaZ, OnGround); + } + + break; + case PacketTypesIn.EntityPositionAndRotation: + if (handler.GetEntityHandlingEnabled()) + { + int EntityID = dataTypes.ReadNextVarInt(packetData); + + Double DeltaX, DeltaY, DeltaZ; + + if (protocolVersion < MC_1_9_Version) + { + DeltaX = dataTypes.ReadNextByte(packetData) / 32.0D; + DeltaY = dataTypes.ReadNextByte(packetData) / 32.0D; + DeltaZ = dataTypes.ReadNextByte(packetData) / 32.0D; + } + else + { + DeltaX = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); + DeltaY = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); + DeltaZ = Convert.ToDouble(dataTypes.ReadNextShort(packetData)); + } + + + byte _yaw = dataTypes.ReadNextByte(packetData); + byte _pitch = dataTypes.ReadNextByte(packetData); + bool OnGround = dataTypes.ReadNextBool(packetData); + DeltaX = DeltaX / (128 * 32); + DeltaY = DeltaY / (128 * 32); + DeltaZ = DeltaZ / (128 * 32); + + 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(); + for (int i = 0; i < NumberOfProperties; i++) + { + string _key = dataTypes.ReadNextString(packetData); + Double _value = dataTypes.ReadNextDouble(packetData); + + List op0 = new(); + List op1 = new(); + List op2 = new(); + int NumberOfModifiers = dataTypes.ReadNextVarInt(packetData); + for (int j = 0; j < NumberOfModifiers; j++) + { + dataTypes.ReadNextUUID(packetData); + Double amount = dataTypes.ReadNextDouble(packetData); + byte operation = dataTypes.ReadNextByte(packetData); + switch (operation) + { + case 0: + op0.Add(amount); + break; + case 1: + op1.Add(amount); + break; + case 2: + op2.Add(amount + 1); + break; + } + } + + if (op0.Count > 0) _value += op0.Sum(); + if (op1.Count > 0) _value *= 1 + op1.Sum(); + if (op2.Count > 0) _value *= op2.Aggregate((a, _x) => a * _x); + keys.Add(_key, _value); + } + + handler.OnEntityProperties(EntityID, keys); + } + + break; + case PacketTypesIn.EntityMetadata: + if (handler.GetEntityHandlingEnabled()) + { + int EntityID = dataTypes.ReadNextVarInt(packetData); + Dictionary metadata = + dataTypes.ReadNextMetadata(packetData, itemPalette, entityMetadataPalette); + + // Also make a palette for field? Will be a lot of work + int healthField = protocolVersion switch + { + > MC_1_20_Version => throw new NotImplementedException(Translations + .exception_palette_healthfield), + // 1.17 and above + >= MC_1_17_Version => 9, + // 1.14 and above + >= MC_1_14_Version => 8, + // 1.10 and above + >= MC_1_10_Version => 7, + // 1.8 and above + >= MC_1_8_Version => 6, + _ => throw new NotImplementedException(Translations.exception_palette_healthfield) + }; + + if (metadata.TryGetValue(healthField, out var healthObj) && healthObj != null && + healthObj is float) + handler.OnEntityHealth(EntityID, (float)healthObj); + + handler.OnEntityMetadata(EntityID, metadata); + } + + break; + case PacketTypesIn.EntityStatus: + if (handler.GetEntityHandlingEnabled()) + { + int entityId = dataTypes.ReadNextInt(packetData); + byte status = dataTypes.ReadNextByte(packetData); + handler.OnEntityStatus(entityId, status); + } + + break; + case PacketTypesIn.TimeUpdate: + long WorldAge = dataTypes.ReadNextLong(packetData); + long TimeOfday = dataTypes.ReadNextLong(packetData); + handler.OnTimeUpdate(WorldAge, TimeOfday); + break; + case PacketTypesIn.EntityTeleport: + if (handler.GetEntityHandlingEnabled()) + { + int EntityID = dataTypes.ReadNextVarInt(packetData); + + double x, y, z; + + if (protocolVersion < MC_1_9_Version) + { + x = dataTypes.ReadNextInt(packetData) / 32.0D; + y = dataTypes.ReadNextInt(packetData) / 32.0D; + z = dataTypes.ReadNextInt(packetData) / 32.0D; + } + else + { + x = dataTypes.ReadNextDouble(packetData); + y = dataTypes.ReadNextDouble(packetData); + 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); + } + + break; + case PacketTypesIn.UpdateHealth: + float health = dataTypes.ReadNextFloat(packetData); + int food; + if (protocolVersion >= MC_1_8_Version) + food = dataTypes.ReadNextVarInt(packetData); + else + food = dataTypes.ReadNextShort(packetData); + dataTypes.ReadNextFloat(packetData); // Food Saturation + handler.OnUpdateHealth(health, food); + break; + case PacketTypesIn.SetExperience: + float experiencebar = dataTypes.ReadNextFloat(packetData); + int totalexperience, level; + level = dataTypes.ReadNextVarInt(packetData); + totalexperience = dataTypes.ReadNextVarInt(packetData); + handler.OnSetExperience(experiencebar, level, totalexperience); + break; + case PacketTypesIn.Explosion: + Location explosionLocation; + if (protocolVersion >= MC_1_19_3_Version) + explosionLocation = new(dataTypes.ReadNextDouble(packetData), + dataTypes.ReadNextDouble(packetData), dataTypes.ReadNextDouble(packetData)); + else + explosionLocation = new(dataTypes.ReadNextFloat(packetData), + dataTypes.ReadNextFloat(packetData), dataTypes.ReadNextFloat(packetData)); + + float explosionStrength = dataTypes.ReadNextFloat(packetData); + int explosionBlockCount = protocolVersion >= MC_1_17_Version + ? dataTypes.ReadNextVarInt(packetData) + : dataTypes.ReadNextInt(packetData); + + for (int i = 0; i < explosionBlockCount; i++) + dataTypes.ReadData(3, packetData); + + float playerVelocityX = dataTypes.ReadNextFloat(packetData); + float playerVelocityY = dataTypes.ReadNextFloat(packetData); + float playerVelocityZ = dataTypes.ReadNextFloat(packetData); + + handler.OnExplosion(explosionLocation, explosionStrength, explosionBlockCount); + break; + case PacketTypesIn.HeldItemChange: + byte slot = dataTypes.ReadNextByte(packetData); + handler.OnHeldItemChange(slot); + break; + case PacketTypesIn.ScoreboardObjective: + string objectivename = dataTypes.ReadNextString(packetData); + byte mode = dataTypes.ReadNextByte(packetData); + string objectivevalue = String.Empty; + int type2 = -1; + if (mode == 0 || mode == 2) + { + objectivevalue = dataTypes.ReadNextString(packetData); + type2 = dataTypes.ReadNextVarInt(packetData); + } + + handler.OnScoreboardObjective(objectivename, mode, objectivevalue, type2); + break; + case PacketTypesIn.UpdateScore: + string entityname = dataTypes.ReadNextString(packetData); + int action3 = protocolVersion >= MC_1_18_2_Version + ? dataTypes.ReadNextVarInt(packetData) + : dataTypes.ReadNextByte(packetData); + string objectivename2 = string.Empty; + int value = -1; + if (action3 != 1 || protocolVersion >= MC_1_8_Version) + objectivename2 = dataTypes.ReadNextString(packetData); + if (action3 != 1) + value = dataTypes.ReadNextVarInt(packetData); + handler.OnUpdateScore(entityname, action3, objectivename2, value); + break; + case PacketTypesIn.BlockChangedAck: + handler.OnBlockChangeAck(dataTypes.ReadNextVarInt(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); + handler.OnBlockBreakAnimation(playerId, blockLocation, stage); + } + + break; + case PacketTypesIn.EntityAnimation: + if (handler.GetEntityHandlingEnabled()) + { + int playerId2 = dataTypes.ReadNextVarInt(packetData); + byte animation = dataTypes.ReadNextByte(packetData); + handler.OnEntityAnimation(playerId2, animation); + } + + break; + + case PacketTypesIn.OpenSignEditor: + var signLocation = dataTypes.ReadNextLocation(packetData); + var isFrontText = true; + + if (protocolVersion >= MC_1_20_Version) + isFrontText = dataTypes.ReadNextBool(packetData); + + // TODO: Use + break; + + // Temporarily disabled until I find a fix + /*case PacketTypesIn.BlockEntityData: + var location_ = dataTypes.ReadNextLocation(packetData); + var type_ = dataTypes.ReadNextInt(packetData); + var nbt = dataTypes.ReadNextNbt(packetData); + var nbtJson = JsonConvert.SerializeObject(nbt["messages"]); + + //log.Info($"BLOCK ENTITY DATA -> {location_.ToString()} [{type_}] -> NBT: {nbtJson}"); + + break;*/ + + default: + return false; //Ignored packet + } + + return true; //Packet processed + } + catch (Exception innerException) + { + 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( + string.Format(Translations.exception_packet_process, + packetPalette.GetIncommingTypeById(packetID), + packetID, + protocolVersion, + login_phase, + innerException.GetType()), + innerException); + } + } + + /// + /// Start the updating thread. Should be called after login success. + /// + private void StartUpdating() + { + 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); + } + + /// + /// Get net read thread (main thread) ID + /// + /// Net read thread ID + public int GetNetMainThreadId() + { + return netMain != null ? netMain.Item1.ManagedThreadId : -1; + } + + /// + /// Disconnect from the server, cancel network reading. + /// + public void Dispose() + { + try + { + if (netMain != null) + { + netMain.Item2.Cancel(); + } + + if (netReader != null) + { + netReader.Item2.Cancel(); + socketWrapper.Disconnect(); + } + } + catch + { + } + } + + /// + /// Send a packet to the server. Packet ID, compression, and encryption will be handled automatically. + /// + /// packet type + /// packet Data + private void SendPacket(PacketTypesOut packet, IEnumerable packetData) + { + SendPacket(packetPalette.GetOutgoingIdByType(packet), packetData); + } + + /// + /// Send a packet to the server. Compression and encryption will be handled automatically. + /// + /// packet ID + /// packet Data + private void SendPacket(int packetID, IEnumerable packetData) + { + if (handler.GetNetworkPacketCaptureEnabled()) + { + List clone = packetData.ToList(); + handler.OnNetworkPacket(packetID, clone, 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 (the_packet.Length >= compression_treshold) //Packet long enough for compressing? + { + byte[] compressed_packet = ZlibUtils.Compress(the_packet); + the_packet = dataTypes.ConcatBytes(DataTypes.GetVarInt(the_packet.Length), compressed_packet); + } + else + { + byte[] uncompressed_length = DataTypes.GetVarInt(0); //Not compressed (short packet) + the_packet = dataTypes.ConcatBytes(uncompressed_length, the_packet); + } + } + + //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)); + } + + /// + /// Do the Minecraft login. + /// + /// True if login successful + public bool Login(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); + + List fullLoginPacket = new(); + fullLoginPacket.AddRange(dataTypes.GetString(handler.GetUsername())); // Username + + // 1.19 - 1.19.2 + if (protocolVersion >= MC_1_19_Version && protocolVersion < MC_1_19_3_Version) + { + if (playerKeyPair == null) + fullLoginPacket.AddRange(dataTypes.GetBool(false)); // Has Sig Data + else + { + fullLoginPacket.AddRange(dataTypes.GetBool(true)); // Has Sig Data + fullLoginPacket.AddRange( + DataTypes.GetLong(playerKeyPair.GetExpirationMilliseconds())); // Expiration time + fullLoginPacket.AddRange( + dataTypes.GetArray(playerKeyPair.PublicKey.Key)); // Public key received from Microsoft API + if (protocolVersion >= MC_1_19_2_Version) + fullLoginPacket.AddRange( + dataTypes.GetArray(playerKeyPair.PublicKey + .SignatureV2!)); // Public key signature received from Microsoft API + else + fullLoginPacket.AddRange( + dataTypes.GetArray(playerKeyPair.PublicKey + .Signature!)); // Public key signature received from Microsoft API + } + } + + if (protocolVersion >= MC_1_19_2_Version) + { + Guid uuid = handler.GetUserUuid(); + + if (uuid == Guid.Empty) + fullLoginPacket.AddRange(dataTypes.GetBool(false)); // Has UUID + else + { + fullLoginPacket.AddRange(dataTypes.GetBool(true)); // Has UUID + fullLoginPacket.AddRange(DataTypes.GetUUID(uuid)); // UUID + } + } + + SendPacket(0x00, fullLoginPacket); + + while (true) + { + (int packetID, Queue packetData) = ReadNextPacket(); + if (packetID == 0x00) //Login rejected + { + handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, + ChatParser.ParseText(dataTypes.ReadNextString(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); + } + else if (packetID == 0x02) //Login successful + { + log.Info("§8" + Translations.mcc_server_offline); + login_phase = false; + + if (!pForge.CompleteForgeHandshake()) + { + log.Error("§8" + Translations.error_forge); + return false; + } + + StartUpdating(); + return true; //No need to check session or start encryption + } + else HandlePacket(packetID, packetData); + } + } + + /// + /// 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) + { + RSACryptoServiceProvider RSAService = CryptoHandler.DecodeRSAPublicKey(serverPublicKey)!; + byte[] secretKey = CryptoHandler.ClientAESPrivateKey ?? CryptoHandler.GenerateAESPrivateKey(); + + log.Debug("§8" + Translations.debug_crypto); + + if (serverIDhash != "-") + { + 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 (ProtocolHandler.SessionCheck(uuid, sessionID, serverHash)) + { + session.ServerIDhash = serverIDhash; + session.ServerPublicKey = serverPublicKey; + SessionCache.Store(InternalConfig.Account.Login.ToLower(), session); + } + else + { + handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, Translations.mcc_session_fail); + return false; + } + } + } + + // Encryption Response packet + List encryptionResponse = new(); + encryptionResponse.AddRange(dataTypes.GetArray(RSAService.Encrypt(secretKey, false))); // Shared Secret + + // 1.19 - 1.19.2 + if (protocolVersion >= MC_1_19_Version && protocolVersion < MC_1_19_3_Version) + { + if (playerKeyPair == null) + { + encryptionResponse.AddRange(dataTypes.GetBool(true)); // Has Verify Token + encryptionResponse.AddRange(dataTypes.GetArray(RSAService.Encrypt(token, false))); // Verify Token + } + else + { + byte[] salt = GenerateSalt(); + byte[] messageSignature = playerKeyPair.PrivateKey.SignData(dataTypes.ConcatBytes(token, salt)); + + encryptionResponse.AddRange(dataTypes.GetBool(false)); // Has Verify Token + encryptionResponse.AddRange(salt); // Salt + encryptionResponse.AddRange(dataTypes.GetArray(messageSignature)); // Message Signature + } + } + else + { + encryptionResponse.AddRange(dataTypes.GetArray(RSAService.Encrypt(token, false))); // Verify Token + } + + SendPacket(0x01, encryptionResponse); + + //Start client-side encryption + socketWrapper.SwitchToEncrypted(secretKey); // pre switch + + //Process the next packet + int loopPrevention = UInt16.MaxValue; + while (true) + { + (int packetID, Queue packetData) = ReadNextPacket(); + 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); + return false; + } + else if (packetID == 0x00) //Login rejected + { + handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, + ChatParser.ParseText(dataTypes.ReadNextString(packetData))); + return false; + } + else if (packetID == 0x02) //Login successful + { + Guid uuidReceived; + if (protocolVersion >= Protocol18Handler.MC_1_16_Version) + uuidReceived = dataTypes.ReadNextUUID(packetData); + else + uuidReceived = Guid.Parse(dataTypes.ReadNextString(packetData)); + string userName = dataTypes.ReadNextString(packetData); + Tuple[]? playerProperty = null; + if (protocolVersion >= Protocol18Handler.MC_1_19_Version) + { + int count = dataTypes.ReadNextVarInt(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; + playerProperty[i] = new Tuple(name, value, signature); + } + } + + handler.OnLoginSuccess(uuidReceived, userName, playerProperty); + + login_phase = false; + + if (!pForge.CompleteForgeHandshake()) + { + log.Error("§8" + Translations.error_forge_encrypt); + return false; + } + + StartUpdating(); + return true; + } + else HandlePacket(packetID, packetData); + } + } + + /// + /// Disconnect from the server + /// + public void Disconnect() + { + socketWrapper.Disconnect(); + } + + /// + /// Autocomplete text while typing username or command + /// + /// Text behind cursor + /// Completed text + int IAutoComplete.AutoComplete(string BehindCursor) + { + if (string.IsNullOrEmpty(BehindCursor)) + return -1; + + byte[] transaction_id = DataTypes.GetVarInt(autocomplete_transaction_id); + byte[] assume_command = new byte[] { 0x00 }; + byte[] has_position = new byte[] { 0x00 }; + + byte[] tabcomplete_packet = Array.Empty(); + + if (protocolVersion >= MC_1_8_Version) + { + if (protocolVersion >= MC_1_13_Version) + { + tabcomplete_packet = dataTypes.ConcatBytes(tabcomplete_packet, transaction_id); + tabcomplete_packet = dataTypes.ConcatBytes(tabcomplete_packet, + dataTypes.GetString(BehindCursor.Replace(' ', (char)0x00))); + } + else + { + tabcomplete_packet = dataTypes.ConcatBytes(tabcomplete_packet, dataTypes.GetString(BehindCursor)); + + if (protocolVersion >= MC_1_9_Version) + tabcomplete_packet = dataTypes.ConcatBytes(tabcomplete_packet, assume_command); + + tabcomplete_packet = dataTypes.ConcatBytes(tabcomplete_packet, has_position); + } + } + else + { + tabcomplete_packet = dataTypes.ConcatBytes(dataTypes.GetString(BehindCursor)); + } + + ConsoleIO.AutoCompleteDone = false; + SendPacket(PacketTypesOut.TabComplete, tabcomplete_packet); + return autocomplete_transaction_id; + } + + /// + /// 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) + { + string version = ""; + TcpClient tcp = ProxyHandler.NewTcpClient(host, port); + tcp.ReceiveTimeout = 30000; // 30 seconds + tcp.ReceiveBufferSize = 1024 * 1024; + SocketWrapper socketWrapper = new(tcp); + DataTypes dataTypes = new(MC_1_8_Version); + + byte[] packet_id = DataTypes.GetVarInt(0); + byte[] protocol_version = DataTypes.GetVarInt(-1); + byte[] server_port = BitConverter.GetBytes((ushort)port); + Array.Reverse(server_port); + byte[] next_state = DataTypes.GetVarInt(1); + 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); + + byte[] status_request = DataTypes.GetVarInt(0); + byte[] request_packet = dataTypes.ConcatBytes(DataTypes.GetVarInt(status_request.Length), status_request); + + socketWrapper.SendDataRAW(request_packet); + + int packetLength = dataTypes.ReadNextVarIntRAW(socketWrapper); + if (packetLength > 0) //Read Response length + { + Queue packetData = new(socketWrapper.ReadDataRAW(packetLength)); + if (dataTypes.ReadNextVarInt(packetData) == 0x00) //Read Packet ID + { + string result = dataTypes.ReadNextString(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 (!String.IsNullOrEmpty(result) && result.StartsWith("{") && result.EndsWith("}")) + { + Json.JSONData jsonData = Json.ParseJson(result); + if (jsonData.Type == Json.JSONData.DataType.Object && + jsonData.Properties.ContainsKey("version")) + { + Json.JSONData versionData = jsonData.Properties["version"]; + + //Retrieve display name of the Minecraft version + if (versionData.Properties.ContainsKey("name")) + version = versionData.Properties["name"].StringValue; + + //Retrieve protocol version number for handling this server + if (versionData.Properties.ContainsKey("protocol")) + protocolVersion = int.Parse(versionData.Properties["protocol"].StringValue, + NumberStyles.Any, CultureInfo.CurrentCulture); + + // Check for forge on the server. + Protocol18Forge.ServerInfoCheckForge(jsonData, ref forgeInfo); + + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_server_protocol, version, + protocolVersion + (forgeInfo != null ? Translations.mcc_with_forge : ""))); + + return true; + } + } + } + } + + return false; + } + + /// + /// Get max length for chat messages + /// + /// Max length, in characters + public int GetMaxChatMessageLength() + { + return protocolVersion > MC_1_10_Version + ? 256 + : 100; + } + + /// + /// Get the current protocol version. + /// + /// + /// Version-specific operations should be handled inside the Protocol handled whenever possible. + /// + /// Minecraft Protocol version number + public int GetProtocolVersion() + { + return protocolVersion; + } + + /// + /// Send MessageAcknowledgment packet + /// + /// Message acknowledgment + /// True if properly sent + public bool SendMessageAcknowledgment(LastSeenMessageList.Acknowledgment acknowledgment) + { + try + { + byte[] fields = dataTypes.GetAcknowledgment(acknowledgment, + isOnlineMode && Config.Signature.LoginWithSecureProfile); + + SendPacket(PacketTypesOut.MessageAcknowledgment, fields); + + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + /// + /// Send MessageAcknowledgment packet + /// + /// Message acknowledgment + /// True if properly sent + public bool SendMessageAcknowledgment(int messageCount) + { + try + { + byte[] fields = DataTypes.GetVarInt(messageCount); + + SendPacket(PacketTypesOut.MessageAcknowledgment, fields); + + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + public LastSeenMessageList.Acknowledgment ConsumeAcknowledgment() + { + pendingAcknowledgments = 0; + return new LastSeenMessageList.Acknowledgment(lastSeenMessagesCollector.GetLastSeenMessages(), + lastReceivedMessage); + } + + public void Acknowledge(ChatMessage message) + { + LastSeenMessageList.AcknowledgedMessage? entry = message.ToLastSeenMessageEntry(); + + if (entry != null) + { + if (protocolVersion >= MC_1_19_3_Version) + { + if (lastSeenMessagesCollector.Add_1_19_3(entry, true)) + { + if (lastSeenMessagesCollector.messageCount > 64) + { + int messageCount = lastSeenMessagesCollector.ResetMessageCount(); + if (messageCount > 0) + SendMessageAcknowledgment(messageCount); + } + } + } + else + { + lastSeenMessagesCollector.Add_1_19_2(entry); + lastReceivedMessage = null; + if (pendingAcknowledgments++ > 64) + SendMessageAcknowledgment(ConsumeAcknowledgment()); + } + } + } + + /// + /// Send a chat command to the server - 1.19 and above + /// + /// Command + /// PlayerKeyPair + /// True if properly sent + public bool SendChatCommand(string command, PlayerKeyPair? playerKeyPair) + { + if (String.IsNullOrEmpty(command)) + return true; + + command = Regex.Replace(command, @"\s+", " "); + command = Regex.Replace(command, @"\s$", string.Empty); + + log.Debug("chat command = " + command); + + try + { + List>? needSigned = null; // List< Argument Name, Argument Value > + if (playerKeyPair != null && isOnlineMode && protocolVersion >= MC_1_19_Version + && Config.Signature.LoginWithSecureProfile && Config.Signature.SignMessageInCommand) + needSigned = DeclareCommands.CollectSignArguments(command); + + lock (MessageSigningLock) + { + LastSeenMessageList.Acknowledgment? acknowledgment_1_19_2 = + (protocolVersion == MC_1_19_2_Version) ? ConsumeAcknowledgment() : null; + + (LastSeenMessageList.AcknowledgedMessage[] acknowledgment_1_19_3, byte[] bitset_1_19_3, + int messageCount_1_19_3) = + (protocolVersion >= MC_1_19_3_Version) + ? lastSeenMessagesCollector.Collect_1_19_3() + : new(Array.Empty(), Array.Empty(), 0); + + List fields = new(); + + // Command: String + fields.AddRange(dataTypes.GetString(command)); + + // Timestamp: Instant(Long) + DateTimeOffset timeNow = DateTimeOffset.UtcNow; + fields.AddRange(DataTypes.GetLong(timeNow.ToUnixTimeMilliseconds())); + + if (needSigned == null || needSigned!.Count == 0) + { + fields.AddRange(DataTypes.GetLong(0)); // Salt: Long + fields.AddRange(DataTypes.GetVarInt(0)); // Signature Length: VarInt + } + else + { + Guid uuid = handler.GetUserUuid(); + byte[] salt = GenerateSalt(); + fields.AddRange(salt); // Salt: Long + fields.AddRange(DataTypes.GetVarInt(needSigned.Count)); // Signature Length: VarInt + foreach ((string argName, string message) in needSigned) + { + fields.AddRange(dataTypes.GetString(argName)); // Argument name: String + + byte[] sign; + if (protocolVersion == MC_1_19_Version) + sign = playerKeyPair!.PrivateKey.SignMessage(message, uuid, timeNow, ref salt); + else if (protocolVersion == MC_1_19_2_Version) + sign = playerKeyPair!.PrivateKey.SignMessage(message, uuid, timeNow, ref salt, + acknowledgment_1_19_2!.lastSeen); + else // protocolVersion >= MC_1_19_3_Version + sign = playerKeyPair!.PrivateKey.SignMessage(message, uuid, chatUuid, messageIndex++, + timeNow, ref salt, acknowledgment_1_19_3); + + if (protocolVersion <= MC_1_19_2_Version) + fields.AddRange(DataTypes.GetVarInt(sign.Length)); // Signature length: VarInt + + fields.AddRange(sign); // Signature: Byte Array + } + } + + if (protocolVersion <= MC_1_19_2_Version) + fields.AddRange(dataTypes.GetBool(false)); // Signed Preview: Boolean + + if (protocolVersion == MC_1_19_2_Version) + { + // Message Acknowledgment (1.19.2) + fields.AddRange(dataTypes.GetAcknowledgment(acknowledgment_1_19_2!, + isOnlineMode && Config.Signature.LoginWithSecureProfile)); + } + else if (protocolVersion >= MC_1_19_3_Version) + { + // message count + fields.AddRange(DataTypes.GetVarInt(messageCount_1_19_3)); + + // Acknowledged: BitSet + fields.AddRange(bitset_1_19_3); + } + + SendPacket(PacketTypesOut.ChatCommand, fields); + } + + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + /// + /// Send a chat message to the server + /// + /// Message + /// PlayerKeyPair + /// True if properly sent + public bool SendChatMessage(string message, PlayerKeyPair? playerKeyPair) + { + 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); + + try + { + List fields = new(); + + // Message: String (up to 256 chars) + fields.AddRange(dataTypes.GetString(message)); + + if (protocolVersion >= MC_1_19_Version) + { + lock (MessageSigningLock) + { + LastSeenMessageList.Acknowledgment? acknowledgment_1_19_2 = + (protocolVersion == MC_1_19_2_Version) ? ConsumeAcknowledgment() : null; + + (LastSeenMessageList.AcknowledgedMessage[] acknowledgment_1_19_3, byte[] bitset_1_19_3, + int messageCount_1_19_3) = + (protocolVersion >= MC_1_19_3_Version) + ? lastSeenMessagesCollector.Collect_1_19_3() + : new(Array.Empty(), Array.Empty(), 0); + + // Timestamp: Instant(Long) + DateTimeOffset timeNow = DateTimeOffset.UtcNow; + fields.AddRange(DataTypes.GetLong(timeNow.ToUnixTimeMilliseconds())); + + if (!isOnlineMode || playerKeyPair == null || !Config.Signature.LoginWithSecureProfile || + !Config.Signature.SignChat) + { + fields.AddRange(DataTypes.GetLong(0)); // Salt: Long + if (protocolVersion < MC_1_19_3_Version) + fields.AddRange(DataTypes.GetVarInt(0)); // Signature Length: VarInt (1.19 - 1.19.2) + else + fields.AddRange(dataTypes.GetBool(false)); // Has signature: bool (1.19.3) + } + else + { + // Salt: Long + byte[] salt = GenerateSalt(); + fields.AddRange(salt); + + // Signature Length & Signature: (VarInt) and Byte Array + Guid playerUuid = handler.GetUserUuid(); + byte[] sign; + if (protocolVersion == MC_1_19_Version) // 1.19.1 or lower + sign = playerKeyPair.PrivateKey.SignMessage(message, playerUuid, timeNow, ref salt); + else if (protocolVersion == MC_1_19_2_Version) // 1.19.2 + sign = playerKeyPair.PrivateKey.SignMessage(message, playerUuid, timeNow, ref salt, + acknowledgment_1_19_2!.lastSeen); + else // protocolVersion >= MC_1_19_3_Version + sign = playerKeyPair.PrivateKey.SignMessage(message, playerUuid, chatUuid, + messageIndex++, timeNow, ref salt, acknowledgment_1_19_3); + + if (protocolVersion >= MC_1_19_3_Version) + fields.AddRange(dataTypes.GetBool(true)); + else + fields.AddRange(DataTypes.GetVarInt(sign.Length)); + fields.AddRange(sign); + } + + if (protocolVersion <= MC_1_19_2_Version) + fields.AddRange(dataTypes.GetBool(false)); // Signed Preview: Boolean + + if (protocolVersion >= MC_1_19_3_Version) + { + // message count + fields.AddRange(DataTypes.GetVarInt(messageCount_1_19_3)); + + // Acknowledged: BitSet + fields.AddRange(bitset_1_19_3); + } + else if (protocolVersion == MC_1_19_2_Version) + { + // Message Acknowledgment + fields.AddRange(dataTypes.GetAcknowledgment(acknowledgment_1_19_2!, + isOnlineMode && Config.Signature.LoginWithSecureProfile)); + } + } + } + + SendPacket(PacketTypesOut.ChatMessage, fields); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + public bool SendEntityAction(int PlayerEntityID, int ActionID) + { + try + { + List fields = new(); + fields.AddRange(DataTypes.GetVarInt(PlayerEntityID)); + fields.AddRange(DataTypes.GetVarInt(ActionID)); + fields.AddRange(DataTypes.GetVarInt(0)); + SendPacket(PacketTypesOut.EntityAction, fields); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + /// + /// Send a respawn packet to the server + /// + /// True if properly sent + public bool SendRespawnPacket() + { + try + { + SendPacket(PacketTypesOut.ClientStatus, new byte[] { 0 }); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + /// + /// Tell the server what client is being used to connect to the server + /// + /// Client string describing the client + /// True if brand info was successfully sent + public bool SendBrandInfo(string 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)); + } + else + { + return SendPluginChannelPacket("MC|Brand", dataTypes.GetString(brandInfo)); + } + } + + /// + /// Inform the server of the client's Minecraft settings + /// + /// Client language eg en_US + /// View distance, in chunks + /// Game difficulty (client-side...) + /// Chat mode (allows muting yourself) + /// Show chat colors + /// 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) + { + try + { + List fields = new(); + fields.AddRange(dataTypes.GetString(language)); + fields.Add(viewDistance); + + if (protocolVersion >= MC_1_9_Version) + fields.AddRange(DataTypes.GetVarInt(chatMode)); + else + fields.AddRange(new byte[] { chatMode }); + + fields.Add(chatColors ? (byte)1 : (byte)0); + if (protocolVersion < MC_1_8_Version) + { + fields.Add(difficulty); + fields.Add((byte)(skinParts & 0x1)); //show cape + } + else fields.Add(skinParts); + + if (protocolVersion >= MC_1_9_Version) + fields.AddRange(DataTypes.GetVarInt(mainHand)); + if (protocolVersion >= MC_1_17_Version) + { + if (protocolVersion >= MC_1_18_1_Version) + fields.Add(0); // 1.18 and above - Enable text filtering. (Always false) + else + fields.Add(1); // 1.17 and 1.17.1 - Disable text filtering. (Always true) + } + + if (protocolVersion >= MC_1_18_1_Version) + fields.Add(1); // 1.18 and above - Allow server listings + SendPacket(PacketTypesOut.ClientSettings, fields); + } + catch (SocketException) + { + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + + return false; + } + + + /// + /// Send a location update to the server + /// + /// The new location of the player + /// True if the player is on the ground + /// 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) + { + return SendLocationUpdate(location, onGround, yaw, pitch, true); + } + + public bool SendLocationUpdate(Location location, bool onGround, float? yaw = null, float? pitch = null, + bool forceUpdate = false) + { + if (handler.GetTerrainEnabled()) + { + byte[] yawpitch = Array.Empty(); + PacketTypesOut packetType = PacketTypesOut.PlayerPosition; + + if (Config.Main.Advanced.TemporaryFixBadpacket) + { + if (yaw.HasValue && pitch.HasValue && + (forceUpdate || yaw.Value != LastYaw || pitch.Value != LastPitch)) + { + yawpitch = dataTypes.ConcatBytes(dataTypes.GetFloat(yaw.Value), + dataTypes.GetFloat(pitch.Value)); + packetType = PacketTypesOut.PlayerPositionAndRotation; + + LastYaw = yaw.Value; + LastPitch = pitch.Value; + } + } + else + { + if (yaw.HasValue && pitch.HasValue) + { + yawpitch = dataTypes.ConcatBytes(dataTypes.GetFloat(yaw.Value), + dataTypes.GetFloat(pitch.Value)); + packetType = PacketTypesOut.PlayerPositionAndRotation; + + LastYaw = yaw.Value; + LastPitch = pitch.Value; + } + } + + try + { + SendPacket(packetType, dataTypes.ConcatBytes( + dataTypes.GetDouble(location.X), + dataTypes.GetDouble(location.Y), + protocolVersion < MC_1_8_Version + ? dataTypes.GetDouble(location.Y + 1.62) + : Array.Empty(), + dataTypes.GetDouble(location.Z), + yawpitch, + new byte[] { onGround ? (byte)1 : (byte)0 })); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + else return false; + } + + /// + /// Send a plugin channel packet (0x17) to the server, compression and encryption will be handled automatically + /// + /// Channel to send packet on + /// packet Data + public bool SendPluginChannelPacket(string channel, byte[] data) + { + try + { + // In 1.7, length needs to be included. + // In 1.8, it must not be. + if (protocolVersion < MC_1_8_Version) + { + byte[] length = BitConverter.GetBytes((short)data.Length); + Array.Reverse(length); + + SendPacket(PacketTypesOut.PluginMessage, + dataTypes.ConcatBytes(dataTypes.GetString(channel), length, data)); + } + else + { + SendPacket(PacketTypesOut.PluginMessage, dataTypes.ConcatBytes(dataTypes.GetString(channel), data)); + } + + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + /// + /// Send a Login Plugin Response packet (0x02) + /// + /// Login Plugin Request message Id + /// TRUE if the request was understood + /// Response to the request + /// TRUE if successfully sent + public bool SendLoginPluginResponse(int messageId, bool understood, byte[] data) + { + try + { + SendPacket(0x02, + dataTypes.ConcatBytes(DataTypes.GetVarInt(messageId), dataTypes.GetBool(understood), data)); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + /// + /// Send an Interact Entity Packet to server + /// + /// + /// + /// + public bool SendInteractEntity(int EntityID, int type) + { + try + { + List fields = new(); + fields.AddRange(DataTypes.GetVarInt(EntityID)); + fields.AddRange(DataTypes.GetVarInt(type)); + + // Is player Sneaking (Only 1.16 and above) + // Currently hardcoded to false + // TODO: Update to reflect the real player state + if (protocolVersion >= MC_1_16_Version) + fields.AddRange(dataTypes.GetBool(false)); + + SendPacket(PacketTypesOut.InteractEntity, fields); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.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) + { + try + { + List fields = new(); + fields.AddRange(DataTypes.GetVarInt(EntityID)); + fields.AddRange(DataTypes.GetVarInt(type)); + fields.AddRange(dataTypes.GetFloat(X)); + fields.AddRange(dataTypes.GetFloat(Y)); + fields.AddRange(dataTypes.GetFloat(Z)); + fields.AddRange(DataTypes.GetVarInt(hand)); + // Is player Sneaking (Only 1.16 and above) + // Currently hardcoded to false + // TODO: Update to reflect the real player state + if (protocolVersion >= MC_1_16_Version) + fields.AddRange(dataTypes.GetBool(false)); + SendPacket(PacketTypesOut.InteractEntity, fields); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + public bool SendInteractEntity(int EntityID, int type, int hand) + { + try + { + List fields = new(); + fields.AddRange(DataTypes.GetVarInt(EntityID)); + fields.AddRange(DataTypes.GetVarInt(type)); + fields.AddRange(DataTypes.GetVarInt(hand)); + // Is player Sneaking (Only 1.16 and above) + // Currently hardcoded to false + // TODO: Update to reflect the real player state + if (protocolVersion >= MC_1_16_Version) + fields.AddRange(dataTypes.GetBool(false)); + SendPacket(PacketTypesOut.InteractEntity, fields); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + public bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z) + { + return false; + } + + public bool SendUseItem(int hand, int sequenceId) + { + if (protocolVersion < MC_1_9_Version) + return false; // Packet does not exist prior to MC 1.9 + // According to https://wiki.vg/index.php?title=Protocol&oldid=5486#Player_Block_Placement + // MC 1.7 does this using Player Block Placement with special values + // TODO once Player Block Placement is implemented for older versions + try + { + List packet = new(); + packet.AddRange(DataTypes.GetVarInt(hand)); + if (protocolVersion >= MC_1_19_Version) + packet.AddRange(DataTypes.GetVarInt(sequenceId)); + SendPacket(PacketTypesOut.UseItem, packet); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + public bool SendPlayerDigging(int status, Location location, Direction face, int sequenceId) + { + try + { + List packet = new(); + packet.AddRange(DataTypes.GetVarInt(status)); + packet.AddRange(dataTypes.GetLocation(location)); + packet.AddRange(DataTypes.GetVarInt(dataTypes.GetBlockFace(face))); + if (protocolVersion >= MC_1_19_Version) + packet.AddRange(DataTypes.GetVarInt(sequenceId)); + SendPacket(PacketTypesOut.PlayerDigging, packet); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + public bool SendPlayerBlockPlacement(int hand, Location location, Direction face, int sequenceId) + { + if (protocolVersion < MC_1_14_Version) + { + Container? playerInventory = handler.GetInventory(0); + + if (playerInventory == null) + return false; + + List packet = new List(); + + packet.AddRange(dataTypes.GetLocation(location)); + packet.Add(dataTypes.GetBlockFace(face)); + + Item item = playerInventory.Items[((McClient)handler).GetCurrentSlot()]; + packet.AddRange(dataTypes.GetItemSlot(item, itemPalette)); + + packet.Add((byte)0); // cursorX + packet.Add((byte)0); // cursorY + packet.Add((byte)0); // cursorZ + + SendPacket(PacketTypesOut.PlayerBlockPlacement, packet); + return true; + } + + try + { + List packet = new List(); + packet.AddRange(DataTypes.GetVarInt(hand)); + packet.AddRange(dataTypes.GetLocation(location)); + packet.AddRange(DataTypes.GetVarInt(dataTypes.GetBlockFace(face))); + packet.AddRange(dataTypes.GetFloat(0.5f)); // cursorX + packet.AddRange(dataTypes.GetFloat(0.5f)); // cursorY + packet.AddRange(dataTypes.GetFloat(0.5f)); // cursorZ + packet.Add(0); // insideBlock = false; + if (protocolVersion >= MC_1_19_Version) + packet.AddRange(DataTypes.GetVarInt(sequenceId)); + SendPacket(PacketTypesOut.PlayerBlockPlacement, packet); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + public bool SendHeldItemChange(short slot) + { + try + { + List packet = new(); + packet.AddRange(dataTypes.GetShort(slot)); + SendPacket(PacketTypesOut.HeldItemChange, packet); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + public bool SendWindowAction(int windowId, int slotId, WindowActionType action, Item? item, + List> changedSlots, int stateId) + { + try + { + short actionNumber; + lock (window_actions) + { + if (!window_actions.ContainsKey(windowId)) + window_actions[windowId] = 0; + actionNumber = (short)(window_actions[windowId] + 1); + window_actions[windowId] = actionNumber; + } + + byte button = 0; + byte mode = 0; + + switch (action) + { + case WindowActionType.LeftClick: + button = 0; + break; + case WindowActionType.RightClick: + button = 1; + break; + case WindowActionType.MiddleClick: + button = 2; + mode = 3; + break; + case WindowActionType.ShiftClick: + button = 0; + mode = 1; + item = new Item(ItemType.Null, 0, null); + break; + case WindowActionType.DropItem: + button = 0; + mode = 4; + item = new Item(ItemType.Null, 0, null); + break; + case WindowActionType.DropItemStack: + button = 1; + mode = 4; + item = new Item(ItemType.Null, 0, null); + break; + case WindowActionType.StartDragLeft: + button = 0; + mode = 5; + item = new Item(ItemType.Null, 0, null); + slotId = -999; + break; + case WindowActionType.StartDragRight: + button = 4; + mode = 5; + item = new Item(ItemType.Null, 0, null); + slotId = -999; + break; + case WindowActionType.StartDragMiddle: + button = 8; + mode = 5; + item = new Item(ItemType.Null, 0, null); + slotId = -999; + break; + case WindowActionType.EndDragLeft: + button = 2; + mode = 5; + item = new Item(ItemType.Null, 0, null); + slotId = -999; + break; + case WindowActionType.EndDragRight: + button = 6; + mode = 5; + item = new Item(ItemType.Null, 0, null); + slotId = -999; + break; + case WindowActionType.EndDragMiddle: + button = 10; + mode = 5; + item = new Item(ItemType.Null, 0, null); + slotId = -999; + break; + case WindowActionType.AddDragLeft: + button = 1; + mode = 5; + item = new Item(ItemType.Null, 0, null); + break; + case WindowActionType.AddDragRight: + button = 5; + mode = 5; + item = new Item(ItemType.Null, 0, null); + break; + case WindowActionType.AddDragMiddle: + button = 9; + mode = 5; + item = new Item(ItemType.Null, 0, null); + break; + } + + List packet = new() + { + (byte)windowId // Window ID + }; + + // 1.18+ + if (protocolVersion >= MC_1_18_1_Version) + { + packet.AddRange(DataTypes.GetVarInt(stateId)); // State ID + packet.AddRange(dataTypes.GetShort((short)slotId)); // Slot ID + } + // 1.17.1 + else if (protocolVersion == MC_1_17_1_Version) + { + packet.AddRange(dataTypes.GetShort((short)slotId)); // Slot ID + packet.AddRange(DataTypes.GetVarInt(stateId)); // State ID + } + // Older + else + { + packet.AddRange(dataTypes.GetShort((short)slotId)); // Slot ID + } + + packet.Add(button); // Button + + if (protocolVersion < MC_1_17_Version) + packet.AddRange(dataTypes.GetShort(actionNumber)); + + if (protocolVersion >= MC_1_9_Version) + packet.AddRange(DataTypes.GetVarInt(mode)); // Mode + else packet.Add(mode); + + // 1.17+ Array of changed slots + if (protocolVersion >= MC_1_17_Version) + { + packet.AddRange(DataTypes.GetVarInt(changedSlots.Count)); // Length of the array + foreach (var slot in changedSlots) + { + packet.AddRange(dataTypes.GetShort(slot.Item1)); // slot ID + packet.AddRange(dataTypes.GetItemSlot(slot.Item2, itemPalette)); // slot Data + } + } + + packet.AddRange(dataTypes.GetItemSlot(item, itemPalette)); // Carried item (Clicked item) + + SendPacket(PacketTypesOut.ClickWindow, packet); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + public bool 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); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + public bool ClickContainerButton(int windowId, int buttonId) + { + try + { + List packet = new(); + packet.Add((byte)windowId); + packet.Add((byte)buttonId); + SendPacket(PacketTypesOut.ClickWindowButton, packet); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + public bool SendAnimation(int animation, int playerid) + { + try + { + if (animation == 0 || animation == 1) + { + List packet = new(); + + if (protocolVersion < MC_1_8_Version) + { + packet.AddRange(DataTypes.GetInt(playerid)); + packet.Add((byte)1); // Swing arm + } + else if (protocolVersion < MC_1_9_Version) + { + // No fields in 1.8.X + } + else // MC 1.9+ + { + packet.AddRange(DataTypes.GetVarInt(animation)); + } + + SendPacket(PacketTypesOut.Animation, packet); + return true; + } + else + { + return false; + } + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + public bool SendCloseWindow(int windowId) + { + try + { + lock (window_actions) + { + if (window_actions.ContainsKey(windowId)) + window_actions[windowId] = 0; + } + + SendPacket(PacketTypesOut.CloseWindow, new[] { (byte)windowId }); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + public bool SendUpdateSign(Location sign, string line1, string line2, string line3, string line4, bool isFrontText = true) + { + try + { + if (line1.Length > 23) + line1 = line1[..23]; + if (line2.Length > 23) + line2 = line1[..23]; + if (line3.Length > 23) + line3 = line1[..23]; + if (line4.Length > 23) + line4 = line1[..23]; + + List packet = new(); + packet.AddRange(dataTypes.GetLocation(sign)); + if(protocolVersion >= MC_1_20_Version) + packet.AddRange(dataTypes.GetBool((isFrontText))); + packet.AddRange(dataTypes.GetString(line1)); + packet.AddRange(dataTypes.GetString(line2)); + packet.AddRange(dataTypes.GetString(line3)); + packet.AddRange(dataTypes.GetString(line4)); + SendPacket(PacketTypesOut.UpdateSign, packet); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + public bool UpdateCommandBlock(Location location, string command, CommandBlockMode mode, + CommandBlockFlags flags) + { + if (protocolVersion <= MC_1_13_Version) + { + try + { + List packet = new(); + packet.AddRange(dataTypes.GetLocation(location)); + packet.AddRange(dataTypes.GetString(command)); + packet.AddRange(DataTypes.GetVarInt((int)mode)); + packet.Add((byte)flags); + SendPacket(PacketTypesOut.UpdateSign, packet); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + else + { + return false; + } + } + + public bool SendWindowConfirmation(byte windowID, short actionID, bool accepted) + { + try + { + List packet = new(); + packet.Add(windowID); + packet.AddRange(dataTypes.GetShort(actionID)); + packet.Add(accepted ? (byte)1 : (byte)0); + SendPacket(PacketTypesOut.WindowConfirmation, packet); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + public bool SelectTrade(int selectedSlot) + { + // MC 1.13 or greater + if (protocolVersion >= MC_1_13_Version) + { + try + { + List packet = new(); + packet.AddRange(DataTypes.GetVarInt(selectedSlot)); + SendPacket(PacketTypesOut.SelectTrade, packet); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + else + { + return false; + } + } + + public bool SendSpectate(Guid UUID) + { + // MC 1.8 or greater + if (protocolVersion >= MC_1_8_Version) + { + try + { + List packet = new(); + packet.AddRange(DataTypes.GetUUID(UUID)); + SendPacket(PacketTypesOut.Spectate, packet); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + else + { + return false; + } + } + + public bool SendPlayerSession(PlayerKeyPair? playerKeyPair) + { + if (playerKeyPair == null || !isOnlineMode) + return false; + + if (protocolVersion >= MC_1_19_3_Version) + { + try + { + List packet = new(); + + packet.AddRange(DataTypes.GetUUID(chatUuid)); + packet.AddRange(DataTypes.GetLong(playerKeyPair.GetExpirationMilliseconds())); + packet.AddRange(DataTypes.GetVarInt(playerKeyPair.PublicKey.Key.Length)); + packet.AddRange(playerKeyPair.PublicKey.Key); + packet.AddRange(DataTypes.GetVarInt(playerKeyPair.PublicKey.SignatureV2!.Length)); + packet.AddRange(playerKeyPair.PublicKey.SignatureV2); + + log.Debug( + $"SendPlayerSession MessageUUID = {chatUuid.ToString()}, len(PublicKey) = {playerKeyPair.PublicKey.Key.Length}, len(SignatureV2) = {playerKeyPair.PublicKey.SignatureV2!.Length}"); + + SendPacket(PacketTypesOut.PlayerSession, packet); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + else + { + return false; + } + } + + public bool SendRenameItem(string itemName) + { + try + { + List packet = new(); + packet.AddRange(dataTypes.GetString(itemName.Length > 50 ? itemName[..50] : itemName)); + SendPacket(PacketTypesOut.NameItem, packet); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + private byte[] GenerateSalt() + { + byte[] salt = new byte[8]; + randomGen.GetNonZeroBytes(salt); + return salt; + } + } } \ No newline at end of file diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index 0128d460..de7c104a 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -1,991 +1,991 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Authentication; -using System.Text; -using DnsClient; -using MinecraftClient.Protocol.Handlers; -using MinecraftClient.Protocol.Handlers.Forge; -using MinecraftClient.Protocol.Session; -using MinecraftClient.Proxy; -using static MinecraftClient.Settings; -using static MinecraftClient.Settings.MainConfigHealper.MainConfig.GeneralConfig; - -namespace MinecraftClient.Protocol -{ - /// - /// Handle login, session, server ping and provide a protocol handler for interacting with a minecraft server. - /// - /// - /// Typical update steps for marking a new Minecraft version as supported: - /// - Add protocol ID in GetProtocolHandler() - /// - Add 1.X.X case in MCVer2ProtocolVersion() - /// - public static class ProtocolHandler - { - /// - /// Perform a DNS lookup for a Minecraft Service using the specified domain name - /// - /// 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) - { - bool foundService = false; - string domainVal = domain; - ushort portVal = port; - - if (!String.IsNullOrEmpty(domain) && domain.Any(c => char.IsLetter(c))) - { - AutoTimeout.Perform(() => - { - try - { - 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; - } - } - 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)); - } - - domain = domainVal; - port = portVal; - return foundService; - } - - /// - /// Retrieve information about a Minecraft server - /// - /// Server IP to ping - /// 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) - { - bool success = false; - int protocolversionTmp = 0; - ForgeInfo? forgeInfoTmp = null; - if (AutoTimeout.Perform(() => - { - try - { - 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); - } - catch (Exception e) - { - ConsoleIO.WriteLineFormatted(String.Format("§8{0}: {1}", e.GetType().FullName, e.Message)); - } - }, 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; - } - } - - /// - /// Get a protocol handler for the specified Minecraft version - /// - /// Tcp Client connected to the server - /// Protocol version to handle - /// Handler with the appropriate callbacks - /// - public static IMinecraftCom GetProtocolHandler(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); - - 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, 761, 762, 763 }; - - if (Array.IndexOf(supportedVersions_Protocol18, ProtocolVersion) > -1) - return new Protocol18Handler(Client, ProtocolVersion, Handler, forgeInfo); - - throw new NotSupportedException(string.Format(Translations.exception_version_unsupport, ProtocolVersion)); - } - - /// - /// Convert a human-readable Minecraft version number to network protocol version number - /// - /// The Minecraft version number - /// The protocol version number or 0 if could not determine protocol version: error, unknown, not supported - public static int MCVer2ProtocolVersion(string MCVersion) - { - if (MCVersion.Contains('.')) - { - switch (MCVersion.Split(' ')[0].Trim()) - { - case "1.0": - case "1.0.0": - case "1.0.1": - return 22; - case "1.1": - case "1.1.0": - return 23; - case "1.2": - case "1.2.0": - case "1.2.1": - case "1.2.2": - case "1.2.3": - return 28; - case "1.2.4": - case "1.2.5": - return 29; - case "1.3": - case "1.3.0": - case "1.3.1": - case "1.3.2": - return 39; - case "1.4": - case "1.4.0": - case "1.4.1": - case "1.4.2": - return 48; // 47 conflicts with 1.8 - case "1.4.3": - return 48; - case "1.4.4": - case "1.4.5": - return 49; - case "1.4.6": - case "1.4.7": - return 51; - case "1.5": - case "1.5.0": - case "1.5.1": - return 60; - case "1.5.2": - return 61; - case "1.6": - case "1.6.0": - return 72; - case "1.6.1": - return 73; - case "1.6.2": - return 74; - case "1.6.3": - return 77; - case "1.6.4": - return 78; - case "1.7": - case "1.7.0": - case "1.7.1": - return 3; - case "1.7.2": - case "1.7.3": - case "1.7.4": - case "1.7.5": - return 4; - case "1.7.6": - case "1.7.7": - case "1.7.8": - case "1.7.9": - case "1.7.10": - return 5; - case "1.8": - case "1.8.0": - case "1.8.1": - case "1.8.2": - case "1.8.3": - case "1.8.4": - case "1.8.5": - case "1.8.6": - case "1.8.7": - case "1.8.8": - case "1.8.9": - return 47; - case "1.9": - case "1.9.0": - return 107; - case "1.9.1": - return 108; - case "1.9.2": - return 109; - case "1.9.3": - case "1.9.4": - return 110; - case "1.10": - case "1.10.0": - case "1.10.1": - case "1.10.2": - return 210; - case "1.11": - case "1.11.0": - return 315; - case "1.11.1": - case "1.11.2": - return 316; - case "1.12": - case "1.12.0": - return 335; - case "1.12.1": - return 338; - case "1.12.2": - return 340; - case "1.13": - case "1.13.0": - return 393; - case "1.13.1": - return 401; - case "1.13.2": - return 404; - case "1.14": - case "1.14.0": - return 477; - case "1.14.1": - return 480; - case "1.14.2": - return 485; - case "1.14.3": - return 490; - case "1.14.4": - return 498; - case "1.15": - case "1.15.0": - return 573; - case "1.15.1": - return 575; - case "1.15.2": - return 578; - case "1.16": - case "1.16.0": - return 735; - case "1.16.1": - return 736; - case "1.16.2": - return 751; - case "1.16.3": - return 753; - case "1.16.4": - case "1.16.5": - return 754; - case "1.17": - case "1.17.0": - return 755; - case "1.17.1": - return 756; - case "1.18": - case "1.18.0": - case "1.18.1": - return 757; - case "1.18.2": - return 758; - case "1.19": - case "1.19.0": - return 759; - case "1.19.1": - case "1.19.2": - return 760; - case "1.19.3": - return 761; - case "1.19.4": - return 762; - case "1.20": - case "1.20.1": - return 763; - default: - return 0; - } - } - else - { - try - { - return int.Parse(MCVersion, NumberStyles.Any, CultureInfo.CurrentCulture); - } - catch - { - return 0; - } - } - } - - /// - /// Convert a network protocol version number to human-readable Minecraft version number - /// - /// Some Minecraft versions share the same protocol number. In that case, the lowest version for that protocol is returned. - /// The Minecraft protocol version number - /// The 1.X.X version number, or 0.0 if could not determine protocol version - public static string ProtocolVersion2MCVer(int protocol) - { - return protocol switch - { - 22 => "1.0", - 23 => "1.1", - 28 => "1.2.3", - 29 => "1.2.5", - 39 => "1.3.2", - // case 47: return "1.4.2"; - 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", - 761 => "1.19.3", - 762 => "1.19.4", - 763 => "1.20", - _ => "0.0" - }; - } - - /// - /// Check if we can force-enable Forge support for a Minecraft version without using server Ping - /// - /// Minecraft protocol version - /// TRUE if we can force-enable Forge support without using server Ping - public static bool ProtocolMayForceForge(int protocol) - { - return Protocol18Forge.ServerMayForceForge(protocol); - } - - /// - /// Server Info: Consider Forge to be enabled regardless of server Ping - /// - /// Minecraft protocol version - /// ForgeInfo item stating that Forge is enabled - public static ForgeInfo ProtocolForceForge(int protocol) - { - return Protocol18Forge.ServerForceForge(protocol); - } - - public enum LoginResult { OtherError, ServiceUnavailable, SSLError, Success, WrongPassword, AccountMigrated, NotPremium, LoginRequired, InvalidToken, InvalidResponse, NullError, UserCancel }; - public enum AccountType { Mojang, Microsoft }; - - /// - /// Allows to login to a premium Minecraft account using the Yggdrasil authentication scheme. - /// - /// Login - /// 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) - { - if (type == LoginType.microsoft) - { - if (Config.Main.General.Method == LoginMethod.mcc) - return MicrosoftMCCLogin(user, pass, out session); - else - return MicrosoftBrowserLogin(out session, user); - } - else if (type == LoginType.mojang) - { - return MojangLogin(user, pass, out session); - } - else throw new InvalidOperationException("Account type must be Mojang or Microsoft"); - } - - /// - /// Login using Mojang account. Will be outdated after account migration - /// - /// - /// - /// - /// - private static LoginResult MojangLogin(string user, string pass, out SessionToken session) - { - 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); - if (code == 200) - { - if (result.Contains("availableProfiles\":[]}")) - { - return LoginResult.NotPremium; - } - 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")) - { - 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; - } - else return LoginResult.InvalidResponse; - } - } - else if (code == 403) - { - if (result.Contains("UserMigratedException")) - { - return LoginResult.AccountMigrated; - } - else return LoginResult.WrongPassword; - } - else if (code == 503) - { - return LoginResult.ServiceUnavailable; - } - else - { - ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.error_http_code, code)); - return LoginResult.OtherError; - } - } - catch (System.Security.Authentication.AuthenticationException e) - { - if (Settings.Config.Logging.DebugMessages) - { - ConsoleIO.WriteLineFormatted("§8" + e.ToString()); - } - return LoginResult.SSLError; - } - catch (System.IO.IOException e) - { - if (Settings.Config.Logging.DebugMessages) - { - ConsoleIO.WriteLineFormatted("§8" + e.ToString()); - } - if (e.Message.Contains("authentication")) - { - return LoginResult.SSLError; - } - else return LoginResult.OtherError; - } - catch (Exception e) - { - if (Settings.Config.Logging.DebugMessages) - { - ConsoleIO.WriteLineFormatted("§8" + e.ToString()); - } - return LoginResult.OtherError; - } - } - - /// - /// Sign-in to Microsoft Account without using browser. Only works if 2FA is disabled. - /// Might not work well in some rare cases. - /// - /// - /// - /// - /// - private static LoginResult MicrosoftMCCLogin(string email, string password, out SessionToken session) - { - try - { - var msaResponse = XboxLive.UserLogin(email, password, XboxLive.PreAuth()); - // Remove refresh token for MCC sign method - msaResponse.RefreshToken = string.Empty; - return MicrosoftLogin(msaResponse, out session); - } - catch (Exception e) - { - session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; - ConsoleIO.WriteLineFormatted("§cMicrosoft authenticate failed: " + e.Message); - if (Settings.Config.Logging.DebugMessages) - { - ConsoleIO.WriteLineFormatted("§c" + e.StackTrace); - } - return LoginResult.WrongPassword; // Might not always be wrong password - } - } - - /// - /// Sign-in to Microsoft Account by asking user to open sign-in page using browser. - /// - /// - /// The downside is this require user to copy and paste lengthy content from and to console. - /// Sign-in page: 218 chars - /// Response URL: around 1500 chars - /// - /// - /// - public static LoginResult MicrosoftBrowserLogin(out SessionToken session, string loginHint = "") - { - 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"); - - ConsoleIO.WriteLine(Translations.mcc_browser_login_code); - string code = ConsoleIO.ReadLine(); - ConsoleIO.WriteLine(string.Format(Translations.mcc_connecting, "Microsoft")); - - var msaResponse = Microsoft.RequestAccessToken(code); - return MicrosoftLogin(msaResponse, out session); - } - - public static LoginResult MicrosoftLoginRefresh(string refreshToken, out SessionToken session) - { - var msaResponse = Microsoft.RefreshAccessToken(refreshToken); - return MicrosoftLogin(msaResponse, out session); - } - - private static LoginResult MicrosoftLogin(Microsoft.LoginResponse msaResponse, out SessionToken session) - { - session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; - - try - { - var xblResponse = XboxLive.XblAuthenticate(msaResponse); - var xsts = XboxLive.XSTSAuthenticate(xblResponse); // Might throw even password correct - - string accessToken = MinecraftWithXbox.LoginWithXbox(xsts.UserHash, xsts.Token); - bool hasGame = MinecraftWithXbox.UserHasGame(accessToken); - if (hasGame) - { - var profile = MinecraftWithXbox.GetUserProfile(accessToken); - session.PlayerName = profile.UserName; - session.PlayerID = profile.UUID; - session.ID = accessToken; - session.RefreshToken = msaResponse.RefreshToken; - InternalConfig.Account.Login = msaResponse.Email; - return LoginResult.Success; - } - else - { - return LoginResult.NotPremium; - } - } - catch (Exception e) - { - ConsoleIO.WriteLineFormatted("§cMicrosoft authenticate failed: " + e.Message); - if (Settings.Config.Logging.DebugMessages) - { - ConsoleIO.WriteLineFormatted("§c" + e.StackTrace); - } - return LoginResult.WrongPassword; // Might not always be wrong password - } - } - - /// - /// 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) - { - 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 - { - // Token expired - return LoginResult.LoginRequired; - } - } - - /// - /// Refreshes invalid token - /// - /// 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) - { - 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); - if (code == 200) - { - if (result == null) - { - return LoginResult.NullError; - } - 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")) - { - 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; - } - else return LoginResult.InvalidResponse; - } - } - else if (code == 403 && result.Contains("InvalidToken")) - { - return LoginResult.InvalidToken; - } - else - { - ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.error_auth, code)); - return LoginResult.OtherError; - } - } - catch - { - return LoginResult.OtherError; - } - } - - /// - /// Check session using Mojang's Yggdrasil authentication scheme. Allows to join an online-mode server - /// - /// Username - /// Session ID - /// Server ID - /// TRUE if session was successfully checked - public static bool SessionCheck(string uuid, string accesstoken, string 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); - } - catch { return false; } - } - - /// - /// Retrieve available Realms worlds of a player and display them - /// - /// Player Minecraft username - /// Player UUID - /// Access token - /// List of ID of available Realms worlds - public static List RealmsListWorlds(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); - 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) - { - List availableWorlds = new(); // Store string to print - int index = 0; - foreach (Json.JSONData realmsServer in realmsWorlds.Properties["servers"].DataArray) - { - if (realmsServer.Properties.ContainsKey("name") - && realmsServer.Properties.ContainsKey("owner") - && realmsServer.Properties.ContainsKey("id") - && realmsServer.Properties.ContainsKey("expired")) - { - 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); - } - } - } - if (availableWorlds.Count > 0) - { - ConsoleIO.WriteLine(Translations.mcc_realms_available); - foreach (var world in availableWorlds) - ConsoleIO.WriteLine(world); - ConsoleIO.WriteLine(Translations.mcc_realms_join); - } - } - - } - catch (Exception e) - { - ConsoleIO.WriteLineFormatted("§8" + e.GetType().ToString() + ": " + e.Message); - if (Settings.Config.Logging.DebugMessages) - { - ConsoleIO.WriteLineFormatted("§8" + e.StackTrace); - } - } - return realmsWorldsResult; - } - - /// - /// Get the server address of a Realms world by world ID - /// - /// The world ID of the Realms world - /// Player Minecraft username - /// 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) - { - 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); - if (statusCode == 200) - { - Json.JSONData serverAddress = Json.ParseJson(result); - if (serverAddress.Properties.ContainsKey("address")) - return serverAddress.Properties["address"].StringValue; - else - { - ConsoleIO.WriteLine(Translations.error_realms_ip_error); - return ""; - } - } - else - { - ConsoleIO.WriteLine(Translations.error_realms_access_denied); - return ""; - } - } - catch (Exception e) - { - ConsoleIO.WriteLineFormatted("§8" + e.GetType().ToString() + ": " + e.Message); - if (Settings.Config.Logging.DebugMessages) - { - ConsoleIO.WriteLineFormatted("§8" + e.StackTrace); - } - return ""; - } - } - - /// - /// Make a HTTPS GET request to the specified endpoint of the Mojang API - /// - /// Host to connect to - /// Endpoint for making the request - /// Cookies for making the request - /// Request result - /// HTTP Status code - private static int DoHTTPSGet(string host, string endpoint, string cookies, ref string result) - { - List http_request = new() - { - "GET " + endpoint + " HTTP/1.1", - "Cookie: " + cookies, - "Cache-Control: no-cache", - "Pragma: no-cache", - "Host: " + host, - "User-Agent: Java/1.6.0_27", - "Accept-Charset: ISO-8859-1,UTF-8;q=0.7,*;q=0.7", - "Connection: close", - "", - "" - }; - return DoHTTPSRequest(http_request, host, ref result); - } - - /// - /// Make a HTTPS POST request to the specified endpoint of the Mojang API - /// - /// Host to connect to - /// Endpoint for making the request - /// Request payload - /// Request result - /// HTTP Status code - private static int DoHTTPSPost(string host, string endpoint, string request, ref string result) - { - List http_request = new() - { - "POST " + endpoint + " HTTP/1.1", - "Host: " + host, - "User-Agent: MCC/" + Program.Version, - "Content-Type: application/json", - "Content-Length: " + Encoding.ASCII.GetBytes(request).Length, - "Connection: close", - "", - request - }; - return DoHTTPSRequest(http_request, host, ref result); - } - - /// - /// Manual HTTPS request since we must directly use a TcpClient because of the proxy. - /// This method connects to the server, enables SSL, do the request and read the response. - /// - /// Request headers and optional body (POST) - /// Host to connect to - /// Request result - /// HTTP Status code - private static int DoHTTPSRequest(List headers, string host, ref string result) - { - string? postResult = null; - int statusCode = 520; - Exception? exception = null; - AutoTimeout.Perform(() => - { - try - { - if (Settings.Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.debug_request, 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 (Settings.Config.Logging.DebugMessages) - foreach (string line in headers) - ConsoleIO.WriteLineFormatted("§8> " + line); - - stream.Write(Encoding.ASCII.GetBytes(String.Join("\r\n", headers.ToArray()))); - System.IO.StreamReader sr = new(stream); - string raw_result = sr.ReadToEnd(); - - if (Settings.Config.Logging.DebugMessages) - { - ConsoleIO.WriteLine(""); - 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; - } - - /// - /// Encode a string to a json string. - /// Will convert special chars to \u0000 unicode escape sequences. - /// - /// Source text - /// Encoded text - private static string JsonEncode(string text) - { - StringBuilder result = new(); - - foreach (char c in text) - { - if ((c >= '0' && c <= '9') || - (c >= 'a' && c <= 'z') || - (c >= 'A' && c <= 'Z')) - { - result.Append(c); - } - else - { - result.AppendFormat(@"\u{0:x4}", (int)c); - } - } - - return result.ToString(); - } - - /// - /// Convert a TimeStamp (in second) to DateTime object - /// - /// TimeStamp in second - /// DateTime object of the TimeStamp - public static DateTime UnixTimeStampToDateTime(long unixTimeStamp) - { - // Unix timestamp is seconds past epoch - DateTime dateTime = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - dateTime = dateTime.AddSeconds(unixTimeStamp).ToLocalTime(); - return dateTime; - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Text; +using DnsClient; +using MinecraftClient.Protocol.Handlers; +using MinecraftClient.Protocol.Handlers.Forge; +using MinecraftClient.Protocol.Session; +using MinecraftClient.Proxy; +using static MinecraftClient.Settings; +using static MinecraftClient.Settings.MainConfigHealper.MainConfig.GeneralConfig; + +namespace MinecraftClient.Protocol +{ + /// + /// Handle login, session, server ping and provide a protocol handler for interacting with a minecraft server. + /// + /// + /// Typical update steps for marking a new Minecraft version as supported: + /// - Add protocol ID in GetProtocolHandler() + /// - Add 1.X.X case in MCVer2ProtocolVersion() + /// + public static class ProtocolHandler + { + /// + /// Perform a DNS lookup for a Minecraft Service using the specified domain name + /// + /// 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) + { + bool foundService = false; + string domainVal = domain; + ushort portVal = port; + + if (!String.IsNullOrEmpty(domain) && domain.Any(c => char.IsLetter(c))) + { + AutoTimeout.Perform(() => + { + try + { + 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; + } + } + 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)); + } + + domain = domainVal; + port = portVal; + return foundService; + } + + /// + /// Retrieve information about a Minecraft server + /// + /// Server IP to ping + /// 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) + { + bool success = false; + int protocolversionTmp = 0; + ForgeInfo? forgeInfoTmp = null; + if (AutoTimeout.Perform(() => + { + try + { + 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); + } + catch (Exception e) + { + ConsoleIO.WriteLineFormatted(String.Format("§8{0}: {1}", e.GetType().FullName, e.Message)); + } + }, 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; + } + } + + /// + /// Get a protocol handler for the specified Minecraft version + /// + /// Tcp Client connected to the server + /// Protocol version to handle + /// Handler with the appropriate callbacks + /// + public static IMinecraftCom GetProtocolHandler(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); + + 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, 761, 762, 763 }; + + if (Array.IndexOf(supportedVersions_Protocol18, ProtocolVersion) > -1) + return new Protocol18Handler(Client, ProtocolVersion, Handler, forgeInfo); + + throw new NotSupportedException(string.Format(Translations.exception_version_unsupport, ProtocolVersion)); + } + + /// + /// Convert a human-readable Minecraft version number to network protocol version number + /// + /// The Minecraft version number + /// The protocol version number or 0 if could not determine protocol version: error, unknown, not supported + public static int MCVer2ProtocolVersion(string MCVersion) + { + if (MCVersion.Contains('.')) + { + switch (MCVersion.Split(' ')[0].Trim()) + { + case "1.0": + case "1.0.0": + case "1.0.1": + return 22; + case "1.1": + case "1.1.0": + return 23; + case "1.2": + case "1.2.0": + case "1.2.1": + case "1.2.2": + case "1.2.3": + return 28; + case "1.2.4": + case "1.2.5": + return 29; + case "1.3": + case "1.3.0": + case "1.3.1": + case "1.3.2": + return 39; + case "1.4": + case "1.4.0": + case "1.4.1": + case "1.4.2": + return 48; // 47 conflicts with 1.8 + case "1.4.3": + return 48; + case "1.4.4": + case "1.4.5": + return 49; + case "1.4.6": + case "1.4.7": + return 51; + case "1.5": + case "1.5.0": + case "1.5.1": + return 60; + case "1.5.2": + return 61; + case "1.6": + case "1.6.0": + return 72; + case "1.6.1": + return 73; + case "1.6.2": + return 74; + case "1.6.3": + return 77; + case "1.6.4": + return 78; + case "1.7": + case "1.7.0": + case "1.7.1": + return 3; + case "1.7.2": + case "1.7.3": + case "1.7.4": + case "1.7.5": + return 4; + case "1.7.6": + case "1.7.7": + case "1.7.8": + case "1.7.9": + case "1.7.10": + return 5; + case "1.8": + case "1.8.0": + case "1.8.1": + case "1.8.2": + case "1.8.3": + case "1.8.4": + case "1.8.5": + case "1.8.6": + case "1.8.7": + case "1.8.8": + case "1.8.9": + return 47; + case "1.9": + case "1.9.0": + return 107; + case "1.9.1": + return 108; + case "1.9.2": + return 109; + case "1.9.3": + case "1.9.4": + return 110; + case "1.10": + case "1.10.0": + case "1.10.1": + case "1.10.2": + return 210; + case "1.11": + case "1.11.0": + return 315; + case "1.11.1": + case "1.11.2": + return 316; + case "1.12": + case "1.12.0": + return 335; + case "1.12.1": + return 338; + case "1.12.2": + return 340; + case "1.13": + case "1.13.0": + return 393; + case "1.13.1": + return 401; + case "1.13.2": + return 404; + case "1.14": + case "1.14.0": + return 477; + case "1.14.1": + return 480; + case "1.14.2": + return 485; + case "1.14.3": + return 490; + case "1.14.4": + return 498; + case "1.15": + case "1.15.0": + return 573; + case "1.15.1": + return 575; + case "1.15.2": + return 578; + case "1.16": + case "1.16.0": + return 735; + case "1.16.1": + return 736; + case "1.16.2": + return 751; + case "1.16.3": + return 753; + case "1.16.4": + case "1.16.5": + return 754; + case "1.17": + case "1.17.0": + return 755; + case "1.17.1": + return 756; + case "1.18": + case "1.18.0": + case "1.18.1": + return 757; + case "1.18.2": + return 758; + case "1.19": + case "1.19.0": + return 759; + case "1.19.1": + case "1.19.2": + return 760; + case "1.19.3": + return 761; + case "1.19.4": + return 762; + case "1.20": + case "1.20.1": + return 763; + default: + return 0; + } + } + else + { + try + { + return int.Parse(MCVersion, NumberStyles.Any, CultureInfo.CurrentCulture); + } + catch + { + return 0; + } + } + } + + /// + /// Convert a network protocol version number to human-readable Minecraft version number + /// + /// Some Minecraft versions share the same protocol number. In that case, the lowest version for that protocol is returned. + /// The Minecraft protocol version number + /// The 1.X.X version number, or 0.0 if could not determine protocol version + public static string ProtocolVersion2MCVer(int protocol) + { + return protocol switch + { + 22 => "1.0", + 23 => "1.1", + 28 => "1.2.3", + 29 => "1.2.5", + 39 => "1.3.2", + // case 47: return "1.4.2"; + 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", + 761 => "1.19.3", + 762 => "1.19.4", + 763 => "1.20", + _ => "0.0" + }; + } + + /// + /// Check if we can force-enable Forge support for a Minecraft version without using server Ping + /// + /// Minecraft protocol version + /// TRUE if we can force-enable Forge support without using server Ping + public static bool ProtocolMayForceForge(int protocol) + { + return Protocol18Forge.ServerMayForceForge(protocol); + } + + /// + /// Server Info: Consider Forge to be enabled regardless of server Ping + /// + /// Minecraft protocol version + /// ForgeInfo item stating that Forge is enabled + public static ForgeInfo ProtocolForceForge(int protocol) + { + return Protocol18Forge.ServerForceForge(protocol); + } + + public enum LoginResult { OtherError, ServiceUnavailable, SSLError, Success, WrongPassword, AccountMigrated, NotPremium, LoginRequired, InvalidToken, InvalidResponse, NullError, UserCancel }; + public enum AccountType { Mojang, Microsoft }; + + /// + /// Allows to login to a premium Minecraft account using the Yggdrasil authentication scheme. + /// + /// Login + /// 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) + { + if (type == LoginType.microsoft) + { + if (Config.Main.General.Method == LoginMethod.mcc) + return MicrosoftMCCLogin(user, pass, out session); + else + return MicrosoftBrowserLogin(out session, user); + } + else if (type == LoginType.mojang) + { + return MojangLogin(user, pass, out session); + } + else throw new InvalidOperationException("Account type must be Mojang or Microsoft"); + } + + /// + /// Login using Mojang account. Will be outdated after account migration + /// + /// + /// + /// + /// + private static LoginResult MojangLogin(string user, string pass, out SessionToken session) + { + 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); + if (code == 200) + { + if (result.Contains("availableProfiles\":[]}")) + { + return LoginResult.NotPremium; + } + 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")) + { + 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; + } + else return LoginResult.InvalidResponse; + } + } + else if (code == 403) + { + if (result.Contains("UserMigratedException")) + { + return LoginResult.AccountMigrated; + } + else return LoginResult.WrongPassword; + } + else if (code == 503) + { + return LoginResult.ServiceUnavailable; + } + else + { + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.error_http_code, code)); + return LoginResult.OtherError; + } + } + catch (System.Security.Authentication.AuthenticationException e) + { + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLineFormatted("§8" + e.ToString()); + } + return LoginResult.SSLError; + } + catch (System.IO.IOException e) + { + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLineFormatted("§8" + e.ToString()); + } + if (e.Message.Contains("authentication")) + { + return LoginResult.SSLError; + } + else return LoginResult.OtherError; + } + catch (Exception e) + { + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLineFormatted("§8" + e.ToString()); + } + return LoginResult.OtherError; + } + } + + /// + /// Sign-in to Microsoft Account without using browser. Only works if 2FA is disabled. + /// Might not work well in some rare cases. + /// + /// + /// + /// + /// + private static LoginResult MicrosoftMCCLogin(string email, string password, out SessionToken session) + { + try + { + var msaResponse = XboxLive.UserLogin(email, password, XboxLive.PreAuth()); + // Remove refresh token for MCC sign method + msaResponse.RefreshToken = string.Empty; + return MicrosoftLogin(msaResponse, out session); + } + catch (Exception e) + { + session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; + ConsoleIO.WriteLineFormatted("§cMicrosoft authenticate failed: " + e.Message); + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLineFormatted("§c" + e.StackTrace); + } + return LoginResult.WrongPassword; // Might not always be wrong password + } + } + + /// + /// Sign-in to Microsoft Account by asking user to open sign-in page using browser. + /// + /// + /// The downside is this require user to copy and paste lengthy content from and to console. + /// Sign-in page: 218 chars + /// Response URL: around 1500 chars + /// + /// + /// + public static LoginResult MicrosoftBrowserLogin(out SessionToken session, string loginHint = "") + { + 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"); + + ConsoleIO.WriteLine(Translations.mcc_browser_login_code); + string code = ConsoleIO.ReadLine(); + ConsoleIO.WriteLine(string.Format(Translations.mcc_connecting, "Microsoft")); + + var msaResponse = Microsoft.RequestAccessToken(code); + return MicrosoftLogin(msaResponse, out session); + } + + public static LoginResult MicrosoftLoginRefresh(string refreshToken, out SessionToken session) + { + var msaResponse = Microsoft.RefreshAccessToken(refreshToken); + return MicrosoftLogin(msaResponse, out session); + } + + private static LoginResult MicrosoftLogin(Microsoft.LoginResponse msaResponse, out SessionToken session) + { + session = new SessionToken() { ClientID = Guid.NewGuid().ToString().Replace("-", "") }; + + try + { + var xblResponse = XboxLive.XblAuthenticate(msaResponse); + var xsts = XboxLive.XSTSAuthenticate(xblResponse); // Might throw even password correct + + string accessToken = MinecraftWithXbox.LoginWithXbox(xsts.UserHash, xsts.Token); + bool hasGame = MinecraftWithXbox.UserHasGame(accessToken); + if (hasGame) + { + var profile = MinecraftWithXbox.GetUserProfile(accessToken); + session.PlayerName = profile.UserName; + session.PlayerID = profile.UUID; + session.ID = accessToken; + session.RefreshToken = msaResponse.RefreshToken; + InternalConfig.Account.Login = msaResponse.Email; + return LoginResult.Success; + } + else + { + return LoginResult.NotPremium; + } + } + catch (Exception e) + { + ConsoleIO.WriteLineFormatted("§cMicrosoft authenticate failed: " + e.Message); + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLineFormatted("§c" + e.StackTrace); + } + return LoginResult.WrongPassword; // Might not always be wrong password + } + } + + /// + /// 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) + { + 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 + { + // Token expired + return LoginResult.LoginRequired; + } + } + + /// + /// Refreshes invalid token + /// + /// 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) + { + 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); + if (code == 200) + { + if (result == null) + { + return LoginResult.NullError; + } + 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")) + { + 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; + } + else return LoginResult.InvalidResponse; + } + } + else if (code == 403 && result.Contains("InvalidToken")) + { + return LoginResult.InvalidToken; + } + else + { + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.error_auth, code)); + return LoginResult.OtherError; + } + } + catch + { + return LoginResult.OtherError; + } + } + + /// + /// Check session using Mojang's Yggdrasil authentication scheme. Allows to join an online-mode server + /// + /// Username + /// Session ID + /// Server ID + /// TRUE if session was successfully checked + public static bool SessionCheck(string uuid, string accesstoken, string 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); + } + catch { return false; } + } + + /// + /// Retrieve available Realms worlds of a player and display them + /// + /// Player Minecraft username + /// Player UUID + /// Access token + /// List of ID of available Realms worlds + public static List RealmsListWorlds(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); + 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) + { + List availableWorlds = new(); // Store string to print + int index = 0; + foreach (Json.JSONData realmsServer in realmsWorlds.Properties["servers"].DataArray) + { + if (realmsServer.Properties.ContainsKey("name") + && realmsServer.Properties.ContainsKey("owner") + && realmsServer.Properties.ContainsKey("id") + && realmsServer.Properties.ContainsKey("expired")) + { + 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); + } + } + } + if (availableWorlds.Count > 0) + { + ConsoleIO.WriteLine(Translations.mcc_realms_available); + foreach (var world in availableWorlds) + ConsoleIO.WriteLine(world); + ConsoleIO.WriteLine(Translations.mcc_realms_join); + } + } + + } + catch (Exception e) + { + ConsoleIO.WriteLineFormatted("§8" + e.GetType().ToString() + ": " + e.Message); + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLineFormatted("§8" + e.StackTrace); + } + } + return realmsWorldsResult; + } + + /// + /// Get the server address of a Realms world by world ID + /// + /// The world ID of the Realms world + /// Player Minecraft username + /// 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) + { + 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); + if (statusCode == 200) + { + Json.JSONData serverAddress = Json.ParseJson(result); + if (serverAddress.Properties.ContainsKey("address")) + return serverAddress.Properties["address"].StringValue; + else + { + ConsoleIO.WriteLine(Translations.error_realms_ip_error); + return ""; + } + } + else + { + ConsoleIO.WriteLine(Translations.error_realms_access_denied); + return ""; + } + } + catch (Exception e) + { + ConsoleIO.WriteLineFormatted("§8" + e.GetType().ToString() + ": " + e.Message); + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLineFormatted("§8" + e.StackTrace); + } + return ""; + } + } + + /// + /// Make a HTTPS GET request to the specified endpoint of the Mojang API + /// + /// Host to connect to + /// Endpoint for making the request + /// Cookies for making the request + /// Request result + /// HTTP Status code + private static int DoHTTPSGet(string host, string endpoint, string cookies, ref string result) + { + List http_request = new() + { + "GET " + endpoint + " HTTP/1.1", + "Cookie: " + cookies, + "Cache-Control: no-cache", + "Pragma: no-cache", + "Host: " + host, + "User-Agent: Java/1.6.0_27", + "Accept-Charset: ISO-8859-1,UTF-8;q=0.7,*;q=0.7", + "Connection: close", + "", + "" + }; + return DoHTTPSRequest(http_request, host, ref result); + } + + /// + /// Make a HTTPS POST request to the specified endpoint of the Mojang API + /// + /// Host to connect to + /// Endpoint for making the request + /// Request payload + /// Request result + /// HTTP Status code + private static int DoHTTPSPost(string host, string endpoint, string request, ref string result) + { + List http_request = new() + { + "POST " + endpoint + " HTTP/1.1", + "Host: " + host, + "User-Agent: MCC/" + Program.Version, + "Content-Type: application/json", + "Content-Length: " + Encoding.ASCII.GetBytes(request).Length, + "Connection: close", + "", + request + }; + return DoHTTPSRequest(http_request, host, ref result); + } + + /// + /// Manual HTTPS request since we must directly use a TcpClient because of the proxy. + /// This method connects to the server, enables SSL, do the request and read the response. + /// + /// Request headers and optional body (POST) + /// Host to connect to + /// Request result + /// HTTP Status code + private static int DoHTTPSRequest(List headers, string host, ref string result) + { + string? postResult = null; + int statusCode = 520; + Exception? exception = null; + AutoTimeout.Perform(() => + { + try + { + if (Settings.Config.Logging.DebugMessages) + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.debug_request, 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 (Settings.Config.Logging.DebugMessages) + foreach (string line in headers) + ConsoleIO.WriteLineFormatted("§8> " + line); + + stream.Write(Encoding.ASCII.GetBytes(String.Join("\r\n", headers.ToArray()))); + System.IO.StreamReader sr = new(stream); + string raw_result = sr.ReadToEnd(); + + if (Settings.Config.Logging.DebugMessages) + { + ConsoleIO.WriteLine(""); + 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; + } + + /// + /// Encode a string to a json string. + /// Will convert special chars to \u0000 unicode escape sequences. + /// + /// Source text + /// Encoded text + private static string JsonEncode(string text) + { + StringBuilder result = new(); + + foreach (char c in text) + { + if ((c >= '0' && c <= '9') || + (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z')) + { + result.Append(c); + } + else + { + result.AppendFormat(@"\u{0:x4}", (int)c); + } + } + + return result.ToString(); + } + + /// + /// Convert a TimeStamp (in second) to DateTime object + /// + /// TimeStamp in second + /// DateTime object of the TimeStamp + public static DateTime UnixTimeStampToDateTime(long unixTimeStamp) + { + // Unix timestamp is seconds past epoch + DateTime dateTime = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + dateTime = dateTime.AddSeconds(unixTimeStamp).ToLocalTime(); + return dateTime; + } + } +} diff --git a/MinecraftClient/Protocol/Session/SessionCache.cs b/MinecraftClient/Protocol/Session/SessionCache.cs index 8fd17e95..6cd51a99 100644 --- a/MinecraftClient/Protocol/Session/SessionCache.cs +++ b/MinecraftClient/Protocol/Session/SessionCache.cs @@ -1,272 +1,272 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.Serialization; -using System.Runtime.Serialization.Formatters.Binary; -using System.Timers; -using static MinecraftClient.Settings; -using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig; - -namespace MinecraftClient.Protocol.Session -{ - /// - /// Handle sessions caching and storage. - /// - public static 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" - ); - - 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(); - - /// - /// 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); - } - - /// - /// 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 - { - sessions.Add(login, session); - } - - 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(); - } - } - - /// - /// 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(); - } - - /// - /// 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 sessions 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 SessionTokens into SessionCache. - /// - /// True if data is successfully loaded - private static bool LoadFromDisk() - { - //Grab sessions in the Minecraft directory - if (File.Exists(SessionCacheFileMinecraft)) - { - 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 */ } - } - } - } - } - } - - //Serialized session cache file in binary format - if (File.Exists(SessionCacheFileSerialized)) - { - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_converting, SessionCacheFileSerialized)); - - 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) - { - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, item.Key, item.Value.ID)); - sessions[item.Key] = item.Value; - } - } - catch (IOException ex) - { - ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail, ex.Message)); - } - catch (SerializationException ex2) - { - ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_malformed, ex2.Message)); - } - } - - //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)); - - 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)); - } - } - - return sessions.Count > 0; - } - - /// - /// Saves SessionToken's from SessionCache into cache file. - /// - private static void SaveToDisk() - { - if (Config.Logging.DebugMessages) - ConsoleIO.WriteLineFormatted("§8" + Translations.cache_saving, acceptnewlines: true); - - List sessionCacheLines = new() - { - "# 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()); - - try - { - FileMonitor.WriteAllLinesWithRetries(SessionCacheFilePlaintext, sessionCacheLines); - } - catch (IOException e) - { - ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail, e.Message)); - } - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using System.Timers; +using static MinecraftClient.Settings; +using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig; + +namespace MinecraftClient.Protocol.Session +{ + /// + /// Handle sessions caching and storage. + /// + public static 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" + ); + + 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(); + + /// + /// 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); + } + + /// + /// 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 + { + sessions.Add(login, session); + } + + 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(); + } + } + + /// + /// 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(); + } + + /// + /// 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 sessions 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 SessionTokens into SessionCache. + /// + /// True if data is successfully loaded + private static bool LoadFromDisk() + { + //Grab sessions in the Minecraft directory + if (File.Exists(SessionCacheFileMinecraft)) + { + 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 */ } + } + } + } + } + } + + //Serialized session cache file in binary format + if (File.Exists(SessionCacheFileSerialized)) + { + if (Config.Logging.DebugMessages) + ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_converting, SessionCacheFileSerialized)); + + 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) + { + if (Config.Logging.DebugMessages) + ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, item.Key, item.Value.ID)); + sessions[item.Key] = item.Value; + } + } + catch (IOException ex) + { + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail, ex.Message)); + } + catch (SerializationException ex2) + { + ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_malformed, ex2.Message)); + } + } + + //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)); + + 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)); + } + } + + return sessions.Count > 0; + } + + /// + /// Saves SessionToken's from SessionCache into cache file. + /// + private static void SaveToDisk() + { + if (Config.Logging.DebugMessages) + ConsoleIO.WriteLineFormatted("§8" + Translations.cache_saving, acceptnewlines: true); + + List sessionCacheLines = new() + { + "# 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()); + + try + { + FileMonitor.WriteAllLinesWithRetries(SessionCacheFilePlaintext, sessionCacheLines); + } + catch (IOException e) + { + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail, e.Message)); + } + } + } +} diff --git a/docs/guide/creating-text-script.md b/docs/guide/creating-text-script.md index bdcc7c5c..0c4aa0a9 100644 --- a/docs/guide/creating-text-script.md +++ b/docs/guide/creating-text-script.md @@ -1,26 +1,26 @@ ---- -title: Creating Simple Script ---- - -# Creating Simple Script - -A simple script is a text file with one command per line. See [Internal Commands](https://mccteam.github.io/guide/usage.html#internal-commands) section or type `/help` in the console to see available commands. Any line beginning with `#` is ignored and treated as a comment. - -Application variables defined using the set command or [AppVars] INI section can be used. The following read-only variables can also be used: `%username%, %login%, %serverip%, %serverport%, %datetime%` - -## Example - -`sample-script.txt`: Send a hello message, wait 60 seconds and disconnect from server. -``` -# This is a sample script for Minecraft Console Client -# Any line beginning with "#" is ignored and treated as a comment. - -send Hello World! I'm a bot scripted using Minecraft Console Client. -wait 60 -send Now quitting. Bye :) -exit -``` - -Go to [example scripts](https://github.com/MCCTeam/Minecraft-Console-Client/tree/master/MinecraftClient/config) to see more example. - +--- +title: Creating Simple Script +--- + +# Creating Simple Script + +A simple script is a text file with one command per line. See [Internal Commands](https://mccteam.github.io/guide/usage.html#internal-commands) section or type `/help` in the console to see available commands. Any line beginning with `#` is ignored and treated as a comment. + +Application variables defined using the set command or [AppVars] INI section can be used. The following read-only variables can also be used: `%username%, %login%, %serverip%, %serverport%, %datetime%` + +## Example + +`sample-script.txt`: Send a hello message, wait 60 seconds and disconnect from server. +``` +# This is a sample script for Minecraft Console Client +# Any line beginning with "#" is ignored and treated as a comment. + +send Hello World! I'm a bot scripted using Minecraft Console Client. +wait 60 +send Now quitting. Bye :) +exit +``` + +Go to [example scripts](https://github.com/MCCTeam/Minecraft-Console-Client/tree/master/MinecraftClient/config) to see more example. + If you want need advanced functions, please see [Creating Chat Bots](creating-bots.md) \ No newline at end of file diff --git a/docs/guide/websocket/README.md b/docs/guide/websocket/README.md index b1b70454..b5eff123 100644 --- a/docs/guide/websocket/README.md +++ b/docs/guide/websocket/README.md @@ -1,142 +1,142 @@ -# Web Socket Chat Bot documentation - -This is a documentation page on the Web Socket chat bot and on how to make a library that uses web socket to execute commands in the MCC and processes events sent by the MCC. - -Please read the [Important things](#important-things) before everything. - -# Page index - -- [Important things](#important-things) - - [Prerequisites](#prerequisites) - - [Limitations](#limitations) - - [Precision of information](#precisionvalidity-of-the-information-in-this-guide) -- [How does it work?](#how-does-it-work) -- [Sending commands](#sending-commands-to-mcc) -- [Websocket Commands](Commands.md) -- [Websocket Events](Events.md) -- [Reference Implementation](#reference-implementation) - -## Reference implementation - -I have made a reference implementation in TypeScript/JavaScript, it is avaliable here: - -[https://github.com/milutinke/MCC.js](https://github.com/milutinke/MCC.js) - -It is great for better understanding how this works. - -## Important things - -### Prerequisites - -This guide/documentation assumes that you have enough of programming knowledge to know: - - - What Web Socket is - - Basics of networking and concurency - - What JSON is - - What are the various data types such as boolean, integer, long, float, double, object, dictionary/hash map - -Without knowing those, I highly recommend learning about those concepts before trying to implement your own library. - -### Limitations - -The Web Socket chat bot should be considered experimental and prone to change, it has not been fully tested and might change, keep an eye on updates on our official Discord server. - -### Precision/Validity of the information in this guide - -This guide has been mostly generated from the code itself, so the types are C# types, except in few cases where I have manually changed them. - -For some thing you will have to dig in to the MCC C# code of the Chat Bot and various helper classes. - -**Some information sent by the MCC, for example entity metadata, block ids, item ids, or various other data is different for each Minecraft Version, thus you need to map it for each minecraft version.** - -Some events might not be that useful, eg. `OnNetworkPacket` - -## How does it work? - -So, basically, this Web Socket Chat Bot is a chat bot that has a Web Socket server running while you're connected to a minecraft server. - -It sends events, and listens for commands and responds to commands. - -It has build in authentication, which requires you to send a command to authenticate if the the password is set, if it is not set, it should automatically authenticate you on the first command. - -You also can name every connection (session) with an alias. - -The flow of the protocol is the following: - -``` -Connect to the chat bot via web socket - - | - | - \ / - ` - -Optionally set a session alias/name with "ChangeSessionId" command -(this can be done multiple times at any point) - - | - | - \ / - ` - -Send an "Authenticate" command if there is a password set - - | - | - \ / - ` - -Send commands and listen for events -``` - -In order to implement a library that communicates witht this chat bot, you need to make a way to send commands, remember the sent commands via the `requestId` value, and listen for `OnWsCommandResponse` event in which you need to detect if your command has been executed by looking for the `requestId` that matches the one you've sent. I also recommend you put a 5-10 seconds command execution timeout, where you discard the command if it has not been executed in the given timeout range. - -## Sending commands to MCC - -You can send text in the chat, execute client commands or execute remote procedures (WebSocket Chat Bot commands). - -Each thing that is sent to the chat bot results in a response through the [`OnWsCommandResponse`](#onwscommandresponse) event. - -### Sending chat messages - -To send a chat message just send a plain text with your message to via the web socket. - -### Executing client commands - -To execute a client command, just send plain text with your command. - -Example: `/move suth` - -### Execution remote procedures (WebSocket Chat Bot commands) - -In order to execute a remote procedure, you need to send a json encoded string in the following format: - -```json -{ - "command": "", - "requestId": "", - "parameters": [ 1, "some string", true, "etc.." ] -} -``` - -#### `command` - - Refers to the name of the command - -#### `requestId` - - Is a unique indentifier you generate on each command, it will be returned in the response of the command execution ([`OnWsCommandResponse`](#onwscommandresponse)), use it to track if a command has been successfully executed or not, and to get the return value if it has been successfully executed. (*It's recommended to generate at least 7 characters to avoid collision, best to use an UUID format*). - -#### `parameters` - - Are parameters (attibutes) of the procedure you're executing, they're sent as an array of data of various types, the Web Socket chat bot does parsing and conversion and returns an error if you have sent a wrong type for the given parameters, of if you haven't send enough of them. - - **Example:** - - ```json - { - "command": "Authenticate", - "requestId": "8w9u60-q39ik", - "parameters": ["wspass12345"] - } +# Web Socket Chat Bot documentation + +This is a documentation page on the Web Socket chat bot and on how to make a library that uses web socket to execute commands in the MCC and processes events sent by the MCC. + +Please read the [Important things](#important-things) before everything. + +# Page index + +- [Important things](#important-things) + - [Prerequisites](#prerequisites) + - [Limitations](#limitations) + - [Precision of information](#precisionvalidity-of-the-information-in-this-guide) +- [How does it work?](#how-does-it-work) +- [Sending commands](#sending-commands-to-mcc) +- [Websocket Commands](Commands.md) +- [Websocket Events](Events.md) +- [Reference Implementation](#reference-implementation) + +## Reference implementation + +I have made a reference implementation in TypeScript/JavaScript, it is avaliable here: + +[https://github.com/milutinke/MCC.js](https://github.com/milutinke/MCC.js) + +It is great for better understanding how this works. + +## Important things + +### Prerequisites + +This guide/documentation assumes that you have enough of programming knowledge to know: + + - What Web Socket is + - Basics of networking and concurency + - What JSON is + - What are the various data types such as boolean, integer, long, float, double, object, dictionary/hash map + +Without knowing those, I highly recommend learning about those concepts before trying to implement your own library. + +### Limitations + +The Web Socket chat bot should be considered experimental and prone to change, it has not been fully tested and might change, keep an eye on updates on our official Discord server. + +### Precision/Validity of the information in this guide + +This guide has been mostly generated from the code itself, so the types are C# types, except in few cases where I have manually changed them. + +For some thing you will have to dig in to the MCC C# code of the Chat Bot and various helper classes. + +**Some information sent by the MCC, for example entity metadata, block ids, item ids, or various other data is different for each Minecraft Version, thus you need to map it for each minecraft version.** + +Some events might not be that useful, eg. `OnNetworkPacket` + +## How does it work? + +So, basically, this Web Socket Chat Bot is a chat bot that has a Web Socket server running while you're connected to a minecraft server. + +It sends events, and listens for commands and responds to commands. + +It has build in authentication, which requires you to send a command to authenticate if the the password is set, if it is not set, it should automatically authenticate you on the first command. + +You also can name every connection (session) with an alias. + +The flow of the protocol is the following: + +``` +Connect to the chat bot via web socket + + | + | + \ / + ` + +Optionally set a session alias/name with "ChangeSessionId" command +(this can be done multiple times at any point) + + | + | + \ / + ` + +Send an "Authenticate" command if there is a password set + + | + | + \ / + ` + +Send commands and listen for events +``` + +In order to implement a library that communicates witht this chat bot, you need to make a way to send commands, remember the sent commands via the `requestId` value, and listen for `OnWsCommandResponse` event in which you need to detect if your command has been executed by looking for the `requestId` that matches the one you've sent. I also recommend you put a 5-10 seconds command execution timeout, where you discard the command if it has not been executed in the given timeout range. + +## Sending commands to MCC + +You can send text in the chat, execute client commands or execute remote procedures (WebSocket Chat Bot commands). + +Each thing that is sent to the chat bot results in a response through the [`OnWsCommandResponse`](#onwscommandresponse) event. + +### Sending chat messages + +To send a chat message just send a plain text with your message to via the web socket. + +### Executing client commands + +To execute a client command, just send plain text with your command. + +Example: `/move suth` + +### Execution remote procedures (WebSocket Chat Bot commands) + +In order to execute a remote procedure, you need to send a json encoded string in the following format: + +```json +{ + "command": "", + "requestId": "", + "parameters": [ 1, "some string", true, "etc.." ] +} +``` + +#### `command` + + Refers to the name of the command + +#### `requestId` + + Is a unique indentifier you generate on each command, it will be returned in the response of the command execution ([`OnWsCommandResponse`](#onwscommandresponse)), use it to track if a command has been successfully executed or not, and to get the return value if it has been successfully executed. (*It's recommended to generate at least 7 characters to avoid collision, best to use an UUID format*). + +#### `parameters` + + Are parameters (attibutes) of the procedure you're executing, they're sent as an array of data of various types, the Web Socket chat bot does parsing and conversion and returns an error if you have sent a wrong type for the given parameters, of if you haven't send enough of them. + + **Example:** + + ```json + { + "command": "Authenticate", + "requestId": "8w9u60-q39ik", + "parameters": ["wspass12345"] + } ``` \ No newline at end of file