Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of HowTheyDied v0.1.1
HowTheyDied.dll
Decompiled a day agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text.RegularExpressions; 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: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("HowTheyDied")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyInformationalVersion("0.1.0")] [assembly: AssemblyProduct("HowTheyDied")] [assembly: AssemblyTitle("HowTheyDied")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.0.0")] [module: UnverifiableCode] [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 HowTheyDied { internal static class CauseTracker { internal static Func<PlayerAvatar, DeathContext> Current; private static readonly Dictionary<int, DeathContext> remote = new Dictionary<int, DeathContext>(); private static readonly Dictionary<PlayerHealth, KeyValuePair<int, float>> healthBefore = new Dictionary<PlayerHealth, KeyValuePair<int, float>>(); private static Type tentacleType; private static bool tentacleSearched; internal static void Remote(int viewID, DeathContext context) { context.Time = Time.time; remote[viewID] = context; } internal static void RecordHurt(PlayerHealth health) { healthBefore[health] = new KeyValuePair<int, float>(health.health, Time.time); } internal static bool WasOneShot(PlayerHealth health) { if ((Object)(object)health != (Object)null && healthBefore.TryGetValue(health, out var value) && Time.time - value.Value < 0.5f) { return value.Key >= health.maxHealth; } return false; } internal static DeathContext Classify(PlayerAvatar victim, int enemyIndex) { DeathContext deathContext = null; if (FogTentacleHolds(victim)) { return new DeathContext { Cause = Cause.Tentacle }; } if (enemyIndex >= 0) { Enemy val = SemiFunc.EnemyGetFromIndex(enemyIndex); if ((Object)(object)val != (Object)null) { return DeathContext.FromEnemy(val); } } if (Current != null) { deathContext = Safe(() => Current(victim)); if (deathContext != null) { return deathContext; } } if ((Object)(object)victim.photonView != (Object)null && remote.TryGetValue(victim.photonView.ViewID, out var value) && Time.time - value.Time < 2f) { return value; } return FromStack(victim) ?? new DeathContext { Cause = Cause.Unknown }; } internal static DeathContext ForOther(PlayerAvatar victim, int enemyIndex) { if (enemyIndex >= 0) { Enemy val = SemiFunc.EnemyGetFromIndex(enemyIndex); if ((Object)(object)val != (Object)null) { return DeathContext.FromEnemy(val); } } if (Current != null) { DeathContext deathContext = Safe(() => Current(victim)); if (deathContext != null) { return deathContext; } } return FromStack(victim); } internal static DeathContext FromHurtCollider(HurtCollider hurt, PlayerAvatar victim) { if ((Object)(object)hurt == (Object)null) { return null; } if (hurt.deathPit) { return new DeathContext { Cause = Cause.DeathPit }; } if ((Object)(object)hurt.enemyHost != (Object)null) { return DeathContext.FromEnemy(hurt.enemyHost); } PlayerAvatar val = (((Object)(object)hurt.playerCausingHurtOverride != (Object)null) ? hurt.playerCausingHurtOverride : hurt.playerCausingHurt); bool flag = (Object)(object)val != (Object)null && (Object)(object)val == (Object)(object)victim; string text = (((Object)(object)val != (Object)null && !flag) ? val.playerName : ""); if ((Object)(object)((Component)hurt).GetComponentInParent<ParticlePrefabExplosion>() != (Object)null) { return new DeathContext { Cause = ((text != "") ? Cause.TeammateExplosion : (flag ? Cause.SelfExplosion : Cause.Explosion)), Teammate = text }; } if ((Object)(object)((Component)hurt).GetComponentInParent<ItemGunBullet>() != (Object)null) { return new DeathContext { Cause = ((text != "") ? Cause.TeammateShot : Cause.SelfExplosion), Teammate = text }; } ItemAttributes componentInParent = ((Component)hurt).GetComponentInParent<ItemAttributes>(); if ((Object)(object)componentInParent != (Object)null) { string item = (((Object)(object)componentInParent.item != (Object)null) ? componentInParent.item.itemName : DeathContext.Clean(((Object)((Component)componentInParent).gameObject).name)); PhysGrabObject component = ((Component)componentInParent).GetComponent<PhysGrabObject>(); PlayerAvatar val2 = (((Object)(object)component != (Object)null) ? component.lastPlayerGrabbing : null); if ((Object)(object)val2 != (Object)null && (Object)(object)val2 != (Object)(object)victim) { return new DeathContext { Cause = Cause.TeammateMelee, Item = item, Teammate = val2.playerName }; } return new DeathContext { Cause = (((Object)(object)val2 == (Object)(object)victim) ? Cause.ObjectSelf : Cause.Hazard), Item = item }; } return new DeathContext { Cause = Cause.Hazard, Item = DeathContext.Clean(((Object)((Component)hurt).transform.root).name) }; } internal static DeathContext FromImpact(PhysGrabObjectImpactDetector impact, PlayerAvatar victim) { if ((Object)(object)impact == (Object)null) { return null; } ItemAttributes component = ((Component)impact).GetComponent<ItemAttributes>(); string item = (((Object)(object)component != (Object)null && (Object)(object)component.item != (Object)null) ? component.item.itemName : DeathContext.Clean(((Object)((Component)impact).gameObject).name)); PhysGrabObject component2 = ((Component)impact).GetComponent<PhysGrabObject>(); PlayerAvatar val = (((Object)(object)component2 != (Object)null) ? component2.lastPlayerGrabbing : null); if ((Object)(object)val != (Object)null && (Object)(object)val == (Object)(object)victim) { return new DeathContext { Cause = Cause.ObjectSelf, Item = item }; } if ((Object)(object)val != (Object)null) { return new DeathContext { Cause = Cause.TeammateObject, Item = item, Teammate = val.playerName }; } return new DeathContext { Cause = Cause.Object, Item = item }; } private static DeathContext FromStack(PlayerAvatar victim) { StackFrame[] frames; try { frames = new StackTrace(fNeedFileInfo: false).GetFrames(); } catch { return null; } if (frames == null) { return null; } StackFrame[] array = frames; for (int i = 0; i < array.Length; i++) { MethodBase method = array[i].GetMethod(); Type type = ((method != null) ? method.DeclaringType : null); if (type == null) { continue; } string text = (type.FullName ?? type.Name) + "." + method.Name; if (!text.StartsWith("HowTheyDied.") && !text.StartsWith("HarmonyLib.")) { if (text.Contains("LostCause")) { return new DeathContext { Cause = Cause.GaveUp }; } if (text.Contains("TaxmansOrders.Imposter")) { return new DeathContext { Cause = Cause.Imposter }; } if (text.Contains("FogMod")) { return new DeathContext { Cause = Cause.Tentacle }; } if (text.Contains("Betrayal")) { return new DeathContext { Cause = Cause.LeftBehind }; } if (text.Contains("SelfDestruct") || text.StartsWith("MenuPageEsc")) { return new DeathContext { Cause = Cause.SelfDestruct }; } if (text.StartsWith("PlayerTumble.HitEnemy")) { return new DeathContext { Cause = Cause.TumbledIntoEnemy }; } if (text.StartsWith("PlayerTumble")) { return new DeathContext { Cause = Cause.Impact }; } if (text.StartsWith("SemiAffectExploding")) { return new DeathContext { Cause = Cause.Explosion }; } if (text.StartsWith("Arena")) { return new DeathContext { Cause = Cause.Arena }; } if (text.StartsWith("HealthDrainer") || text.StartsWith("PunManager") || text.StartsWith("PlayerHealthGrab") || text.StartsWith("CosmeticWorldObject")) { return new DeathContext { Cause = Cause.Drain }; } } } return null; } private static bool FogTentacleHolds(PlayerAvatar victim) { try { if (!tentacleSearched) { tentacleSearched = true; tentacleType = AccessTools.TypeByName("FogMod.Tentacle"); } if (tentacleType == null) { return false; } object obj = AccessTools.Field(tentacleType, "instance")?.GetValue(null); if (obj == null) { return false; } string? obj2 = AccessTools.Field(tentacleType, "state")?.GetValue(obj)?.ToString(); object? obj3 = AccessTools.Field(tentacleType, "victim")?.GetValue(obj); PlayerAvatar val = (PlayerAvatar)((obj3 is PlayerAvatar) ? obj3 : null); return obj2 == "Grab" && (Object)(object)val == (Object)(object)victim; } catch { return false; } } private static DeathContext Safe(Func<DeathContext> get) { try { return get(); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Kunde inte läsa skadekällan: " + ex.Message)); return null; } } } [HarmonyPatch(typeof(HurtCollider), "PlayerHurt")] internal static class HurtColliderPatch { private static void Prefix(HurtCollider __instance) { CauseTracker.Current = (PlayerAvatar victim) => CauseTracker.FromHurtCollider(__instance, victim); } private static void Finalizer() { CauseTracker.Current = null; } } [HarmonyPatch(typeof(PhysGrabObjectImpactDetector), "OnCollisionStay")] internal static class ImpactPatch { private static void Prefix(PhysGrabObjectImpactDetector __instance) { CauseTracker.Current = (PlayerAvatar victim) => CauseTracker.FromImpact(__instance, victim); } private static void Finalizer() { CauseTracker.Current = null; } } [HarmonyPatch(typeof(PlayerHealth), "Hurt")] internal static class HurtPatch { private static void Prefix(PlayerHealth __instance) { CauseTracker.RecordHurt(__instance); } } [HarmonyPatch(typeof(PlayerHealth), "HurtOther")] internal static class HurtOtherPatch { private static void Prefix(PlayerHealth __instance, int damage, int enemyIndex) { PlayerAvatar playerAvatar = __instance.playerAvatar; if (SemiFunc.IsMultiplayer() && !((Object)(object)playerAvatar == (Object)null) && !((Object)(object)playerAvatar.photonView == (Object)null) && !playerAvatar.photonView.IsMine && !playerAvatar.deadSet && damage >= __instance.health) { DeathContext deathContext = CauseTracker.ForOther(playerAvatar, enemyIndex); if (deathContext != null) { DeathNetwork.SendContext(playerAvatar, deathContext); } } } } [HarmonyPatch(typeof(PlayerAvatar), "PlayerDeath")] internal static class PlayerDeathPatch { private static void Prefix(PlayerAvatar __instance, int enemyIndex) { if (__instance.deadSet || (SemiFunc.IsMultiplayer() && !__instance.photonView.IsMine)) { return; } try { DeathContext deathContext = CauseTracker.Classify(__instance, enemyIndex); bool flag = CauseTracker.WasOneShot(__instance.playerHealth); Plugin.Log.LogInfo((object)$"{__instance.playerName} dog: {deathContext} (one-shot {flag})."); DeathLog.Report(__instance, deathContext, flag); } catch (Exception arg) { Plugin.Log.LogError((object)$"Dödsorsak misslyckades: {arg}"); } } } internal enum Cause { Unknown, Enemy, TeammateShot, TeammateExplosion, TeammateMelee, TeammateObject, SelfExplosion, Explosion, Object, ObjectSelf, Hazard, Impact, TumbledIntoEnemy, DeathPit, SelfDestruct, LeftBehind, GaveUp, Tentacle, Drain, Arena, Imposter } internal sealed class DeathContext { internal Cause Cause; internal string EnemyKey = ""; internal string EnemyName = ""; internal string Item = ""; internal string Teammate = ""; internal float Time = Time.time; internal static DeathContext FromEnemy(Enemy enemy) { DeathContext deathContext = new DeathContext { Cause = Cause.Enemy }; EnemyParent val = (((Object)(object)enemy != (Object)null) ? enemy.EnemyParent : null); if ((Object)(object)val != (Object)null) { deathContext.EnemyName = val.enemyName; deathContext.EnemyKey = Key(((Object)((Component)val).gameObject).name); } if (string.IsNullOrEmpty(deathContext.EnemyName)) { deathContext.EnemyName = "something"; } return deathContext; } internal static string Key(string objectName) { string text = Clean(objectName).ToLowerInvariant(); if (text.StartsWith("enemy")) { text = text.Substring(5); } return Regex.Replace(text, "[^a-z0-9]", ""); } internal static string Clean(string objectName) { if (string.IsNullOrEmpty(objectName)) { return ""; } return Regex.Replace(objectName.Replace("(Clone)", ""), "^(Valuable|Item|Enemy)\\s*-?\\s*", "", RegexOptions.IgnoreCase).Trim(); } internal string Pack() { string[] array = new string[5]; int cause = (int)Cause; array[0] = cause.ToString(); array[1] = EnemyKey; array[2] = EnemyName; array[3] = Item; array[4] = Teammate; return string.Join("\u001f", array); } internal static DeathContext Unpack(string packed) { string[] array = (packed ?? "").Split('\u001f'); if (array.Length < 5 || !int.TryParse(array[0], out var result)) { return null; } return new DeathContext { Cause = (Cause)result, EnemyKey = array[1], EnemyName = array[2], Item = array[3], Teammate = array[4] }; } public override string ToString() { return $"{Cause} fiende={EnemyName}({EnemyKey}) föremål={Item} lagkamrat={Teammate}"; } } internal sealed class DeathRecord { internal string Text; internal int Level; internal int Seconds; internal object[] Pack() { return new object[3] { Text, Level, Seconds }; } internal static DeathRecord Unpack(object[] data) { if (data.Length != 3 || !(data[0] is string text) || !(data[1] is int level) || !(data[2] is int seconds)) { return null; } return new DeathRecord { Text = text, Level = level, Seconds = seconds }; } internal string Format() { return $"<color=#B01818>[L{Level} {Seconds / 60:00}:{Seconds % 60:00}]</color> {Text}"; } } internal static class DeathLog { internal static readonly List<DeathRecord> Records = new List<DeathRecord>(); private static float levelStart; internal static void Report(PlayerAvatar victim, DeathContext context, bool oneShot) { DeathNetwork.SendDeath(new DeathRecord { Text = Lines.Build(victim.playerName, context, oneShot), Level = (((Object)(object)RunManager.instance != (Object)null) ? (RunManager.instance.levelsCompleted + 1) : 0), Seconds = Mathf.Max(0, Mathf.RoundToInt(Time.time - levelStart)) }); } internal static void Add(DeathRecord record) { Records.Add(record); Plugin.Log.LogInfo((object)$"Dödsfall {Records.Count}: [L{record.Level} {record.Seconds}s] {record.Text}"); } internal static void LevelStarted() { levelStart = Time.time; if (Plugin.TestFakeDeaths.Value && SemiFunc.RunIsLevel()) { AddFakeDeaths(); } } private static void AddFakeDeaths() { string[] array = new string[3] { "Testbot", "Dummy", "Crash Test" }; DeathContext[] obj = new DeathContext[7] { new DeathContext { Cause = Cause.Enemy, EnemyKey = "hunter", EnemyName = "Huntsman" }, new DeathContext { Cause = Cause.Enemy, EnemyKey = "gnome", EnemyName = "Gnome" }, new DeathContext { Cause = Cause.TeammateMelee, Item = "Frying Pan", Teammate = "Dummy" }, new DeathContext { Cause = Cause.Object, Item = "Big Vase" }, new DeathContext { Cause = Cause.DeathPit }, new DeathContext { Cause = Cause.GaveUp }, new DeathContext { Cause = Cause.Tentacle } }; int level = ((!((Object)(object)RunManager.instance != (Object)null)) ? 1 : (RunManager.instance.levelsCompleted + 1)); DeathContext[] array2 = obj; foreach (DeathContext context in array2) { Add(new DeathRecord { Text = Lines.Build(array[Random.Range(0, array.Length)], context, Random.value < 0.3f), Level = level, Seconds = Random.Range(10, 400) }); } } internal static void Clear() { if (Records.Count > 0) { Plugin.Log.LogInfo((object)"Ny run: dödslistan nollställd."); } Records.Clear(); } } [HarmonyPatch(typeof(RunManager), "ResetProgress")] internal static class ResetProgressPatch { private static void Postfix() { DeathLog.Clear(); } } [HarmonyPatch(typeof(RoundDirector), "Start")] internal static class RoundStartPatch { private static void Postfix() { DeathNetwork.Register(); DeathLog.LevelStarted(); } } internal static class DeathNetwork { private const byte EventContext = 170; private const byte EventDeath = 171; private static bool registered; internal static void Register() { if (!registered && PhotonNetwork.NetworkingClient != null) { PhotonNetwork.NetworkingClient.EventReceived += OnEvent; registered = true; } } internal static void SendContext(PlayerAvatar victim, DeathContext context) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Expected O, but got Unknown //IL_0053: Unknown result type (might be due to invalid IL or missing references) Player owner = victim.photonView.Owner; if (owner != null) { object[] obj = new object[2] { victim.photonView.ViewID, context.Pack() }; RaiseEventOptions val = new RaiseEventOptions(); val.TargetActors = new int[1] { owner.ActorNumber }; PhotonNetwork.RaiseEvent((byte)170, (object)obj, val, SendOptions.SendReliable); } } internal static void SendDeath(DeathRecord record) { //IL_0019: 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_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Expected O, but got Unknown if (!SemiFunc.IsMultiplayer()) { DeathLog.Add(record); return; } PhotonNetwork.RaiseEvent((byte)171, (object)record.Pack(), new RaiseEventOptions { Receivers = (ReceiverGroup)1 }, SendOptions.SendReliable); } private static void OnEvent(EventData e) { if (e.Code == 170 && e.CustomData is object[] array && array.Length == 2 && array[0] is int viewID && array[1] is string packed) { DeathContext deathContext = DeathContext.Unpack(packed); if (deathContext != null) { CauseTracker.Remote(viewID, deathContext); } } else if (e.Code == 171 && e.CustomData is object[] data) { DeathRecord deathRecord = DeathRecord.Unpack(data); if (deathRecord != null) { DeathLog.Add(deathRecord); } } } } internal static class Lines { private static readonly Dictionary<string, string[]> EnemyLines = new Dictionary<string, string[]> { ["hunter"] = new string[7] { "got a free lead injection from {enemy}.", "was hunted down like a sad little deer by {enemy}.", "found out {enemy} can't see, but he can absolutely aim.", "made one (1) noise and {enemy} handled the rest.", "was turned into swiss cheese by {enemy}.", "got shotgunned into next week by {enemy}.", "learned that tiptoeing is not optional around {enemy}." }, ["bang"] = new string[6] { "was blown to pieces by {enemy}.", "hugged a {enemy}. It hugged back. Explosively.", "got confetti'd by {enemy}.", "found out {enemy} is a bomb with legs. Briefly.", "was redistributed across the room by {enemy}.", "got lit up by {enemy}, and not in a fun way." }, ["duck"] = new string[6] { "bothered the duck. The duck bothered back.", "was eaten alive by {enemy}. Quack.", "learned why it's called {enemy}.", "thought the duck was cute. The duck thought they were lunch.", "got absolutely wrecked by a fucking duck.", "poked {enemy} one time too many." }, ["robe"] = new string[6] { "got the long-arm hug of death from {enemy}.", "was ripped to pieces by {enemy}.", "looked {enemy} in the eyes. Bad move.", "got folded like laundry by {enemy}.", "was personally escorted to the afterlife by {enemy}.", "turned around and {enemy} was already there." }, ["head"] = new string[5] { "got their face chewed off by {enemy}.", "was bitten in half by a floating head. Normal day.", "got headbutted into the afterlife by {enemy}.", "was used as a chew toy by {enemy}.", "lost a staring contest and a lot of blood to {enemy}." }, ["slowwalker"] = new string[5] { "got bonked so hard by {enemy} they became a pancake.", "somehow got caught by the slowest thing alive: {enemy}.", "was flattened by {enemy}. Respect the trudge.", "underestimated {enemy}'s walking speed and overestimated their own.", "was introduced to {enemy}'s big stick." }, ["floater"] = new string[5] { "was yeeted into the ceiling by {enemy}.", "got telekinetically slammed by {enemy}.", "floated for a bit. Then didn't. Thanks, {enemy}.", "was levitated and dunked on by {enemy}.", "experienced zero gravity, then all of it, courtesy of {enemy}." }, ["hidden"] = new string[5] { "was dragged away by something they never saw.", "heard heavy breathing. That was the warning. {enemy} did the rest.", "got kidnapped by {enemy} and never came back.", "was taken for a walk by {enemy}. One way.", "got yoinked into the dark by {enemy}." }, ["ceilingeye"] = new string[5] { "stared at {enemy} until their brain melted.", "looked up. Mistake. {enemy} was looking down.", "had an eye-opening experience with {enemy}.", "lost an eye contact battle against {enemy}.", "was death-stared by the ceiling. Literally." }, ["slowmouth"] = new string[5] { "got a face full of {enemy} and a mouth full of puke.", "was face-hugged and vomited to death by {enemy}.", "got spewed on until they stopped moving.", "made out with {enemy}. Zero stars.", "drowned in {enemy}'s lunch." }, ["gnome"] = new string[5] { "was pickaxed to death by tiny fucking gnomes.", "lost a fight to a {enemy}. A garden ornament.", "got mined like a diamond by {enemy}.", "was swarmed by {enemy}s. Death by a thousand tiny cuts.", "got gnomed." }, ["thinman"] = new string[5] { "blinked. {enemy} didn't.", "was teleport-stabbed by {enemy}.", "got spooked to death by {enemy}.", "saw {enemy} in the corner. Then right in their face.", "was erased by {enemy}." }, ["tumbler"] = new string[5] { "got diced and sliced by {enemy}.", "was the main course for {enemy}.", "got cleaved in half by a bouncing chef.", "was tenderized by {enemy}.", "got cooked. Well done. By {enemy}." }, ["runner"] = new string[5] { "got reaped by {enemy}.", "tried to outrun {enemy}. Couldn't.", "was shredded by {enemy}'s blades.", "got blendered by {enemy}.", "was harvested by {enemy}." }, ["beamer"] = new string[5] { "got lasered by a fucking clown.", "was vaporized by {enemy}'s beam.", "laughed at {enemy}. {enemy} laughed last.", "was fried to a crisp by {enemy}.", "got a laser show they did not buy tickets for." }, ["animal"] = new string[4] { "was body-slammed by {enemy}.", "got trampled by {enemy}'s zoomies.", "was rammed into the wall by {enemy}.", "got bulldozed by {enemy}." }, ["bowtie"] = new string[4] { "was screamed to death by {enemy}.", "got blown off their feet by {enemy}'s scream.", "couldn't handle {enemy}'s volume.", "got their ears and organs burst by {enemy}." }, ["valuablethrower"] = new string[4] { "was killed by a toddler throwing valuables. Embarrassing.", "got a priceless vase to the face from {enemy}.", "was stoned to death by {enemy}'s toys.", "lost a fight to a baby. {enemy}, to be exact." }, ["upscream"] = new string[4] { "got screamed at point-blank by {enemy} and died of it.", "was ambushed by {enemy}. Face first.", "had {enemy} jump on their face and stay there.", "got jumpscared to death by {enemy}." }, ["shadow"] = new string[4] { "was grabbed from the shadows by {enemy}.", "should've turned the flashlight on. {enemy} agrees.", "was pulled into the dark by {enemy}.", "got strangled by {enemy}'s long, long arms." }, ["elsa"] = new string[4] { "was ripped to pieces by {enemy}.", "got snipped by {enemy}.", "got a very permanent haircut from {enemy}.", "was cut into confetti by {enemy}." }, ["hearthugger"] = new string[4] { "was hugged to death by {enemy}. Wholesome. Fatal.", "got loved a little too hard by {enemy}.", "accepted {enemy}'s hug. Never again.", "got squeezed until something popped. Thanks, {enemy}." }, ["tricycle"] = new string[4] { "got run over by a tricycle. A TRICYCLE.", "was hit and run by {enemy}.", "became roadkill for {enemy}.", "should have looked both ways for {enemy}." }, ["birthdayboy"] = new string[4] { "was the party favor at {enemy}'s party.", "got popped like a balloon by {enemy}.", "didn't bring a present for {enemy}. Fatal mistake.", "was invited to {enemy}'s party. Couldn't leave." }, ["headgrabber"] = new string[4] { "had their head grabbed. And kept. By {enemy}.", "lost their head to {enemy}. Literally.", "got decapitated by {enemy}.", "had {enemy} take their head as a souvenir." }, ["tick"] = new string[4] { "was sucked dry by {enemy}.", "got drained like a juice box by {enemy}.", "became {enemy}'s snack.", "found out {enemy} drinks blood. All of it." }, ["bombthrower"] = new string[7] { "got headshotted by {enemy}. With its actual head.", "watched {enemy} lose its head. Then lost everything else.", "was cleaned up by {enemy}. Permanently.", "got blown up by {enemy}'s exploding head. Mind-blowing.", "was too close when {enemy} blew its top.", "found out {enemy}'s head is a bomb. Too late.", "got cleaned off the map by {enemy}." }, ["snatcher"] = new string[4] { "was snatched up by {enemy}.", "got grabbed and gone by {enemy}.", "was snatched bald, then dead, by {enemy}.", "got picked up by {enemy} and put down permanently." }, ["spinny"] = new string[4] { "got spun into a smoothie by {enemy}.", "was put through the spin cycle by {enemy}.", "got dizzy. Then dead. Thanks, {enemy}.", "was eaten by {enemy}'s big spinning mouth." }, ["oogly"] = new string[4] { "saw {enemy}'s face and their heart just gave up.", "got ooglied to death.", "was killed by the ugliest thing in the building: {enemy}.", "made eye contact with {enemy}. Big mistake." } }; private static readonly string[] GenericEnemy = new string[20] { "was ripped to pieces by {enemy}.", "was mauled by {enemy}.", "was absolutely destroyed by {enemy}.", "got deleted by {enemy}.", "became a smear on the floor thanks to {enemy}.", "was turned into a fine paste by {enemy}.", "got bodied by {enemy}.", "made friends with {enemy}. It did not go well.", "was dismantled by {enemy}.", "got violated by {enemy}.", "found out what {enemy} does. The hard way.", "was killed by {enemy}. Skill issue.", "couldn't escape {enemy}.", "was gutted like a fish by {enemy}.", "got wrecked by {enemy}.", "was ended by {enemy}.", "tried to reason with {enemy}.", "was {enemy}'s lunch.", "got sent to the scrapyard by {enemy}.", "was taken apart bolt by bolt by {enemy}." }; private static readonly string[] OneShotEnemy = new string[12] { "was one-shotted by {enemy}. Full health, by the way.", "went from 100 to 0 thanks to {enemy}.", "didn't even get to feel it. {enemy} one-tapped them.", "was instantly erased from existence by {enemy}.", "got one-shot by {enemy}. Not even close.", "had full health. Had. {enemy} said no.", "got deleted in one hit by {enemy}.", "was killed so fast by {enemy} their health bar is still confused.", "speedran death against {enemy}.", "blinked and {enemy} had already won.", "got one-tapped by {enemy} like a training dummy.", "was full HP and full of confidence. {enemy} removed both." }; private static readonly Dictionary<Cause, string[]> CauseLines = new Dictionary<Cause, string[]> { [Cause.TeammateShot] = new string[10] { "was shot by {player}. Friendly fire is still fire.", "got domed by {player}.", "was mistaken for a monster by {player}.", "was executed by {player}. Allegedly an accident.", "caught {player}'s bullet with their body.", "was shot in the back by {player}. Classic.", "got 360-no-scoped by {player}.", "was used for target practice by {player}.", "trusted {player} with a gun.", "got clapped by {player}. Teamwork!" }, [Cause.TeammateExplosion] = new string[10] { "was blown up by {player}.", "got turned into confetti by {player}'s explosion.", "was standing too close to {player}'s bright idea.", "got friendly-fired into orbit by {player}.", "was redistributed across the level by {player}.", "got caught in {player}'s blast. Oops.", "was nuked by {player}.", "died for {player}'s fireworks show.", "got blown to bits by {player}. Nice throw.", "was {player}'s collateral damage." }, [Cause.TeammateMelee] = new string[8] { "was beaten to death with a {item} by {player}.", "got bonked with a {item} by {player}.", "was {item}'d to death by {player}.", "caught {player}'s {item} with their skull.", "was murdered by {player} with a {item}. In cold blood.", "got smacked into the afterlife by {player}'s {item}.", "was assaulted with a {item} by {player}.", "got whacked by {player}. Weapon of choice: {item}." }, [Cause.TeammateObject] = new string[10] { "got hit by a {item} thrown by {player}.", "was crushed by {player}'s {item}.", "caught {player}'s {item} with their face.", "was killed by a flying {item}. Pilot: {player}.", "got yeeted on by {player}. Projectile: {item}.", "was flattened by a {item} courtesy of {player}.", "died to {player}'s throwing arm and a {item}.", "got {item}'d by {player}.", "was hit by {player}'s {item} at terminal velocity.", "got a {item} delivery from {player}. Express. Fatal." }, [Cause.SelfExplosion] = new string[8] { "blew themselves up. Big brain.", "played themselves. Explosively.", "found out how explosions work. From the inside.", "pulled the pin and kept the grenade.", "was their own worst enemy. And their own explosion.", "self-destructed by accident. Allegedly.", "shot themselves. Somehow.", "was killed by the person they trusted most: themselves." }, [Cause.Explosion] = new string[12] { "was turned into confetti.", "got blown to pieces.", "was redistributed across the room.", "was in the blast radius. Very in.", "exploded. Just exploded.", "got turned into a fine red mist.", "was launched into orbit by an explosion.", "learned explosions are hot. Very hot.", "went boom.", "was vaporized by a kaboom.", "got exploded into tiny little robot pieces.", "had a blast. A fatal one." }, [Cause.Object] = new string[12] { "was crushed by a {item}.", "got flattened by a falling {item}.", "was killed by a {item}. An object. A thing.", "lost a fight to a {item}.", "got bonked to death by a {item}.", "was murdered by a rogue {item}.", "was hit by a {item} going way too fast.", "got pancaked by a {item}.", "was taken out by a {item}. No witnesses.", "was ambushed by a {item}.", "got physics'd to death by a {item}.", "was squished by a {item}." }, [Cause.ObjectSelf] = new string[8] { "dropped a {item} on their own head.", "killed themselves with a {item}. Somehow.", "threw a {item} and it came back. Fast.", "was defeated by their own {item}.", "fumbled a {item} so hard it killed them.", "lost a fight to a {item} they were carrying.", "got betrayed by their own {item}.", "carried a {item}. The {item} carried them to the grave." }, [Cause.Hazard] = new string[6] { "was killed by a {item}.", "walked into a {item} and died.", "lost to the environment. Specifically a {item}.", "got merked by a {item}.", "was taken out by a {item}. Watch your step.", "found the level's {item}. Painfully." }, [Cause.Impact] = new string[15] { "forgot how floors work.", "slammed into a wall at full speed.", "ragdolled into the afterlife.", "tripped. Fell. Died. In that order.", "hit the ground. The ground hit back harder.", "bounced off the walls like a pinball until they broke.", "discovered gravity is mandatory.", "tumbled to their death like a sack of bolts.", "fell over and couldn't get up. Ever.", "was killed by physics.", "faceplanted into the shadow realm.", "went full ragdoll and didn't come back.", "lost a fight with a wall.", "achieved terminal velocity. Emphasis on terminal.", "flew. Briefly. Then landed. Permanently." }, [Cause.TumbledIntoEnemy] = new string[6] { "rolled straight into a monster. Bold strategy.", "tumbled into an enemy and paid the price.", "bowled into a monster. Strike! Also, death.", "ragdolled into an enemy's arms.", "delivered themselves to a monster like a pizza.", "tried to body-check a monster." }, [Cause.DeathPit] = new string[12] { "fell into the void.", "jumped into the pit. Why?", "found the bottomless pit. It's still going.", "fell off the map. Literally.", "took the shortcut down. There's no bottom.", "yeeted themselves into the abyss.", "did a backflip into the pit. 0/10 landing.", "stared into the abyss. Then fell in.", "is still falling, probably.", "missed the jump. Badly.", "tested if the pit had a floor. It doesn't.", "went down the hole. Didn't come back up." }, [Cause.SelfDestruct] = new string[10] { "pressed the self-destruct button. It worked.", "rage quit life.", "chose violence. Against themselves.", "pressed the big red button. Of course they did.", "self-destructed. Nobody asked them to.", "exploded on purpose. Respect.", "took the easy way out.", "decided enough was enough.", "Alt-F4'd their own existence.", "uninstalled themselves." }, [Cause.LeftBehind] = new string[8] { "was left behind by the truck and blew up.", "missed the truck. The truck didn't miss them.", "was abandoned by their friends. Fatally.", "didn't make it to the truck in time. Taxman says bye.", "was too slow for the truck.", "got ghosted by the truck. Then exploded.", "should have run faster.", "was cut from the team. Explosively." }, [Cause.GaveUp] = new string[10] { "ate the barrel.", "said 'Goodbye World' and meant it.", "gave up on the quota. And on life.", "took the shotgun way out.", "voted yes. Then made it personal.", "decided the quota wasn't worth it.", "chose the barrel over the paperwork.", "resigned. Permanently.", "blew their own head off. Literally. It flew.", "couldn't face Taxman." }, [Cause.Tentacle] = new string[10] { "was dragged into the fog.", "got taken by the tentacle.", "was pulled into the mist and never seen again.", "got slapped around the level by a giant tentacle.", "should have used the flashlight.", "was the tentacle's chew toy.", "got slammed into every wall by the tentacle.", "went for a ride with the tentacle. One way.", "was squeezed to death by something in the fog.", "got wrapped up and taken away." }, [Cause.Drain] = new string[6] { "had their health drained to zero.", "slowly withered away.", "ran out of juice.", "got drained like a phone battery.", "died of mysterious health loss.", "healed so hard they died." }, [Cause.Arena] = new string[6] { "lost in the arena.", "was eliminated. Better luck next run.", "didn't survive the arena.", "got knocked out of the arena.", "was the first to go. Again.", "lost the final showdown." }, [Cause.Imposter] = new string[8] { "was the imposter. Was.", "failed their secret mission and exploded. Sus.", "was ejected. Violently. From existence.", "had one job. Several, actually. Exploded anyway.", "said the forbidden word. Kaboom.", "was too sus to live.", "got remote-detonated by Taxman for poor performance.", "blew their cover. And themselves." }, [Cause.Unknown] = new string[10] { "died. Nobody knows how.", "just stopped existing.", "died mysteriously. Suspicious.", "kicked the bucket. The bucket was fine.", "died of unknown causes. Probably skill issue.", "exploded. Maybe. We think.", "was killed by... something.", "died and the coroner gave up.", "fell over and never got back up. Reason unknown.", "is dead. That's all we know." } }; internal static string Build(string victim, DeathContext context, bool oneShot) { string text = Pick(context, oneShot); text = text.Replace("{enemy}", Name(context.EnemyName, "something")).Replace("{item}", Name(context.Item, "thing")).Replace("{player}", Name(context.Teammate, "a teammate")); return "<b>" + victim + "</b> " + text; } private static string Pick(DeathContext context, bool oneShot) { if (context.Cause == Cause.Enemy) { if (oneShot && Random.value < 0.5f) { return RandomOf(OneShotEnemy); } string[] array = EnemyFor(context.EnemyKey); if (array != null && Random.value < 0.75f) { return RandomOf(array); } return RandomOf(GenericEnemy); } if (CauseLines.TryGetValue(context.Cause, out var value)) { return RandomOf(value); } return RandomOf(CauseLines[Cause.Unknown]); } private static string[] EnemyFor(string key) { if (string.IsNullOrEmpty(key)) { return null; } if (EnemyLines.TryGetValue(key, out var value)) { return value; } string text = (from k in EnemyLines.Keys where key.Contains(k) orderby k.Length descending select k).FirstOrDefault(); if (text == null) { return null; } return EnemyLines[text]; } private static string Name(string value, string fallback) { if (!string.IsNullOrEmpty(value)) { return value; } return fallback; } private static string RandomOf(string[] list) { return list[Random.Range(0, list.Length)]; } internal static int Count() { return EnemyLines.Values.Sum((string[] l) => l.Length) + GenericEnemy.Length + OneShotEnemy.Length + CauseLines.Values.Sum((string[] l) => l.Length); } } [BepInPlugin("kinglir.howtheydied", "HowTheyDied", "0.1.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { public const string Guid = "kinglir.howtheydied"; public const string Name = "HowTheyDied"; public const string Version = "0.1.1"; internal static ManualLogSource Log; internal static ConfigEntry<int> DeathsPerPage; internal static ConfigEntry<float> PageSeconds; internal static ConfigEntry<bool> ShowInShop; internal static ConfigEntry<bool> ReplaceTruckEmojis; internal static ConfigEntry<bool> TestFakeDeaths; private void Awake() { //IL_00b9: Unknown result type (might be due to invalid IL or missing references) Log = ((BaseUnityPlugin)this).Logger; DeathsPerPage = ((BaseUnityPlugin)this).Config.Bind<int>("Truck screen", "DeathsPerPage", 3, "Antal dödsfall per sida på lastbilsskärmen (skärmen rymmer ca 12 rader)."); PageSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Truck screen", "PageSeconds", 8f, "Sekunder mellan sidbyten."); ShowInShop = ((BaseUnityPlugin)this).Config.Bind<bool>("Truck screen", "ShowInShop", true, "Visa runnens dödsfall på lastbilsskärmen i shoppen, i arenan efter game over och i lobbyn efter en misslyckad nivå."); ReplaceTruckEmojis = ((BaseUnityPlugin)this).Config.Bind<bool>("Truck screen", "ReplaceTruckEmojis", true, "Ersätt Taxmans emoji-rader (spelare in i lastbilen + hälsa) med dödsrapporten vid nivåns slut."); TestFakeDeaths = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "TestFakeDeaths", false, "Endast för test: lägger till påhittade dödsfall i början av varje nivå, och skärmen visar dem direkt."); DeathNetwork.Register(); new Harmony("kinglir.howtheydied").PatchAll(); Log.LogInfo((object)string.Format("{0} {1} laddad ({2} dödsrader).", "HowTheyDied", "0.1.1", Lines.Count())); } } internal sealed class TruckReport : MonoBehaviour { private TruckScreenText screen; private bool active; private bool shopChecked; private float created; private float nextPage; private int page; private int postedPages; private void Awake() { screen = ((Component)this).GetComponent<TruckScreenText>(); created = Time.time; if (Plugin.TestFakeDeaths.Value) { shopChecked = true; ((MonoBehaviour)this).Invoke("Activate", 6f); } } internal void Activate() { if (!active) { active = true; page = 0; postedPages = 0; nextPage = Time.time + 1.5f; Plugin.Log.LogInfo((object)$"Dödsrapport på lastbilsskärmen ({DeathLog.Records.Count} dödsfall)."); } } private void Update() { if ((Object)(object)screen == (Object)null || !SemiFunc.IsMasterClientOrSingleplayer()) { return; } if (!shopChecked && (Object)(object)RunManager.instance != (Object)null && AutoShowHere() && screen.started && Time.time - created > 6f) { shopChecked = true; if (Plugin.ShowInShop.Value) { Activate(); } } if (active && !(Time.time < nextPage) && !screen.isTyping) { Post(); } } private bool AutoShowHere() { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Invalid comparison between Unknown and I4 RunManager instance = RunManager.instance; if (!instance.levelIsShop && !SemiFunc.RunIsArena()) { if ((int)screen.screenType == 1) { return instance.levelFailed; } return false; } return true; } private void Post() { List<DeathRecord> records = DeathLog.Records; if (records.Count == 0) { screen.MessageSendCustom("", "<b>R.I.P.</b>\nNobody died this run. Suspicious.", 0); active = false; return; } int num = Mathf.Max(1, Plugin.DeathsPerPage.Value); int num2 = (records.Count + num - 1) / num; page %= num2; string text = string.Format("<b>R.I.P.</b> {0} death{1} this run <color=#555555>({2}/{3})</color>", records.Count, (records.Count == 1) ? "" : "s", page + 1, num2); for (int i = page * num; i < Mathf.Min(records.Count, (page + 1) * num); i++) { text = text + "\n" + records[i].Format(); } screen.MessageSendCustom("", text, 0); postedPages++; page++; nextPage = Time.time + Mathf.Max(2f, Plugin.PageSeconds.Value); if (num2 == 1) { active = false; } } } [HarmonyPatch(typeof(TruckScreenText), "Start")] internal static class TruckScreenStartPatch { private static void Postfix(TruckScreenText __instance) { if ((Object)(object)((Component)__instance).GetComponent<TruckReport>() == (Object)null) { ((Component)__instance).gameObject.AddComponent<TruckReport>(); } } } [HarmonyPatch(typeof(TruckScreenText), "MessageSendCustom")] internal static class TruckMessagePatch { private static bool Prefix(TruckScreenText __instance, string playerName, string message) { if (!Plugin.ReplaceTruckEmojis.Value || playerName != "" || message == null || !message.Contains("{arrowright}{truck}{check}")) { return true; } TruckReport component = ((Component)__instance).GetComponent<TruckReport>(); if ((Object)(object)component != (Object)null) { component.Activate(); } return false; } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }