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();
}
}
}