using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net.Sockets; using System.Threading; using System.IO; using System.Net; using MinecraftClient.ChatBots; using MinecraftClient.Protocol; using MinecraftClient.Proxy; using MinecraftClient.Protocol.Handlers.Forge; using MinecraftClient.Mapping; using MinecraftClient.Inventory; using MinecraftClient.Logger; namespace MinecraftClient { /// /// The main client class, used to connect to a Minecraft server. /// public class McClient : IMinecraftComHandler { public static int ReconnectionAttemptsLeft = 0; private static readonly List cmd_names = new List(); private static readonly Dictionary cmds = new Dictionary(); private readonly Dictionary onlinePlayers = new Dictionary(); private static bool commandsLoaded = false; private Queue chatQueue = new Queue(); private static DateTime nextMessageSendTime = DateTime.MinValue; private Queue threadTasks = new Queue(); private object threadTasksLock = new object(); private readonly List bots = new List(); private static readonly List botsOnHold = new List(); private static Dictionary inventories = new Dictionary(); private readonly Dictionary> registeredBotPluginChannels = new Dictionary>(); private readonly List registeredServerPluginChannels = new List(); private bool terrainAndMovementsEnabled; private bool terrainAndMovementsRequested = false; private bool inventoryHandlingEnabled; private bool inventoryHandlingRequested = false; private bool entityHandlingEnabled; private object locationLock = new object(); private bool locationReceived = false; private World world = new World(); 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; private string host; private int port; private int protocolversion; private string username; private string uuid; private string sessionid; private DateTime lastKeepAlive; private object lastKeepAliveLock = new object(); private int respawnTicks = 0; private int gamemode = 0; private int playerEntityID; // player health and hunger private float playerHealth; private int playerFoodSaturation; private int playerLevel; private int playerTotalExperience; private byte CurrentSlot = 0; // Entity handling private Dictionary entities = new Dictionary(); // server TPS private long lastAge = 0; private DateTime lastTime; private double serverTPS = 0; private double averageTPS = 20; private const int maxSamples = 5; private List tpsSamples = new List(maxSamples); private double sampleSum = 0; // players latency private Dictionary playersLatency = new Dictionary(); // ChatBot OnNetworkPacket event private bool networkPacketCaptureEnabled = false; public int GetServerPort() { return port; } public string GetServerHost() { return host; } public string GetUsername() { return username; } public string GetUserUUID() { return uuid; } public string GetSessionID() { return sessionid; } public Location GetCurrentLocation() { return location; } public float GetYaw() { return playerYaw; } public float GetPitch() { return playerPitch; } public World GetWorld() { return world; } public Double GetServerTPS() { return averageTPS; } 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 this.Log; } public int GetPlayerEntityID() { return playerEntityID; } public List GetLoadedChatBots() { return new List(bots); } TcpClient client; IMinecraftCom handler; Thread cmdprompt; Thread timeoutdetector; public ILogger Log; /// /// Starts the main chat client /// /// The chosen username of a premium Minecraft Account /// The player's UUID for online-mode authentication /// A valid sessionID obtained after logging in /// The server IP /// The server port to use /// Minecraft protocol version to use public McClient(string username, string uuid, string sessionID, int protocolversion, ForgeInfo forgeInfo, string server_ip, ushort port) { StartClient(username, uuid, sessionID, server_ip, port, protocolversion, forgeInfo, false, ""); } /// /// Starts the main chat client in single command sending mode /// /// The chosen username of a premium Minecraft Account /// The player's UUID for online-mode authentication /// A valid sessionID obtained after logging in /// The server IP /// The server port to use /// Minecraft protocol version to use /// The text or command to send. public McClient(string username, string uuid, string sessionID, string server_ip, ushort port, int protocolversion, ForgeInfo forgeInfo, string command) { StartClient(username, uuid, sessionID, server_ip, port, protocolversion, forgeInfo, true, command); } /// /// Starts the main chat client, wich will login to the server using the MinecraftCom class. /// /// The chosen username of a premium Minecraft Account /// A valid sessionID obtained with MinecraftCom.GetLogin() /// The server IP /// The server port to use /// Minecraft protocol version to use /// The player's UUID for online-mode authentication /// If set to true, the client will send a single command and then disconnect from the server /// The text or command to send. Will only be sent if singlecommand is set to true. private void StartClient(string user, string uuid, string sessionID, string server_ip, ushort port, int protocolversion, ForgeInfo forgeInfo, bool singlecommand, string command) { terrainAndMovementsEnabled = Settings.TerrainAndMovements; inventoryHandlingEnabled = Settings.InventoryHandling; entityHandlingEnabled = Settings.EntityHandling; bool retry = false; this.sessionid = sessionID; this.uuid = uuid; this.username = user; this.host = server_ip; this.port = port; this.protocolversion = protocolversion; this.Log = Settings.LogToFile ? new FileLogLogger(Settings.ExpandVars(Settings.LogFile), Settings.PrependTimestamp) : new FilteredLogger(); Log.DebugEnabled = Settings.DebugMessages; Log.InfoEnabled = Settings.InfoMessages; Log.ChatEnabled = Settings.ChatMessages; Log.WarnEnabled = Settings.WarningMessages; Log.ErrorEnabled = Settings.ErrorMessages; if (!singlecommand) { /* Load commands from Commands namespace */ LoadCommands(); if (botsOnHold.Count == 0) { if (Settings.AntiAFK_Enabled) { BotLoad(new ChatBots.AntiAFK(Settings.AntiAFK_Delay)); } if (Settings.Hangman_Enabled) { BotLoad(new ChatBots.HangmanGame(Settings.Hangman_English)); } if (Settings.Alerts_Enabled) { BotLoad(new ChatBots.Alerts()); } if (Settings.ChatLog_Enabled) { BotLoad(new ChatBots.ChatLog(Settings.ExpandVars(Settings.ChatLog_File), Settings.ChatLog_Filter, Settings.ChatLog_DateTime)); } if (Settings.PlayerLog_Enabled) { BotLoad(new ChatBots.PlayerListLogger(Settings.PlayerLog_Delay, Settings.ExpandVars(Settings.PlayerLog_File))); } if (Settings.AutoRelog_Enabled) { BotLoad(new ChatBots.AutoRelog(Settings.AutoRelog_Delay_Min, Settings.AutoRelog_Delay_Max, Settings.AutoRelog_Retries)); } if (Settings.ScriptScheduler_Enabled) { BotLoad(new ChatBots.ScriptScheduler(Settings.ExpandVars(Settings.ScriptScheduler_TasksFile))); } if (Settings.RemoteCtrl_Enabled) { BotLoad(new ChatBots.RemoteControl()); } if (Settings.AutoRespond_Enabled) { BotLoad(new ChatBots.AutoRespond(Settings.AutoRespond_Matches)); } if (Settings.AutoAttack_Enabled) { BotLoad(new ChatBots.AutoAttack(Settings.AutoAttack_Mode, Settings.AutoAttack_Priority, Settings.AutoAttack_OverrideAttackSpeed, Settings.AutoAttack_CooldownSeconds)); } if (Settings.AutoFishing_Enabled) { BotLoad(new ChatBots.AutoFishing()); } if (Settings.AutoEat_Enabled) { BotLoad(new ChatBots.AutoEat(Settings.AutoEat_hungerThreshold)); } if (Settings.Mailer_Enabled) { BotLoad(new ChatBots.Mailer()); } if (Settings.AutoCraft_Enabled) { BotLoad(new AutoCraft(Settings.AutoCraft_configFile)); } if (Settings.AutoDrop_Enabled) { BotLoad(new AutoDrop(Settings.AutoDrop_Mode, Settings.AutoDrop_items)); } if (Settings.ReplayMod_Enabled) { BotLoad(new ReplayCapture(Settings.ReplayMod_BackupInterval)); } //Add your ChatBot here by uncommenting and adapting //BotLoad(new ChatBots.YourBot()); } } try { client = ProxyHandler.newTcpClient(host, port); client.ReceiveBufferSize = 1024 * 1024; client.ReceiveTimeout = 30000; // 30 seconds handler = Protocol.ProtocolHandler.GetProtocolHandler(client, protocolversion, forgeInfo, this); Log.Info(Translations.Get("mcc.version_supported")); if (!singlecommand) { timeoutdetector = new Thread(new ThreadStart(TimeoutDetector)); timeoutdetector.Name = "MCC Connection timeout detector"; timeoutdetector.Start(); } try { if (handler.Login()) { if (singlecommand) { handler.SendChatMessage(command); Log.Info(Translations.Get("mcc.single_cmd", command)); Thread.Sleep(5000); handler.Disconnect(); Thread.Sleep(1000); } else { foreach (ChatBot bot in botsOnHold) BotLoad(bot, false); botsOnHold.Clear(); Log.Info(Translations.Get("mcc.joined", (Settings.internalCmdChar == ' ' ? "" : "" + Settings.internalCmdChar))); cmdprompt = new Thread(new ThreadStart(CommandPrompt)); cmdprompt.Name = "MCC Command prompt"; cmdprompt.Start(); } } else { Log.Error(Translations.Get("error.login_failed")); retry = true; } } catch (Exception e) { Log.Error(e.GetType().Name + ": " + e.Message); Log.Error(Translations.Get("error.join")); retry = true; } } catch (SocketException e) { Log.Error(e.Message); Log.Error(Translations.Get("error.connect")); retry = true; } if (retry) { if (timeoutdetector != null) { timeoutdetector.Abort(); timeoutdetector = null; } if (ReconnectionAttemptsLeft > 0) { Log.Info(Translations.Get("mcc.reconnect", ReconnectionAttemptsLeft)); Thread.Sleep(5000); ReconnectionAttemptsLeft--; Program.Restart(); } else if (!singlecommand && Settings.interactiveMode) { Program.HandleFailure(); } } } /// /// Allows the user to send chat messages, commands, and leave the server. /// Enqueue text typed in the command prompt for processing on the main thread. /// private void CommandPrompt() { try { Thread.Sleep(500); while (client.Client.Connected) { string text = ConsoleIO.ReadLine(); ScheduleTask(new Action(() => { HandleCommandPromptText(text); })); } } catch (IOException) { } catch (NullReferenceException) { } } /// /// 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.Substring(1).Split((char)0x00); switch (command[0].ToLower()) { case "autocomplete": if (command.Length > 1) { ConsoleIO.WriteLine((char)0x00 + "autocomplete" + (char)0x00 + handler.AutoComplete(command[1])); } else Console.WriteLine((char)0x00 + "autocomplete" + (char)0x00); break; } } else { text = text.Trim(); if (text.Length > 0) { if (Settings.internalCmdChar == ' ' || text[0] == Settings.internalCmdChar) { string response_msg = ""; string command = Settings.internalCmdChar == ' ' ? text : text.Substring(1); if (!PerformInternalCommand(Settings.ExpandVars(command), ref response_msg) && Settings.internalCmdChar == '/') { SendText(text); } else if (response_msg.Length > 0) { Log.Info(response_msg); } } else SendText(text); } } } /// /// Periodically checks for server keepalives and consider that connection has been lost if the last received keepalive is too old. /// private void TimeoutDetector() { UpdateKeepAlive(); do { Thread.Sleep(TimeSpan.FromSeconds(15)); lock (lastKeepAliveLock) { if (lastKeepAlive.AddSeconds(30) < DateTime.Now) { OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, Translations.Get("error.timeout")); return; } } } while (true); } /// /// Update last keep alive to current time /// private void UpdateKeepAlive() { lock (lastKeepAliveLock) { lastKeepAlive = DateTime.Now; } } /// /// 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 string response_msg, Dictionary localVars = null) { /* Process the provided command */ string command_name = command.Split(' ')[0].ToLower(); if (command_name == "help") { if (Command.hasArg(command)) { string help_cmdname = Command.getArgs(command)[0].ToLower(); if (help_cmdname == "help") { response_msg = Translations.Get("icmd.help"); } else if (cmds.ContainsKey(help_cmdname)) { response_msg = cmds[help_cmdname].GetCmdDescTranslated(); } else response_msg = Translations.Get("icmd.unknown", command_name); } else response_msg = Translations.Get("icmd.list", String.Join(", ", cmd_names.ToArray()), Settings.internalCmdChar); } else if (cmds.ContainsKey(command_name)) { response_msg = cmds[command_name].Run(this, command, localVars); foreach (ChatBot bot in bots.ToArray()) { try { bot.OnInternalCommand(command_name, string.Join(" ",Command.getArgs(command)),response_msg); } catch (Exception e) { if (!(e is ThreadAbortException)) { Log.Warn(Translations.Get("icmd.error", bot.ToString(), e.ToString())); } else throw; //ThreadAbortException should not be caught } } } else { response_msg = Translations.Get("icmd.unknown", command_name); return 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); cmds[cmd.CmdName.ToLower()] = cmd; cmd_names.Add(cmd.CmdName.ToLower()); foreach (string alias in cmd.getCMDAliases()) cmds[alias.ToLower()] = cmd; } catch (Exception e) { Log.Warn(e.Message); } } } commandsLoaded = true; } } /// /// 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.Abort(); if (timeoutdetector != null) { timeoutdetector.Abort(); 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) { world.Clear(); if (timeoutdetector != null) { if (Thread.CurrentThread != timeoutdetector) timeoutdetector.Abort(); timeoutdetector = null; } bool will_restart = false; switch (reason) { case ChatBot.DisconnectReason.ConnectionLost: message = Translations.Get("mcc.disconnect.lost"); Log.Info(message); break; case ChatBot.DisconnectReason.InGameKick: Log.Info(Translations.Get("mcc.disconnect.server")); Log.Info(message); break; case ChatBot.DisconnectReason.LoginRejected: Log.Info(Translations.Get("mcc.disconnect.login")); Log.Info(message); break; case ChatBot.DisconnectReason.UserLogout: throw new InvalidOperationException(Translations.Get("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 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 ThreadAbortException)) { Log.Warn("OnDisconnect: Got error from " + bot.ToString() + ": " + e.ToString()); } else throw; //ThreadAbortException should not be caught } } if (!will_restart) Program.HandleFailure(); } /// /// 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 ThreadAbortException)) { Log.Warn("Update: Got error from " + bot.ToString() + ": " + e.ToString()); } else throw; //ThreadAbortException should not be caught } } lock (chatQueue) { if (chatQueue.Count > 0 && nextMessageSendTime < DateTime.Now) { string text = chatQueue.Dequeue(); handler.SendChatMessage(text); nextMessageSendTime = DateTime.Now + Settings.messageCooldown; } } if (terrainAndMovementsEnabled && locationReceived) { lock (locationLock) { for (int i = 0; i < 2; 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); 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 (Settings.AutoRespawn && respawnTicks > 0) { respawnTicks--; if (respawnTicks == 0) SendRespawnPacket(); } lock (threadTasksLock) { while (threadTasks.Count > 0) { var taskToRun = threadTasks.Dequeue(); taskToRun.Execute(); taskToRun.Release(); } } } /// /// Register a custom console command /// /// Name of the command /// Description/usage of the command /// Method for handling the command /// True if successfully registered public bool RegisterCommand(string cmdName, string cmdDesc, string cmdUsage, ChatBot.CommandRunner callback) { if (cmds.ContainsKey(cmdName.ToLower())) { return false; } else { Command cmd = new ChatBot.ChatBotCommand(cmdName, cmdDesc, cmdUsage, callback); cmds.Add(cmdName.ToLower(), cmd); cmd_names.Add(cmdName.ToLower()); return true; } } /// /// Unregister a console command /// /// /// There is no check for the command is registered by above method or is embedded command. /// Which mean this can unload any command /// /// The name of command to be unregistered /// public bool UnregisterCommand(string cmdName) { if (cmds.ContainsKey(cmdName.ToLower())) { cmds.Remove(cmdName.ToLower()); cmd_names.Remove(cmdName.ToLower()); return true; } else return false; } /// /// Schedule a task to run on the main thread /// /// Task to run public object ScheduleTask(Delegate task) { if (!InvokeRequired()) { return task.DynamicInvoke(); } else { var taskAndResult = new TaskWithResult(task); lock (threadTasksLock) { threadTasks.Enqueue(taskAndResult); } taskAndResult.Block(); return taskAndResult.Result; } } /// /// Check if calling thread is main thread or other thread /// /// True if calling thread is other thread public bool InvokeRequired() { int callingThreadId = Thread.CurrentThread.ManagedThreadId; if (handler != null) { return handler.GetNetReadThreadId() != callingThreadId; } else { // net read thread (main thread) not yet ready return false; } } /// /// 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 } #region Management: Load/Unload ChatBots and Enable/Disable settings /// /// Load a new bot /// public void BotLoad(ChatBot b, bool init = true) { b.SetHandler(this); bots.Add(b); if (init) DispatchBotEvent(bot => bot.Initialize(), new ChatBot[] { b }); if (this.handler != null) DispatchBotEvent(bot => bot.AfterGameJoined(), new ChatBot[] { b }); Settings.SingleCommand = ""; } /// /// Unload a bot /// public void BotUnLoad(ChatBot b) { bots.RemoveAll(item => object.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() { bots.Clear(); } /// /// Get Terrain and Movements status. /// public bool GetTerrainEnabled() { return terrainAndMovementsEnabled; } /// /// Get Inventory Handling Mode /// public bool GetInventoryEnabled() { return inventoryHandlingEnabled; } /// /// 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 (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 (enabled) { if (!inventoryHandlingEnabled) { inventoryHandlingRequested = true; return false; } } else { inventoryHandlingEnabled = false; inventoryHandlingRequested = false; inventories.Clear(); } return true; } /// /// Get entity handling status /// /// /// Entity Handling cannot be enabled in runtime (or after joining server) public bool GetEntityHandlingEnabled() { return entityHandlingEnabled; } /// /// 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 (!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) { 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 all inventories. ID 0 is the player inventory. /// /// All inventories public Dictionary GetInventories() { return inventories; } /// /// Get all Entities /// /// All Entities public Dictionary GetEntities() { return entities; } /// /// Get all players latency /// /// All players latency public Dictionary GetPlayersLatency() { 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 (inventories.ContainsKey(inventoryID)) return inventories[inventoryID]; 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) { return onlinePlayers.Values.Distinct().ToArray(); } } /// /// 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 Dictionary(); lock (onlinePlayers) { foreach (Guid key in onlinePlayers.Keys) { uuid2Player.Add(key.ToString(), onlinePlayers[key]); } } return uuid2Player; } #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 /// True if a path has been found public bool MoveTo(Location location, bool allowUnsafe = false, bool allowDirectTeleport = false) { lock (locationLock) { if (allowDirectTeleport) { // 1-step path to the desired location without checking anything UpdateLocation(location, location); // Update yaw and pitch to look at next step handler.SendLocationUpdate(location, Movement.IsOnGround(world, location), _yaw, _pitch); return true; } else { // Calculate path through pathfinding. Path contains a list of 1-block movement that will be divided into steps if (Movement.GetAvailableMoves(world, this.location, allowUnsafe).Contains(location)) path = new Queue(new[] { location }); else path = Movement.CalculatePath(world, this.location, location, allowUnsafe); return path != null; } } } /// /// Send a chat message or command to the server /// /// Text to send to the server public void SendText(string text) { lock (chatQueue) { if (String.IsNullOrEmpty(text)) return; int maxLength = handler.GetMaxChatMessageLength(); if (text.Length > maxLength) //Message is too long? { if (text[0] == '/') { //Send the first 100/256 chars of the command text = text.Substring(0, maxLength); chatQueue.Enqueue(text); } else { //Split the message into several messages while (text.Length > maxLength) { chatQueue.Enqueue(text.Substring(0, maxLength)); text = text.Substring(maxLength, text.Length - maxLength); } chatQueue.Enqueue(text); } } else chatQueue.Enqueue(text); } } /// /// Allow to respawn after death /// /// True if packet successfully sent public bool 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 (registeredBotPluginChannels.ContainsKey(channel)) { registeredBotPluginChannels[channel].Add(bot); } else { List bots = new List(); bots.Add(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 (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 (!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 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 handler.SendUseItem(0); } /// /// Click a slot in the specified window /// /// TRUE if the slot was successfully clicked public bool DoWindowAction(int windowId, int slotId, WindowActionType action) { Item item = null; if (inventories.ContainsKey(windowId) && inventories[windowId].Items.ContainsKey(slotId)) item = inventories[windowId].Items[slotId]; // Inventory update must be after sending packet bool result = handler.SendWindowAction(windowId, slotId, action, item); // Update our inventory base on action type var inventory = GetInventory(windowId); var 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 var itemTmp = playerInventory.Items[-1]; playerInventory.Items[-1] = inventory.Items[slotId]; inventory.Items[slotId] = itemTmp; } } else { // Put cursor item to target inventory.Items[slotId] = playerInventory.Items[-1]; playerInventory.Items.Remove(-1); } } 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); } } 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 var itemTmp = playerInventory.Items[-1]; playerInventory.Items[-1] = inventory.Items[slotId]; inventory.Items[slotId] = itemTmp; } } else { // Drop 1 item count from cursor var itemTmp = playerInventory.Items[-1]; var itemClone = new Item(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; } } } } break; case WindowActionType.ShiftClick: if (slotId == 0) break; if (inventory.Items.ContainsKey(slotId)) { /* Target slot have item */ int upperStartSlot = 9; int upperEndSlot = 35; switch (inventory.Type) { case ContainerType.PlayerInventory: upperStartSlot = 9; upperEndSlot = 35; break; case ContainerType.Crafting: upperStartSlot = 1; upperEndSlot = 9; break; // TODO: Define more container type here } // 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 if (slotId <= upperEndSlot) { // Clicked slot is on upper side inventory, put it to hotbar // Now try to find same item and put on them var itemsClone = playerInventory.Items.ToDictionary(entry => entry.Key, entry => entry.Value); foreach (KeyValuePair _item in itemsClone) { if (_item.Key <= upperEndSlot) continue; int maxCount = _item.Value.Type.StackCount(); if (_item.Value.Type == inventory.Items[slotId].Type && _item.Value.Count < maxCount) { // Put item on that stack int spaceLeft = maxCount - _item.Value.Count; if (inventory.Items[slotId].Count <= spaceLeft) { // Can fit into the stack inventory.Items[_item.Key].Count += inventory.Items[slotId].Count; inventory.Items.Remove(slotId); } else { inventory.Items[slotId].Count -= spaceLeft; inventory.Items[_item.Key].Count = inventory.Items[_item.Key].Type.StackCount(); } } } if (inventory.Items[slotId].Count > 0) { int[] emptySlots = inventory.GetEmpytSlots(); int emptySlot = -2; foreach (int slot in emptySlots) { if (slot <= upperEndSlot) continue; emptySlot = slot; break; } if (emptySlot != -2) { var itemTmp = inventory.Items[slotId]; inventory.Items[emptySlot] = new Item(itemTmp.Type, itemTmp.Count, itemTmp.NBT); inventory.Items.Remove(slotId); } } } else { // Clicked slot is on hotbar, put it to upper inventory // Now try to find same item and put on them var itemsClone = playerInventory.Items.ToDictionary(entry => entry.Key, entry => entry.Value); foreach (KeyValuePair _item in itemsClone) { if (_item.Key < upperStartSlot) continue; if (_item.Key >= upperEndSlot) break; int maxCount = _item.Value.Type.StackCount(); if (_item.Value.Type == inventory.Items[slotId].Type && _item.Value.Count < maxCount) { // Put item on that stack int spaceLeft = maxCount - _item.Value.Count; if (inventory.Items[slotId].Count <= spaceLeft) { // Can fit into the stack inventory.Items[_item.Key].Count += inventory.Items[slotId].Count; inventory.Items.Remove(slotId); } else { inventory.Items[slotId].Count -= spaceLeft; inventory.Items[_item.Key].Count = inventory.Items[_item.Key].Type.StackCount(); } } } if (inventory.Items[slotId].Count > 0) { int[] emptySlots = inventory.GetEmpytSlots(); int emptySlot = -2; foreach (int slot in emptySlots) { if (slot < upperStartSlot) continue; if (slot >= upperEndSlot) break; emptySlot = slot; break; } if (emptySlot != -2) { var itemTmp = inventory.Items[slotId]; inventory.Items[emptySlot] = new Item(itemTmp.Type, itemTmp.Count, itemTmp.NBT); inventory.Items.Remove(slotId); } } } } break; case WindowActionType.DropItem: if (inventory.Items.ContainsKey(slotId)) inventory.Items[slotId].Count--; if (inventory.Items[slotId].Count <= 0) inventory.Items.Remove(slotId); break; case WindowActionType.DropItemStack: inventory.Items.Remove(slotId); break; } } return result; } /// /// 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 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 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 (inventories.ContainsKey(windowId)) { if (windowId != 0) inventories.Remove(windowId); return handler.SendCloseWindow(windowId); } return false; } /// /// Clean all inventory /// /// TRUE if the uccessfully clear public bool ClearInventories() { if (inventoryHandlingEnabled) { inventories.Clear(); inventories[0] = new Container(0, ContainerType.PlayerInventory, "Player Inventory"); return true; } else { return false; } } /// /// Interact with an entity /// /// /// 0: interact, 1: attack, 2: interact at /// Hand.MainHand or Hand.OffHand /// TRUE if interaction succeeded public bool InteractEntity(int EntityID, int type, Hand hand = Hand.MainHand) { if (entities.ContainsKey(EntityID)) { if (type == 0) { return handler.SendInteractEntity(EntityID, type, (int)hand); } else { return handler.SendInteractEntity(EntityID, type); } } else { 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 handler.SendPlayerBlockPlacement((int)hand, location, blockFace); } /// /// 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) { if (GetTerrainEnabled()) { // TODO select best face from current player location Direction blockFace = Direction.Down; // 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 return handler.SendPlayerDigging(0, location, blockFace) && (!swingArms || DoAnimation((int)Hand.MainHand)) && handler.SendPlayerDigging(2, location, blockFace); } else return false; } /// /// 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) { CurrentSlot = Convert.ToByte(slot); return handler.SendHeldItemChange(slot); } else return false; } /// /// 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 handler.SendUpdateSign(location, line1, line2, line3, line4); } /// /// Select villager trade /// /// The slot of the trade, starts at 0. public bool SelectTrade(int selectedSlot) { return 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 handler.UpdateCommandBlock(location, command, mode, flags); } #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 ThreadAbortException)) { //Retrieve parent method name to determine which event caused the exception System.Diagnostics.StackFrame frame = new System.Diagnostics.StackFrame(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() { if (!String.IsNullOrWhiteSpace(Settings.BrandInfo)) handler.SendBrandInfo(Settings.BrandInfo.Trim()); if (Settings.MCSettings_Enabled) handler.SendClientSettings( Settings.MCSettings_Locale, Settings.MCSettings_RenderDistance, Settings.MCSettings_Difficulty, Settings.MCSettings_ChatMode, Settings.MCSettings_ChatColors, Settings.MCSettings_Skin_All, Settings.MCSettings_MainHand); if (inventoryHandlingRequested) { inventoryHandlingRequested = false; inventoryHandlingEnabled = true; Log.Info(Translations.Get("extra.inventory_enabled")); } ClearInventories(); DispatchBotEvent(bot => bot.AfterGameJoined()); } /// /// Called when the player respawns, which happens on login, respawn and world change. /// public void OnRespawn() { if (terrainAndMovementsRequested) { terrainAndMovementsEnabled = true; terrainAndMovementsRequested = false; Log.Info(Translations.Get("extra.terrainandmovement_enabled")); } if (terrainAndMovementsEnabled) { world.Clear(); } entities.Clear(); ClearInventories(); DispatchBotEvent(bot => bot.OnRespawn()); } /// /// 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) { this._yaw = yaw; this._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.Get("exception.unknown_direction"), "direction"); } UpdateLocation(location, yaw, pitch); } /// /// Received some text from the server /// /// Text received /// TRUE if the text is JSON-Encoded public void OnTextReceived(string text, bool isJson) { UpdateKeepAlive(); List links = new List(); string json = null; if (isJson) { json = text; text = ChatParser.ParseText(json, links); } Log.Chat(text); if (Settings.DisplayChatLinks) foreach (string link in links) Log.Chat(Translations.Get("mcc.link", link), false); DispatchBotEvent(bot => bot.GetText(text)); DispatchBotEvent(bot => bot.GetText(text, json)); } /// /// 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(Translations.Get("extra.inventory_open", inventoryID, inventory.Title)); Log.Info(Translations.Get("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(Translations.Get("extra.inventory_close", inventoryID)); DispatchBotEvent(bot => bot.OnInventoryClose(inventoryID)); } } /// /// When received window items from server. /// /// Inventory ID /// Item list, key = slot ID, value = Item information public void OnWindowItems(byte inventoryID, Dictionary itemList) { if (inventories.ContainsKey(inventoryID)) { inventories[inventoryID].Items = itemList; 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) { // 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 /// /// UUID of the player /// Name of the player public void OnPlayerJoin(Guid uuid, string name) { //Ignore placeholders eg 0000tab# from TabListPlus if (!ChatBot.IsValidName(name)) return; lock (onlinePlayers) { onlinePlayers[uuid] = name; } DispatchBotEvent(bot => bot.OnPlayerJoin(uuid, 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]; onlinePlayers.Remove(uuid); } } DispatchBotEvent(bot => bot.OnPlayerLeave(uuid, username)); } /// /// 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) { 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) { string playerName = null; if (onlinePlayers.ContainsKey(uuid)) playerName = onlinePlayers[uuid]; Entity playerEntity = new Entity(entityID, EntityType.Player, location, uuid, playerName); 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]; if (playerName == this.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.ContainsKey(a)) { DispatchBotEvent(bot => bot.OnEntityDespawn(entities[a])); 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 Location(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 (Settings.AutoRespawn) { Log.Info(Translations.Get("mcc.player_dead_respawn")); respawnTicks = 10; } else { Log.Info(Translations.Get("mcc.player_dead")); } 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) { string playerName = null; if (onlinePlayers.ContainsKey(uuid)) { playerName = onlinePlayers[uuid]; playersLatency[playerName] = latency; 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 map data /// /// /// /// /// /// public void OnMapData(int mapid, byte scale, bool trackingposition, bool locked, int iconcount) { DispatchBotEvent(bot => bot.OnMapData(mapid, scale, trackingposition, locked, iconcount)); } /// /// 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, byte 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; if (entity.Type.ContainsItem() && metadata.ContainsKey(7) && metadata[7] != null && metadata[7].GetType() == typeof(Item)) { Item item = (Item)metadata[7]; if (item == null) entity.Item = new Item(ItemType.Air, 0, null); else entity.Item = item; } if (metadata.ContainsKey(6) && metadata[6] != null && metadata[6].GetType() == typeof(Int32)) { entity.Pose = (EntityPose)metadata[6]; } if (metadata.ContainsKey(2) && metadata[2] != null && metadata[2].GetType() == typeof(string)) { entity.CustomNameJson = metadata[2].ToString(); entity.CustomName = ChatParser.ParseText(metadata[2].ToString()); } if (metadata.ContainsKey(3) && metadata[3] != null && metadata[3].GetType() == typeof(bool)) { entity.IsCustomNameVisible = bool.Parse(metadata[3].ToString()); } 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)); } #endregion } }