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 RepoDeathCounter v1.1.0
Matsu.RepoDeathCounter.dll
Decompiled 39 minutes agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using ExitGames.Client.Photon; using HarmonyLib; using Microsoft.CodeAnalysis; using Photon.Pun; using Photon.Realtime; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("Matsu.RepoDeathCounter")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.1.0.0")] [assembly: AssemblyInformationalVersion("1.1.0")] [assembly: AssemblyProduct("Matsu.RepoDeathCounter")] [assembly: AssemblyTitle("Matsu.RepoDeathCounter")] [assembly: AssemblyVersion("1.1.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [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 Matsu.RepoDeathCounter { internal sealed class DeathSyncBehaviour : MonoBehaviour, IInRoomCallbacks, IOnEventCallback { internal const byte DeathSyncEventCode = 77; private static MethodInfo? isMasterOrSpMethod; private static bool isMasterResolved; private static FieldInfo? statsInstanceField; private static FieldInfo? saveFileCurrentField; private static bool statsResolved; private string? loadedSaveFile; private float pollTimer; private bool callbacksRegistered; private static string SaveDir => Path.Combine(Application.persistentDataPath, "MatsuDeathCounter"); private void OnDestroy() { if (callbacksRegistered) { try { PhotonNetwork.RemoveCallbackTarget((object)this); } catch { } callbacksRegistered = false; } } private void Update() { try { bool flag = false; try { flag = PhotonNetwork.InRoom && !PhotonNetwork.OfflineMode; } catch { } if (flag && !callbacksRegistered) { try { PhotonNetwork.AddCallbackTarget((object)this); callbacksRegistered = true; } catch (Exception ex) { Plugin.TrackerErr("[DeathCounter] AddCallbackTarget failed: " + ex.Message); } } else if (!flag && callbacksRegistered) { try { PhotonNetwork.RemoveCallbackTarget((object)this); } catch { } callbacksRegistered = false; } if (!IsHost()) { return; } pollTimer += Time.deltaTime; if (pollTimer < 1f) { return; } pollTimer = 0f; string saveFileCurrent = GetSaveFileCurrent(); if (!(saveFileCurrent == loadedSaveFile)) { loadedSaveFile = saveFileCurrent; Plugin.DeathCountsBySteamId.Clear(); Plugin.PlayerNamesBySteamId.Clear(); if (!string.IsNullOrEmpty(saveFileCurrent)) { LoadFromDisk(saveFileCurrent); BroadcastDeaths(); } } } catch (Exception ex2) { Plugin.TrackerErr("[DeathCounter] Update error: " + ex2.Message); } } public void OnEvent(EventData photonEvent) { if (photonEvent.Code != 77 || IsHost()) { return; } try { if (!(photonEvent.CustomData is object[] array) || array.Length < 3) { return; } string[] array2 = array[0] as string[]; int[] array3 = array[1] as int[]; string[] array4 = array[2] as string[]; if (array2 == null || array3 == null || array4 == null) { return; } Plugin.DeathCountsBySteamId.Clear(); Plugin.PlayerNamesBySteamId.Clear(); for (int i = 0; i < array2.Length && i < array3.Length; i++) { if (!string.IsNullOrEmpty(array2[i])) { Plugin.DeathCountsBySteamId[array2[i]] = array3[i]; if (i < array4.Length && !string.IsNullOrEmpty(array4[i])) { Plugin.PlayerNamesBySteamId[array2[i]] = array4[i]; } } } } catch (Exception ex) { Plugin.TrackerErr("[DeathCounter] apply snapshot failed: " + ex.Message); } } public void OnPlayerEnteredRoom(Player newPlayer) { if (IsHost()) { BroadcastDeaths(); } } public void OnPlayerLeftRoom(Player otherPlayer) { } public void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged) { } public void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps) { } public void OnMasterClientSwitched(Player newMasterClient) { } internal static void OnHostDeath(string steamId, string name) { string text = ((!string.IsNullOrEmpty(steamId)) ? steamId : name); if (!string.IsNullOrEmpty(text)) { Plugin.DeathCountsBySteamId.TryGetValue(text, out var value); Plugin.DeathCountsBySteamId[text] = value + 1; if (!string.IsNullOrEmpty(name)) { Plugin.PlayerNamesBySteamId[text] = name; } Plugin.TrackerLog($"[DeathCounter] {(string.IsNullOrEmpty(name) ? text : name)} died. Total: {Plugin.DeathCountsBySteamId[text]}"); SaveToDisk(); BroadcastDeaths(); } } private static void BroadcastDeaths() { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Expected O, but got Unknown try { if (PhotonNetwork.InRoom && !PhotonNetwork.OfflineMode) { PhotonNetwork.RaiseEvent((byte)77, (object)Pack(), new RaiseEventOptions { Receivers = (ReceiverGroup)0 }, SendOptions.SendReliable); } } catch (Exception ex) { Plugin.TrackerErr("[DeathCounter] broadcast failed: " + ex.Message); } } private static object[] Pack() { int count = Plugin.DeathCountsBySteamId.Count; string[] array = new string[count]; int[] array2 = new int[count]; string[] array3 = new string[count]; int num = 0; foreach (KeyValuePair<string, int> item in Plugin.DeathCountsBySteamId) { array[num] = item.Key; array2[num] = item.Value; array3[num] = (Plugin.PlayerNamesBySteamId.TryGetValue(item.Key, out string value) ? value : ""); num++; } return new object[3] { array, array2, array3 }; } private static string SavePathFor(string saveFile) { return Path.Combine(SaveDir, Sanitize(saveFile) + ".tsv"); } private static string Sanitize(string s) { char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); foreach (char oldChar in invalidFileNameChars) { s = s.Replace(oldChar, '_'); } return s; } internal static void SaveToDisk() { string saveFileCurrent = GetSaveFileCurrent(); if (string.IsNullOrEmpty(saveFileCurrent)) { return; } try { Directory.CreateDirectory(SaveDir); StringBuilder stringBuilder = new StringBuilder(); foreach (KeyValuePair<string, int> item in Plugin.DeathCountsBySteamId) { string text = (Plugin.PlayerNamesBySteamId.TryGetValue(item.Key, out string value) ? value : ""); text = text.Replace('\t', ' ').Replace('\n', ' ').Replace('\r', ' '); stringBuilder.Append(item.Key).Append('\t').Append(item.Value) .Append('\t') .Append(text) .Append('\n'); } File.WriteAllText(SavePathFor(saveFileCurrent), stringBuilder.ToString()); } catch (Exception ex) { Plugin.TrackerErr("[DeathCounter] save failed: " + ex.Message); } } private static void LoadFromDisk(string saveFile) { try { string path = SavePathFor(saveFile); if (!File.Exists(path)) { return; } string[] array = File.ReadAllLines(path); foreach (string text in array) { if (string.IsNullOrWhiteSpace(text)) { continue; } string[] array2 = text.Split(new char[1] { '\t' }); if (array2.Length < 2) { continue; } string text2 = array2[0]; if (!string.IsNullOrEmpty(text2) && int.TryParse(array2[1], out var result)) { Plugin.DeathCountsBySteamId[text2] = result; if (array2.Length >= 3 && !string.IsNullOrEmpty(array2[2])) { Plugin.PlayerNamesBySteamId[text2] = array2[2]; } } } } catch (Exception ex) { Plugin.TrackerErr("[DeathCounter] load failed: " + ex.Message); } } internal static bool IsHost() { if (!isMasterResolved) { Type type = AccessTools.TypeByName("SemiFunc"); isMasterOrSpMethod = ((type == null) ? null : AccessTools.Method(type, "IsMasterClientOrSingleplayer", (Type[])null, (Type[])null)); isMasterResolved = true; } if (isMasterOrSpMethod != null) { try { return (bool)isMasterOrSpMethod.Invoke(null, null); } catch { } } try { return PhotonNetwork.OfflineMode || !PhotonNetwork.InRoom || PhotonNetwork.IsMasterClient; } catch { return true; } } private static string GetSaveFileCurrent() { if (!statsResolved) { Type type = AccessTools.TypeByName("StatsManager"); statsInstanceField = ((type == null) ? null : AccessTools.Field(type, "instance")); saveFileCurrentField = ((type == null) ? null : AccessTools.Field(type, "saveFileCurrent")); statsResolved = true; } try { object obj = statsInstanceField?.GetValue(null); if (obj == null) { return ""; } return (saveFileCurrentField?.GetValue(obj) as string) ?? ""; } catch { return ""; } } } public enum OverlayCorner { TopRight, TopLeft, BottomRight, BottomLeft } public enum SortMode { DeathsAscending, DeathsDescending, NameAscending, NameDescending } [BepInPlugin("matsu.repo.deathcounter", "REPO Death Counter", "1.1.0")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "matsu.repo.deathcounter"; public const string PluginName = "REPO Death Counter"; public const string PluginVersion = "1.1.0"; internal static ManualLogSource? Log; public static Dictionary<string, int> DeathCountsBySteamId = new Dictionary<string, int>(); public static Dictionary<string, string> PlayerNamesBySteamId = new Dictionary<string, string>(); private static ConfigEntry<bool> showOverlay = null; private static ConfigEntry<OverlayCorner> overlayCorner = null; private static ConfigEntry<int> titleFontSize = null; private static ConfigEntry<int> itemFontSize = null; private static ConfigEntry<string> titleColorHex = null; private static ConfigEntry<string> itemColorHex = null; private static ConfigEntry<SortMode> sortMode = null; private static bool runManagerReflectionResolved; private static FieldInfo? runManagerInstanceField; private static FieldInfo? runManagerLevelCurrentField; private static FieldInfo? runManagerLevelLobbyField; private Harmony harmony; private void Awake() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Expected O, but got Unknown //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_015e: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; harmony = new Harmony("matsu.repo.deathcounter"); showOverlay = ((BaseUnityPlugin)this).Config.Bind<bool>("Display", "ShowOverlay", true, "Show the death-count overlay in the truck lobby."); overlayCorner = ((BaseUnityPlugin)this).Config.Bind<OverlayCorner>("Display", "Corner", OverlayCorner.TopRight, "Which screen corner the overlay is anchored to."); titleFontSize = ((BaseUnityPlugin)this).Config.Bind<int>("Display", "TitleFontSize", 24, "Font size of the header."); itemFontSize = ((BaseUnityPlugin)this).Config.Bind<int>("Display", "FontSize", 20, "Font size of each player line."); titleColorHex = ((BaseUnityPlugin)this).Config.Bind<string>("Display", "TitleColorHex", "FF3333", "Header text color, hex RGB (e.g. FF3333). '#' optional."); itemColorHex = ((BaseUnityPlugin)this).Config.Bind<string>("Display", "TextColorHex", "FFFFFF", "Player-line text color, hex RGB (e.g. FFFFFF). '#' optional."); sortMode = ((BaseUnityPlugin)this).Config.Bind<SortMode>("Display", "SortMode", SortMode.DeathsAscending, "List order. DeathsAscending = fewest deaths on top."); try { Type type = AccessTools.TypeByName("PlayerAvatar"); MethodInfo methodInfo = ((type == null) ? null : AccessTools.Method(type, "PlayerDeathRPC", (Type[])null, (Type[])null)); if (methodInfo != null) { harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(AccessTools.Method(typeof(Plugin), "PlayerDeathRpcPrefix", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Log.LogInfo((object)"Patched PlayerAvatar.PlayerDeathRPC for the death counter."); } else { Log.LogWarning((object)"PlayerAvatar.PlayerDeathRPC not found; the death counter is disabled for this game version."); } } catch (Exception arg) { Log.LogError((object)$"Failed to patch PlayerDeathRPC: {arg}"); } ((Component)this).gameObject.AddComponent<DeathSyncBehaviour>(); Log.LogInfo((object)"REPO Death Counter 1.1.0 loaded."); } private void OnGUI() { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_0144: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Expected O, but got Unknown //IL_0154: Unknown result type (might be due to invalid IL or missing references) //IL_0175: Unknown result type (might be due to invalid IL or missing references) //IL_017a: Unknown result type (might be due to invalid IL or missing references) //IL_0181: Unknown result type (might be due to invalid IL or missing references) //IL_0188: Unknown result type (might be due to invalid IL or missing references) //IL_0189: Unknown result type (might be due to invalid IL or missing references) //IL_0192: Expected O, but got Unknown //IL_0199: Unknown result type (might be due to invalid IL or missing references) if (!showOverlay.Value || !IsInTruckLobby()) { return; } int num = Mathf.Clamp(titleFontSize.Value, 6, 100); int num2 = Mathf.Clamp(itemFontSize.Value, 6, 100); Color textColor = ParseColor(titleColorHex.Value, Color.red); Color textColor2 = ParseColor(itemColorHex.Value, Color.white); OverlayCorner value = overlayCorner.Value; bool num3 = value == OverlayCorner.TopRight || value == OverlayCorner.BottomRight; bool flag = value == OverlayCorner.BottomLeft || value == OverlayCorner.BottomRight; TextAnchor alignment = (TextAnchor)(num3 ? 2 : 0); List<string> list = BuildSortedLines(); int num4 = Mathf.Max(1, list.Count); float num5 = (float)num + 10f + (float)num4 * ((float)num2 + 6f) + 20f; float num6 = (num3 ? ((float)Screen.width - 440f - 20f) : 20f); float num7 = (flag ? Mathf.Max(20f, (float)Screen.height - num5 - 20f) : 20f); GUILayout.BeginArea(new Rect(num6, num7, 440f, num5)); GUI.color = Color.white; GUIStyle val = new GUIStyle(GUI.skin.label) { fontSize = num, fontStyle = (FontStyle)1, alignment = alignment }; val.normal.textColor = textColor; GUILayout.Label("--- Player Deaths ---", val, Array.Empty<GUILayoutOption>()); GUIStyle val2 = new GUIStyle(GUI.skin.label) { fontSize = num2, fontStyle = (FontStyle)0, alignment = alignment }; val2.normal.textColor = textColor2; if (list.Count == 0) { GUILayout.Label("No deaths recorded yet.", val2, Array.Empty<GUILayoutOption>()); } else { foreach (string item in list) { GUILayout.Label(item, val2, Array.Empty<GUILayoutOption>()); } } GUILayout.EndArea(); } private static List<string> BuildSortedLines() { List<KeyValuePair<string, int>> list = new List<KeyValuePair<string, int>>(DeathCountsBySteamId); switch (sortMode.Value) { case SortMode.DeathsAscending: list.Sort((KeyValuePair<string, int> a, KeyValuePair<string, int> b) => a.Value.CompareTo(b.Value)); break; case SortMode.DeathsDescending: list.Sort((KeyValuePair<string, int> a, KeyValuePair<string, int> b) => b.Value.CompareTo(a.Value)); break; case SortMode.NameAscending: list.Sort((KeyValuePair<string, int> a, KeyValuePair<string, int> b) => string.Compare(NameOf(a.Key), NameOf(b.Key), StringComparison.OrdinalIgnoreCase)); break; case SortMode.NameDescending: list.Sort((KeyValuePair<string, int> a, KeyValuePair<string, int> b) => string.Compare(NameOf(b.Key), NameOf(a.Key), StringComparison.OrdinalIgnoreCase)); break; } List<string> list2 = new List<string>(list.Count); foreach (KeyValuePair<string, int> item in list) { list2.Add($"{NameOf(item.Key)}: {item.Value}"); } return list2; } private static string NameOf(string steamId) { if (!PlayerNamesBySteamId.TryGetValue(steamId, out string value) || string.IsNullOrEmpty(value)) { return steamId; } return value; } private static Color ParseColor(string hex, Color fallback) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrWhiteSpace(hex)) { return fallback; } if (!hex.StartsWith("#")) { hex = "#" + hex; } Color result = default(Color); if (!ColorUtility.TryParseHtmlString(hex, ref result)) { return fallback; } return result; } private static bool IsInTruckLobby() { try { if (!runManagerReflectionResolved) { Type type = AccessTools.TypeByName("RunManager"); if (type != null) { runManagerInstanceField = AccessTools.Field(type, "instance"); runManagerLevelCurrentField = AccessTools.Field(type, "levelCurrent"); runManagerLevelLobbyField = AccessTools.Field(type, "levelLobby"); } runManagerReflectionResolved = true; } if (runManagerInstanceField == null || runManagerLevelCurrentField == null || runManagerLevelLobbyField == null) { return false; } object value = runManagerInstanceField.GetValue(null); if (value == null) { return false; } object value2 = runManagerLevelCurrentField.GetValue(value); object value3 = runManagerLevelLobbyField.GetValue(value); return value2 != null && value3 != null && value2 == value3; } catch { return false; } } private static void PlayerDeathRpcPrefix(object __instance) { try { if (__instance != null && DeathSyncBehaviour.IsHost()) { Type type = __instance.GetType(); string steamId = (AccessTools.Field(type, "steamID")?.GetValue(__instance) as string) ?? ""; string name = (AccessTools.Field(type, "playerName")?.GetValue(__instance) as string) ?? ""; DeathSyncBehaviour.OnHostDeath(steamId, name); } } catch (Exception arg) { ManualLogSource? log = Log; if (log != null) { log.LogError((object)$"[DeathCounter] PlayerDeathRpcPrefix error: {arg}"); } } } internal static void TrackerLog(string message) { ManualLogSource? log = Log; if (log != null) { log.LogInfo((object)message); } } internal static void TrackerErr(string message) { ManualLogSource? log = Log; if (log != null) { log.LogError((object)message); } } } }