Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of BetterChat v1.6.4
BetterChat.dll
Decompiled 3 days agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using ConditionalConfigSync; using HarmonyLib; using Splatform; using TMPro; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("BetterChat")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Crystal")] [assembly: AssemblyProduct("BetterChat")] [assembly: AssemblyCopyright("Copyright © 2026 Crystal")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("a81c5907-42c8-48a8-a758-24fcacbfd3aa")] [assembly: AssemblyFileVersion("1.6.4.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyVersion("1.6.4.0")] namespace BetterChat; [BepInPlugin("dev.crystal.betterchat", "Better Chat", "1.6.4.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInProcess("valheim.exe")] [BepInProcess("valheim_server.exe")] public class BetterChatPlugin : BaseUnityPlugin { [HarmonyPatch(typeof(Chat))] private static class Chat_Patches { [HarmonyPatch("Awake")] [HarmonyPostfix] private static void Awake_Postfix(Chat __instance) { __instance.m_hideDelay = HideDelay.Value; sChat = __instance; Graphic[] componentsInChildren = ((Component)((Terminal)__instance).m_chatWindow).GetComponentsInChildren<Graphic>(); for (int i = 0; i < componentsInChildren.Length; i++) { componentsInChildren[i].raycastTarget = false; } } } [HarmonyPatch(typeof(Player))] private static class Player_Patches { [HarmonyPatch("Awake")] [HarmonyPostfix] private static void Awake_Postfix(Player __instance) { Talker component = ((Component)__instance).GetComponent<Talker>(); ApplyChatDistances(component); sTalkers.Add(component); } [HarmonyPatch("OnDestroy")] [HarmonyPrefix] private static void OnDestroy_Prefix(Player __instance) { sTalkers.Remove(((Component)__instance).GetComponent<Talker>()); } } [HarmonyPatch(typeof(Chat))] private static class Chat_AlwaysShow_Patch { [HarmonyPatch("Update")] [HarmonyPrefix] private static bool Update_Prefix(Chat __instance) { sHideTimerField.SetValue(__instance, 0f); return true; } } [HarmonyPatch(typeof(Chat))] private static class Chat_Slash_Patches { private enum TranspilerState { Searching, Inserting, Searching2, Labeling, Finishing } [HarmonyPatch("Update")] [HarmonyTranspiler] private static IEnumerable<CodeInstruction> Update_Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator) { LocalBuilder isSlashPressed = generator.DeclareLocal(typeof(bool)); isSlashPressed.SetLocalSymInfo("isSlashPressed"); Label label = generator.DefineLabel(); yield return new CodeInstruction(OpCodes.Ldc_I4_0, (object)null); yield return new CodeInstruction(OpCodes.Stloc, (object)isSlashPressed.LocalIndex); TranspilerState state = TranspilerState.Searching; foreach (CodeInstruction instruction in instructions) { switch (state) { case TranspilerState.Inserting: if (instruction.opcode != OpCodes.Brtrue) { throw new InvalidOperationException(string.Format("[BetterChat] {0} encountered unexpected IL code. Unable to patch. This is most likely due to a game update changing the target code. Disable {1} in the config as a workaround until the mod can be fixed. Details: Search failed, opcode={2}", "Chat_Slash_Patches", "SlashOpensChat", instruction.opcode)); } yield return instruction.Clone(); yield return new CodeInstruction(OpCodes.Ldc_I4, (object)47); yield return new CodeInstruction(OpCodes.Call, (object)typeof(Input).GetMethod("GetKeyDown", new Type[1] { typeof(KeyCode) })); yield return new CodeInstruction(OpCodes.Stloc, (object)isSlashPressed.LocalIndex); yield return new CodeInstruction(OpCodes.Ldloc, (object)isSlashPressed.LocalIndex); state = TranspilerState.Searching2; break; case TranspilerState.Labeling: instruction.labels.Add(label); state = TranspilerState.Finishing; break; } yield return instruction; if (state == TranspilerState.Searching && instruction.opcode == OpCodes.Call) { if (((MethodBase)instruction.operand).Name == "GetButtonDown") { state = TranspilerState.Inserting; } } else if (state == TranspilerState.Searching2 && instruction.opcode == OpCodes.Callvirt && ((MethodBase)instruction.operand).Name == "ActivateInputField") { yield return new CodeInstruction(OpCodes.Ldloc, (object)isSlashPressed.LocalIndex); yield return new CodeInstruction(OpCodes.Brfalse, (object)label); yield return new CodeInstruction(OpCodes.Ldarg_0, (object)null); yield return new CodeInstruction(OpCodes.Ldfld, (object)typeof(Terminal).GetField("m_input")); yield return new CodeInstruction(OpCodes.Ldstr, (object)"/"); yield return new CodeInstruction(OpCodes.Call, (object)typeof(TMP_InputField).GetMethod("set_text")); yield return new CodeInstruction(OpCodes.Ldc_I4_1, (object)null); yield return new CodeInstruction(OpCodes.Stsfld, (object)typeof(BetterChatPlugin).GetField("sMoveCaretToEnd", BindingFlags.Static | BindingFlags.NonPublic)); state = TranspilerState.Labeling; } } if (state != TranspilerState.Finishing) { throw new InvalidOperationException("[BetterChat] Chat_Slash_Patches encountered unexpected IL code. Unable to patch. This is most likely due to a game update changing the target code. Disable SlashOpensChat in the config as a workaround until the mod can be fixed. Details: Never reached finishing state"); } } [HarmonyPatch("LateUpdate")] [HarmonyPostfix] private static void LateUpdate_Postfix(Chat __instance) { if (sMoveCaretToEnd) { ((TMP_InputField)((Terminal)__instance).m_input).MoveTextEnd(false); sMoveCaretToEnd = false; } } } [HarmonyPatch(typeof(Chat))] private static class Chat_Hide_Patch { private static float sLastHideTimer; [HarmonyPatch("OnNewChatMessage")] [HarmonyPrefix] private static void OnNewChatMessage_Pretfix(Chat __instance, GameObject go, long senderID, Vector3 pos, Type type, UserInfo sender, string text) { sLastHideTimer = (float)sHideTimerField.GetValue(__instance); } [HarmonyPatch("OnNewChatMessage")] [HarmonyPostfix] private static void OnNewChatMessage_Postfix(Chat __instance, GameObject go, long senderID, Vector3 pos, Type type, UserInfo sender, string text) { sHideTimerField.SetValue(__instance, sLastHideTimer); } } [HarmonyPatch(typeof(Terminal))] private static class Chat_MixedCase_Terminal_Patch { [HarmonyTranspiler] [HarmonyPatch("AddString", new Type[] { typeof(string), typeof(string), typeof(Type), typeof(bool) })] [HarmonyPatch("AddString", new Type[] { typeof(PlatformUserID), typeof(string), typeof(Type), typeof(bool) })] private static IEnumerable<CodeInstruction> AddString_Transpiler(IEnumerable<CodeInstruction> instructions) { return StripForcedCase(instructions); } } [HarmonyPatch(typeof(Chat))] private static class Chat_MixedCase_Chat_Patch { [HarmonyPatch("AddInworldText")] [HarmonyTranspiler] private static IEnumerable<CodeInstruction> AddInworldText_Transpiler(IEnumerable<CodeInstruction> instructions) { return StripForcedCase(instructions); } } [HarmonyPatch(typeof(Chat))] private static class Chat_Shout_Patch { [HarmonyPatch("InputText")] [HarmonyTranspiler] private static IEnumerable<CodeInstruction> InputText_Transpiler(IEnumerable<CodeInstruction> instructions) { foreach (CodeInstruction instruction in instructions) { if (instruction.opcode == OpCodes.Ldstr && instruction.operand.Equals("say ")) { instruction.operand = "s "; } yield return instruction; } } } [HarmonyPatch(typeof(Minimap))] private static class Minimap_Patches { [HarmonyPatch("UpdateDynamicPins")] [HarmonyTranspiler] private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { List<CodeInstruction> list = new List<CodeInstruction>(instructions); for (int i = 0; i < list.Count - 1; i++) { if (list[i + 1].opcode == OpCodes.Call && ((MethodBase)list[i + 1].operand).Name == "UpdateShoutPins") { list.RemoveRange(i, 2); break; } } return list; } } public const string ModId = "dev.crystal.betterchat"; public const string ModName = "Better Chat"; public const string ModVersion = "1.6.4.0"; internal static readonly ConfigSync ConfigSync; public static ConfigEntry<bool> ShowOnNewMessage; public static ConfigEntry<bool> AlwaysVisible; public static ConfigEntry<float> HideDelay; public static ConfigEntry<bool> ForceCase; public static ConfigEntry<bool> SlashOpensChat; public static ConfigEntry<bool> DefaultShout; public static ConfigEntry<bool> ShowShoutPings; public static ConfigEntry<float> TalkDistance; public static ConfigEntry<float> WhisperDistance; private static Harmony sChatAwakeHarmony; private static Harmony sPlayerHarmony; private static Harmony sChatHideHarmony; private static Harmony sChatAlwaysShowHarmony; private static Harmony sChatMixedCaseHarmony; private static Harmony sChatShoutHarmony; private static Harmony sMinimapHarmony; private static Harmony sChatSlashHarmony; private static Chat sChat; private static List<Talker> sTalkers; private static readonly FieldInfo sHideTimerField; private static bool sMoveCaretToEnd; static BetterChatPlugin() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown ConfigSync = new ConfigSync("dev.crystal.betterchat") { DisplayName = "Better Chat", CurrentVersion = "1.6.4.0", MinimumRequiredVersion = "1.6.4.0", ModRequired = false, ModRequirementMode = (ModRequirementMode)1 }; sMoveCaretToEnd = false; sTalkers = new List<Talker>(); sHideTimerField = typeof(Chat).GetField("m_hideTimer", BindingFlags.Instance | BindingFlags.NonPublic); } private void Awake() { //IL_0298: Unknown result type (might be due to invalid IL or missing references) //IL_02a2: Expected O, but got Unknown //IL_02a7: Unknown result type (might be due to invalid IL or missing references) //IL_02b1: Expected O, but got Unknown //IL_02b6: Unknown result type (might be due to invalid IL or missing references) //IL_02c0: Expected O, but got Unknown //IL_02c5: Unknown result type (might be due to invalid IL or missing references) //IL_02cf: Expected O, but got Unknown //IL_02d4: Unknown result type (might be due to invalid IL or missing references) //IL_02de: Expected O, but got Unknown //IL_02e3: Unknown result type (might be due to invalid IL or missing references) //IL_02ed: Expected O, but got Unknown //IL_02f2: Unknown result type (might be due to invalid IL or missing references) //IL_02fc: Expected O, but got Unknown //IL_0301: Unknown result type (might be due to invalid IL or missing references) //IL_030b: Expected O, but got Unknown ShowOnNewMessage = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "ShowOnNewMessage", true, "If True, the chat window will appear when new messages are received. If False, the chat window will remain hidden until opened via player input. The AlwaysVisible setting overrides this setting, if enabled."); ShowOnNewMessage.SettingChanged += ShowChat_SettingChanged; ((ConditionalConfigSync)ConfigSync).AddConfigEntry<bool>(ShowOnNewMessage, (ConfigSyncMode)2); AlwaysVisible = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "AlwaysVisible", false, "If True, the chat window will remain visible at all times. If False, the behavior will depend on the ShowOnNewMessage setting."); AlwaysVisible.SettingChanged += ShowChat_SettingChanged; ((ConditionalConfigSync)ConfigSync).AddConfigEntry<bool>(AlwaysVisible, (ConfigSyncMode)2); HideDelay = ((BaseUnityPlugin)this).Config.Bind<float>("Chat", "HideDelay", 10f, "The time, in seconds, to keep the chat window visible after sending or receiving a message. Minimum is 0.5. Has no effect if AlwaysVisible=true."); HideDelay.SettingChanged += HideDelay_SettingChanged; ((ConditionalConfigSync)ConfigSync).AddConfigEntry<float>(HideDelay, (ConfigSyncMode)2); ForceCase = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "ForceCase", false, "If True, shout will be in all caps and whisper will be in all lowercase (game default). If False, messages will appear as they were originally entered."); ForceCase.SettingChanged += ForceCase_SettingChanged; ((ConditionalConfigSync)ConfigSync).AddConfigEntry<bool>(ForceCase, (ConfigSyncMode)2); SlashOpensChat = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "SlashOpensChat", true, "If True, pressing the slash key (/) will open the chat window and start a message."); SlashOpensChat.SettingChanged += SlashOpensChat_SettingChanged; ((ConditionalConfigSync)ConfigSync).AddConfigEntry<bool>(SlashOpensChat, (ConfigSyncMode)2); DefaultShout = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "DefaultShout", false, "If True, text entered will shout by default - type /say for talk. If False, chat will be talk by default - type /s for shout."); DefaultShout.SettingChanged += DefaultShout_SettingChanged; ((ConditionalConfigSync)ConfigSync).AddConfigEntry<bool>(DefaultShout, (ConfigSyncMode)2); ShowShoutPings = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "ShowShoutPings", true, "If True, pings will show on your map when players shout (game default). If False, the pings will not show. (Other players can still see your shout pings.) [The value may be enforced on a server via sync policy.]"); ShowShoutPings.SettingChanged += ShowShoutPings_SettingChanged; ((ConditionalConfigSync)ConfigSync).AddConfigEntry<bool>(ShowShoutPings, (ConfigSyncMode)1, true); TalkDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Chat", "TalkDistance", 15f, "The maximum distance from a player at which you will receive their normal chat messages (not whisper or shout). Game default is 15. Acceptable range is 1-100. [The value may be enforced on a server via sync policy.]"); TalkDistance.SettingChanged += Distance_SettingChanged; ((ConditionalConfigSync)ConfigSync).AddConfigEntry<float>(TalkDistance, (ConfigSyncMode)1, true); WhisperDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Chat", "WhisperDistance", 4f, "The maximum distance from a player at which you will receive their whispered chat messages. Game default is 4. Acceptable range is 1-20. [The value may be enforced on a server via sync policy.]"); WhisperDistance.SettingChanged += Distance_SettingChanged; ((ConditionalConfigSync)ConfigSync).AddConfigEntry<float>(WhisperDistance, (ConfigSyncMode)1, true); ClampConfig(); sChatAwakeHarmony = new Harmony("dev.crystal.betterchat_ChatAwake"); sPlayerHarmony = new Harmony("dev.crystal.betterchat_Player"); sChatHideHarmony = new Harmony("dev.crystal.betterchat_ChatHide"); sChatAlwaysShowHarmony = new Harmony("dev.crystal.betterchat_ChatAlwaysShow"); sChatMixedCaseHarmony = new Harmony("dev.crystal.betterchat_ChatMixedCase"); sChatShoutHarmony = new Harmony("dev.crystal.betterchat_ChatShout"); sMinimapHarmony = new Harmony("dev.crystal.betterchat_Minimap"); sChatSlashHarmony = new Harmony("dev.crystal.betterchat_ChatSlash"); sChatAwakeHarmony.PatchAll(typeof(Chat_Patches)); sPlayerHarmony.PatchAll(typeof(Player_Patches)); if (AlwaysVisible.Value) { sChatAlwaysShowHarmony.PatchAll(typeof(Chat_AlwaysShow_Patch)); } else if (!ShowOnNewMessage.Value) { sChatHideHarmony.PatchAll(typeof(Chat_Hide_Patch)); } if (!ForceCase.Value) { sChatMixedCaseHarmony.PatchAll(typeof(Chat_MixedCase_Terminal_Patch)); sChatMixedCaseHarmony.PatchAll(typeof(Chat_MixedCase_Chat_Patch)); } if (DefaultShout.Value) { sChatShoutHarmony.PatchAll(typeof(Chat_Shout_Patch)); } if (!ShowShoutPings.Value) { sMinimapHarmony.PatchAll(typeof(Minimap_Patches)); } if (SlashOpensChat.Value) { sChatSlashHarmony.PatchAll(typeof(Chat_Slash_Patches)); } } private void OnDestroy() { sChatAwakeHarmony.UnpatchSelf(); sPlayerHarmony.UnpatchSelf(); sChatHideHarmony.UnpatchSelf(); sChatAlwaysShowHarmony.UnpatchSelf(); sChatMixedCaseHarmony.UnpatchSelf(); sChatShoutHarmony.UnpatchSelf(); sMinimapHarmony.UnpatchSelf(); sChatSlashHarmony.UnpatchSelf(); } private static void ClampConfig() { if (HideDelay.Value < 0.5f) { HideDelay.Value = 0.5f; } if (HideDelay.Value > 3600f) { HideDelay.Value = 3600f; } if (TalkDistance.Value < 1f) { TalkDistance.Value = 1f; } if (TalkDistance.Value > 100f) { TalkDistance.Value = 100f; } if (WhisperDistance.Value < 1f) { WhisperDistance.Value = 1f; } if (WhisperDistance.Value > 20f) { WhisperDistance.Value = 20f; } } private void ShowChat_SettingChanged(object sender, EventArgs e) { sChatHideHarmony.UnpatchSelf(); sChatAlwaysShowHarmony.UnpatchSelf(); if (AlwaysVisible.Value) { sChatAlwaysShowHarmony.PatchAll(typeof(Chat_AlwaysShow_Patch)); } else if (!ShowOnNewMessage.Value) { sChatHideHarmony.PatchAll(typeof(Chat_Hide_Patch)); } } private void HideDelay_SettingChanged(object sender, EventArgs e) { ClampConfig(); if ((Object)(object)sChat != (Object)null) { sChat.m_hideDelay = HideDelay.Value; } } private void ForceCase_SettingChanged(object sender, EventArgs e) { if (ForceCase.Value) { sChatMixedCaseHarmony.UnpatchSelf(); return; } sChatMixedCaseHarmony.PatchAll(typeof(Chat_MixedCase_Terminal_Patch)); sChatMixedCaseHarmony.PatchAll(typeof(Chat_MixedCase_Chat_Patch)); } private void SlashOpensChat_SettingChanged(object sender, EventArgs e) { if (SlashOpensChat.Value) { sChatSlashHarmony.PatchAll(typeof(Chat_Slash_Patches)); } else { sChatSlashHarmony.UnpatchSelf(); } } private void DefaultShout_SettingChanged(object sender, EventArgs e) { if (DefaultShout.Value) { sChatShoutHarmony.PatchAll(typeof(Chat_Shout_Patch)); } else { sChatShoutHarmony.UnpatchSelf(); } } private void ShowShoutPings_SettingChanged(object sender, EventArgs e) { if (ShowShoutPings.Value) { sMinimapHarmony.UnpatchSelf(); } else { sMinimapHarmony.PatchAll(typeof(Minimap_Patches)); } } private void Distance_SettingChanged(object sender, EventArgs e) { ClampConfig(); foreach (Talker sTalker in sTalkers) { ApplyChatDistances(sTalker); } } private static void ApplyChatDistances(Talker talker) { talker.m_visperDistance = WhisperDistance.Value; talker.m_normalDistance = TalkDistance.Value; } private static IEnumerable<CodeInstruction> StripForcedCase(IEnumerable<CodeInstruction> instructions) { List<CodeInstruction> list = new List<CodeInstruction>(instructions); for (int i = 0; i < list.Count; i++) { if (list[i].opcode == OpCodes.Callvirt) { MethodBase methodBase = list[i].operand as MethodBase; if (methodBase != null && (methodBase.Name == "ToLowerInvariant" || methodBase.Name == "ToUpper")) { list.RemoveRange(i - 1, 3); i -= 2; } } } return list; } }