diff --git a/MinecraftClient.sln b/MinecraftClient.sln new file mode 100644 index 00000000..666d8566 --- /dev/null +++ b/MinecraftClient.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinecraftClient", "MinecraftClient\MinecraftClient.csproj", "{1E2FACE4-F5CA-4323-9641-740C6A551770}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1E2FACE4-F5CA-4323-9641-740C6A551770}.Debug|x86.ActiveCfg = Debug|x86 + {1E2FACE4-F5CA-4323-9641-740C6A551770}.Debug|x86.Build.0 = Debug|x86 + {1E2FACE4-F5CA-4323-9641-740C6A551770}.Release|x86.ActiveCfg = Release|x86 + {1E2FACE4-F5CA-4323-9641-740C6A551770}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/MinecraftClient/Bots.cs b/MinecraftClient/Bots.cs new file mode 100644 index 00000000..e9187c18 --- /dev/null +++ b/MinecraftClient/Bots.cs @@ -0,0 +1,845 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MinecraftClient +{ + /// + /// Welcome to the Bot API file ! + /// The virtual class "ChatBot" contains anything you need for creating chat bots + /// Inherit from this class while adding your bot class to the namespace "Bots", below. + /// Once your bot is created, simply edit the switch in Program.cs to add the corresponding command-line argument! + /// + + /// + /// The virtual class containing anything you need for creating chat bots. + /// + + public abstract class ChatBot + { + public enum DisconnectReason { InGameKick, LoginRejected, ConnectionLost }; + + #region MinecraftCom Handler for this bot + + //Will be automatically set on bot loading, don't worry about this + public void SetHandler(MinecraftCom handler) { this.handler = handler; } + private MinecraftCom handler; + + #endregion + + /// + /// Anything you want to initialize your bot, will be called on load by MinecraftCom + /// + + public virtual void Initialize() { } + + /// + /// Will be called every ~100ms (10fps) if loaded in MinecraftCom + /// + + public virtual void Update() { } + + /// + /// Any text sent by the server will be sent here by MinecraftCom + /// + /// Text from the server + + public virtual void GetText(string text) { } + + /// + /// Is called when the client has been disconnected fom the server + /// + /// Disconnect Reason + /// Kick message, if any + /// Return TRUE if the client is about to restart + + public virtual bool OnDisconnect(DisconnectReason reason, string message) { return false; } + + #region ToolBox + + /// + /// Send text to the server. Can be anything such as chat messages or commands + /// + /// Text to send to the server + + protected void SendText(string text) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + ConsoleIO.WriteLine("BOT:" + text); + handler.SendChatMessage(text); + Console.ForegroundColor = ConsoleColor.Gray; + } + + /// + /// Remove color codes ("§c") from a text message received from the server + /// + + protected static string getVerbatim(string text) + { + string verbatim = ""; + for (int i = 0; i < text.Length; i++) + { + if (text[i] == '§') + { + i++; //Will skip also the next char + } + else verbatim += text[i]; //Add the char + } + return verbatim; + } + + /// + /// Verify that a string contains only a-z A-Z 0-9 and _ characters. + /// + + protected static bool isValidName(string username) + { + if (username == "") { return false; } + string validchars = + "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "1234567890_"; + + bool passe = false; + bool ok = true; + for (int i = 0; i < username.Length; i++) + { + passe = false; + for (int j = 0; j < validchars.Length; j++) + { + if (username[i] == validchars[j]) + { + passe = true; + break; + } + } + if (!passe) + { + ok = false; + break; + } + } + return ok; + } + + /// + /// Returns true is the text passed is a private message sent to the bot + /// + /// text to test + /// if it's a private message, this will contain the message + /// if it's a private message, this will contain the player name that sends the message + /// Returns true if the text is a private message + + protected static bool isPrivateMessage(string text, ref string message, ref string sender) + { + if (text == "") { return false; } + string[] tmp = text.Split(' '); + + try + { + //Detect vanilla /tell messages + //Someone whispers message + if (tmp.Length > 2 && tmp[1] == "whispers") + { + message = text.Substring(tmp[0].Length + 10); + sender = tmp[0]; + return isValidName(sender); + } + + //Detect Essentials (Bukkit) /m messages + //[Someone -> me] message + else if (text[0] == '[' && tmp.Length > 3 && tmp[1] == "->" + && (tmp[2] == "me]" || tmp[2] == "moi]")) //'me' is replaced by 'moi' in french servers + { + message = text.Substring(tmp[0].Length + 4 + tmp[2].Length + 1); + sender = tmp[0].Substring(1); + if (sender[0] == '~') { sender = sender.Substring(1); } + return isValidName(sender); + } + else return false; + } + catch (IndexOutOfRangeException) { return false; } + } + + /// + /// Returns true is the text passed is a public message written by a player on the chat + /// + /// text to test + /// if it's message, this will contain the message + /// if it's message, this will contain the player name that sends the message + /// Returns true if the text is a chat message + + protected static bool isChatMessage(string text, ref string message, ref string sender) + { + //Detect chat messages + // message + //<*Faction Someone> message + //<*Faction Someone>: message + //<*Faction ~Nicknamed>: message + if (text == "") { return false; } + if (text[0] == '<') + { + try + { + text = text.Substring(1); + string[] tmp = text.Split('>'); + sender = tmp[0]; + message = text.Substring(sender.Length + 2); + if (message.Length > 1 && message[0] == ' ') + { message = message.Substring(1); } + tmp = sender.Split(' '); + sender = tmp[tmp.Length - 1]; + if (sender[0] == '~') { sender = sender.Substring(1); } + return isValidName(sender); + } + catch (IndexOutOfRangeException) { return false; } + } + else return false; + } + + /// + /// Writes some text in the console. Nothing will be sent to the server. + /// + /// Log text to write + + public static void LogToConsole(string text) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + ConsoleIO.WriteLine("[BOT] " + text); + Console.ForegroundColor = ConsoleColor.Gray; + } + + /// + /// Disconnect from the server and restart the program + /// It will unload & reload all the bots and then reconnect to the server + /// + + protected void ReconnectToTheServer() { ReconnectToTheServer(3); } + + /// + /// Disconnect from the server and restart the program + /// It will unload & reload all the bots and then reconnect to the server + /// + /// If connection fails, the client will make X extra attempts + + protected void ReconnectToTheServer(int ExtraAttempts) + { + McTcpClient.AttemptsLeft = ExtraAttempts; + Program.Restart(); + } + + /// + /// Unload the chatbot, and release associated memory. + /// + + protected void UnloadBot() + { + handler.BotUnLoad(this); + } + + #endregion + } + + namespace Bots + { + /// + /// Example of message receiving. + /// + + public class TestBot : ChatBot + { + public override void GetText(string text) + { + string message = ""; + string username = ""; + text = getVerbatim(text); + + if (isPrivateMessage(text, ref message, ref username)) + { + ConsoleIO.WriteLine("Bot: " + username + " told me : " + message); + } + else if (isChatMessage(text, ref message, ref username)) + { + ConsoleIO.WriteLine("Bot: " + username + " said : " + message); + } + } + } + + /// + /// This bot sends a /ping command every 60 seconds in order to stay non-afk. + /// + + public class AntiAFK : ChatBot + { + private int count; + private int timeping; + + /// + /// This bot sends a /ping command every X seconds in order to stay non-afk. + /// + /// Time amount between each ping (10 = 1s, 600 = 1 minute, etc.) + + public AntiAFK(int pingparam) + { + count = 0; + timeping = pingparam; + if (timeping < 10) { timeping = 10; } //To avoid flooding + } + + public override void Update() + { + count++; + if (count == timeping) + { + SendText("/ping"); + count = 0; + } + } + } + + /// + /// This bot sends a /list command every X seconds and save the result. + /// + + public class PlayerListLogger : ChatBot + { + private int count; + private int timeping; + private string file; + + /// + /// This bot sends a /list command every X seconds and save the result. + /// + /// Time amount between each list ping (10 = 1s, 600 = 1 minute, etc.) + + public PlayerListLogger(int pingparam, string filetosavein) + { + count = 0; + file = filetosavein; + timeping = pingparam; + if (timeping < 10) { timeping = 10; } //To avoid flooding + + } + + public override void Update() + { + count++; + if (count == timeping) + { + SendText("/list"); + count = 0; + } + } + + public override void GetText(string text) + { + if (text.Contains("Joueurs en ligne") || text.Contains("Connected:") || text.Contains("online:")) + { + LogToConsole("Saving Player List"); + DateTime now = DateTime.Now; + string TimeStamp = "[" + now.Year + '/' + now.Month + '/' + now.Day + ' ' + now.Hour + ':' + now.Minute + ']'; + System.IO.File.AppendAllText(file, TimeStamp + "\n" + getVerbatim(text) + "\n\n"); + } + } + } + + /// + /// "Le jeu du Pendu" (Hangman game) + /// + + public class Pendu : ChatBot + { + private int vie = 0; + private int vie_param = 10; + private int compteur = 0; + private int compteur_param = 3000; //5 minutes + private bool running = false; + private bool[] discovered; + private string word = ""; + private string letters = ""; + private string[] owners; + private bool English; + + /// + /// "Le jeu du Pendu" (Hangman Game) + /// + /// if true, the game will be in english. If false, the game will be in french. + + public Pendu(bool english) + { + English = english; + } + + public override void Initialize() + { + owners = getowners(); + } + + public override void Update() + { + if (running) + { + if (compteur > 0) + { + compteur--; + } + else + { + SendText(English ? "You took too long to try a letter." : "Temps imparti écoulé !"); + SendText(English ? "Game canceled." : "Partie annulée."); + running = false; + } + } + } + + public override void GetText(string text) + { + string message = ""; + string username = ""; + text = getVerbatim(text); + + if (isPrivateMessage(text, ref message, ref username)) + { + if (owners.Contains(username.ToUpper())) + { + switch (message) + { + case "start": + start(); + break; + case "stop": + running = false; + break; + default: + break; + } + } + } + else + { + if (running && isChatMessage(text, ref message, ref username)) + { + if (message.Length == 1) + { + char letter = message.ToUpper()[0]; + if (letter >= 'A' && letter <= 'Z') + { + if (letters.Contains(letter)) + { + SendText(English ? ("Letter " + letter + " has already been tried.") : ("Le " + letter + " a déjà été proposé.")); + } + else + { + letters += letter; + compteur = compteur_param; + + if (word.Contains(letter)) + { + for (int i = 0; i < word.Length; i++) { if (word[i] == letter) { discovered[i] = true; } } + SendText(English ? ("Yes, the word contains a " + letter + '!') : ("Le " + letter + " figurait bien dans le mot :)")); + } + else + { + vie--; + if (vie == 0) + { + SendText(English ? "Game Over! :]" : "Perdu ! Partie terminée :]"); + SendText(English ? ("The word was: " + word) : ("Le mot était : " + word)); + running = false; + } + else SendText(English ? ("The " + letter + "? No.") : ("Le " + letter + " ? Non.")); + } + + if (running) + { + SendText(English ? ("Mysterious word: " + word_cached + " (lives : " + vie + ")") + : ("Mot mystère : " + word_cached + " (vie : " + vie + ")")); + } + + if (winner) + { + SendText(English ? ("Congrats, " + username + '!') : ("Félicitations, " + username + " !")); + running = false; + } + } + } + } + } + } + } + + private void start() + { + vie = vie_param; + running = true; + letters = ""; + word = chooseword(); + compteur = compteur_param; + discovered = new bool[word.Length]; + + SendText(English ? "Hangman v1.0 - By ORelio" : "Pendu v1.0 - Par ORelio"); + SendText(English ? ("Mysterious word: " + word_cached + " (lives : " + vie + ")") + : ("Mot mystère : " + word_cached + " (vie : " + vie + ")")); + SendText(English ? ("Try some letters ... :)") : ("Proposez une lettre ... :)")); + } + + private string chooseword() + { + if (System.IO.File.Exists(English ? "words.txt" : "mots.txt")) + { + string[] dico = System.IO.File.ReadAllLines(English ? "words.txt" : "mots.txt"); + return dico[new Random().Next(dico.Length)]; + } + else + { + LogToConsole(English ? "Cannot find words.txt !" : "Fichier mots.txt introuvable !"); + return English ? "WORDSAREMISSING" : "DICOMANQUANT"; + } + } + + private string[] getowners() + { + List owners = new List(); + owners.Add("CONSOLE"); + if (System.IO.File.Exists("bot-owners.txt")) + { + foreach (string s in System.IO.File.ReadAllLines("bot-owners.txt")) + { + owners.Add(s.ToUpper()); + } + } + else LogToConsole(English ? "Cannot find bot-owners.txt !" : "Fichier bot-owners.txt introuvable !"); + return owners.ToArray(); + } + + private string word_cached + { + get + { + string printed = ""; + for (int i = 0; i < word.Length; i++) + { + if (discovered[i]) + { + printed += word[i]; + } + else printed += '_'; + } + return printed; + } + } + + private bool winner + { + get + { + for (int i = 0; i < discovered.Length; i++) + { + if (!discovered[i]) + { + return false; + } + } + return true; + } + } + } + + /// + /// This bot make the console beep on some specified words. Useful to detect when someone is talking to you, for example. + /// + + public class Alerts : ChatBot + { + private string[] dictionnary = new string[0]; + private string[] excludelist = new string[0]; + + public override void Initialize() + { + if (System.IO.File.Exists("alerts.txt")) + { + dictionnary = System.IO.File.ReadAllLines("alerts.txt"); + + for (int i = 0; i < dictionnary.Length; i++) + { + dictionnary[i] = dictionnary[i].ToLower(); + } + } + else LogToConsole("Cannot find alerts.txt !"); + + if (System.IO.File.Exists("alerts-exclude.txt")) + { + excludelist = System.IO.File.ReadAllLines("alerts-exclude.txt"); + + for (int i = 0; i < excludelist.Length; i++) + { + excludelist[i] = excludelist[i].ToLower(); + } + } + else LogToConsole("Cannot find alerts-exclude.txt !"); + } + + public override void GetText(string text) + { + text = getVerbatim(text); + string comp = text.ToLower(); + foreach (string alert in dictionnary) + { + if (comp.Contains(alert)) + { + bool ok = true; + + foreach (string exclusion in excludelist) + { + if (comp.Contains(exclusion)) + { + ok = false; + break; + } + } + + if (ok) + { + Console.Beep(); //Text found ! + #region Displaying the text with the interesting part highlighted + + Console.BackgroundColor = ConsoleColor.DarkGray; + Console.ForegroundColor = ConsoleColor.White; + + //Will be used for text displaying + string[] temp = comp.Split(alert.Split(','), StringSplitOptions.RemoveEmptyEntries); + int p = 0; + + //Special case : alert in the beginning of the text + string test = ""; + for (int i = 0; i < alert.Length; i++) + { + test += comp[i]; + } + if (test == alert) + { + Console.BackgroundColor = ConsoleColor.Yellow; + Console.ForegroundColor = ConsoleColor.Red; + for (int i = 0; i < alert.Length; i++) + { + ConsoleIO.Write(text[p]); + p++; + } + } + + //Displaying the rest of the text + for (int i = 0; i < temp.Length; i++) + { + Console.BackgroundColor = ConsoleColor.DarkGray; + Console.ForegroundColor = ConsoleColor.White; + for (int j = 0; j < temp[i].Length; j++) + { + ConsoleIO.Write(text[p]); + p++; + } + Console.BackgroundColor = ConsoleColor.Yellow; + Console.ForegroundColor = ConsoleColor.Red; + try + { + for (int j = 0; j < alert.Length; j++) + { + ConsoleIO.Write(text[p]); + p++; + } + } + catch (IndexOutOfRangeException) { } + } + Console.BackgroundColor = ConsoleColor.Black; + Console.ForegroundColor = ConsoleColor.Gray; + ConsoleIO.Write('\n'); + + #endregion + + } + } + } + } + } + + /// + /// This bot saves the received messages in a text file. + /// + + public class ChatLog : ChatBot + { + public enum MessageFilter { AllText, AllMessages, OnlyChat, OnlyWhispers }; + private bool dateandtime; + private bool saveOther = true; + private bool saveChat = true; + private bool savePrivate = true; + private string logfile; + + /// + /// This bot saves the messages received in the specified file, with some filters and date/time tagging. + /// + /// The file to save the log in + /// The kind of messages to save + /// Add a date and time before each message + + public ChatLog(string file, MessageFilter filter, bool AddDateAndTime) + { + dateandtime = AddDateAndTime; + logfile = file; + switch (filter) + { + case MessageFilter.AllText: + saveOther = true; + savePrivate = true; + saveChat = true; + break; + case MessageFilter.AllMessages: + saveOther = false; + savePrivate = true; + saveChat = true; + break; + case MessageFilter.OnlyChat: + saveOther = false; + savePrivate = false; + saveChat = true; + break; + case MessageFilter.OnlyWhispers: + saveOther = false; + savePrivate = true; + saveChat = false; + break; + } + } + + public override void GetText(string text) + { + text = getVerbatim(text); + string sender = ""; + string message = ""; + + if (saveChat && isChatMessage(text, ref message, ref sender)) + { + save("Chat " + sender + ": " + message); + } + else if (savePrivate && isPrivateMessage(text, ref message, ref sender)) + { + save("Private " + sender + ": " + message); + } + else if (saveOther) + { + save("Other: " + text); + } + } + + private void save(string tosave) + { + if (dateandtime) + { + int day = DateTime.Now.Day, month = DateTime.Now.Month; + int hour = DateTime.Now.Hour, minute = DateTime.Now.Minute, second = DateTime.Now.Second; + + string D = day < 10 ? "0" + day : "" + day; + string M = month < 10 ? "0" + month : "" + day; + string Y = "" + DateTime.Now.Year; + + string h = hour < 10 ? "0" + hour : "" + hour; + string m = minute < 10 ? "0" + minute : "" + minute; + string s = second < 10 ? "0" + second : "" + second; + + tosave = "" + D + '-' + M + '-' + Y + ' ' + h + ':' + m + ':' + s + ' ' + tosave; + } + + System.IO.FileStream stream = new System.IO.FileStream(logfile, System.IO.FileMode.OpenOrCreate); + System.IO.StreamWriter writer = new System.IO.StreamWriter(stream); + stream.Seek(0, System.IO.SeekOrigin.End); + writer.WriteLine(tosave); + writer.Dispose(); + stream.Close(); + } + } + + /// + /// This bot automatically re-join the server if kick message contains predefined string (Server is restarting ...) + /// + + public class AutoRelog : ChatBot + { + private string[] dictionnary = new string[0]; + private int attempts; + private int delay; + + /// + /// This bot automatically re-join the server if kick message contains predefined string + /// + /// Delay before re-joining the server (in seconds) + /// Number of retries if connection fails (-1 = infinite) + + public AutoRelog(int DelayBeforeRelog, int retries) + { + attempts = retries; + if (attempts == -1) { attempts = int.MaxValue; } + McTcpClient.AttemptsLeft = attempts; + delay = DelayBeforeRelog; + if (delay < 1) { delay = 1; } + } + + public override void Initialize() + { + McTcpClient.AttemptsLeft = attempts; + if (System.IO.File.Exists("kickmessages.txt")) + { + dictionnary = System.IO.File.ReadAllLines("kickmessages.txt"); + + for (int i = 0; i < dictionnary.Length; i++) + { + dictionnary[i] = dictionnary[i].ToLower(); + } + } + else LogToConsole("Cannot find kickmessages.txt !"); + } + + public override bool OnDisconnect(DisconnectReason reason, string message) + { + message = getVerbatim(message); + string comp = message.ToLower(); + foreach (string msg in dictionnary) + { + if (comp.Contains(msg)) + { + LogToConsole("Waiting " + delay + " seconds before reconnecting..."); + System.Threading.Thread.Sleep(delay * 1000); + McTcpClient.AttemptsLeft = attempts; + ReconnectToTheServer(); + return true; + } + } + return false; + } + } + + /// + /// Automatically send login command on servers usign the xAuth plugin + /// + + public class xAuth : ChatBot + { + private string password; + private int countdown = 50; + + public xAuth(string pass) + { + password = pass; + } + + public override void Update() + { + countdown--; + if (countdown == 0) + { + SendText("/login " + password); + UnloadBot(); //This bot is no more needed. + } + } + } + } +} diff --git a/MinecraftClient/BouncyCastle.Crypto.dll b/MinecraftClient/BouncyCastle.Crypto.dll new file mode 100644 index 00000000..8531f7fc Binary files /dev/null and b/MinecraftClient/BouncyCastle.Crypto.dll differ diff --git a/MinecraftClient/ChatParser.cs b/MinecraftClient/ChatParser.cs new file mode 100644 index 00000000..b0d07e3f --- /dev/null +++ b/MinecraftClient/ChatParser.cs @@ -0,0 +1,297 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MinecraftClient +{ + /// + /// This class parses JSON chat data from MC 1.6+ and returns the appropriate string to be printed. + /// + + static class ChatParser + { + /// + /// The main function to convert text from MC 1.6+ JSON to MC 1.5.2 formatted text + /// + /// JSON serialized text + /// Returns the translated text + + public static string ParseText(string json) + { + int cursorpos = 0; + JSONData jsonData = String2Data(json, ref cursorpos); + return JSONData2String(jsonData).Replace("u0027", "'"); + } + + /// + /// An internal class to store unserialized JSON data + /// The data can be an object, an array or a string + /// + + private class JSONData + { + public enum DataType { Object, Array, String }; + private DataType type; + public DataType Type { get { return type; } } + public Dictionary Properties; + public List DataArray; + public string StringValue; + public JSONData(DataType datatype) + { + type = datatype; + Properties = new Dictionary(); + DataArray = new List(); + StringValue = String.Empty; + } + } + + /// + /// Get the classic color tag corresponding to a color name + /// + /// Color Name + /// Color code + + private static string color2tag(string colorname) + { + switch(colorname.ToLower()) + { + case "black": return "§0"; + case "dark_blue": return "§1"; + case "dark_green" : return "§2"; + case "dark_cyan": return "§3"; + case "dark_cyanred": return "§4"; + case "dark_magenta": return "§5"; + case "dark_yellow": return "§6"; + case "gray": return "§7"; + case "dark_gray": return "§8"; + case "blue": return "§9"; + case "green": return "§a"; + case "cyan": return "§b"; + case "red": return "§c"; + case "magenta": return "§d"; + case "yellow": return "§e"; + case "white": return "§f"; + default: return ""; + } + } + + /// + /// Rules for text translation + /// + + private static bool init = false; + private static Dictionary TranslationRules = new Dictionary(); + public static void InitTranslations() { if (!init) { InitRules(); init = true; } } + private static void InitRules() + { + //Small default dictionnary of translation rules + TranslationRules["chat.type.admin"] = "[%s: %s]"; + TranslationRules["chat.type.announcement"] = "§d[%s] %s"; + TranslationRules["chat.type.emote"] = " * %s %s"; + TranslationRules["chat.type.text"] = "<%s> %s"; + TranslationRules["multiplayer.player.joined"] = "§e%s joined the game."; + TranslationRules["multiplayer.player.left"] = "§e%s left the game."; + TranslationRules["commands.message.display.incoming"] = "§7%s whispers to you: %s"; + TranslationRules["commands.message.display.outgoing"] = "§7You whisper to %s: %s"; + + //Load an external dictionnary of translation rules + if (System.IO.File.Exists("translations.lang")) + { + string[] translations = System.IO.File.ReadAllLines("translations.lang"); + foreach (string line in translations) + { + if (line.Length > 0) + { + string[] splitted = line.Split('='); + if (splitted.Length == 2) + { + TranslationRules[splitted[0]] = splitted[1]; + } + } + } + + Console.ForegroundColor = ConsoleColor.DarkGray; + ConsoleIO.WriteLine("Translations file loaded."); + Console.ForegroundColor = ConsoleColor.Gray; + } + else //No external dictionnary found. + { + Console.ForegroundColor = ConsoleColor.DarkGray; + ConsoleIO.WriteLine("MC 1.6+ warning: Translations file \"translations.lang\" not found." + + "\nYou can pick a translation file from .minecraft\\assets\\lang\\" + + "\nCopy to the same folder as MinecraftClient & rename to \"translations.lang\"" + + "\nSome messages won't be properly printed without this file."); + Console.ForegroundColor = ConsoleColor.Gray; + } + } + + /// + /// Format text using a specific formatting rule. + /// Example : * %s %s + ["ORelio", "is doing something"] = * ORelio is doing something + /// + /// Name of the rule, chosen by the server + /// Data to be used in the rule + /// Returns the formatted text according to the given data + + private static string TranslateString(string rulename, List using_data) + { + if (!init) { InitRules(); init = true; } + if (TranslationRules.ContainsKey(rulename)) + { + string[] syntax = TranslationRules[rulename].Split(new string[2] { "%s", "%d" }, StringSplitOptions.None); + while (using_data.Count < syntax.Length - 1) { using_data.Add(""); } + string[] using_array = using_data.ToArray(); + string translated = ""; + for (int i = 0; i < syntax.Length - 1; i++) + { + translated += syntax[i]; + translated += using_array[i]; + } + translated += syntax[syntax.Length - 1]; + return translated; + } + else return "[" + rulename + "] " + String.Join(" ", using_data); + } + + /// + /// Parse a JSON string to build a JSON object + /// + /// String to parse + /// Cursor start (set to 0 for function init) + /// + + private static JSONData String2Data(string toparse, ref int cursorpos) + { + try + { + JSONData data; + switch (toparse[cursorpos]) + { + //Object + case '{': + data = new JSONData(JSONData.DataType.Object); + cursorpos++; + while (toparse[cursorpos] != '}') + { + if (toparse[cursorpos] == '"') + { + JSONData propertyname = String2Data(toparse, ref cursorpos); + if (toparse[cursorpos] == ':') { cursorpos++; } else { /* parse error ? */ } + JSONData propertyData = String2Data(toparse, ref cursorpos); + data.Properties[propertyname.StringValue] = propertyData; + } + else cursorpos++; + } + cursorpos++; + break; + + //Array + case '[': + data = new JSONData(JSONData.DataType.Array); + cursorpos++; + while (toparse[cursorpos] != ']') + { + if (toparse[cursorpos] == ',') { cursorpos++; } + JSONData arrayItem = String2Data(toparse, ref cursorpos); + data.DataArray.Add(arrayItem); + } + cursorpos++; + break; + + //String + case '"': + data = new JSONData(JSONData.DataType.String); + cursorpos++; + while (toparse[cursorpos] != '"') + { + if (toparse[cursorpos] == '\\') { cursorpos++; } + data.StringValue += toparse[cursorpos]; + cursorpos++; + } + cursorpos++; + break; + + //Boolean : true + case 't': + data = new JSONData(JSONData.DataType.String); + cursorpos++; + if (toparse[cursorpos] == 'r') { cursorpos++; } + if (toparse[cursorpos] == 'u') { cursorpos++; } + if (toparse[cursorpos] == 'e') { cursorpos++; data.StringValue = "true"; } + break; + + //Boolean : false + case 'f': + data = new JSONData(JSONData.DataType.String); + cursorpos++; + if (toparse[cursorpos] == 'a') { cursorpos++; } + if (toparse[cursorpos] == 'l') { cursorpos++; } + if (toparse[cursorpos] == 's') { cursorpos++; } + if (toparse[cursorpos] == 'e') { cursorpos++; data.StringValue = "false"; } + break; + + //Unknown data + default: + cursorpos++; + return String2Data(toparse, ref cursorpos); + } + return data; + } + catch (IndexOutOfRangeException) + { + return new JSONData(JSONData.DataType.String); + } + } + + /// + /// Use a JSON Object to build the corresponding string + /// + /// JSON object to convert + /// returns the Minecraft-formatted string + + private static string JSONData2String(JSONData data) + { + string colorcode = ""; + switch (data.Type) + { + case JSONData.DataType.Object: + if (data.Properties.ContainsKey("color")) + { + colorcode = color2tag(JSONData2String(data.Properties["color"])); + } + if (data.Properties.ContainsKey("text")) + { + return colorcode + JSONData2String(data.Properties["text"]) + colorcode; + } + else if (data.Properties.ContainsKey("translate")) + { + List using_data = new List(); + if (data.Properties.ContainsKey("using")) + { + JSONData[] array = data.Properties["using"].DataArray.ToArray(); + for (int i = 0; i < array.Length; i++) + { + using_data.Add(JSONData2String(array[i])); + } + } + return colorcode + TranslateString(JSONData2String(data.Properties["translate"]), using_data) + colorcode; + } + else return ""; + + case JSONData.DataType.Array: + string result = ""; + foreach (JSONData item in data.DataArray) + { + result += JSONData2String(item); + } + return result; + + case JSONData.DataType.String: + return data.StringValue; + } + + return ""; + } + } +} diff --git a/MinecraftClient/ConsoleIO.cs b/MinecraftClient/ConsoleIO.cs new file mode 100644 index 00000000..d2a181f5 --- /dev/null +++ b/MinecraftClient/ConsoleIO.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MinecraftClient +{ + /// + /// Allows simultaneous console input and output without breaking user input + /// (Without having this annoying behaviour : User inp[Some Console output]ut) + /// + + public static class ConsoleIO + { + public static void Reset() { if (reading) { reading = false; Console.Write("\b \b"); } } + private static LinkedList previous = new LinkedList(); + private static string buffer = ""; + private static string buffer2 = ""; + private static bool consolelock = false; + private static bool reading = false; + private static bool writing = false; + + #region Read User Input + public static string ReadLine() + { + ConsoleKeyInfo k = new ConsoleKeyInfo(); + Console.Write('>'); + reading = true; + buffer = ""; + buffer2 = ""; + + while (k.Key != ConsoleKey.Enter) + { + k = Console.ReadKey(true); + while (writing) { } + consolelock = true; + switch (k.Key) + { + case ConsoleKey.Escape: + ClearLineAndBuffer(); + break; + case ConsoleKey.Backspace: + RemoveOneChar(); + break; + case ConsoleKey.Enter: + Console.Write('\n'); + break; + case ConsoleKey.LeftArrow: + GoLeft(); + break; + case ConsoleKey.RightArrow: + GoRight(); + break; + case ConsoleKey.Home: + while (buffer.Length > 0) { GoLeft(); } + break; + case ConsoleKey.End: + while (buffer2.Length > 0) { GoRight(); } + break; + case ConsoleKey.Delete: + if (buffer2.Length > 0) + { + GoRight(); + RemoveOneChar(); + } + break; + case ConsoleKey.Oem6: + break; + case ConsoleKey.DownArrow: + if (previous.Count > 0) + { + ClearLineAndBuffer(); + buffer = previous.First.Value; + previous.AddLast(buffer); + previous.RemoveFirst(); + Console.Write(buffer); + } + break; + case ConsoleKey.UpArrow: + if (previous.Count > 0) + { + ClearLineAndBuffer(); + buffer = previous.Last.Value; + previous.AddFirst(buffer); + previous.RemoveLast(); + Console.Write(buffer); + } + break; + default: + AddChar(k.KeyChar); + break; + } + consolelock = false; + } + while (writing) { } + reading = false; + previous.AddLast(buffer + buffer2); + return buffer + buffer2; + } + #endregion + + #region Console Output + public static void Write(string text) + { + while (consolelock) { } + writing = true; + if (reading) + { + ConsoleColor fore = Console.ForegroundColor; + ConsoleColor back = Console.BackgroundColor; + string buf = buffer; + string buf2 = buffer2; + ClearLineAndBuffer(); + if (Console.CursorLeft == 0) + { + Console.CursorLeft = Console.BufferWidth - 1; + Console.CursorTop--; + Console.Write(' '); + Console.CursorLeft = Console.BufferWidth - 1; + Console.CursorTop--; + } + else Console.Write("\b \b"); + Console.Write(text); + Console.ForegroundColor = ConsoleColor.Gray; + Console.BackgroundColor = ConsoleColor.Black; + buffer = buf; + buffer2 = buf2; + Console.Write(">" + buffer); + if (buffer2.Length > 0) + { + Console.Write(buffer2 + " \b"); + for (int i = 0; i < buffer2.Length; i++) { GoBack(); } + } + Console.ForegroundColor = fore; + Console.BackgroundColor = back; + } + else Console.Write(text); + writing = false; + } + + public static void WriteLine(string line) + { + Write(line + '\n'); + } + + public static void Write(char c) + { + Write("" + c); + } + #endregion + + #region subfunctions + private static void ClearLineAndBuffer() + { + while (buffer2.Length > 0) { GoRight(); } + while (buffer.Length > 0) { RemoveOneChar(); } + } + private static void RemoveOneChar() + { + if (buffer.Length > 0) + { + if (Console.CursorLeft == 0) + { + Console.CursorLeft = Console.BufferWidth - 1; + Console.CursorTop--; + Console.Write(' '); + Console.CursorLeft = Console.BufferWidth - 1; + Console.CursorTop--; + } + else Console.Write("\b \b"); + buffer = buffer.Substring(0, buffer.Length - 1); + + if (buffer2.Length > 0) + { + Console.Write(buffer2 + " \b"); + for (int i = 0; i < buffer2.Length; i++) { GoBack(); } + } + } + } + private static void GoBack() + { + if (buffer.Length > 0) + { + if (Console.CursorLeft == 0) + { + Console.CursorLeft = Console.BufferWidth - 1; + Console.CursorTop--; + } + else Console.Write('\b'); + } + } + private static void GoLeft() + { + if (buffer.Length > 0) + { + buffer2 = "" + buffer[buffer.Length - 1] + buffer2; + buffer = buffer.Substring(0, buffer.Length - 1); + Console.Write('\b'); + } + } + private static void GoRight() + { + if (buffer2.Length > 0) + { + buffer = buffer + buffer2[0]; + Console.Write(buffer2[0]); + buffer2 = buffer2.Substring(1); + } + } + private static void AddChar(char c) + { + Console.Write(c); + buffer += c; + Console.Write(buffer2); + for (int i = 0; i < buffer2.Length; i++) { GoBack(); } + } + #endregion + } +} diff --git a/MinecraftClient/Crypto.cs b/MinecraftClient/Crypto.cs new file mode 100644 index 00000000..91b78953 --- /dev/null +++ b/MinecraftClient/Crypto.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Security.Cryptography; +using java.security; +using java.security.spec; +using javax.crypto; +using javax.crypto.spec; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; + +namespace MinecraftClient +{ + /// + /// Cryptographic functions ported from Minecraft Source Code (Java). Decompiled with MCP. Copy, paste, little adjustements. + /// + + public class Crypto + { + public static PublicKey GenerateRSAPublicKey(byte[] key) + { + X509EncodedKeySpec localX509EncodedKeySpec = new X509EncodedKeySpec(key); + KeyFactory localKeyFactory = KeyFactory.getInstance("RSA"); + return localKeyFactory.generatePublic(localX509EncodedKeySpec); + } + + public static SecretKey GenerateAESPrivateKey() + { + CipherKeyGenerator var0 = new CipherKeyGenerator(); + var0.Init(new KeyGenerationParameters(new Org.BouncyCastle.Security.SecureRandom(), 128)); + return new SecretKeySpec(var0.GenerateKey(), "AES"); + } + + public static byte[] getServerHash(String toencode, PublicKey par1PublicKey, SecretKey par2SecretKey) + { + return digest("SHA-1", new byte[][] { Encoding.GetEncoding("iso-8859-1").GetBytes(toencode), par2SecretKey.getEncoded(), par1PublicKey.getEncoded() }); + } + + public static byte[] Encrypt(Key par0Key, byte[] par1ArrayOfByte) + { + return func_75885_a(1, par0Key, par1ArrayOfByte); + } + + private static byte[] digest(String par0Str, byte[][] par1ArrayOfByte) + { + MessageDigest var2 = MessageDigest.getInstance(par0Str); + byte[][] var3 = par1ArrayOfByte; + int var4 = par1ArrayOfByte.Length; + + for (int var5 = 0; var5 < var4; ++var5) + { + byte[] var6 = var3[var5]; + var2.update(var6); + } + + return var2.digest(); + } + private static byte[] func_75885_a(int par0, Key par1Key, byte[] par2ArrayOfByte) + { + try + { + return cypherencrypt(par0, par1Key.getAlgorithm(), par1Key).doFinal(par2ArrayOfByte); + } + catch (IllegalBlockSizeException var4) + { + var4.printStackTrace(); + } + catch (BadPaddingException var5) + { + var5.printStackTrace(); + } + + Console.Error.WriteLine("Cipher data failed!"); + return null; + } + private static Cipher cypherencrypt(int par0, String par1Str, Key par2Key) + { + try + { + Cipher var3 = Cipher.getInstance(par1Str); + var3.init(par0, par2Key); + return var3; + } + catch (InvalidKeyException var4) + { + var4.printStackTrace(); + } + catch (NoSuchAlgorithmException var5) + { + var5.printStackTrace(); + } + catch (NoSuchPaddingException var6) + { + var6.printStackTrace(); + } + + Console.Error.WriteLine("Cipher creation failed!"); + return null; + } + + public static AesStream SwitchToAesMode(System.IO.Stream stream, Key key) + { + return new AesStream(stream, key.getEncoded()); + } + + /// + /// An encrypted stream using AES + /// + + public class AesStream : System.IO.Stream + { + CryptoStream enc; + CryptoStream dec; + public AesStream(System.IO.Stream stream, byte[] key) + { + BaseStream = stream; + enc = new CryptoStream(stream, GenerateAES(key).CreateEncryptor(), CryptoStreamMode.Write); + dec = new CryptoStream(stream, GenerateAES(key).CreateDecryptor(), CryptoStreamMode.Read); + } + public System.IO.Stream BaseStream { get; set; } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return true; } + } + + public override void Flush() + { + BaseStream.Flush(); + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get + { + throw new NotSupportedException(); + } + set + { + throw new NotSupportedException(); + } + } + + public override int ReadByte() + { + return dec.ReadByte(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return dec.Read(buffer, offset, count); + } + + public override long Seek(long offset, System.IO.SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void WriteByte(byte b) + { + enc.WriteByte(b); + } + + public override void Write(byte[] buffer, int offset, int count) + { + enc.Write(buffer, offset, count); + } + + private RijndaelManaged GenerateAES(byte[] key) + { + RijndaelManaged cipher = new RijndaelManaged(); + cipher.Mode = CipherMode.CFB; + cipher.Padding = PaddingMode.None; + cipher.KeySize = 128; + cipher.FeedbackSize = 8; + cipher.Key = key; + cipher.IV = key; + return cipher; + } + } + } +} diff --git a/MinecraftClient/IKVM.OpenJDK.Core.dll b/MinecraftClient/IKVM.OpenJDK.Core.dll new file mode 100644 index 00000000..328ab6d4 Binary files /dev/null and b/MinecraftClient/IKVM.OpenJDK.Core.dll differ diff --git a/MinecraftClient/IKVM.OpenJDK.Security.dll b/MinecraftClient/IKVM.OpenJDK.Security.dll new file mode 100644 index 00000000..0e87f2de Binary files /dev/null and b/MinecraftClient/IKVM.OpenJDK.Security.dll differ diff --git a/MinecraftClient/IKVM.OpenJDK.Util.dll b/MinecraftClient/IKVM.OpenJDK.Util.dll new file mode 100644 index 00000000..8113cc39 Binary files /dev/null and b/MinecraftClient/IKVM.OpenJDK.Util.dll differ diff --git a/MinecraftClient/IKVM.Runtime.dll b/MinecraftClient/IKVM.Runtime.dll new file mode 100644 index 00000000..d69e3f53 Binary files /dev/null and b/MinecraftClient/IKVM.Runtime.dll differ diff --git a/MinecraftClient/McTcpClient.cs b/MinecraftClient/McTcpClient.cs new file mode 100644 index 00000000..e31f280e --- /dev/null +++ b/MinecraftClient/McTcpClient.cs @@ -0,0 +1,243 @@ +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; + +namespace MinecraftClient +{ + /// + /// The main client class, used to connect to a Minecraft server. + /// It allows message sending and text receiving. + /// + + class McTcpClient + { + public static int AttemptsLeft = 0; + + string host; + int port; + string username; + string text; + Thread t_updater; + Thread t_sender; + TcpClient client; + MinecraftCom handler; + + /// + /// 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 (serveradress or serveradress:port) + + public McTcpClient(string username, string sessionID, string server_port, MinecraftCom handler) + { + StartClient(username, sessionID, server_port, false, handler, ""); + } + + /// + /// Starts the main chat client in single command sending mode, wich will login to the server using the MinecraftCom class, send the command and close. + /// + /// The chosen username of a premium Minecraft Account + /// A valid sessionID obtained with MinecraftCom.GetLogin() + /// The server IP (serveradress or serveradress:port) + /// The text or command to send. + + public McTcpClient(string username, string sessionID, string server_port, MinecraftCom handler, string command) + { + StartClient(username, sessionID, server_port, true, handler, 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 (serveradress or serveradress:port)/param> + /// 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 sessionID, string server_port, bool singlecommand, MinecraftCom handler, string command) + { + this.handler = handler; + username = user; + string[] sip = server_port.Split(':'); + host = sip[0]; + if (sip.Length == 1) + { + port = 25565; + } + else + { + try + { + port = Convert.ToInt32(sip[1]); + } + catch (FormatException) { port = 25565; } + } + + try + { + Console.WriteLine("Connecting..."); + client = new TcpClient(host, port); + client.ReceiveBufferSize = 1024 * 1024; + handler.setClient(client); + byte[] token = new byte[1]; string serverID = ""; + if (handler.Handshake(user, sessionID, ref serverID, ref token, host, port)) + { + Console.WriteLine("Logging in..."); + + if (handler.FinalizeLogin()) + { + //Single command sending + if (singlecommand) + { + handler.SendChatMessage(command); + Console.Write("Command "); + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.Write(command); + Console.ForegroundColor = ConsoleColor.Gray; + Console.WriteLine(" sent."); + Thread.Sleep(5000); + handler.Disconnect("disconnect.quitting"); + Thread.Sleep(1000); + } + else + { + Console.WriteLine("Server was successfuly joined.\nType '/quit' to leave the server."); + + //Command sending thread, allowing user input + t_sender = new Thread(new ThreadStart(StartTalk)); + t_sender.Name = "CommandSender"; + t_sender.Start(); + + //Data receiving thread, allowing text receiving + t_updater = new Thread(new ThreadStart(Updater)); + t_updater.Name = "PacketHandler"; + t_updater.Start(); + } + } + else + { + Console.WriteLine("Login failed."); + if (!singlecommand) { Program.ReadLineReconnect(); } + } + } + else + { + Console.WriteLine("Invalid session ID."); + if (!singlecommand) { Program.ReadLineReconnect(); } + } + } + catch (SocketException) + { + Console.WriteLine("Failed to connect to this IP."); + if (AttemptsLeft > 0) + { + ChatBot.LogToConsole("Waiting 5 seconds (" + AttemptsLeft + " attempts left)..."); + Thread.Sleep(5000); AttemptsLeft--; Program.Restart(); + } + else if (!singlecommand){ Console.ReadLine(); } + } + } + + /// + /// Allows the user to send chat messages, commands, and to leave the server. + /// Will be automatically called on a separate Thread by StartClient() + /// + + private void StartTalk() + { + try + { + while (client.Client.Connected) + { + text = ConsoleIO.ReadLine(); + if (text == "/quit" || text == "/reco" || text == "/reconnect") { break; } + while (text.Length > 0 && text[0] == ' ') { text = text.Substring(1); } + if (text != "") + { + //Message is too long + if (text.Length > 100) + { + if (text[0] == '/') + { + //Send the first 100 chars of the command + text = text.Substring(0, 100); + handler.SendChatMessage(text); + } + else + { + //Send the message splitted in sereval messages + while (text.Length > 100) + { + handler.SendChatMessage(text.Substring(0, 100)); + text = text.Substring(100, text.Length - 100); + } + handler.SendChatMessage(text); + } + } + else handler.SendChatMessage(text); + } + } + + if (text == "/quit") + { + ConsoleIO.WriteLine("You have left the server."); + Disconnect(); + } + + else if (text == "/reco" || text == "/reconnect") + { + ConsoleIO.WriteLine("You have left the server."); + Program.Restart(); + } + } + catch (IOException) { } + } + + /// + /// Receive the data (including chat messages) from the server, and keep the connection alive. + /// Will be automatically called on a separate Thread by StartClient() + /// + + private void Updater() + { + try + { + //handler.DebugDump(); + do + { + Thread.Sleep(100); + } while (handler.Update()); + } + catch (IOException) { } + catch (SocketException) { } + catch (ObjectDisposedException) { } + + if (!handler.HasBeenKicked) + { + ConsoleIO.WriteLine("Connection has been lost."); + if (!handler.OnConnectionLost() && !Program.ReadLineReconnect()) { t_sender.Abort(); } + } + else if (Program.ReadLineReconnect()) { t_sender.Abort(); } + } + + /// + /// Disconnect the client from the server + /// + + public void Disconnect() + { + handler.Disconnect("disconnect.quitting"); + Thread.Sleep(1000); + if (t_updater != null) { t_updater.Abort(); } + if (t_sender != null) { t_sender.Abort(); } + if (client != null) { client.Close(); } + } + } +} diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj new file mode 100644 index 00000000..b1b033e6 --- /dev/null +++ b/MinecraftClient/MinecraftClient.csproj @@ -0,0 +1,127 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {1E2FACE4-F5CA-4323-9641-740C6A551770} + Exe + Properties + MinecraftClient + MinecraftClient + v4.0 + Client + 512 + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + x86 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + false + + + + False + .\BouncyCastle.Crypto.dll + + + False + .\IKVM.OpenJDK.Core.dll + + + False + .\IKVM.OpenJDK.Security.dll + + + False + .\IKVM.OpenJDK.Util.dll + + + + + + + + + + + + + + + + + + + + + + False + Microsoft .NET Framework 4 Client Profile %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + false + + + False + Windows Installer 3.1 + true + + + + + + + + + + + + \ No newline at end of file diff --git a/MinecraftClient/MinecraftClient.csproj.user b/MinecraftClient/MinecraftClient.csproj.user new file mode 100644 index 00000000..1aaa11ad --- /dev/null +++ b/MinecraftClient/MinecraftClient.csproj.user @@ -0,0 +1,21 @@ + + + + + + + + publish\ + + + + + + en-US + false + + + + + + \ No newline at end of file diff --git a/MinecraftClient/MinecraftCom.cs b/MinecraftClient/MinecraftCom.cs new file mode 100644 index 00000000..9508f280 --- /dev/null +++ b/MinecraftClient/MinecraftCom.cs @@ -0,0 +1,825 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Net; +using System.Net.Sockets; + +namespace MinecraftClient +{ + /// + /// The class containing all the core functions needed to communicate with a Minecraft server. + /// + + public class MinecraftCom + { + #region Login to Minecraft.net, Obtaining a session ID + + public enum LoginResult { Error, Success, WrongPassword, Blocked, AccountMigrated, NotPremium }; + + /// + /// Allows to login to a premium Minecraft account, and retrieve the session ID. + /// + /// Login + /// Password + /// Will contain the data returned by Minecraft.net, if the login is successful : Version:UpdateTicket:Username:SessionID + /// Returns the status of the login (Success, Failure, etc.) + + public static LoginResult GetLogin(string user, string pass, ref string outdata) + { + try + { + Console.ForegroundColor = ConsoleColor.DarkGray; + WebClient wClient = new WebClient(); + Console.WriteLine("https://login.minecraft.net/?user=" + user + "&password=<******>&version=13"); + string result = wClient.DownloadString("https://login.minecraft.net/?user=" + user + "&password=" + pass + "&version=13"); + outdata = result; + Console.WriteLine(result); + Console.ForegroundColor = ConsoleColor.Gray; + if (result == "Bad login") { return LoginResult.WrongPassword; } + if (result == "User not premium") { return LoginResult.NotPremium; } + if (result == "Too many failed logins") { return LoginResult.Blocked; } + if (result == "Account migrated, use e-mail as username.") { return LoginResult.AccountMigrated; } + else return LoginResult.Success; + } + catch (WebException) { return LoginResult.Error; } + } + + #endregion + + #region Keep-Alive for a Minecraft.net session, should be called every 5 minutes (currently unused) + + /// + /// The session ID will expire within 5 minutes unless this function is called every 5 minutes + /// + /// Username + /// Session ID to keep alive + + public static void SessionKeepAlive(string user, string sessionID) + { + new WebClient().DownloadString("https://login.minecraft.net/session?name=" + user + "&session=" + sessionID); + } + + #endregion + + #region Session checking when joining a server in online mode + + /// + /// This method allows to join an online-mode server. + /// It Should be called between the handshake and the login attempt. + /// + /// Username + /// A valid session ID for this username + /// Hash returned by the server during the handshake + /// Returns true if the check was successful + + public static bool SessionCheck(string user, string sessionID, string hash) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + WebClient client = new WebClient(); + Console.Write("http://session.minecraft.net/game/joinserver.jsp?user=" + user + "&sessionId=" + sessionID + "&serverId=" + hash + " ... "); + string result = client.DownloadString("http://session.minecraft.net/game/joinserver.jsp?user=" + user + "&sessionId=" + sessionID + "&serverId=" + hash); + Console.WriteLine(result); + Console.ForegroundColor = ConsoleColor.Gray; + return (result == "OK"); + } + + #endregion + + #region Server-side session checking (programmed for testing purposes) + + /// + /// Reproduces the username checking done by an online-mode server during the login process. + /// + /// Username + /// Hash sent by the server during the handshake + /// Returns true if the user is allowed to join the server + + public static bool ServerSessionCheck(string user, string hash) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + WebClient client = new WebClient(); + ConsoleIO.WriteLine("http://session.minecraft.net/game/checkserver.jsp?user=" + user + "&serverId=" + hash); + string result = client.DownloadString("http://session.minecraft.net/game/checkserver.jsp?user=" + user + "&serverId=" + hash); + ConsoleIO.WriteLine(result); + Console.ForegroundColor = ConsoleColor.Gray; + return (result == "YES"); + } + + #endregion + + TcpClient c = new TcpClient(); + Crypto.AesStream s; + + public bool HasBeenKicked { get { return connectionlost; } } + bool connectionlost = false; + bool encrypted = false; + byte protocolversion; + + public bool Update() + { + for (int i = 0; i < bots.Count; i++) { bots[i].Update(); } + if (c.Client == null || !c.Connected) { return false; } + byte id = 0; + + try + { + while (c.Client.Available > 0) + { + id = readNextByte(); + ProcessResult result = processPacket(id); + + //Debug : Print packet IDs that are beign processed. Green = OK, Red = Unknown packet + //If the client gets out of sync, check the last green packet processing code. + //if (result == ProcessResult.OK) { printstring("§a0x" + id.ToString("X"), false); } + //else { printstring("§c0x" + id.ToString("X"), false); } + + if (result == ProcessResult.ConnectionLost) + { + return false; + } + } + } + catch (SocketException) { return false; } + return true; + } + public void DebugDump() + { + byte[] cache = new byte[128000]; + Receive(cache, 0, 128000, SocketFlags.None); + string dump = BitConverter.ToString(cache); + System.IO.File.WriteAllText("debug.txt", dump); + System.Diagnostics.Process.Start("debug.txt"); + } + public bool OnConnectionLost() + { + if (!connectionlost) + { + connectionlost = true; + for (int i = 0; i < bots.Count; i++) + { + if (bots[i].OnDisconnect(ChatBot.DisconnectReason.ConnectionLost, "Connection has been lost.")) + { + return true; //The client is about to restart + } + } + } + return false; + } + + private enum ProcessResult { OK, ConnectionLost, UnknownPacket } + private ProcessResult processPacket(int id) + { + int nbr = 0; + switch (id) + { + case 0x00: byte[] keepalive = new byte[5] { 0, 0, 0, 0, 0 }; + Receive(keepalive, 1, 4, SocketFlags.None); + Send(keepalive); break; + case 0x01: readData(4); readNextString(); readData(5); break; + case 0x02: readData(1); readNextString(); readNextString(); readData(4); break; + case 0x03: + string message = readNextString(); + if (protocolversion >= 72) + { + //printstring("§8" + message, false); //Debug + message = ChatParser.ParseText(message); + printstring(message, false); + } + else printstring(message, false); + for (int i = 0; i < bots.Count; i++) { bots[i].GetText(message); } break; + case 0x04: readData(16); break; + case 0x05: readData(6); readNextItemSlot(); break; + case 0x06: readData(12); break; + case 0x07: readData(9); break; + case 0x08: if (protocolversion >= 72) { readData(10); } else readData(8); break; + case 0x09: readData(8); readNextString(); break; + case 0x0A: readData(1); break; + case 0x0B: readData(33); break; + case 0x0C: readData(9); break; + case 0x0D: readData(41); break; + case 0x0E: readData(11); break; + case 0x0F: readData(10); readNextItemSlot(); readData(3); break; + case 0x10: readData(2); break; + case 0x11: readData(14); break; + case 0x12: readData(5); break; + case 0x13: if (protocolversion >= 72) { readData(9); } else readData(5); break; + case 0x14: readData(4); readNextString(); readData(16); readNextEntityMetaData(); break; + case 0x16: readData(8); break; + case 0x17: readData(19); readNextObjectData(); break; + case 0x18: readData(26); readNextEntityMetaData(); break; + case 0x19: readData(4); readNextString(); readData(16); break; + case 0x1A: readData(18); break; + case 0x1B: if (protocolversion >= 72) { readData(10); } break; + case 0x1C: readData(10); break; + case 0x1D: nbr = (int)readNextByte(); readData(nbr * 4); break; + case 0x1E: readData(4); break; + case 0x1F: readData(7); break; + case 0x20: readData(6); break; + case 0x21: readData(9); break; + case 0x22: readData(18); break; + case 0x23: readData(5); break; + case 0x26: readData(5); break; + case 0x27: if (protocolversion >= 72) { readData(9); } else readData(8); break; + case 0x28: readData(4); readNextEntityMetaData(); break; + case 0x29: readData(8); break; + case 0x2A: readData(5); break; + case 0x2B: readData(8); break; + case 0x2C: if (protocolversion >= 72) { readNextEntityProperties(protocolversion); } break; + case 0x33: readData(13); nbr = readNextInt(); readData(nbr); break; + case 0x34: readData(10); nbr = readNextInt(); readData(nbr); break; + case 0x35: readData(12); break; + case 0x36: readData(14); break; + case 0x37: readData(17); break; + case 0x38: readNextChunkBulkData(); break; + case 0x3C: readData(28); nbr = readNextInt(); readData(3 * nbr); readData(12); break; + case 0x3D: readData(18); break; + case 0x3E: readNextString(); readData(17); break; + case 0x3F: if (protocolversion > 51) { readNextString(); readData(32); } break; + case 0x46: readData(2); break; + case 0x47: readData(17); break; + case 0x64: readNextWindowData(protocolversion); break; + case 0x65: readData(1); break; + case 0x66: readData(7); readNextItemSlot(); break; + case 0x67: readData(3); readNextItemSlot(); break; + case 0x68: readData(1); for (nbr = readNextShort(); nbr > 0; nbr--) { readNextItemSlot(); } break; + case 0x69: readData(5); break; + case 0x6A: readData(4); break; + case 0x6B: readData(2); readNextItemSlot(); break; + case 0x6C: readData(2); break; + case 0x82: readData(10); readNextString(); readNextString(); readNextString(); readNextString(); break; + case 0x83: readData(4); nbr = readNextShort(); readData(nbr); break; + case 0x84: readData(11); nbr = readNextShort(); if (nbr > 0) { readData(nbr); } break; + case 0x85: if (protocolversion >= 74) { readData(13); } break; + case 0xC8: if (protocolversion >= 72) { readData(8); } else readData(5); break; + case 0xC9: readNextString(); readData(3); break; + case 0xCA: if (protocolversion >= 72) { readData(9); } else readData(3); break; + case 0xCB: readNextString(); break; + case 0xCC: readNextString(); readData(4); break; + case 0xCD: readData(1); break; + case 0xCE: if (protocolversion > 51) { readNextString(); readNextString(); readData(1); } break; + 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 0xFF: string reason = readNextString(); + ConsoleIO.WriteLine("Disconnected by Server :"); printstring(reason, true); connectionlost = true; + for (int i = 0; i < bots.Count; i++) { bots[i].OnDisconnect(ChatBot.DisconnectReason.InGameKick, reason); } return ProcessResult.ConnectionLost; + default: return ProcessResult.UnknownPacket; //unknown packet! + } + return ProcessResult.OK; //packet has been successfully skipped + } + private void readData(int offset) + { + if (offset > 0) + { + try + { + byte[] cache = new byte[offset]; + Receive(cache, 0, offset, SocketFlags.None); + } + catch (OutOfMemoryException) { } + } + } + private string readNextString() + { + short lenght = readNextShort(); + if (lenght > 0) + { + byte[] cache = new byte[lenght * 2]; + Receive(cache, 0, lenght * 2, SocketFlags.None); + string result = ByteArrayToString(cache); + return result; + } + else return ""; + } + private byte[] readNextByteArray() + { + short len = readNextShort(); + byte[] data = new byte[len]; + Receive(data, 0, len, SocketFlags.None); + return data; + } + private short readNextShort() + { + byte[] tmp = new byte[2]; + Receive(tmp, 0, 2, SocketFlags.None); + Array.Reverse(tmp); + return BitConverter.ToInt16(tmp, 0); + } + private int readNextInt() + { + byte[] tmp = new byte[4]; + Receive(tmp, 0, 4, SocketFlags.None); + Array.Reverse(tmp); + return BitConverter.ToInt32(tmp, 0); + } + private byte readNextByte() + { + byte[] result = new byte[1]; + Receive(result, 0, 1, SocketFlags.None); + return result[0]; + } + private void readNextItemSlot() + { + short itemid = readNextShort(); + //If slot not empty (item ID != -1) + if (itemid != -1) + { + readData(1); //Item count + readData(2); //Item damage + short length = readNextShort(); + //If lenght of optional NBT data > 0, read it + if (length > 0) { readData(length); } + } + } + private void readNextEntityMetaData() + { + do + { + byte[] id = new byte[1]; + Receive(id, 0, 1, SocketFlags.None); + if (id[0] == 0x7F) { break; } + int index = id[0] & 0x1F; + int type = id[0] >> 5; + switch (type) + { + case 0: readData(1); break; //Byte + case 1: readData(2); break; //Short + case 2: readData(4); break; //Int + case 3: readData(4); break; //Float + case 4: readNextString(); break; //String + case 5: readNextItemSlot(); break; //Slot + case 6: readData(12); break; //Vector (3 Int) + } + } while (true); + } + private void readNextObjectData() + { + int id = readNextInt(); + if (id != 0) { readData(6); } + } + private void readNextTeamData() + { + readNextString(); //Internal Name + byte mode = readNextByte(); + + if (mode == 0 || mode == 2) + { + readNextString(); //Display Name + readNextString(); //Prefix + readNextString(); //Suffix + readData(1); //Friendly Fire + } + + if (mode == 0 || mode == 3 || mode == 4) + { + short count = readNextShort(); + for (int i = 0; i < count; i++) + { + readNextString(); //Players + } + } + } + private void readNextEntityProperties(int protocolversion) + { + if (protocolversion >= 72) + { + if (protocolversion >= 74) + { + //Minecraft 1.6.2 + readNextInt(); //Entity ID + int count = readNextInt(); + for (int i = 0; i < count; i++) + { + readNextString(); //Property name + readData(8); //Property value (Double) + short othercount = readNextShort(); + readData(25 * othercount); + } + } + else + { + //Minecraft 1.6.0 / 1.6.1 + readNextInt(); //Entity ID + int count = readNextInt(); + for (int i = 0; i < count; i++) + { + readNextString(); //Property name + readData(8); //Property value (Double) + } + } + } + } + private void readNextWindowData(int protocolversion) + { + readData(1); + byte windowtype = readNextByte(); + readNextString(); + readData(1); + if (protocolversion > 51) + { + readData(1); + if (protocolversion >= 72 && windowtype == 0xb) + { + readNextInt(); + } + } + } + private void readNextChunkBulkData() + { + short chunkcount = readNextShort(); + int datalen = readNextInt(); + readData(1); + readData(datalen); + readData(12 * (chunkcount)); + } + + private void setcolor(char c) + { + switch (c) + { + case '0': Console.ForegroundColor = ConsoleColor.Gray; break; //Should be Black but Black is non-readable on a black background + case '1': Console.ForegroundColor = ConsoleColor.DarkBlue; break; + case '2': Console.ForegroundColor = ConsoleColor.DarkGreen; break; + case '3': Console.ForegroundColor = ConsoleColor.DarkCyan; break; + case '4': Console.ForegroundColor = ConsoleColor.DarkRed; break; + case '5': Console.ForegroundColor = ConsoleColor.DarkMagenta; break; + case '6': Console.ForegroundColor = ConsoleColor.DarkYellow; break; + case '7': Console.ForegroundColor = ConsoleColor.Gray; break; + case '8': Console.ForegroundColor = ConsoleColor.DarkGray; break; + case '9': Console.ForegroundColor = ConsoleColor.Blue; break; + case 'a': Console.ForegroundColor = ConsoleColor.Green; break; + case 'b': Console.ForegroundColor = ConsoleColor.Cyan; break; + case 'c': Console.ForegroundColor = ConsoleColor.Red; break; + case 'd': Console.ForegroundColor = ConsoleColor.Magenta; break; + case 'e': Console.ForegroundColor = ConsoleColor.Yellow; break; + case 'f': Console.ForegroundColor = ConsoleColor.White; break; + case 'r': Console.ForegroundColor = ConsoleColor.White; break; + } + } + private void printstring(string str, bool acceptnewlines) + { + if (str != "") + { + char prev = ' '; + foreach (char c in str) + { + if (c == '§') + { + prev = c; + continue; + } + else if (prev == '§') + { + setcolor(c); + prev = c; + } + else + { + if (c == '\n' && !acceptnewlines) { continue; } + else ConsoleIO.Write(c); + } + } + ConsoleIO.Write('\n'); + } + Console.ForegroundColor = ConsoleColor.Gray; + } + private string ReverseString(string a) + { + char[] tmp = a.ToCharArray(); + Array.Reverse(tmp); + return new string(tmp); + } + private string ByteArrayToString(byte[] ba) + { + string conv = ""; + for (int i = 1; i < ba.Length; i += 2) + { + conv += (char)ba[i]; + } + return conv; + } + + public void setVersion(byte ver) { protocolversion = ver; } + public void setClient(TcpClient n) { c = n; } + private void setEncryptedClient(Crypto.AesStream n) { s = n; encrypted = true; } + private void Receive(byte[] buffer, int start, int offset, SocketFlags f) + { + while (c.Client.Available < start + offset) { } + if (encrypted) + { + s.Read(buffer, start, offset); + } + else c.Client.Receive(buffer, start, offset, f); + } + private void Send(byte[] buffer) + { + if (encrypted) + { + s.Write(buffer, 0, buffer.Length); + } + else c.Client.Send(buffer); + } + + public static bool GetServerInfo(string serverIP, ref byte protocolversion, ref string version) + { + try + { + string host; int port; + string[] sip = serverIP.Split(':'); + host = sip[0]; + + if (sip.Length == 1) + { + port = 25565; + } + else + { + try + { + port = Convert.ToInt32(sip[1]); + } + catch (FormatException) { port = 25565; } + } + + TcpClient tcp = new TcpClient(host, port); + byte[] ping = new byte[2] { 0xfe, 0x01 }; + tcp.Client.Send(ping, SocketFlags.None); + + tcp.Client.Receive(ping, 0, 1, SocketFlags.None); + if (ping[0] == 0xff) + { + MinecraftCom ComTmp = new MinecraftCom(); + ComTmp.setClient(tcp); + string result = ComTmp.readNextString(); + Console.ForegroundColor = ConsoleColor.DarkGray; + //Console.WriteLine(result.Replace((char)0x00, ' ')); + if (result.Length > 2 && result[0] == '§' && result[1] == '1') + { + string[] tmp = result.Split((char)0x00); + protocolversion = (byte)Int16.Parse(tmp[1]); + version = tmp[2]; + } + else + { + protocolversion = (byte)39; + version = "B1.8.1 - 1.3.2"; + } + Console.WriteLine("Server version : MC " + version + " (protocol v" + protocolversion + ")."); + Console.ForegroundColor = ConsoleColor.Gray; + return true; + } + else + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine("Unexpected answer from the server (is that a Minecraft server ?)"); + Console.ForegroundColor = ConsoleColor.Gray; + return false; + } + } + catch + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine("An error occured while attempting to connect to this IP."); + Console.ForegroundColor = ConsoleColor.Gray; + return false; + } + } + public bool Handshake(string username, string sessionID, ref string serverID, ref byte[] token, string host, int port) + { + username = ReverseString(username); + //array + byte[] data = new byte[10 + (username.Length * 2) + (host.Length * 2)]; + + //packet id + data[0] = (byte)2; + + //Protocol Version - Minecraft 1.3.1 & 1.3.2 + //data[1] = (byte)39; + + //Protocol Version - Minecraft 1.4.2 + //data[1] = (byte)47; + + //Protocol Version - Minecraft 1.4.4 + //data[1] = (byte)49; + + //Protocol Version - Minecraft 1.4.6 & 1.4.7 + //data[1] = (byte)51; + + //Protocol Version - Minecraft 1.5.0 + //data[1] = (byte)60; + + //Protocol Version - Custom + data[1] = protocolversion; + + //short len + byte[] sh = new byte[2]; + sh = BitConverter.GetBytes((short)username.Length); + Array.Reverse(sh); + sh.CopyTo(data, 2); + + //username + byte[] name = Encoding.Unicode.GetBytes(username); + Array.Reverse(name); + name.CopyTo(data, 4); + + //short len + sh = new byte[2]; + sh = BitConverter.GetBytes((short)host.Length); + Array.Reverse(sh); + sh.CopyTo(data, 4 + (username.Length * 2)); + + //host + byte[] bhost = Encoding.Unicode.GetBytes(username); + Array.Reverse(bhost); + bhost.CopyTo(data, 6 + (username.Length * 2)); + + //port + sh = new byte[4]; + sh = BitConverter.GetBytes(port); + Array.Reverse(sh); + sh.CopyTo(data, 6 + (username.Length * 2) + (host.Length * 2)); + + Send(data); + + byte[] pid = new byte[1]; + Receive(pid, 0, 1, SocketFlags.None); + while (pid[0] == 0xFA) //Skip some early plugin messages + { + processPacket(pid[0]); + Receive(pid, 0, 1, SocketFlags.None); + } + if (pid[0] == 0xFD) + { + serverID = readNextString(); + byte[] Serverkey_RAW = readNextByteArray(); + token = readNextByteArray(); + + if (serverID == "-") + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine("Server is in offline mode."); + Console.ForegroundColor = ConsoleColor.Gray; + return true; //No need to check session or start encryption + } + else + { + var PublicServerkey = Crypto.GenerateRSAPublicKey(Serverkey_RAW); + var SecretKey = Crypto.GenerateAESPrivateKey(); + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine("Handshake sussessful. (Server ID: " + serverID + ')'); + Console.ForegroundColor = ConsoleColor.Gray; + return StartEncryption(ReverseString(username), sessionID, token, serverID, PublicServerkey, SecretKey); + } + } + else return false; + } + public bool StartEncryption(string username, string sessionID, byte[] token, string serverIDhash, java.security.PublicKey serverKey, javax.crypto.SecretKey secretKey) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + ConsoleIO.WriteLine("Crypto keys & hash generated."); + Console.ForegroundColor = ConsoleColor.Gray; + + if (serverIDhash != "-") + { + Console.WriteLine("Checking Session..."); + if (!SessionCheck(username, sessionID, new java.math.BigInteger(Crypto.getServerHash(serverIDhash, serverKey, secretKey)).toString(16))) + { + return false; + } + } + + //Encrypt the data + byte[] key_enc = Crypto.Encrypt(serverKey, secretKey.getEncoded()); + byte[] token_enc = Crypto.Encrypt(serverKey, token); + byte[] keylen = BitConverter.GetBytes((short)key_enc.Length); + byte[] tokenlen = BitConverter.GetBytes((short)token_enc.Length); + + Array.Reverse(keylen); + Array.Reverse(tokenlen); + + //Building the packet + byte[] data = new byte[5 + (short)key_enc.Length + (short)token_enc.Length]; + data[0] = 0xFC; + keylen.CopyTo(data, 1); + key_enc.CopyTo(data, 3); + tokenlen.CopyTo(data, 3 + (short)key_enc.Length); + token_enc.CopyTo(data, 5 + (short)key_enc.Length); + + //Send it back + Send(data); + + //Getting the next packet + byte[] pid = new byte[1]; + Receive(pid, 0, 1, SocketFlags.None); + if (pid[0] == 0xFC) + { + readData(4); + setEncryptedClient(Crypto.SwitchToAesMode(c.GetStream(), secretKey)); + return true; + } + else return false; + } + public bool FinalizeLogin() + { + //Creating byte array + byte[] data = new byte[2]; + data[0] = 0xCD; + data[1] = 0; + + Send(data); + try + { + byte[] pid = new byte[1]; + try + { + if (c.Connected) + { + Receive(pid, 0, 1, SocketFlags.None); + while (pid[0] >= 0xC0 && pid[0] != 0xFF) //Skip some early packets or plugin messages + { + processPacket(pid[0]); + Receive(pid, 0, 1, SocketFlags.None); + } + if (pid[0] == (byte)1) + { + readData(4); readNextString(); readData(5); + return true; //The Server accepted the request + } + else if (pid[0] == (byte)0xFF) + { + string reason = readNextString(); + Console.WriteLine("Login rejected by Server :"); printstring(reason, true); + for (int i = 0; i < bots.Count; i++) { bots[i].OnDisconnect(ChatBot.DisconnectReason.LoginRejected, reason); } + return false; + } + } + } + catch + { + //Connection failed + return false; + } + } + catch + { + //Network error + Console.WriteLine("Connection Lost."); + return false; + } + return false; //Login was unsuccessful (received a kick...) + } + public bool SendChatMessage(string message) + { + try + { + message = ReverseString(message); + byte[] chat = new byte[3 + (message.Length * 2)]; + chat[0] = (byte)3; + + byte[] msglen = new byte[2]; + msglen = BitConverter.GetBytes((short)message.Length); + Array.Reverse(msglen); + msglen.CopyTo(chat, 1); + + byte[] msg = new byte[message.Length * 2]; + msg = Encoding.Unicode.GetBytes(message); + Array.Reverse(msg); + msg.CopyTo(chat, 3); + + Send(chat); + return true; + } + catch (SocketException) { return false; } + } + public void Disconnect(string message) + { + try + { + message = ReverseString(message); + byte[] reason = new byte[3 + (message.Length * 2)]; + reason[0] = (byte)0xff; + + byte[] msglen = new byte[2]; + msglen = BitConverter.GetBytes((short)message.Length); + Array.Reverse(msglen); + msglen.CopyTo(reason, 1); + + byte[] msg = new byte[message.Length * 2]; + msg = Encoding.Unicode.GetBytes(message); + Array.Reverse(msg); + msg.CopyTo(reason, 3); + + Send(reason); + } + catch (SocketException) { } + catch (System.IO.IOException) { } + } + + private List bots = new List(); + public void BotLoad(ChatBot b) { b.SetHandler(this); bots.Add(b); b.Initialize(); } + public void BotUnLoad(ChatBot b) { bots.RemoveAll(item => object.ReferenceEquals(item, b)); } + public void BotClear() { bots.Clear(); } + } +} diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs new file mode 100644 index 00000000..73431430 --- /dev/null +++ b/MinecraftClient/Program.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MinecraftClient +{ + /// + /// Minecraft Console Client by ORelio (c) 2012-2013. + /// Allows to connect to any Minecraft server, send and receive text, automated scripts. + /// This source code is released under the CDDL 1.0 License. + /// + + class Program + { + private static McTcpClient Client; + private static string loginusername = ""; + private static string user = ""; + private static string pass = ""; + private static string ip = ""; + private static string command = ""; + private static string[] startupargs; + + /// + /// The main entry point of Minecraft Console Client + /// + + static void Main(string[] args) + { + Console.WriteLine("Console Client for MC 1.4.6 to 1.6.2 - v1.5.2 - By ORelio (or3L1o@live.fr)"); + + //Processing Command-line arguments + + if (args.Length >= 1) + { + user = args[0]; + if (args.Length >= 2) + { + pass = args[1]; + if (args.Length >= 3) + { + ip = args[2]; + if (args.Length >= 4) + { + command = args[3]; + } + } + } + } + + //Asking the user to type in missing data such as Username and Password + + if (user == "") { + Console.Write("Username : "); + user = Console.ReadLine(); + } + if (pass == "") { + Console.Write("Password : "); + pass = Console.ReadLine(); + + //Hide the password + Console.CursorTop--; + Console.Write("Password : <******>"); + for (int i = 19; i < Console.BufferWidth; i++) { Console.Write(' '); } + } + + //Save the arguments + startupargs = args; + loginusername = user; + + //Start the Client + InitializeClient(); + } + + /// + /// Start a new Client + /// + + private static void InitializeClient() + { + MinecraftCom.LoginResult result; + string logindata = ""; + + if (pass == "-") + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine("You chose to run in offline mode."); + Console.ForegroundColor = ConsoleColor.Gray; + result = MinecraftCom.LoginResult.Success; + logindata = "0:deprecated:" + user + ":0"; + } + else + { + Console.WriteLine("Connecting to Minecraft.net..."); + result = MinecraftCom.GetLogin(loginusername, pass, ref logindata); + } + if (result == MinecraftCom.LoginResult.Success) + { + user = logindata.Split(':')[2]; + string sessionID = logindata.Split(':')[3]; + Console.WriteLine("Success. (session ID: " + sessionID + ')'); + if (ip == "") + { + Console.Write("Server IP : "); + ip = Console.ReadLine(); + } + + //Get server version + Console.WriteLine("Retrieving Server Info..."); + byte protocolversion = 0; string version = ""; + if (MinecraftCom.GetServerInfo(ip, ref protocolversion, ref version)) + { + //Supported protocol version ? + int[] supportedVersions = { 51, 60, 61, 72, 73, 74 }; + if (Array.IndexOf(supportedVersions, protocolversion) > -1) + { + //Minecraft 1.6+ ? Load translations + if (protocolversion >= 72) { ChatParser.InitTranslations(); } + + //Will handle the connection for this client + Console.WriteLine("Version is supported."); + MinecraftCom handler = new MinecraftCom(); + handler.setVersion(protocolversion); + + //Load & initialize bots if needed + foreach (string arg in startupargs) + { + if (arg.Length > 4 && arg.Substring(0, 4).ToLower() == "bot:") + { + int param; + string[] botargs = arg.ToLower().Split(':'); + switch (botargs[1]) + { + case "antiafk": + #region Arguments for the AntiAFK bot + param = 600; + if (botargs.Length > 2) + { + try { param = Convert.ToInt32(botargs[2]); } + catch (FormatException) { } + } + #endregion + handler.BotLoad(new Bots.AntiAFK(param)); break; + + case "pendu": handler.BotLoad(new Bots.Pendu(false)); break; + case "hangman": handler.BotLoad(new Bots.Pendu(true)); break; + case "alerts": handler.BotLoad(new Bots.Alerts()); break; + + case "log": + #region Arguments for the ChatLog bot + bool datetime = true; + string file = "chat-" + ip + ".log"; + Bots.ChatLog.MessageFilter filter = Bots.ChatLog.MessageFilter.AllMessages; + if (botargs.Length > 2) + { + datetime = (botargs[2] != "0"); + if (botargs.Length > 3) + { + switch (botargs[3]) + { + case "all": filter = Bots.ChatLog.MessageFilter.AllText; break; + case "messages": filter = Bots.ChatLog.MessageFilter.AllMessages; break; + case "chat": filter = Bots.ChatLog.MessageFilter.OnlyChat; break; + case "private": filter = Bots.ChatLog.MessageFilter.OnlyWhispers; break; + } + if (botargs.Length > 4 && botargs[4] != "") { file = botargs[4]; } + } + } + #endregion + handler.BotLoad(new Bots.ChatLog(file, filter, datetime)); break; + + case "logplayerlist": + #region Arguments for the PlayerListLogger bot + param = 600; + if (botargs.Length > 2) + { + try { param = Convert.ToInt32(botargs[2]); } + catch (FormatException) { } + } + #endregion + handler.BotLoad(new Bots.PlayerListLogger(param, "connected-" + ip + ".log")); break; + + case "autorelog": + #region Arguments for the AutoRelog bot + int delay = 10; + if (botargs.Length > 2) + { + try { delay = Convert.ToInt32(botargs[2]); } + catch (FormatException) { } + } + int retries = 3; + if (botargs.Length > 3) + { + try { retries = Convert.ToInt32(botargs[3]); } + catch (FormatException) { } + } + #endregion + handler.BotLoad(new Bots.AutoRelog(delay, retries)); break; + + case "xauth": + if (botargs.Length > 2) { handler.BotLoad(new Bots.xAuth(botargs[2])); } break; + } + + command = ""; + } + } + + //Start the main TCP client + if (command != "") + { + Client = new McTcpClient(user, sessionID, ip, handler, command); + } + else Client = new McTcpClient(user, sessionID, ip, handler); + } + else + { + Console.WriteLine("Cannot connect to the server : This version is not supported !"); + ReadLineReconnect(); + } + } + else + { + Console.WriteLine("Failed to ping this IP."); + ReadLineReconnect(); + } + } + else + { + Console.ForegroundColor = ConsoleColor.Gray; + Console.Write("Connection failed : "); + switch (result) + { + case MinecraftCom.LoginResult.AccountMigrated: Console.WriteLine("Account migrated, use e-mail as username."); break; + case MinecraftCom.LoginResult.Blocked: Console.WriteLine("Too many failed logins. Please try again later."); break; + case MinecraftCom.LoginResult.WrongPassword: Console.WriteLine("Incorrect password."); break; + case MinecraftCom.LoginResult.NotPremium: Console.WriteLine("User not premium."); break; + case MinecraftCom.LoginResult.Error: Console.WriteLine("Network error."); break; + } + while (Console.KeyAvailable) { Console.ReadKey(false); } + if (command == "") { ReadLineReconnect(); } + } + } + + /// + /// Disconnect the current client from the server and restart it + /// + + public static void Restart() + { + new System.Threading.Thread(new System.Threading.ThreadStart(t_restart)).Start(); + } + + /// + /// Pause the program, usually when an error or a kick occured, letting the user press Enter to quit OR type /reconnect + /// + /// Return True if the user typed "/reconnect" + + public static bool ReadLineReconnect() + { + string text = Console.ReadLine(); + if (text == "reco" || text == "reconnect" || text == "/reco" || text == "/reconnect") + { + Program.Restart(); + return true; + } + else return false; + } + + /// + /// Private thread for restarting the program. Called through Restart() + /// + + private static void t_restart() + { + if (Client != null) { Client.Disconnect(); ConsoleIO.Reset(); } + Console.WriteLine("Restarting Minecraft Console Client..."); + InitializeClient(); + } + } +} diff --git a/MinecraftClient/Properties/AssemblyInfo.cs b/MinecraftClient/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..bc2d0307 --- /dev/null +++ b/MinecraftClient/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Minecraft Console Client")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MinecraftClient")] +[assembly: AssemblyCopyright("Copyright © ORelio 2012-2013")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("78af6200-1f48-4daa-b473-109a9728b61f")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.3")] +[assembly: AssemblyFileVersion("1.3")]