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); } /// /// 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(Settings.TranslationsFile)) { string[] translations = System.IO.File.ReadAllLines(Settings.TranslationsFile); 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 not found: \"" + Settings.TranslationsFile + "\"" + "\nYou can pick a translation file from .minecraft\\assets\\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)) { if ((TranslationRules[rulename].IndexOf("%1$s") >= 0 && TranslationRules[rulename].IndexOf("%2$s") >= 0) && (TranslationRules[rulename].IndexOf("%1$s") > TranslationRules[rulename].IndexOf("%2$s"))) { while (using_data.Count < 2) { using_data.Add(""); } string tmp = using_data[0]; using_data[0] = using_data[1]; using_data[1] = tmp; } string[] syntax = TranslationRules[rulename].Split(new string[] { "%s", "%d", "%1$s", "%2$s" }, 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] == '\\') { try //Unicode character \u0123 { if (toparse[cursorpos + 1] == 'u' && isHex(toparse[cursorpos + 2]) && isHex(toparse[cursorpos + 3]) && isHex(toparse[cursorpos + 4]) && isHex(toparse[cursorpos + 5])) { //"abc\u0123abc" => "0123" => 0123 => Unicode char n°0123 => Add char to string data.StringValue += char.ConvertFromUtf32(int.Parse(toparse.Substring(cursorpos + 2, 4), System.Globalization.NumberStyles.HexNumber)); cursorpos += 6; continue; } else cursorpos++; //Normal character escapement \" } catch (IndexOutOfRangeException) { cursorpos++; } // \u01 catch (ArgumentOutOfRangeException) { cursorpos++; } // Unicode index 0123 was invalid } 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 ""; } /// /// Small function for checking if a char is an hexadecimal char (0-9 A-F a-f) /// /// Char to test /// True if hexadecimal private static bool isHex(char c) { return ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')); } } }