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 KillCounts = new Dictionary(); private static readonly Dictionary DamageCounts = new Dictionary(); private static readonly HashSet CountedDeaths = new HashSet(); private static readonly Dictionary KnownHealth = new Dictionary(); private static readonly Dictionary KillerByVictim = new Dictionary(); private static readonly Dictionary WaitingForKiller = new Dictionary(); 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 list = null; foreach (KeyValuePair item in WaitingForKiller) { if (item.Value.Frame < Time.frameCount) { Credit(item.Value, -1); if (list == null) { list = new List(); } 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(); if ((Object)(object)componentInParent != (Object)null && (Object)(object)componentInParent.playerClient != (Object)null) { return componentInParent.playerClient.PlayerId; } ClientInstance componentInParent2 = ((Component)t).GetComponentInParent(); if ((Object)(object)componentInParent2 != (Object)null) { return componentInParent2.PlayerId; } Transform root = t.root; if ((Object)(object)root != (Object)null) { componentInParent = ((Component)root).GetComponentInChildren(true); if ((Object)(object)componentInParent != (Object)null && (Object)(object)componentInParent.playerClient != (Object)null) { return componentInParent.playerClient.PlayerId; } componentInParent2 = ((Component)root).GetComponentInChildren(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 Enabled; public static ConfigEntry NameFormat; public static ConfigEntry HideZero; public static ConfigEntry CountSuicides; public static ConfigEntry TrackDamage; public static ConfigEntry ScaleDamageToHud; public static ConfigEntry ShowOverlay; public static ConfigEntry DebugLogging; public static ConfigEntry 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("General", "Enabled", true, "Master switch. Turn off to disable all tracking and display."); NameFormat = ((BaseUnityPlugin)this).Config.Bind("Display", "NameFormat", "{0} [{1}]", "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} [{1} | {2}]\" to show damage there as well."); HideZero = ((BaseUnityPlugin)this).Config.Bind("Display", "HideZeroKills", false, "If true, players with 0 kills are shown without the counter."); ShowOverlay = ((BaseUnityPlugin)this).Config.Bind("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("Tracking", "TrackDamage", true, "Track how much damage each player dealt."); ScaleDamageToHud = ((BaseUnityPlugin)this).Config.Bind("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("Tracking", "CountSuicides", false, "If true, damage and kills you inflict on yourself count for you. Normally off."); DebugLogging = ((BaseUnityPlugin)this).Config.Bind("Debug", "VerboseLogging", false, "Log every registered hit and kill to the BepInEx console."); TraceRawWrites = ((BaseUnityPlugin)this).Config.Bind("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(); ((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 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 BuildRows() { List list = new List(); Dictionary playerInstances = ClientInstance.playerInstances; if (playerInstances == null) { return list; } foreach (KeyValuePair 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(); } } }