From 1ea8f119d91e3fa4e2db812998fdaabd4825b005 Mon Sep 17 00:00:00 2001 From: Pokechu22 Date: Sun, 7 Feb 2016 14:24:01 -0800 Subject: [PATCH 1/2] Give ChatBots access to plugin channels. Chatbots may find it useful to send messages over plugin channels. This allows REGISTERing, UNREGISTERing, and sending over plugin channels, with built-in checking if the server also registered the channel (which can be disabled by the bot if needed). Unused channels are UNREGISTERed when a bot is disabled. --- MinecraftClient/ChatBot.cs | 55 ++++++++ MinecraftClient/McTcpClient.cs | 117 +++++++++++++++++- .../Protocol/Handlers/Protocol16.cs | 5 +- .../Protocol/Handlers/Protocol18.cs | 4 + .../Protocol/IMinecraftComHandler.cs | 37 +++++- 5 files changed, 215 insertions(+), 3 deletions(-) diff --git a/MinecraftClient/ChatBot.cs b/MinecraftClient/ChatBot.cs index ce65a3a9..c84ff57d 100644 --- a/MinecraftClient/ChatBot.cs +++ b/MinecraftClient/ChatBot.cs @@ -41,6 +41,7 @@ namespace MinecraftClient private McTcpClient Handler { get { return master != null ? master.Handler : _handler; } } private McTcpClient _handler = null; private ChatBot master = null; + private List registeredPluginChannels = new List(); private Queue chatQueue = new Queue(); private DateTime? lastMessageSentTime = DateTime.MinValue; private bool CanSendTextNow @@ -100,6 +101,17 @@ namespace MinecraftClient public virtual bool OnDisconnect(DisconnectReason reason, string message) { return false; } + /// + /// Called when a plugin channel message is received. + /// The given channel must have previously been registered with RegisterPluginChannel. + /// This can be used to communicate with server mods or plugins. See wiki.vg for more + /// information about plugin channels: http://wiki.vg/Plugin_channel + /// + /// The name of the channel + /// The payload for the message + + public virtual void OnPluginMessage(string channel, byte[] data) { } + /* =================================================================== */ /* ToolBox - Methods below might be useful while creating your bot. */ /* You should not need to interact with other classes of the program. */ @@ -596,5 +608,48 @@ namespace MinecraftClient return new string[0]; } } + + /// + /// Registers the given plugin channel for use by this chatbot. + /// + /// The name of the channel to register + + protected void RegisterPluginChannel(string channel) + { + this.registeredPluginChannels.Add(channel); + Handler.RegisterPluginChannel(channel, this); + } + + /// + /// Unregisters the given plugin channel, meaning this chatbot can no longer use it. + /// + /// The name of the channel to unregister + + protected void UnregisterPluginChannel(string channel) + { + this.registeredPluginChannels.RemoveAll(chan => chan == channel); + Handler.UnregisterPluginChannel(channel, this); + } + + /// + /// Sends the given plugin channel message to the server, if the channel has been registered. + /// See http://wiki.vg/Plugin_channel for more information about plugin channels. + /// + /// The channel to send the message on. + /// The data to send. + /// Should the message be sent even if it hasn't been registered by the server or this bot? (Some Minecraft channels aren't registered) + /// Whether the message was successfully sent. False if there was a network error or if the channel wasn't registered. + + protected bool SendPluginChannelMessage(string channel, byte[] data, bool sendEvenIfNotRegistered = false) + { + if (!sendEvenIfNotRegistered) + { + if (!this.registeredPluginChannels.Contains(channel)) + { + return false; + } + } + return Handler.SendPluginChannelMessage(channel, data, sendEvenIfNotRegistered); + } } } diff --git a/MinecraftClient/McTcpClient.cs b/MinecraftClient/McTcpClient.cs index b45d0a85..7e9c95b7 100644 --- a/MinecraftClient/McTcpClient.cs +++ b/MinecraftClient/McTcpClient.cs @@ -28,9 +28,21 @@ namespace MinecraftClient private readonly List bots = new List(); private static readonly List scripts_on_hold = new List(); public void BotLoad(ChatBot b) { b.SetHandler(this); bots.Add(b); b.Initialize(); Settings.SingleCommand = ""; } - public void BotUnLoad(ChatBot b) { bots.RemoveAll(item => object.ReferenceEquals(item, b)); } + 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); + } + } public void BotClear() { bots.Clear(); } + private readonly Dictionary> registeredBotPluginChannels = new Dictionary>(); + private readonly List registeredServerPluginChannels = new List(); + private object locationLock = new object(); private bool locationReceived = false; private World world = new World(); @@ -573,5 +585,108 @@ namespace MinecraftClient return onlinePlayers.Values.Distinct().ToArray(); } } + + /// + /// 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); + } + + /// + /// 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)) + { + foreach (ChatBot bot in registeredBotPluginChannels[channel]) + { + bot.OnPluginMessage(channel, data); + } + } + } } } diff --git a/MinecraftClient/Protocol/Handlers/Protocol16.cs b/MinecraftClient/Protocol/Handlers/Protocol16.cs index 07ee63dd..905ee97f 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol16.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol16.cs @@ -164,7 +164,10 @@ namespace MinecraftClient.Protocol.Handlers case 0xCF: if (protocolversion > 51) { readNextString(); readData(1); readNextString(); } readData(4); break; case 0xD0: if (protocolversion > 51) { readData(1); readNextString(); } break; case 0xD1: if (protocolversion > 51) { readNextTeamData(); } break; - case 0xFA: readNextString(); nbr = readNextShort(); readData(nbr); break; + case 0xFA: string channel = readNextString(); + byte[] payload = readNextByteArray(); + handler.OnPluginChannelMessage(channel, payload); + break; case 0xFF: string reason = readNextString(); handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick, reason); break; default: return false; //unknown packet! diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 5bbd7da5..d6e8f880 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -303,6 +303,10 @@ namespace MinecraftClient.Protocol.Handlers readNextVarShort(packetData); } } + + // The remaining data in the array is the entire payload of the packet. + handler.OnPluginChannelMessage(channel, packetData.ToArray()); + if (forgeInfo != null) { if (channel == "FML|HS") diff --git a/MinecraftClient/Protocol/IMinecraftComHandler.cs b/MinecraftClient/Protocol/IMinecraftComHandler.cs index b9de09d8..7ca8cea6 100644 --- a/MinecraftClient/Protocol/IMinecraftComHandler.cs +++ b/MinecraftClient/Protocol/IMinecraftComHandler.cs @@ -14,7 +14,7 @@ namespace MinecraftClient.Protocol public interface IMinecraftComHandler { - /* The MinecraftCom Hanler must + /* The MinecraftCom Handler must * provide these getters */ int GetServerPort(); @@ -72,5 +72,40 @@ namespace MinecraftClient.Protocol /// void OnUpdate(); + + /// + /// Registers the given plugin channel for the given bot. + /// + /// The channel to register. + /// The bot to register the channel for. + + void RegisterPluginChannel(string channel, ChatBot bot); + + /// + /// Unregisters the given plugin channel for the given bot. + /// + /// The channel to unregister. + /// The bot to unregister the channel for. + + void UnregisterPluginChannel(string channel, ChatBot bot); + + /// + /// 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. + + bool SendPluginChannelMessage(string channel, byte[] data, bool sendEvenIfNotRegistered = false); + + /// + /// Called when a plugin channel message was sent from the server. + /// + /// The channel the message was sent on + /// The data from the channel + + void OnPluginChannelMessage(string channel, byte[] data); } } From 6d4ec86619d6a9b1129c7275e71adc3b459a7208 Mon Sep 17 00:00:00 2001 From: Pokechu22 Date: Sun, 7 Feb 2016 14:47:03 -0800 Subject: [PATCH 2/2] Add an "AfterGameJoined" method to ChatBot.cs. Since messages can't be sent in Initialize(), a method that's called when the chat bot can first send messages is needed. This method is called when the login completes or when the bot is loaded if the login is already been completed. --- MinecraftClient/ChatBot.cs | 12 ++++++++++++ MinecraftClient/McTcpClient.cs | 13 ++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/MinecraftClient/ChatBot.cs b/MinecraftClient/ChatBot.cs index c84ff57d..fd58719a 100644 --- a/MinecraftClient/ChatBot.cs +++ b/MinecraftClient/ChatBot.cs @@ -75,10 +75,22 @@ namespace MinecraftClient /// /// Anything you want to initialize your bot, will be called on load by MinecraftCom + /// + /// NOTE: Chat messages cannot be sent at this point in the login process. If you want to send + /// a message when the bot is loaded, use AfterGameJoined. /// public virtual void Initialize() { } + /// + /// Called after the server has been joined successfully and chat messages are able to be sent. + /// + /// NOTE: This is not always right after joining the server - if the bot was loaded after logging + /// in this is still called. + /// + + public virtual void AfterGameJoined() { } + /// /// Will be called every ~100ms (10fps) if loaded in MinecraftCom /// diff --git a/MinecraftClient/McTcpClient.cs b/MinecraftClient/McTcpClient.cs index 7e9c95b7..35eaa124 100644 --- a/MinecraftClient/McTcpClient.cs +++ b/MinecraftClient/McTcpClient.cs @@ -27,7 +27,16 @@ namespace MinecraftClient private readonly List bots = new List(); private static readonly List scripts_on_hold = new List(); - public void BotLoad(ChatBot b) { b.SetHandler(this); bots.Add(b); b.Initialize(); Settings.SingleCommand = ""; } + public void BotLoad(ChatBot b) { + b.SetHandler(this); + bots.Add(b); + b.Initialize(); + if (this.handler != null) + { + b.AfterGameJoined(); + } + Settings.SingleCommand = ""; + } public void BotUnLoad(ChatBot b) { bots.RemoveAll(item => object.ReferenceEquals(item, b)); @@ -351,6 +360,8 @@ namespace MinecraftClient { if (!String.IsNullOrWhiteSpace(Settings.BrandInfo)) handler.SendBrandInfo(Settings.BrandInfo.Trim()); + foreach (ChatBot bot in bots) + bot.AfterGameJoined(); } ///