using 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 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 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 DeathCountsBySteamId = new Dictionary(); public static Dictionary PlayerNamesBySteamId = new Dictionary(); private static ConfigEntry showOverlay = null; private static ConfigEntry overlayCorner = null; private static ConfigEntry titleFontSize = null; private static ConfigEntry itemFontSize = null; private static ConfigEntry titleColorHex = null; private static ConfigEntry itemColorHex = null; private static ConfigEntry 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("Display", "ShowOverlay", true, "Show the death-count overlay in the truck lobby."); overlayCorner = ((BaseUnityPlugin)this).Config.Bind("Display", "Corner", OverlayCorner.TopRight, "Which screen corner the overlay is anchored to."); titleFontSize = ((BaseUnityPlugin)this).Config.Bind("Display", "TitleFontSize", 24, "Font size of the header."); itemFontSize = ((BaseUnityPlugin)this).Config.Bind("Display", "FontSize", 20, "Font size of each player line."); titleColorHex = ((BaseUnityPlugin)this).Config.Bind("Display", "TitleColorHex", "FF3333", "Header text color, hex RGB (e.g. FF3333). '#' optional."); itemColorHex = ((BaseUnityPlugin)this).Config.Bind("Display", "TextColorHex", "FFFFFF", "Player-line text color, hex RGB (e.g. FFFFFF). '#' optional."); sortMode = ((BaseUnityPlugin)this).Config.Bind("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(); 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 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()); 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()); } else { foreach (string item in list) { GUILayout.Label(item, val2, Array.Empty()); } } GUILayout.EndArea(); } private static List BuildSortedLines() { List> list = new List>(DeathCountsBySteamId); switch (sortMode.Value) { case SortMode.DeathsAscending: list.Sort((KeyValuePair a, KeyValuePair b) => a.Value.CompareTo(b.Value)); break; case SortMode.DeathsDescending: list.Sort((KeyValuePair a, KeyValuePair b) => b.Value.CompareTo(a.Value)); break; case SortMode.NameAscending: list.Sort((KeyValuePair a, KeyValuePair b) => string.Compare(NameOf(a.Key), NameOf(b.Key), StringComparison.OrdinalIgnoreCase)); break; case SortMode.NameDescending: list.Sort((KeyValuePair a, KeyValuePair b) => string.Compare(NameOf(b.Key), NameOf(a.Key), StringComparison.OrdinalIgnoreCase)); break; } List list2 = new List(list.Count); foreach (KeyValuePair 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); } } } }