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
}
}