Decompiled source of Last Three Hits v1.0.0

BepInEx/plugins/LastThreeHits/LastThreeHits.dll

Decompiled 10 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using RoR2;
using UnityEngine;
using UnityEngine.Networking;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("LastThreeHits")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("LastThreeHits")]
[assembly: AssemblyTitle("LastThreeHits")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace LastThreeHits;

public enum ReportMode
{
	Host,
	Local
}
[BepInPlugin("com.rawteam.lastthreehits", "Last Three Hits", "1.0.0")]
public sealed class Plugin : BaseUnityPlugin
{
	private struct PendingReport
	{
		internal DamageHistory History;

		internal string Player;

		internal float Due;
	}

	public const string PluginGuid = "com.rawteam.lastthreehits";

	public const string PluginVersion = "1.0.0";

	private ConfigEntry<ReportMode> reportMode;

	private readonly List<PendingReport> pending = new List<PendingReport>(4);

	private void Awake()
	{
		reportMode = ((BaseUnityPlugin)this).Config.Bind<ReportMode>("Reports", "Mode", ReportMode.Host, "Host: full reports for the lobby when hosting. Local: reports only in your chat, including when joining an unmodded host. Restart after changing this setting.");
		GlobalEventManager.onServerDamageDealt += OnDamage;
		GlobalEventManager.onCharacterDeathServer += OnDeath;
		GlobalEventManager.onClientDamageNotified += OnClientDamage;
		CharacterBody.onBodyStartGlobal += OnBodyStart;
		Run.onRunDestroyGlobal += OnRunEnd;
		((BaseUnityPlugin)this).Logger.LogInfo((object)("Last Three Hits loaded. Mode: " + reportMode.Value));
	}

	private void OnDestroy()
	{
		GlobalEventManager.onServerDamageDealt -= OnDamage;
		GlobalEventManager.onCharacterDeathServer -= OnDeath;
		GlobalEventManager.onClientDamageNotified -= OnClientDamage;
		CharacterBody.onBodyStartGlobal -= OnBodyStart;
		Run.onRunDestroyGlobal -= OnRunEnd;
		foreach (CharacterBody readOnlyInstances in CharacterBody.readOnlyInstancesList)
		{
			readOnlyInstances.OnBodyDeath = (Action<CharacterBody>)Delegate.Remove(readOnlyInstances.OnBodyDeath, new Action<CharacterBody>(OnClientDeath));
		}
		pending.Clear();
	}

	private void OnRunEnd(Run run)
	{
		pending.Clear();
	}

	private static bool IsLocalBody(CharacterBody body)
	{
		if (!Object.op_Implicit((Object)(object)body))
		{
			return false;
		}
		foreach (LocalUser readOnlyLocalUsers in LocalUserManager.readOnlyLocalUsersList)
		{
			if ((Object)(object)readOnlyLocalUsers.cachedBody == (Object)(object)body || (Object.op_Implicit((Object)(object)body.master) && (Object)(object)readOnlyLocalUsers.cachedMaster == (Object)(object)body.master))
			{
				return true;
			}
		}
		return false;
	}

	private void OnBodyStart(CharacterBody body)
	{
		if (reportMode.Value == ReportMode.Local && !NetworkServer.active)
		{
			body.OnBodyDeath = (Action<CharacterBody>)Delegate.Combine(body.OnBodyDeath, new Action<CharacterBody>(OnClientDeath));
		}
	}

	private void OnClientDamage(DamageDealtMessage message)
	{
		if (reportMode.Value != ReportMode.Local || NetworkServer.active || message == null || !Object.op_Implicit((Object)(object)message.victim) || !IsPositiveFinite(message.damage))
		{
			return;
		}
		CharacterBody component = message.victim.GetComponent<CharacterBody>();
		if (!IsLocalBody(component))
		{
			return;
		}
		try
		{
			GetHistory(component).RecordClient(message);
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogError((object)("Could not record client damage: " + ex));
		}
	}

	private void OnClientDeath(CharacterBody body)
	{
		if (reportMode.Value == ReportMode.Local && !NetworkServer.active && IsLocalBody(body))
		{
			DamageHistory history = GetHistory(body);
			if (!history.Reported && !history.Pending)
			{
				history.Pending = true;
				pending.Add(new PendingReport
				{
					History = history,
					Player = PlayerName(body),
					Due = Time.realtimeSinceStartup + 0.75f
				});
			}
		}
	}

	private void Update()
	{
		for (int num = pending.Count - 1; num >= 0; num--)
		{
			PendingReport pendingReport = pending[num];
			if (!(Time.realtimeSinceStartup < pendingReport.Due))
			{
				pending.RemoveAt(num);
				pendingReport.History.Reported = true;
				string text = BuildReport(pendingReport.Player, pendingReport.History, clientData: true);
				((BaseUnityPlugin)this).Logger.LogInfo((object)text);
				Chat.AddMessage(text);
			}
		}
	}

	private static bool IsPlayerReport(DamageReport report)
	{
		if (NetworkServer.active && report != null && Object.op_Implicit((Object)(object)report.victimBody) && report.victimBody.isPlayerControlled)
		{
			return report.damageInfo != null;
		}
		return false;
	}

	private void OnDamage(DamageReport report)
	{
		if (!IsPlayerReport(report) || report.damageInfo.rejected || !IsPositiveFinite(report.damageDealt) || (reportMode.Value == ReportMode.Local && !IsLocalBody(report.victimBody)))
		{
			return;
		}
		try
		{
			GetHistory(report.victimBody).Record(report);
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogError((object)("Could not record damage: " + ex));
		}
	}

	private void OnDeath(DamageReport report)
	{
		//IL_0087: Unknown result type (might be due to invalid IL or missing references)
		//IL_008c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0098: Expected O, but got Unknown
		if (!IsPlayerReport(report) || (reportMode.Value == ReportMode.Local && !IsLocalBody(report.victimBody)))
		{
			return;
		}
		try
		{
			DamageHistory history = GetHistory(report.victimBody);
			if (!history.Reported)
			{
				if (history.LastDamageInfo != report.damageInfo)
				{
					history.Record(report);
				}
				history.Reported = true;
				string text = BuildReport(PlayerName(report.victimBody), history, clientData: false);
				((BaseUnityPlugin)this).Logger.LogInfo((object)text);
				if (reportMode.Value == ReportMode.Host)
				{
					Chat.SendBroadcastChat((ChatMessageBase)new SimpleChatMessage
					{
						baseToken = text
					});
				}
				else
				{
					Chat.AddMessage(text);
				}
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogError((object)("Could not show the death report: " + ex));
		}
	}

	private static bool IsPositiveFinite(float value)
	{
		if (value > 0f && !float.IsNaN(value))
		{
			return !float.IsInfinity(value);
		}
		return false;
	}

	private static DamageHistory GetHistory(CharacterBody body)
	{
		DamageHistory component = ((Component)body).GetComponent<DamageHistory>();
		if (!Object.op_Implicit((Object)(object)component))
		{
			return ((Component)body).gameObject.AddComponent<DamageHistory>();
		}
		return component;
	}

	private static string PlayerName(CharacterBody body)
	{
		string value = body.GetUserName();
		if (string.IsNullOrWhiteSpace(value))
		{
			value = body.GetDisplayName();
		}
		return CleanLabel(value);
	}

	private static string BuildReport(string player, DamageHistory history, bool clientData)
	{
		StringBuilder stringBuilder = new StringBuilder(512);
		stringBuilder.Append(clientData ? "[Last Three Hits - local] " : "[Last Three Hits] ").Append(player).Append(" - newest first");
		for (int i = 0; i < history.Count; i++)
		{
			Hit hit = history.Newest(i);
			stringBuilder.Append('\n').Append(i + 1).Append(". ")
				.Append(hit.Source)
				.Append(" | ")
				.Append(hit.Effect)
				.Append(" | ")
				.Append(hit.Damage)
				.Append(" damage");
		}
		if (history.Count < 3)
		{
			stringBuilder.Append("\nOnly ").Append(history.Count).Append(" damage event(s) recorded for this body.");
		}
		if (clientData)
		{
			stringBuilder.Append("\nClient data: final hit or effect may be unavailable.");
		}
		return stringBuilder.ToString();
	}

	internal static Hit Describe(DamageReport report)
	{
		//IL_000d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0012: Unknown result type (might be due to invalid IL or missing references)
		//IL_004e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0054: Unknown result type (might be due to invalid IL or missing references)
		//IL_0068: Unknown result type (might be due to invalid IL or missing references)
		//IL_006e: 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_0095: 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_00a7: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ad: Invalid comparison between Unknown and I4
		//IL_00c8: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ce: Unknown result type (might be due to invalid IL or missing references)
		DamageInfo damageInfo = report.damageInfo;
		DamageType damageType = damageInfo.damageType.damageType;
		string value = (Object.op_Implicit((Object)(object)report.attackerBody) ? Util.GetBestBodyName(((Component)report.attackerBody).gameObject) : (Object.op_Implicit((Object)(object)damageInfo.attacker) ? Util.GetBestBodyName(damageInfo.attacker) : null));
		string value2;
		if ((damageType & 0x200000) != 0)
		{
			value = "Environment";
			value2 = "Fall damage";
		}
		else if ((damageType & int.MinValue) == 0 && (damageInfo.damageType.damageTypeExtended & int.MinValue) == 0)
		{
			value2 = (((damageType & 0x10000) != 0) ? "Void death" : (((int)damageInfo.dotIndex != -1) ? SplitName(((object)Unsafe.As<DotIndex, DotIndex>(ref damageInfo.dotIndex)/*cast due to .constrained prefix*/).ToString()) : (((damageType & 0x4000000) != 0) ? "Damage over time (type unknown)" : ((!Object.op_Implicit((Object)(object)damageInfo.inflictor) || !((Object)(object)damageInfo.inflictor != (Object)(object)damageInfo.attacker)) ? "Hit (attack unknown)" : SplitName(((Object)damageInfo.inflictor).name.Replace("(Clone)", ""))))));
		}
		else
		{
			value = "Environment";
			value2 = "Out of bounds";
		}
		return new Hit
		{
			Source = CleanLabel(value),
			Effect = CleanLabel(value2),
			Damage = (IsPositiveFinite(report.damageDealt) ? report.damageDealt.ToString("0.##", CultureInfo.InvariantCulture) : "unknown")
		};
	}

	internal static Hit DescribeClient(DamageDealtMessage message)
	{
		//IL_0006: Unknown result type (might be due to invalid IL or missing references)
		//IL_000b: 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)
		//IL_0034: Unknown result type (might be due to invalid IL or missing references)
		//IL_0045: Unknown result type (might be due to invalid IL or missing references)
		//IL_004b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0054: Unknown result type (might be due to invalid IL or missing references)
		//IL_005e: Unknown result type (might be due to invalid IL or missing references)
		//IL_006f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0075: Unknown result type (might be due to invalid IL or missing references)
		//IL_0080: Unknown result type (might be due to invalid IL or missing references)
		//IL_0086: Unknown result type (might be due to invalid IL or missing references)
		DamageType damageType = message.damageType.damageType;
		string value = (Object.op_Implicit((Object)(object)message.attacker) ? Util.GetBestBodyName(message.attacker) : null);
		string effect = "Hit (attack unknown)";
		if ((damageType & 0x200000) != 0)
		{
			value = "Environment";
			effect = "Fall damage";
		}
		else if ((damageType & int.MinValue) != 0 || (message.damageType.damageTypeExtended & int.MinValue) != 0)
		{
			value = "Environment";
			effect = "Out of bounds";
		}
		else if ((damageType & 0x10000) != 0)
		{
			effect = "Void death";
		}
		else if ((damageType & 0x4000000) != 0)
		{
			effect = "Damage over time (type unknown)";
		}
		return new Hit
		{
			Source = CleanLabel(value),
			Effect = effect,
			Damage = message.damage.ToString("0.##", CultureInfo.InvariantCulture)
		};
	}

	internal static string CleanLabel(string value)
	{
		if (string.IsNullOrWhiteSpace(value))
		{
			return "Unknown source";
		}
		StringBuilder stringBuilder = new StringBuilder(96);
		bool flag = false;
		foreach (char c in value)
		{
			switch (c)
			{
			case '<':
				flag = true;
				continue;
			case '>':
				flag = false;
				continue;
			default:
				if (flag || char.IsControl(c))
				{
					continue;
				}
				stringBuilder.Append(c);
				if (stringBuilder.Length != 96)
				{
					continue;
				}
				break;
			}
			break;
		}
		string text = stringBuilder.ToString().Trim();
		if (text.Length != 0)
		{
			return text;
		}
		return "Unknown source";
	}

	private static string SplitName(string name)
	{
		StringBuilder stringBuilder = new StringBuilder(name.Length + 16);
		for (int i = 0; i < name.Length; i++)
		{
			char c = name[i];
			if (c == '_')
			{
				stringBuilder.Append(' ');
				continue;
			}
			if (i > 0 && char.IsUpper(c) && (char.IsLower(name[i - 1]) || char.IsDigit(name[i - 1])))
			{
				stringBuilder.Append(' ');
			}
			stringBuilder.Append(c);
		}
		return stringBuilder.ToString().Trim();
	}
}
internal struct Hit
{
	public string Source;

	public string Effect;

	public string Damage;
}
internal sealed class DamageHistory : MonoBehaviour
{
	private readonly Hit[] hits = new Hit[3];

	private int next;

	internal int Count { get; private set; }

	internal DamageInfo LastDamageInfo { get; private set; }

	internal bool Reported { get; set; }

	internal bool Pending { get; set; }

	internal void Record(DamageReport report)
	{
		if (!Reported)
		{
			hits[next] = Plugin.Describe(report);
			next = (next + 1) % hits.Length;
			if (Count < hits.Length)
			{
				int count = Count + 1;
				Count = count;
			}
			LastDamageInfo = report.damageInfo;
		}
	}

	internal void RecordClient(DamageDealtMessage message)
	{
		if (!Reported)
		{
			hits[next] = Plugin.DescribeClient(message);
			next = (next + 1) % hits.Length;
			if (Count < hits.Length)
			{
				int count = Count + 1;
				Count = count;
			}
		}
	}

	internal Hit Newest(int offset)
	{
		return hits[(next - 1 - offset + hits.Length) % hits.Length];
	}
}