Decompiled source of KillTracker v1.1.0

StraftatKillTracker.dll

Decompiled 6 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using ComputerysModdingUtilities;
using HarmonyLib;
using KillTracker.Patches;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: StraftatMod(true)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("StraftatKillTracker")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.1.0.0")]
[assembly: AssemblyInformationalVersion("1.1.0")]
[assembly: AssemblyProduct("StraftatKillTracker")]
[assembly: AssemblyTitle("StraftatKillTracker")]
[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.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace KillTracker
{
	public static class MatchStats
	{
		private struct Hit
		{
			public int VictimId;

			public int VictimInstance;

			public float Damage;

			public bool Lethal;

			public int Frame;
		}

		private struct KillerInfo
		{
			public int AttackerId;

			public int Frame;
		}

		private static readonly Dictionary<int, int> KillCounts = new Dictionary<int, int>();

		private static readonly Dictionary<int, float> DamageCounts = new Dictionary<int, float>();

		private static readonly HashSet<int> CountedDeaths = new HashSet<int>();

		private static readonly Dictionary<int, float> KnownHealth = new Dictionary<int, float>();

		private static readonly Dictionary<int, KillerInfo> KillerByVictim = new Dictionary<int, KillerInfo>();

		private static readonly Dictionary<int, Hit> WaitingForKiller = new Dictionary<int, Hit>();

		public static int GetKills(int playerId)
		{
			if (!KillCounts.TryGetValue(playerId, out var value))
			{
				return 0;
			}
			return value;
		}

		public static int GetDamage(int playerId)
		{
			if (!DamageCounts.TryGetValue(playerId, out var value))
			{
				return 0;
			}
			return Mathf.RoundToInt(value);
		}

		public static void ResetRound()
		{
			CountedDeaths.Clear();
			KillerByVictim.Clear();
			WaitingForKiller.Clear();
			KnownHealth.Clear();
			Plugin.Debug("[KillTracker] round reset");
		}

		public static void ResetMatch()
		{
			KillCounts.Clear();
			DamageCounts.Clear();
			KnownHealth.Clear();
			ResetRound();
			Plugin.Log.LogInfo((object)"[KillTracker] match reset - all counters cleared");
		}

		public static void OnHealthChanged(PlayerHealth victim, float newHealth)
		{
			if (!Plugin.Enabled.Value || (Object)(object)victim == (Object)null)
			{
				return;
			}
			int instanceID = ((Object)victim).GetInstanceID();
			float value;
			bool flag = KnownHealth.TryGetValue(instanceID, out value);
			bool flag2 = flag && value <= 0f;
			KnownHealth[instanceID] = newHealth;
			if (flag2 && newHealth <= 0f)
			{
				return;
			}
			float num = ((!(!flag || flag2)) ? value : ((victim.fullHealth > 0f) ? victim.fullHealth : 100f));
			float num2 = num - newHealth;
			if (num2 <= 0.01f)
			{
				return;
			}
			int num3 = ResolvePlayerId(victim);
			if (num3 < 0)
			{
				Plugin.Debug("[KillTracker] health change on an unresolvable PlayerHealth - ignored");
				return;
			}
			float num4 = Mathf.Min(num2, num);
			if (Plugin.ScaleDamageToHud.Value && victim.fullHealth > 0f)
			{
				num4 *= 100f / victim.fullHealth;
			}
			Hit hit = new Hit
			{
				VictimId = num3,
				VictimInstance = instanceID,
				Damage = num4,
				Lethal = (newHealth <= 0f),
				Frame = Time.frameCount
			};
			KillerInfo value2;
			if (victim.fellVoid || victim.suicide)
			{
				Credit(hit, -1);
			}
			else if (KillerByVictim.TryGetValue(instanceID, out value2))
			{
				Credit(hit, value2.AttackerId);
			}
			else
			{
				WaitingForKiller[instanceID] = hit;
			}
		}

		public static void OnKillerSet(PlayerHealth victim, Transform killerTransform)
		{
			if (Plugin.Enabled.Value && !((Object)(object)victim == (Object)null) && !((Object)(object)killerTransform == (Object)null))
			{
				int instanceID = ((Object)victim).GetInstanceID();
				int attackerId = ResolvePlayerId(killerTransform);
				KillerByVictim[instanceID] = new KillerInfo
				{
					AttackerId = attackerId,
					Frame = Time.frameCount
				};
				if (WaitingForKiller.TryGetValue(instanceID, out var value) && value.Frame == Time.frameCount)
				{
					WaitingForKiller.Remove(instanceID);
					Credit(value, attackerId);
				}
			}
		}

		public static void FlushStale()
		{
			if (WaitingForKiller.Count == 0)
			{
				return;
			}
			List<int> list = null;
			foreach (KeyValuePair<int, Hit> item in WaitingForKiller)
			{
				if (item.Value.Frame < Time.frameCount)
				{
					Credit(item.Value, -1);
					if (list == null)
					{
						list = new List<int>();
					}
					list.Add(item.Key);
				}
			}
			if (list != null)
			{
				for (int i = 0; i < list.Count; i++)
				{
					WaitingForKiller.Remove(list[i]);
				}
			}
		}

		private static void Credit(Hit hit, int attackerId)
		{
			if (attackerId < 0)
			{
				if (hit.Lethal)
				{
					CountedDeaths.Add(hit.VictimInstance);
				}
				Plugin.Debug($"[KillTracker] {hit.Damage:0} damage to {NameOf(hit.VictimId)} " + "without an attacker (environment) - not credited");
				return;
			}
			if (attackerId == hit.VictimId && !Plugin.CountSuicides.Value)
			{
				if (hit.Lethal)
				{
					CountedDeaths.Add(hit.VictimInstance);
				}
				Plugin.Debug("[KillTracker] self-damage by " + NameOf(attackerId) + " - not credited");
				return;
			}
			if (Plugin.TrackDamage.Value)
			{
				DamageCounts.TryGetValue(attackerId, out var value);
				DamageCounts[attackerId] = value + hit.Damage;
			}
			if (hit.Lethal && !CountedDeaths.Contains(hit.VictimInstance))
			{
				CountedDeaths.Add(hit.VictimInstance);
				KillCounts.TryGetValue(attackerId, out var value2);
				KillCounts[attackerId] = value2 + 1;
				Plugin.Debug("[KillTracker] KILL " + NameOf(attackerId) + " -> " + NameOf(hit.VictimId) + " " + $"| kills {KillCounts[attackerId]}, damage {GetDamage(attackerId)}");
			}
			else
			{
				Plugin.Debug($"[KillTracker] {hit.Damage:0} damage {NameOf(attackerId)} -> " + $"{NameOf(hit.VictimId)} | total {GetDamage(attackerId)}");
			}
		}

		public static int ResolvePlayerId(PlayerHealth health)
		{
			if ((Object)(object)health == (Object)null)
			{
				return -1;
			}
			if ((Object)(object)health.playerValues != (Object)null && (Object)(object)health.playerValues.playerClient != (Object)null)
			{
				return health.playerValues.playerClient.PlayerId;
			}
			return ResolvePlayerId(((Component)health).transform);
		}

		public static int ResolvePlayerId(Transform t)
		{
			if ((Object)(object)t == (Object)null)
			{
				return -1;
			}
			PlayerValues componentInParent = ((Component)t).GetComponentInParent<PlayerValues>();
			if ((Object)(object)componentInParent != (Object)null && (Object)(object)componentInParent.playerClient != (Object)null)
			{
				return componentInParent.playerClient.PlayerId;
			}
			ClientInstance componentInParent2 = ((Component)t).GetComponentInParent<ClientInstance>();
			if ((Object)(object)componentInParent2 != (Object)null)
			{
				return componentInParent2.PlayerId;
			}
			Transform root = t.root;
			if ((Object)(object)root != (Object)null)
			{
				componentInParent = ((Component)root).GetComponentInChildren<PlayerValues>(true);
				if ((Object)(object)componentInParent != (Object)null && (Object)(object)componentInParent.playerClient != (Object)null)
				{
					return componentInParent.playerClient.PlayerId;
				}
				componentInParent2 = ((Component)root).GetComponentInChildren<ClientInstance>(true);
				if ((Object)(object)componentInParent2 != (Object)null)
				{
					return componentInParent2.PlayerId;
				}
			}
			return -1;
		}

		public static string NameOf(int playerId)
		{
			if (ClientInstance.playerInstances != null && ClientInstance.playerInstances.TryGetValue(playerId, out var value) && (Object)(object)value != (Object)null)
			{
				return value.PlayerName;
			}
			return $"Player {playerId}";
		}
	}
	[BepInPlugin("jachthafen.straftat.killtracker", "Kill Tracker", "1.1.0")]
	public class Plugin : BaseUnityPlugin
	{
		public const string PluginGuid = "jachthafen.straftat.killtracker";

		public const string PluginName = "Kill Tracker";

		public const string PluginVersion = "1.1.0";

		public static ManualLogSource Log;

		public static ConfigEntry<bool> Enabled;

		public static ConfigEntry<string> NameFormat;

		public static ConfigEntry<bool> HideZero;

		public static ConfigEntry<bool> CountSuicides;

		public static ConfigEntry<bool> TrackDamage;

		public static ConfigEntry<bool> ScaleDamageToHud;

		public static ConfigEntry<bool> ShowOverlay;

		public static ConfigEntry<bool> DebugLogging;

		public static ConfigEntry<bool> TraceRawWrites;

		private static readonly Type[] PatchClasses = new Type[5]
		{
			typeof(HealthChangePatch),
			typeof(KillerChangePatch),
			typeof(RoundEndNamePatch),
			typeof(RoundResetPatch),
			typeof(MatchResetPatch)
		};

		private void Awake()
		{
			//IL_0134: Unknown result type (might be due to invalid IL or missing references)
			//IL_013a: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Master switch. Turn off to disable all tracking and display.");
			NameFormat = ((BaseUnityPlugin)this).Config.Bind<string>("Display", "NameFormat", "{0}  <size=60%>[{1}]</size>", "Format of the name on the round-end screen. {0} = player name, {1} = kills, {2} = damage dealt. TextMeshPro rich text is supported, e.g. \"{0} <size=60%>[{1} | {2}]</size>\" to show damage there as well.");
			HideZero = ((BaseUnityPlugin)this).Config.Bind<bool>("Display", "HideZeroKills", false, "If true, players with 0 kills are shown without the counter.");
			ShowOverlay = ((BaseUnityPlugin)this).Config.Bind<bool>("Display", "ShowOverlay", true, "Draw the standalone scoreboard (name / kills / damage) in the top-left corner during the round-end screen. This is where the damage numbers live by default, because the name line on the round-end screen is narrow.");
			TrackDamage = ((BaseUnityPlugin)this).Config.Bind<bool>("Tracking", "TrackDamage", true, "Track how much damage each player dealt.");
			ScaleDamageToHud = ((BaseUnityPlugin)this).Config.Bind<bool>("Tracking", "ScaleDamageToHud", true, "Report damage on the 0-100 scale the HUD shows instead of the game's internal health units (fullHealth is 4 internally). A kill from full health counts as 100.");
			CountSuicides = ((BaseUnityPlugin)this).Config.Bind<bool>("Tracking", "CountSuicides", false, "If true, damage and kills you inflict on yourself count for you. Normally off.");
			DebugLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "VerboseLogging", false, "Log every registered hit and kill to the BepInEx console.");
			TraceRawWrites = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "TraceRawWrites", false, "Diagnostic only. Logs EVERY write to PlayerHealth.health and PlayerHealth.killer, before any filtering. Very noisy - use it to find out which writes actually happen during a death, then turn it off again.");
			Harmony val = new Harmony("jachthafen.straftat.killtracker");
			int num = 0;
			Type[] patchClasses = PatchClasses;
			foreach (Type type in patchClasses)
			{
				try
				{
					val.CreateClassProcessor(type).Patch();
					num++;
				}
				catch (Exception arg)
				{
					((BaseUnityPlugin)this).Logger.LogError((object)$"Patch {type.Name} failed: {arg}");
				}
			}
			((Component)this).gameObject.AddComponent<StatsOverlay>();
			((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0} {1} loaded - {2}/{3} patches applied.", "Kill Tracker", "1.1.0", num, PatchClasses.Length));
		}

		public static void Trace(string message)
		{
			if (TraceRawWrites != null && TraceRawWrites.Value)
			{
				Log.LogInfo((object)message);
			}
		}

		public static void Debug(string message)
		{
			if (DebugLogging != null && DebugLogging.Value)
			{
				Log.LogInfo((object)message);
			}
		}
	}
	public class StatsOverlay : MonoBehaviour
	{
		private struct Row
		{
			public string Name;

			public int Kills;

			public int Damage;
		}

		private static float visibleUntil;

		private const float Duration = 7f;

		public static void Show()
		{
			visibleUntil = Time.unscaledTime + 7f;
		}

		private void LateUpdate()
		{
			MatchStats.FlushStale();
		}

		private void OnGUI()
		{
			//IL_005e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			//IL_009b: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b0: Expected O, but got Unknown
			//IL_00b1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00be: Expected O, but got Unknown
			//IL_00c8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cd: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d7: Expected O, but got Unknown
			//IL_00d9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00de: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e7: Expected O, but got Unknown
			//IL_0139: Unknown result type (might be due to invalid IL or missing references)
			//IL_0162: Unknown result type (might be due to invalid IL or missing references)
			//IL_018b: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f5: Unknown result type (might be due to invalid IL or missing references)
			//IL_021b: Unknown result type (might be due to invalid IL or missing references)
			if (!Plugin.Enabled.Value || !Plugin.ShowOverlay.Value || Time.unscaledTime > visibleUntil)
			{
				return;
			}
			List<Row> list = BuildRows();
			if (list.Count == 0)
			{
				return;
			}
			Rect val = default(Rect);
			((Rect)(ref val))..ctor(24f, 24f, 320f, 52f + (float)list.Count * 24f);
			Color color = GUI.color;
			GUI.color = new Color(0f, 0f, 0f, 0.7f);
			GUI.DrawTexture(val, (Texture)(object)Texture2D.whiteTexture);
			GUI.color = color;
			GUIStyle val2 = new GUIStyle(GUI.skin.label)
			{
				fontSize = 13,
				fontStyle = (FontStyle)1
			};
			GUIStyle val3 = new GUIStyle(val2)
			{
				alignment = (TextAnchor)5
			};
			GUIStyle val4 = new GUIStyle(GUI.skin.label)
			{
				fontSize = 14
			};
			GUIStyle val5 = new GUIStyle(val4)
			{
				alignment = (TextAnchor)5
			};
			float num = ((Rect)(ref val)).x + 10f;
			float num2 = ((Rect)(ref val)).x + 320f - 130f;
			float num3 = ((Rect)(ref val)).x + 320f - 70f;
			GUI.Label(new Rect(num, ((Rect)(ref val)).y + 6f, 160f, 18f), "THIS MATCH", val2);
			GUI.Label(new Rect(num2, ((Rect)(ref val)).y + 6f, 50f, 18f), "KILLS", val3);
			GUI.Label(new Rect(num3, ((Rect)(ref val)).y + 6f, 60f, 18f), "DMG", val3);
			float num4 = ((Rect)(ref val)).y + 28f;
			foreach (Row item in list)
			{
				GUI.Label(new Rect(num, num4, num2 - num - 8f, 24f), item.Name, val4);
				GUI.Label(new Rect(num2, num4, 50f, 24f), item.Kills.ToString(), val5);
				GUI.Label(new Rect(num3, num4, 60f, 24f), item.Damage.ToString(), val5);
				num4 += 24f;
			}
		}

		private static List<Row> BuildRows()
		{
			List<Row> list = new List<Row>();
			Dictionary<int, ClientInstance> playerInstances = ClientInstance.playerInstances;
			if (playerInstances == null)
			{
				return list;
			}
			foreach (KeyValuePair<int, ClientInstance> item in playerInstances)
			{
				if (!((Object)(object)item.Value == (Object)null))
				{
					list.Add(new Row
					{
						Name = item.Value.PlayerName,
						Kills = MatchStats.GetKills(item.Key),
						Damage = MatchStats.GetDamage(item.Key)
					});
				}
			}
			return (from r in list
				orderby r.Kills descending, r.Damage descending
				select r).ToList();
		}
	}
}
namespace KillTracker.Patches
{
	[HarmonyPatch(typeof(PlayerHealth), "sync___set_value_health")]
	internal static class HealthChangePatch
	{
		private static bool errorLogged;

		private static void Postfix(PlayerHealth __instance, float value, bool asServer)
		{
			try
			{
				Plugin.Trace($"[trace] health -> {value:0.##} (asServer={asServer}) " + $"on instance {(((Object)(object)__instance != (Object)null) ? ((Object)__instance).GetInstanceID() : 0)}");
				if (!asServer)
				{
					MatchStats.OnHealthChanged(__instance, value);
				}
			}
			catch (Exception arg)
			{
				if (!errorLogged)
				{
					errorLogged = true;
					Plugin.Log.LogError((object)$"HealthChangePatch failed (further errors suppressed): {arg}");
				}
			}
		}
	}
	[HarmonyPatch(typeof(PlayerHealth), "sync___set_value_killer")]
	internal static class KillerChangePatch
	{
		private static bool errorLogged;

		private static void Postfix(PlayerHealth __instance, Transform value, bool asServer)
		{
			try
			{
				Plugin.Trace("[trace] killer -> " + (((Object)(object)value != (Object)null) ? ((Object)value).name : "null") + " " + $"(asServer={asServer}) " + $"on instance {(((Object)(object)__instance != (Object)null) ? ((Object)__instance).GetInstanceID() : 0)}");
				if (!((Object)(object)value == (Object)null))
				{
					MatchStats.OnKillerSet(__instance, value);
				}
			}
			catch (Exception arg)
			{
				if (!errorLogged)
				{
					errorLogged = true;
					Plugin.Log.LogError((object)$"KillerChangePatch failed (further errors suppressed): {arg}");
				}
			}
		}
	}
	[HarmonyPatch(typeof(RoundManager), "InterfaceSetup")]
	internal static class RoundEndNamePatch
	{
		private static bool errorLogged;

		[HarmonyPriority(800)]
		private static void Prefix(RoundManager __instance)
		{
			try
			{
				if (!Plugin.Enabled.Value)
				{
					return;
				}
				string[] names = __instance.names;
				if (names == null)
				{
					return;
				}
				for (int i = 0; i < names.Length; i++)
				{
					string text = names[i];
					if (!string.IsNullOrEmpty(text))
					{
						int kills = MatchStats.GetKills(i);
						if (kills != 0 || !Plugin.HideZero.Value)
						{
							names[i] = string.Format(Plugin.NameFormat.Value, text, kills, MatchStats.GetDamage(i));
						}
					}
				}
				StatsOverlay.Show();
			}
			catch (Exception arg)
			{
				if (!errorLogged)
				{
					errorLogged = true;
					Plugin.Log.LogError((object)$"RoundEndNamePatch failed (further errors suppressed): {arg}");
				}
			}
		}
	}
	[HarmonyPatch(typeof(RoundManager), "NextRoundCall")]
	internal static class RoundResetPatch
	{
		private static void Postfix()
		{
			MatchStats.ResetRound();
		}
	}
	[HarmonyPatch(typeof(ScoreManager), "ResetScores")]
	internal static class MatchResetPatch
	{
		private static void Postfix()
		{
			MatchStats.ResetMatch();
		}
	}
}