Minecraft-Console-Client/MinecraftClient/Protocol/Message/ChatParser.cs

554 lines
23 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
2023-01-21 01:34:05 +08:00
using System.Net.Http.Json;
using System.Text;
2022-12-11 13:00:19 +08:00
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
2022-10-05 15:02:30 +08:00
using static MinecraftClient.Settings;
namespace MinecraftClient.Protocol.Message
{
/// <summary>
/// This class parses JSON chat data from MC 1.6+ and returns the appropriate string to be printed.
/// </summary>
static class ChatParser
{
2022-08-27 02:10:44 +08:00
public enum MessageType
{
CHAT,
SAY_COMMAND,
MSG_COMMAND_INCOMING,
MSG_COMMAND_OUTGOING,
TEAM_MSG_COMMAND_INCOMING,
TEAM_MSG_COMMAND_OUTGOING,
EMOTE_COMMAND,
RAW_MSG
};
public static Dictionary<int, MessageType>? ChatId2Type;
public static void ReadChatType(Dictionary<string, object> registryCodec)
{
Dictionary<int, MessageType> chatTypeDictionary = ChatId2Type ?? new();
2024-02-28 11:49:50 +01:00
var chatTypeListNbt =
(object[])(((Dictionary<string, object>)registryCodec["minecraft:chat_type"])["value"]);
foreach (var (chatName, chatId) in from Dictionary<string, object> chatTypeNbt in chatTypeListNbt
2024-02-28 11:49:50 +01:00
let chatName = (string)chatTypeNbt["name"]
let chatId = (int)chatTypeNbt["id"]
select (chatName, chatId))
{
chatTypeDictionary[chatId] = chatName switch
{
"minecraft:chat" => MessageType.CHAT,
"minecraft:emote_command" => MessageType.EMOTE_COMMAND,
"minecraft:msg_command_incoming" => MessageType.MSG_COMMAND_INCOMING,
"minecraft:msg_command_outgoing" => MessageType.MSG_COMMAND_OUTGOING,
"minecraft:say_command" => MessageType.SAY_COMMAND,
"minecraft:team_msg_command_incoming" => MessageType.TEAM_MSG_COMMAND_INCOMING,
"minecraft:team_msg_command_outgoing" => MessageType.TEAM_MSG_COMMAND_OUTGOING,
_ => MessageType.CHAT,
};
}
2024-02-28 11:49:50 +01:00
ChatId2Type = chatTypeDictionary;
}
/// <summary>
/// The main function to convert text from MC 1.6+ JSON to MC 1.5.2 formatted text
/// </summary>
/// <param name="json">JSON serialized text</param>
/// <param name="links">Optional container for links from JSON serialized text</param>
/// <returns>Returns the translated text</returns>
public static string ParseText(string json, List<string>? links = null)
{
return JSONData2String(Json.ParseJson(json), "", links);
}
public static string ParseText(Dictionary<string, object> nbt)
{
return NbtToString(nbt);
}
/// <summary>
/// The main function to convert text from MC 1.9+ JSON to MC 1.5.2 formatted text
/// </summary>
/// <param name="message">Message received</param>
/// <param name="links">Optional container for links from JSON serialized text</param>
/// <returns>Returns the translated text</returns>
public static string ParseSignedChat(ChatMessage message, List<string>? links = null)
{
string sender = message.isSenderJson ? ParseText(message.displayName!) : message.displayName!;
2022-10-14 20:34:45 +08:00
string content;
if (Config.Signature.ShowModifiedChat && message.unsignedContent != null)
{
content = ParseText(message.unsignedContent!);
if (string.IsNullOrEmpty(content))
content = message.unsignedContent!;
}
else
{
content = message.isJson ? ParseText(message.content) : message.content;
if (string.IsNullOrEmpty(content))
content = message.content!;
}
2022-08-27 02:10:44 +08:00
string text;
List<string> usingData = new();
2022-08-27 02:10:44 +08:00
MessageType chatType;
if (message.chatTypeId == -1)
2022-08-27 02:10:44 +08:00
chatType = MessageType.RAW_MSG;
else if (!ChatId2Type!.TryGetValue(message.chatTypeId, out chatType))
chatType = MessageType.CHAT;
switch (chatType)
{
2022-08-27 02:10:44 +08:00
case MessageType.CHAT:
usingData.Add(sender);
usingData.Add(content);
text = TranslateString("chat.type.text", usingData);
break;
2022-08-27 02:10:44 +08:00
case MessageType.SAY_COMMAND:
usingData.Add(sender);
usingData.Add(content);
text = TranslateString("chat.type.announcement", usingData);
break;
2022-08-27 02:10:44 +08:00
case MessageType.MSG_COMMAND_INCOMING:
usingData.Add(sender);
usingData.Add(content);
text = TranslateString("commands.message.display.incoming", usingData);
break;
2022-08-27 02:10:44 +08:00
case MessageType.MSG_COMMAND_OUTGOING:
usingData.Add(sender);
usingData.Add(content);
text = TranslateString("commands.message.display.outgoing", usingData);
break;
case MessageType.TEAM_MSG_COMMAND_INCOMING:
usingData.Add(message.teamName!);
usingData.Add(sender);
usingData.Add(content);
text = TranslateString("chat.type.team.text", usingData);
break;
2022-08-27 02:10:44 +08:00
case MessageType.TEAM_MSG_COMMAND_OUTGOING:
usingData.Add(message.teamName!);
usingData.Add(sender);
usingData.Add(content);
text = TranslateString("chat.type.team.sent", usingData);
break;
case MessageType.EMOTE_COMMAND:
usingData.Add(sender);
usingData.Add(content);
text = TranslateString("chat.type.emote", usingData);
break;
2022-08-27 02:10:44 +08:00
case MessageType.RAW_MSG:
text = content;
break;
default:
2022-08-27 02:10:44 +08:00
goto case MessageType.CHAT;
}
2024-02-28 11:49:50 +01:00
return text;
}
/// <summary>
/// Get the classic color tag corresponding to a color name
/// </summary>
/// <param name="colorname">Color Name</param>
/// <returns>Color code</returns>
private static string Color2tag(string colorname)
{
return colorname.ToLower() switch
{
#pragma warning disable format // @formatter:off
/* MC 1.7+ Name || MC 1.6 Name || Classic tag */
"black" => "§0",
"dark_blue" => "§1",
"dark_green" => "§2",
"dark_aqua" or "dark_cyan" => "§3",
"dark_red" => "§4",
"dark_purple" or "dark_magenta" => "§5",
"gold" or "dark_yellow" => "§6",
"gray" => "§7",
"dark_gray" => "§8",
"blue" => "§9",
"green" => "§a",
"aqua" or "cyan" => "§b",
"red" => "§c",
"light_purple" or "magenta" => "§d",
"yellow" => "§e",
"white" => "§f",
_ => "" ,
#pragma warning restore format // @formatter:on
};
}
/// <summary>
/// Specify whether translation rules have been loaded
/// </summary>
private static bool RulesInitialized = false;
/// <summary>
/// Set of translation rules for formatting text
/// </summary>
2022-12-11 13:00:19 +08:00
private static Dictionary<string, string> TranslationRules = new();
/// <summary>
/// Initialize translation rules.
/// Necessary for properly printing some chat messages.
/// </summary>
2024-02-28 11:49:50 +01:00
public static void InitTranslations()
{
if (!RulesInitialized)
{
InitRules();
RulesInitialized = true;
}
}
/// <summary>
/// Internal rule initialization method. Looks for local rule file or download it from Mojang asset servers.
/// </summary>
private static void InitRules()
{
2022-12-11 13:00:19 +08:00
if (Config.Main.Advanced.Language == "en_us")
{
2024-02-28 11:49:50 +01:00
TranslationRules =
JsonSerializer.Deserialize<Dictionary<string, string>>(
(byte[])MinecraftAssets.ResourceManager.GetObject("en_us.json")!)!;
2022-12-11 13:00:19 +08:00
return;
}
//Language file in a subfolder, depending on the language setting
2022-08-27 02:10:44 +08:00
if (!Directory.Exists("lang"))
Directory.CreateDirectory("lang");
2022-12-11 13:00:19 +08:00
string languageFilePath = "lang" + Path.DirectorySeparatorChar + Config.Main.Advanced.Language + ".json";
2023-01-21 01:34:05 +08:00
// Load the external dictionary of translation rules or display an error message
2022-12-11 13:00:19 +08:00
if (File.Exists(languageFilePath))
{
try
{
2024-02-28 11:49:50 +01:00
TranslationRules =
JsonSerializer.Deserialize<Dictionary<string, string>>(File.OpenRead(languageFilePath))!;
}
catch (IOException)
{
}
catch (JsonException)
{
2022-12-11 13:00:19 +08:00
}
}
2024-02-28 11:49:50 +01:00
if (TranslationRules.TryGetValue("Version", out string? version) &&
version == Settings.TranslationsFile_Version)
2022-12-11 13:00:19 +08:00
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(Translations.chat_loaded, acceptnewlines: true);
return;
}
2022-12-11 13:00:19 +08:00
// Try downloading language file from Mojang's servers?
2024-02-28 11:49:50 +01:00
ConsoleIO.WriteLineFormatted(
"§8" + string.Format(Translations.chat_download, Config.Main.Advanced.Language));
2022-12-11 13:00:19 +08:00
HttpClient httpClient = new();
try
{
Task<string> fetch_index = httpClient.GetStringAsync(TranslationsFile_Website_Index);
fetch_index.Wait();
2024-02-28 11:49:50 +01:00
Match match = Regex.Match(fetch_index.Result,
$"minecraft/lang/{Config.Main.Advanced.Language}.json" + @""":\s\{""hash"":\s""([\d\w]{40})""");
2022-12-11 13:00:19 +08:00
fetch_index.Dispose();
if (match.Success && match.Groups.Count == 2)
{
string hash = match.Groups[1].Value;
string translation_file_location = TranslationsFile_Website_Download + '/' + hash[..2] + '/' + hash;
if (Config.Logging.DebugMessages)
2024-02-28 11:49:50 +01:00
ConsoleIO.WriteLineFormatted(
string.Format(Translations.chat_request, translation_file_location));
2024-02-28 11:49:50 +01:00
Task<Dictionary<string, string>?> fetckFileTask =
httpClient.GetFromJsonAsync<Dictionary<string, string>>(translation_file_location);
2023-01-21 01:34:05 +08:00
fetckFileTask.Wait();
if (fetckFileTask.Result != null && fetckFileTask.Result.Count > 0)
{
TranslationRules = fetckFileTask.Result;
TranslationRules["Version"] = TranslationsFile_Version;
2024-02-28 11:49:50 +01:00
File.WriteAllText(languageFilePath,
JsonSerializer.Serialize(TranslationRules, typeof(Dictionary<string, string>)),
Encoding.UTF8);
2023-01-21 01:34:05 +08:00
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.chat_done, languageFilePath));
return;
}
2024-02-28 11:49:50 +01:00
2023-01-21 01:34:05 +08:00
fetckFileTask.Dispose();
}
2022-12-11 13:00:19 +08:00
else
{
2022-11-30 16:08:55 +08:00
ConsoleIO.WriteLineFormatted("§8" + Translations.chat_fail, acceptnewlines: true);
}
}
2022-12-11 13:00:19 +08:00
catch (HttpRequestException)
{
2022-12-11 13:00:19 +08:00
ConsoleIO.WriteLineFormatted("§8" + Translations.chat_fail, acceptnewlines: true);
}
2022-12-11 13:00:19 +08:00
catch (IOException)
{
2024-02-28 11:49:50 +01:00
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.chat_save_fail, languageFilePath),
acceptnewlines: true);
}
catch (Exception e)
{
ConsoleIO.WriteLineFormatted("§8" + Translations.chat_fail, acceptnewlines: true);
2023-01-29 22:39:11 +08:00
ConsoleIO.WriteLine(e.Message);
if (Config.Logging.DebugMessages && !string.IsNullOrEmpty(e.StackTrace))
ConsoleIO.WriteLine(e.StackTrace);
}
2022-12-11 13:00:19 +08:00
finally
{
2022-12-11 13:00:19 +08:00
httpClient.Dispose();
}
2022-12-11 13:00:19 +08:00
2024-02-28 11:49:50 +01:00
TranslationRules =
JsonSerializer.Deserialize<Dictionary<string, string>>(
(byte[])MinecraftAssets.ResourceManager.GetObject("en_us.json")!)!;
2022-12-11 13:00:19 +08:00
ConsoleIO.WriteLine(Translations.chat_use_default);
}
2022-10-17 17:42:00 +08:00
public static string? TranslateString(string rulename)
{
if (TranslationRules.TryGetValue(rulename, out string? result))
return result;
else
return null;
}
/// <summary>
/// Format text using a specific formatting rule.
/// Example : * %s %s + ["ORelio", "is doing something"] = * ORelio is doing something
/// </summary>
/// <param name="rulename">Name of the rule, chosen by the server</param>
/// <param name="using_data">Data to be used in the rule</param>
/// <returns>Returns the formatted text according to the given data</returns>
private static string TranslateString(string rulename, List<string> using_data)
{
2024-02-28 11:49:50 +01:00
if (!RulesInitialized)
{
InitRules();
RulesInitialized = true;
}
if (TranslationRules.ContainsKey(rulename))
{
int using_idx = 0;
string rule = TranslationRules[rulename];
StringBuilder result = new();
for (int i = 0; i < rule.Length; i++)
2013-07-27 12:25:14 +02:00
{
if (rule[i] == '%' && i + 1 < rule.Length)
{
//Using string or int with %s or %d
if (rule[i + 1] == 's' || rule[i + 1] == 'd')
{
if (using_data.Count > using_idx)
{
result.Append(using_data[using_idx]);
using_idx++;
i += 1;
continue;
}
}
//Using specified string or int with %1$s, %2$s...
else if (char.IsDigit(rule[i + 1])
2024-02-28 11:49:50 +01:00
&& i + 3 < rule.Length && rule[i + 2] == '$'
&& (rule[i + 3] == 's' || rule[i + 3] == 'd'))
{
int specified_idx = rule[i + 1] - '1';
if (using_data.Count > specified_idx)
{
result.Append(using_data[specified_idx]);
using_idx++;
i += 3;
continue;
}
}
}
2024-02-28 11:49:50 +01:00
result.Append(rule[i]);
}
2024-02-28 11:49:50 +01:00
return result.ToString();
}
2022-08-27 02:10:44 +08:00
else return "[" + rulename + "] " + string.Join(" ", using_data);
}
/// <summary>
/// Use a JSON Object to build the corresponding string
/// </summary>
/// <param name="data">JSON object to convert</param>
/// <param name="colorcode">Allow parent color code to affect child elements (set to "" for function init)</param>
/// <param name="links">Container for links from JSON serialized text</param>
/// <returns>returns the Minecraft-formatted string</returns>
private static string JSONData2String(Json.JSONData data, string colorcode, List<string>? links)
{
string extra_result = "";
switch (data.Type)
{
case Json.JSONData.DataType.Object:
if (data.Properties.ContainsKey("color"))
{
colorcode = Color2tag(JSONData2String(data.Properties["color"], "", links));
}
2024-02-28 11:49:50 +01:00
if (data.Properties.ContainsKey("clickEvent") && links != null)
{
Json.JSONData clickEvent = data.Properties["clickEvent"];
if (clickEvent.Properties.ContainsKey("action")
&& clickEvent.Properties.ContainsKey("value")
&& clickEvent.Properties["action"].StringValue == "open_url"
2022-08-27 02:10:44 +08:00
&& !string.IsNullOrEmpty(clickEvent.Properties["value"].StringValue))
{
links.Add(clickEvent.Properties["value"].StringValue);
}
2022-08-27 02:10:44 +08:00
}
2024-02-28 11:49:50 +01:00
if (data.Properties.ContainsKey("extra"))
{
Json.JSONData[] extras = data.Properties["extra"].DataArray.ToArray();
foreach (Json.JSONData item in extras)
extra_result = extra_result + JSONData2String(item, colorcode, links) + "§r";
}
2024-02-28 11:49:50 +01:00
if (data.Properties.ContainsKey("text"))
{
return colorcode + JSONData2String(data.Properties["text"], colorcode, links) + extra_result;
}
else if (data.Properties.ContainsKey("translate"))
{
List<string> using_data = new();
if (data.Properties.ContainsKey("using") && !data.Properties.ContainsKey("with"))
data.Properties["with"] = data.Properties["using"];
if (data.Properties.ContainsKey("with"))
{
Json.JSONData[] array = data.Properties["with"].DataArray.ToArray();
for (int i = 0; i < array.Length; i++)
{
using_data.Add(JSONData2String(array[i], colorcode, links));
}
}
2024-02-28 11:49:50 +01:00
return colorcode +
TranslateString(JSONData2String(data.Properties["translate"], "", links), using_data) +
extra_result;
}
else return extra_result;
2013-07-20 11:01:49 +10:00
case Json.JSONData.DataType.Array:
string result = "";
foreach (Json.JSONData item in data.DataArray)
{
result += JSONData2String(item, colorcode, links);
}
2024-02-28 11:49:50 +01:00
return result;
case Json.JSONData.DataType.String:
return colorcode + data.StringValue;
}
return "";
}
private static string NbtToString(Dictionary<string, object> nbt)
{
if (nbt.Count == 1 && nbt.TryGetValue("", out object? rootMessage))
{
// Nameless root tag
return (string)rootMessage;
}
string message = string.Empty;
string colorCode = string.Empty;
StringBuilder extraBuilder = new StringBuilder();
foreach (var kvp in nbt)
{
string key = kvp.Key;
object value = kvp.Value;
switch (key)
{
case "text":
2024-02-28 11:49:50 +01:00
{
message = (string)value;
}
break;
case "extra":
2024-02-28 11:49:50 +01:00
{
object[] extras = (object[])value;
for (var i = 0; i < extras.Length; i++)
{
2024-02-28 11:49:50 +01:00
var extraDict = extras[i] switch
{
2024-02-28 11:49:50 +01:00
int => new Dictionary<string, object> { { "text", $"{extras[i]}" } },
string => new Dictionary<string, object>
2024-02-26 23:39:20 +01:00
{
2024-02-28 11:49:50 +01:00
{ "text", (string)extras[i] }
},
_ => (Dictionary<string, object>)extras[i]
};
2024-02-26 23:39:20 +01:00
2024-02-28 11:49:50 +01:00
extraBuilder.Append(NbtToString(extraDict) + "§r");
}
2024-02-28 11:49:50 +01:00
}
break;
case "translate":
2024-02-28 11:49:50 +01:00
{
if (nbt.TryGetValue("translate", out object translate))
{
2024-02-28 11:49:50 +01:00
var translateKey = (string)translate;
List<string> translateString = new();
if (nbt.TryGetValue("with", out object withComponent))
{
2024-02-28 11:49:50 +01:00
var withs = (object[])withComponent;
for (var i = 0; i < withs.Length; i++)
{
2024-02-28 11:49:50 +01:00
var withDict = withs[i] switch
{
2024-02-28 11:49:50 +01:00
int => new Dictionary<string, object> { { "text", $"{withs[i]}" } },
string => new Dictionary<string, object>
2024-02-26 23:39:20 +01:00
{
2024-02-28 11:49:50 +01:00
{ "text", (string)withs[i] }
},
_ => (Dictionary<string, object>)withs[i]
};
translateString.Add(NbtToString(withDict));
}
}
2024-02-28 11:49:50 +01:00
message = TranslateString(translateKey, translateString);
}
2024-02-28 11:49:50 +01:00
}
break;
case "color":
2024-02-28 11:49:50 +01:00
{
if (nbt.TryGetValue("color", out object color))
{
2024-02-28 11:49:50 +01:00
colorCode = Color2tag((string)color);
}
2024-02-28 11:49:50 +01:00
}
break;
}
}
2024-02-28 11:49:50 +01:00
return colorCode + message + extraBuilder.ToString();
}
}
2024-02-28 11:49:50 +01:00
}