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