using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Steamworks; using TMPro; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("ChatRichText")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("ChatRichText")] [assembly: AssemblyTitle("ChatRichText")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ChatRichText { internal static class ChatColors { private static readonly Dictionary Named = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "red", "FF4040" }, { "orange", "FF8C1A" }, { "yellow", "FFD43B" }, { "green", "4CD964" }, { "lime", "A8E063" }, { "teal", "1ABC9C" }, { "cyan", "3BD6E6" }, { "blue", "4D9DE0" }, { "navy", "3B5BA5" }, { "purple", "A46BE0" }, { "magenta", "E668C8" }, { "pink", "FF8FC4" }, { "brown", "A9744F" }, { "white", "FFFFFF" }, { "grey", "9AA0A6" }, { "gray", "9AA0A6" }, { "black", "202124" } }; public const int MaxValueLength = 16; public static bool TryResolve(string value, out string tag) { tag = null; if (string.IsNullOrEmpty(value) || value.Length > 16) { return false; } if (value[0] == '#') { int num = value.Length - 1; if (num != 6 && num != 8) { return false; } for (int i = 1; i < value.Length; i++) { if (!IsHex(value[i])) { return false; } } tag = ""; return true; } if (Named.TryGetValue(value, out var value2)) { tag = ""; return true; } return false; } public static string NameList() { List list = new List(Named.Keys); list.Sort(StringComparer.OrdinalIgnoreCase); return string.Join(", ", list.ToArray()); } private static bool IsHex(char c) { if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) { if (c >= 'A') { return c <= 'F'; } return false; } return true; } } internal static class ChatPatches { private static bool _loggedFailure; public static int Apply(Harmony harmony) { //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Expected O, but got Unknown //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Expected O, but got Unknown return 0 + Patch(harmony, AccessTools.Method(typeof(ChatManager), "ChatMessage", new Type[2] { typeof(CSteamID), typeof(string) }, (Type[])null), "ChatManager.ChatMessage(CSteamID, string)", new HarmonyMethod(AccessTools.Method(typeof(ChatPatches), "FormatIncomingMessage", (Type[])null, (Type[])null))) + Patch(harmony, AccessTools.Method(typeof(ChatManager), "Awake", (Type[])null, (Type[])null), "ChatManager.Awake", null, new HarmonyMethod(AccessTools.Method(typeof(ChatPatches), "EnableRichText", (Type[])null, (Type[])null))); } private static int Patch(Harmony harmony, MethodBase target, string label, HarmonyMethod prefix = null, HarmonyMethod postfix = null) { if (target == null) { Plugin.Log.LogError((object)("Harmony patch failed: could not find " + label + ". The game may have been updated; chat will be left as the game draws it.")); return 0; } try { harmony.Patch(target, prefix, postfix, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Plugin.Log.LogInfo((object)(" patched: " + label)); return 1; } catch (Exception arg) { Plugin.Log.LogError((object)$"Harmony patch failed on {label}: {arg}"); return 0; } } private static bool FormatIncomingMessage(CSteamID from, string message) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.Settings.Enabled.Value) { return true; } try { TmpLineBuilder tmpLineBuilder = new TmpLineBuilder(); if (from == SteamManager.CurrentLobbyID) { tmpLineBuilder.GameMarkup("[Server] "); } else { tmpLineBuilder.GameMarkup("["); tmpLineBuilder.Literal(SteamFriends.GetFriendPersonaName(from)); tmpLineBuilder.GameMarkup("] "); } RichTextParser.ParseInto(message, tmpLineBuilder); ChatManager.ChatMessage(tmpLineBuilder.Finish()); return false; } catch (Exception arg) { if (!_loggedFailure) { _loggedFailure = true; Plugin.Log.LogError((object)$"Formatting failed, falling back to vanilla chat (further occurrences suppressed): {arg}"); } return true; } } private static void EnableRichText(ChatManager __instance) { try { TextMeshProUGUI messageText = __instance._messageText; if ((Object)(object)messageText != (Object)null && !((TMP_Text)messageText).richText) { ((TMP_Text)messageText).richText = true; Plugin.Log.LogInfo((object)"Enabled richText on the chat log component."); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Could not set richText on the chat log: {arg}"); } } } internal sealed class ChatRichTextConfig { public ConfigEntry Enabled { get; } public ChatRichTextConfig(ConfigFile file) { Enabled = file.Bind("General", "ChatRichTextEnabled", true, "Formats chat messages with Steam-style markdown: *bold*, **bold**, _italic_, ~~strikethrough~~, and [color=NAME]text[/color] or [color=#RRGGBB]text[/color]. Accepted colour names: " + ChatColors.NameList() + ". Set to false to leave chat exactly as the game draws it."); } } [BepInPlugin("com.flabagizza.howtofish.chatrichtext", "ChatRichText", "1.0.0")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.flabagizza.howtofish.chatrichtext"; public const string PluginName = "ChatRichText"; public const string PluginVersion = "1.0.0"; private Harmony _harmony; internal static ManualLogSource Log { get; private set; } internal static ChatRichTextConfig Settings { get; private set; } private void Awake() { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; Settings = new ChatRichTextConfig(((BaseUnityPlugin)this).Config); Log.LogInfo((object)"ChatRichText 1.0.0 loading..."); _harmony = new Harmony("com.flabagizza.howtofish.chatrichtext"); int num = ChatPatches.Apply(_harmony); if (num == 2) { Log.LogInfo((object)"Harmony patches applied."); } else { Log.LogError((object)$"Harmony patches incomplete: {num} of 2 applied. See errors above."); } if (!Settings.Enabled.Value) { Log.LogInfo((object)"ChatRichTextEnabled is false - chat will be left as the game draws it."); } Log.LogInfo((object)"ChatRichText 1.0.0 loaded."); } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } internal static class RichTextParser { private const int MaxDepth = 6; private const int MaxTags = 48; private const string ColorOpen = "[color="; private const string ColorClose = "[/color]"; public static void ParseInto(string raw, TmpLineBuilder builder) { if (!string.IsNullOrEmpty(raw)) { int tagBudget = 48; Parse(raw, 0, raw.Length, builder, 0, ref tagBudget); } } private static void Parse(string s, int start, int end, TmpLineBuilder b, int depth, ref int tagBudget) { int num = start; while (num < end) { if (depth < 6 && tagBudget > 0 && TryMarkup(s, num, end, b, depth, ref tagBudget, out var next)) { num = next; continue; } b.Literal(s[num]); num++; } } private static bool TryMarkup(string s, int i, int end, TmpLineBuilder b, int depth, ref int tagBudget, out int next) { next = i; switch (s[i]) { case '[': return TryColor(s, i, end, b, depth, ref tagBudget, out next); case '~': return TryDelimited(s, i, end, "~~", "", "", b, depth, ref tagBudget, out next); case '*': if (TryDelimited(s, i, end, "**", "", "", b, depth, ref tagBudget, out next)) { return true; } return TryDelimited(s, i, end, "*", "", "", b, depth, ref tagBudget, out next); case '_': return TryItalic(s, i, end, b, depth, ref tagBudget, out next); default: return false; } } private static bool TryDelimited(string s, int i, int end, string delim, string open, string close, TmpLineBuilder b, int depth, ref int tagBudget, out int next) { next = i; if (!Matches(s, i, end, delim)) { return false; } int num = i + delim.Length; if (num >= end || char.IsWhiteSpace(s[num])) { return false; } for (int j = num + 1; j + delim.Length <= end; j++) { if (Matches(s, j, end, delim) && !char.IsWhiteSpace(s[j - 1])) { Emit(s, num, j, open, close, b, depth, ref tagBudget); next = j + delim.Length; return true; } } return false; } private static bool TryItalic(string s, int i, int end, TmpLineBuilder b, int depth, ref int tagBudget, out int next) { next = i; if (s[i] != '_' || !IsBoundary(s, i - 1, end)) { return false; } int num = i + 1; if (num >= end || char.IsWhiteSpace(s[num])) { return false; } for (int j = num + 1; j < end; j++) { if (s[j] == '_' && !char.IsWhiteSpace(s[j - 1]) && IsBoundary(s, j + 1, end)) { Emit(s, num, j, "", "", b, depth, ref tagBudget); next = j + 1; return true; } } return false; } private static bool TryColor(string s, int i, int end, TmpLineBuilder b, int depth, ref int tagBudget, out int next) { next = i; if (!Matches(s, i, end, "[color=")) { return false; } int num = i + "[color=".Length; int num2 = -1; int num3 = num + 16; if (num3 > end) { num3 = end; } for (int j = num; j < num3; j++) { if (s[j] == ']') { num2 = j; break; } } if (num2 < 0) { return false; } if (!ChatColors.TryResolve(s.Substring(num, num2 - num), out var tag)) { return false; } int num4 = num2 + 1; int num5 = FindColorClose(s, num4, end); if (num5 < 0 || num5 == num4) { return false; } Emit(s, num4, num5, tag, "", b, depth, ref tagBudget); next = num5 + "[/color]".Length; return true; } private static int FindColorClose(string s, int from, int end) { int num = 0; for (int i = from; i < end; i++) { if (s[i] != '[') { continue; } if (Matches(s, i, end, "[/color]")) { if (num == 0) { return i; } num--; i += "[/color]".Length - 1; } else if (Matches(s, i, end, "[color=")) { num++; i += "[color=".Length - 1; } } return -1; } private static void Emit(string s, int contentStart, int contentEnd, string open, string close, TmpLineBuilder b, int depth, ref int tagBudget) { tagBudget--; b.Tag(open); Parse(s, contentStart, contentEnd, b, depth + 1, ref tagBudget); b.Tag(close); } private static bool Matches(string s, int i, int end, string token) { if (i + token.Length > end) { return false; } for (int j = 0; j < token.Length; j++) { if (s[i + j] != token[j]) { return false; } } return true; } private static bool IsBoundary(string s, int index, int end) { if (index < 0 || index >= end) { return true; } char c = s[index]; if (!char.IsLetterOrDigit(c)) { return c != '_'; } return false; } } internal sealed class TmpLineBuilder { private readonly StringBuilder _sb = new StringBuilder(160); private bool _inNoParse; public void Literal(string text) { if (!string.IsNullOrEmpty(text)) { for (int i = 0; i < text.Length; i++) { Literal(text[i]); } } } public void Literal(char c) { OpenNoParse(); _sb.Append(c); if (c == '<') { _sb.Append(""); _inNoParse = false; } } public void Tag(string tag) { CloseNoParse(); _sb.Append(tag); } public void GameMarkup(string markup) { CloseNoParse(); _sb.Append(markup); } public string Finish() { CloseNoParse(); return _sb.ToString(); } private void OpenNoParse() { if (!_inNoParse) { _sb.Append(""); _inNoParse = true; } } private void CloseNoParse() { if (_inNoParse) { _sb.Append(""); _inNoParse = false; } } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }