Decompiled source of RepoDeathCounter v1.1.0

Matsu.RepoDeathCounter.dll

Decompiled 39 minutes ago
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<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);
			}
		}
	}
}