diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e09f9ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build/ +include/ +Releases/* +Releases/*.* +!Releases/build.bat \ No newline at end of file diff --git a/.vs/ProjectEvaluation/wackycosmetics.metadata.v5.2 b/.vs/ProjectEvaluation/wackycosmetics.metadata.v5.2 new file mode 100644 index 0000000..ede2bae Binary files /dev/null and b/.vs/ProjectEvaluation/wackycosmetics.metadata.v5.2 differ diff --git a/.vs/ProjectEvaluation/wackycosmetics.projects.v5.2 b/.vs/ProjectEvaluation/wackycosmetics.projects.v5.2 new file mode 100644 index 0000000..2299fbf Binary files /dev/null and b/.vs/ProjectEvaluation/wackycosmetics.projects.v5.2 differ diff --git a/.vs/WackyCosmetics/DesignTimeBuild/.dtbcache.v2 b/.vs/WackyCosmetics/DesignTimeBuild/.dtbcache.v2 new file mode 100644 index 0000000..6e13924 Binary files /dev/null and b/.vs/WackyCosmetics/DesignTimeBuild/.dtbcache.v2 differ diff --git a/.vs/WackyCosmetics/FileContentIndex/219f0928-d921-4b66-bfea-ba0d9d1d1d35.vsidx b/.vs/WackyCosmetics/FileContentIndex/219f0928-d921-4b66-bfea-ba0d9d1d1d35.vsidx new file mode 100644 index 0000000..201f122 Binary files /dev/null and b/.vs/WackyCosmetics/FileContentIndex/219f0928-d921-4b66-bfea-ba0d9d1d1d35.vsidx differ diff --git a/.vs/WackyCosmetics/FileContentIndex/275c3de1-edaf-4c57-a358-303ce66242b6.vsidx b/.vs/WackyCosmetics/FileContentIndex/275c3de1-edaf-4c57-a358-303ce66242b6.vsidx new file mode 100644 index 0000000..566d6a6 Binary files /dev/null and b/.vs/WackyCosmetics/FileContentIndex/275c3de1-edaf-4c57-a358-303ce66242b6.vsidx differ diff --git a/.vs/WackyCosmetics/FileContentIndex/dc14c22c-4597-4e14-8f80-31ba72e5d220.vsidx b/.vs/WackyCosmetics/FileContentIndex/dc14c22c-4597-4e14-8f80-31ba72e5d220.vsidx new file mode 100644 index 0000000..120d604 Binary files /dev/null and b/.vs/WackyCosmetics/FileContentIndex/dc14c22c-4597-4e14-8f80-31ba72e5d220.vsidx differ diff --git a/.vs/WackyCosmetics/FileContentIndex/de1b9edb-be3b-4e2e-89a2-ecad5483dc09.vsidx b/.vs/WackyCosmetics/FileContentIndex/de1b9edb-be3b-4e2e-89a2-ecad5483dc09.vsidx new file mode 100644 index 0000000..0d606e8 Binary files /dev/null and b/.vs/WackyCosmetics/FileContentIndex/de1b9edb-be3b-4e2e-89a2-ecad5483dc09.vsidx differ diff --git a/.vs/WackyCosmetics/FileContentIndex/read.lock b/.vs/WackyCosmetics/FileContentIndex/read.lock new file mode 100644 index 0000000..e69de29 diff --git a/.vs/WackyCosmetics/v17/.futdcache.v2 b/.vs/WackyCosmetics/v17/.futdcache.v2 new file mode 100644 index 0000000..5ed6bff Binary files /dev/null and b/.vs/WackyCosmetics/v17/.futdcache.v2 differ diff --git a/.vs/WackyCosmetics/v17/.suo b/.vs/WackyCosmetics/v17/.suo new file mode 100644 index 0000000..8c7b5ce Binary files /dev/null and b/.vs/WackyCosmetics/v17/.suo differ diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..8e85aa3 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,21 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "dotnet: debug build", + "type": "process", + "command": "dotnet", + "args": [ + "build", + "${workspaceFolder}/Coroner.sln", + "-c Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + "--no-dependencies" + ], + "problemMatcher": [ + "$msCompile" + ] + } + ] +} \ No newline at end of file diff --git a/Art/StungByBees.png b/Art/StungByBees.png new file mode 100644 index 0000000..e21f2dc Binary files /dev/null and b/Art/StungByBees.png differ diff --git a/Art/icon.png b/Art/icon.png new file mode 100644 index 0000000..57442f4 Binary files /dev/null and b/Art/icon.png differ diff --git a/Art/manifest.json b/Art/manifest.json new file mode 100644 index 0000000..81cbfe3 --- /dev/null +++ b/Art/manifest.json @@ -0,0 +1,7 @@ +{ + "name": "Coroner", + "version_number": "1.0.0", + "website_url": "https://github.com/EliteMasterEric/Coroner", + "description": "Rework the Performance Report with new info, including cause of death.", + "dependencies": [] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9977698 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +## 1.0.0 +Initial release. +- Added cause of death to the results screen. +- Added advanced cause-of-death tracking for specific enemies, falling back to built-in tracking on failure. diff --git a/Coroner.sln b/Coroner.sln new file mode 100644 index 0000000..01e4dc3 --- /dev/null +++ b/Coroner.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coroner", "Coroner\Coroner.csproj", "{04FD6926-5D80-4206-8FDF-1ACC140E9DD7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {04FD6926-5D80-4206-8FDF-1ACC140E9DD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04FD6926-5D80-4206-8FDF-1ACC140E9DD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04FD6926-5D80-4206-8FDF-1ACC140E9DD7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04FD6926-5D80-4206-8FDF-1ACC140E9DD7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9973961F-F45A-4406-8788-DFF58FC1CA65} + EndGlobalSection +EndGlobal diff --git a/Coroner/AdvancedCauseOfDeath.cs b/Coroner/AdvancedCauseOfDeath.cs new file mode 100644 index 0000000..5cfbb40 --- /dev/null +++ b/Coroner/AdvancedCauseOfDeath.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using GameNetcodeStuff; +using Steamworks; + +namespace Coroner { + class AdvancedDeathTracker { + private static readonly Dictionary PlayerCauseOfDeath = new Dictionary(); + + public static void ClearDeathTracker() { + PlayerCauseOfDeath.Clear(); + } + + public static void SetCauseOfDeath(int playerIndex, AdvancedCauseOfDeath causeOfDeath) { + PlayerCauseOfDeath[playerIndex] = causeOfDeath; + } + + public static void SetCauseOfDeath(int playerIndex, CauseOfDeath causeOfDeath) { + SetCauseOfDeath(playerIndex, ConvertCauseOfDeath(causeOfDeath)); + } + + public static void SetCauseOfDeath(PlayerControllerB playerController, CauseOfDeath causeOfDeath) { + SetCauseOfDeath((int) playerController.playerClientId, ConvertCauseOfDeath(causeOfDeath)); + } + + public static void SetCauseOfDeath(PlayerControllerB playerController, AdvancedCauseOfDeath causeOfDeath) { + SetCauseOfDeath((int) playerController.playerClientId, causeOfDeath); + } + + public static AdvancedCauseOfDeath GetCauseOfDeath(int playerIndex) { + PlayerControllerB playerController = StartOfRound.Instance.allPlayerScripts[playerIndex]; + + return GetCauseOfDeath(playerController); + } + + public static AdvancedCauseOfDeath GetCauseOfDeath(PlayerControllerB playerController) { + if (!PlayerCauseOfDeath.ContainsKey((int) playerController.playerClientId)) { + return GuessCauseOfDeath(playerController); + } + return PlayerCauseOfDeath[(int) playerController.playerClientId]; + } + + public static AdvancedCauseOfDeath GuessCauseOfDeath(PlayerControllerB playerController) { + if (playerController.isPlayerDead) { + if (playerController.causeOfDeath == CauseOfDeath.Suffocation && playerController.isSinking) { + return AdvancedCauseOfDeath.Quicksand; + } else if (IsHoldingJetpack(playerController)) { + if (playerController.causeOfDeath == CauseOfDeath.Gravity) { + return AdvancedCauseOfDeath.Jetpack_Gravity; + } else if (playerController.causeOfDeath == CauseOfDeath.Blast) { + return AdvancedCauseOfDeath.Jetpack_Blast; + } + } + + return ConvertCauseOfDeath(playerController.causeOfDeath); + } else { + return AdvancedCauseOfDeath.Unknown; + } + } + + public static bool IsHoldingJetpack(PlayerControllerB playerController) { + var heldObject = playerController.currentlyHeldObjectServer.gameObject.GetComponent(); + if (heldObject == null) { + return false; + } + if (heldObject is JetpackItem) { + return true; + } else { + return false; + } + } + + public static AdvancedCauseOfDeath ConvertCauseOfDeath(CauseOfDeath causeOfDeath) { + switch (causeOfDeath) { + case CauseOfDeath.Unknown: + return AdvancedCauseOfDeath.Unknown; + case CauseOfDeath.Bludgeoning: + return AdvancedCauseOfDeath.Bludgeoning; + case CauseOfDeath.Gravity: + return AdvancedCauseOfDeath.Gravity; + case CauseOfDeath.Blast: + return AdvancedCauseOfDeath.Blast; + case CauseOfDeath.Strangulation: + return AdvancedCauseOfDeath.Strangulation; + case CauseOfDeath.Suffocation: + return AdvancedCauseOfDeath.Suffocation; + case CauseOfDeath.Mauling: + return AdvancedCauseOfDeath.Mauling; + case CauseOfDeath.Gunshots: + return AdvancedCauseOfDeath.Gunshots; + case CauseOfDeath.Crushing: + return AdvancedCauseOfDeath.Crushing; + case CauseOfDeath.Drowning: + return AdvancedCauseOfDeath.Drowning; + case CauseOfDeath.Abandoned: + return AdvancedCauseOfDeath.Abandoned; + case CauseOfDeath.Electrocution: + return AdvancedCauseOfDeath.Electrocution; + default: + return AdvancedCauseOfDeath.Unknown; + } + } + + public static string StringifyCauseOfDeath(CauseOfDeath causeOfDeath) { + return StringifyCauseOfDeath(ConvertCauseOfDeath(causeOfDeath)); + } + + public static string StringifyCauseOfDeath(AdvancedCauseOfDeath causeOfDeath) { + switch (causeOfDeath) { + case AdvancedCauseOfDeath.Bludgeoning: + return "Bludgeoned to death."; + case AdvancedCauseOfDeath.Gravity: + return "Fell to their death."; + case AdvancedCauseOfDeath.Blast: + return "Exploded."; + case AdvancedCauseOfDeath.Strangulation: + return "Strangled to death."; + case AdvancedCauseOfDeath.Suffocation: + return "Suffocated to death."; + case AdvancedCauseOfDeath.Mauling: + return "Mauled to death."; + case AdvancedCauseOfDeath.Gunshots: + return "Shot to death by a turret."; + case AdvancedCauseOfDeath.Crushing: + return "Crushed to death."; + case AdvancedCauseOfDeath.Drowning: + return "Drowned to death."; + case AdvancedCauseOfDeath.Abandoned: + return "Abandoned by their coworkers."; + case AdvancedCauseOfDeath.Electrocution: + return "Electrocuted to death."; + + case AdvancedCauseOfDeath.Enemy_DepositItemsDesk: + return "Received a demotion."; + case AdvancedCauseOfDeath.Enemy_Bracken: + return "Had their neck snapped by a Bracken."; + case AdvancedCauseOfDeath.Enemy_EyelessDog: + return "Was eaten by an Eyeless Dog."; + case AdvancedCauseOfDeath.Enemy_ForestGiant: + return "Swallowed whole by a Forest Giant."; + case AdvancedCauseOfDeath.Enemy_CircuitBees: + return "Electro-stung to death by Circuit Bees."; + case AdvancedCauseOfDeath.Enemy_GhostGirl: + return "Died a mysterious death."; + case AdvancedCauseOfDeath.Enemy_EarthLeviathan: + return "Swallowed whole by an Earth Leviathan."; + case AdvancedCauseOfDeath.Enemy_BaboonHawk: + return "Was eaten by a Baboon Hawk."; + case AdvancedCauseOfDeath.Enemy_Jester: + return "Was the butt of a joke."; + + case AdvancedCauseOfDeath.Quicksand: + return "Got stuck in quicksand."; + case AdvancedCauseOfDeath.Jetpack_Gravity: + return "Flew too close to the sun."; + case AdvancedCauseOfDeath.Jetpack_Blast: + return "Went up in a fiery blaze."; + + case AdvancedCauseOfDeath.Unknown: + return "Died somehow."; + default: + return "Died somehow."; + } + } + + internal static void SetCauseOfDeath(PlayerControllerB playerControllerB, object enemy_BaboonHawk) + { + throw new NotImplementedException(); + } + } + + enum AdvancedCauseOfDeath { + // Basic causes of death + Unknown, + Bludgeoning, + Gravity, + Blast, + Strangulation, + Suffocation, + Mauling, + Gunshots, + Crushing, + Drowning, + Abandoned, + Electrocution, + + // Custom causes (enemies) + Enemy_DepositItemsDesk, + Enemy_Bracken, // Also known as Flowerman + Enemy_EyelessDog, // Also known as MouthDog + Enemy_ForestGiant, + Enemy_CircuitBees, // Also known as RedLocustBees + Enemy_GhostGirl, // Also known as DressGirl + Enemy_EarthLeviathan, // Also known as SandWorm + Enemy_BaboonHawk, // Also known as BaboonBird + + // Custom causes (other) + Quicksand, + Enemy_Jester, + Jetpack_Gravity, + Jetpack_Blast, + } +} \ No newline at end of file diff --git a/Coroner/Coroner.csproj b/Coroner/Coroner.csproj new file mode 100644 index 0000000..cf97c18 --- /dev/null +++ b/Coroner/Coroner.csproj @@ -0,0 +1,94 @@ + + + + netstandard2.1 + + EliteMasterEric + Coroner + com.elitemastereric.coroner + 1.0.0 + + Coroner + Rework the Performance Report with new info, including cause of death. + + true + latest + false + + + + + + + + + + + + + + + + + false + ..\include\Assembly-CSharp.dll + + + false + ..\include\DissonanceVoip.dll + + + false + ..\include\Facepunch Transport for Netcode for GameObjects.dll + + + false + ..\include\Facepunch.Steamworks.Win64.dll + + + false + ..\include\Unity.Collections.dll + + + false + ..\include\Unity.RenderPipelines.HighDefinition.Config.Runtime.dll + + + false + ..\include\Unity.RenderPipelines.HighDefinition.Runtime.dll + + + false + ..\include\Unity.Netcode.Components.dll + + + false + ..\include\Unity.Netcode.Runtime.dll + + + false + ..\include\Unity.TextMeshPro.dll + + + false + ..\include\UnityEngine.UI.dll + + + + + + + %(RecursiveDir)%(Filename)%(Extension) + + + + + + + + + diff --git a/Coroner/Directory.Build.props b/Coroner/Directory.Build.props new file mode 100644 index 0000000..6fb5e9d --- /dev/null +++ b/Coroner/Directory.Build.props @@ -0,0 +1,6 @@ + + + ./build/bin + ./build/obj + + diff --git a/Coroner/Patch/CauseOfDeathPatch.cs b/Coroner/Patch/CauseOfDeathPatch.cs new file mode 100644 index 0000000..e88de5e --- /dev/null +++ b/Coroner/Patch/CauseOfDeathPatch.cs @@ -0,0 +1,190 @@ +using GameNetcodeStuff; +using HarmonyLib; +using UnityEngine; + +namespace Coroner.Patch { + /* + * A set of patches dedicated to tracking when a player dies in a specific manner, + * and storing it in the AdvancedDeathTracker, because `causeOfDeath` is not precise enough. + */ + + [HarmonyPatch(typeof(DepositItemsDesk))] + [HarmonyPatch("AnimationGrabPlayer")] + class DepositItemsDeskAnimationGrabPlayerPatch { + public static void Postfix(int playerID) { + Plugin.Instance.PluginLogger.LogInfo("Accessing state after tentacle devouring..."); + + PlayerControllerB playerDying = StartOfRound.Instance.allPlayerScripts[playerID]; + if (playerDying.isPlayerDead) { + Plugin.Instance.PluginLogger.LogInfo("Player is now dead! Setting special cause of death..."); + AdvancedDeathTracker.SetCauseOfDeath(playerDying, AdvancedCauseOfDeath.Enemy_DepositItemsDesk); + } else { + Plugin.Instance.PluginLogger.LogWarning("Player is somehow still alive! Skipping..."); + return; + } + } + } + + [HarmonyPatch(typeof(FlowermanAI))] + [HarmonyPatch("killAnimation")] + class FlowermanAIKillAnimationPatch { + public static void Postfix(FlowermanAI __instance) { + Plugin.Instance.PluginLogger.LogInfo("Accessing state after Bracken snapping neck..."); + + if (__instance.inSpecialAnimationWithPlayer == null) { + Plugin.Instance.PluginLogger.LogWarning("Could not access player after snapping neck!"); + return; + } + + if (__instance.inSpecialAnimationWithPlayer.isPlayerDead) { + Plugin.Instance.PluginLogger.LogInfo("Player is now dead! Setting special cause of death..."); + AdvancedDeathTracker.SetCauseOfDeath(__instance.inSpecialAnimationWithPlayer, AdvancedCauseOfDeath.Enemy_Bracken); + } else { + Plugin.Instance.PluginLogger.LogWarning("Player is somehow still alive! Skipping..."); + return; + } + } + } + + [HarmonyPatch(typeof(MouthDogAI))] + [HarmonyPatch("KillPlayer")] + class MouthDogAIKillPlayerPatch { + public static void Postfix(int playerId) { + Plugin.Instance.PluginLogger.LogInfo("Accessing state after dog devouring..."); + + PlayerControllerB playerDying = StartOfRound.Instance.allPlayerScripts[playerId]; + if (playerDying == null) { + Plugin.Instance.PluginLogger.LogWarning("Could not access player after death!"); + return; + } + + if (playerDying.isPlayerDead) { + Plugin.Instance.PluginLogger.LogInfo("Player is now dead! Setting special cause of death..."); + AdvancedDeathTracker.SetCauseOfDeath(playerDying, AdvancedCauseOfDeath.Enemy_EyelessDog); + } else { + Plugin.Instance.PluginLogger.LogWarning("Player is somehow still alive! Skipping..."); + return; + } + } + } + + [HarmonyPatch(typeof(ForestGiantAI))] + [HarmonyPatch("EatPlayerAnimation")] + class ForestGiantAIEatPlayerAnimationPatch { + public static void Postfix(PlayerControllerB playerBeingEaten) { + Plugin.Instance.PluginLogger.LogInfo("Accessing state after Forest Giant devouring..."); + + if (playerBeingEaten == null) { + Plugin.Instance.PluginLogger.LogWarning("Could not access player after death!"); + return; + } + + if (playerBeingEaten.isPlayerDead) { + Plugin.Instance.PluginLogger.LogInfo("Player is now dead! Setting special cause of death..."); + AdvancedDeathTracker.SetCauseOfDeath(playerBeingEaten, AdvancedCauseOfDeath.Enemy_ForestGiant); + } else { + Plugin.Instance.PluginLogger.LogWarning("Player is somehow still alive! Skipping..."); + return; + } + } + } + + [HarmonyPatch(typeof(RedLocustBees))] + [HarmonyPatch("BeeKillPlayerOnLocalClient")] + class RedLocustBeesBeeKillPlayerOnLocalClientPatch { + public static void Postfix(int playerId) { + Plugin.Instance.PluginLogger.LogInfo("Accessing state after Red Locust devouring..."); + + PlayerControllerB playerDying = StartOfRound.Instance.allPlayerScripts[playerId]; + if (playerDying == null) { + Plugin.Instance.PluginLogger.LogWarning("Could not access player after death!"); + return; + } + + if (playerDying.isPlayerDead) { + Plugin.Instance.PluginLogger.LogInfo("Player is now dead! Setting special cause of death..."); + AdvancedDeathTracker.SetCauseOfDeath(playerDying, AdvancedCauseOfDeath.Enemy_CircuitBees); + } else { + Plugin.Instance.PluginLogger.LogWarning("Player is somehow still alive! Skipping..."); + return; + } + } + } + + [HarmonyPatch(typeof(DressGirlAI))] + [HarmonyPatch("OnCollideWithPlayer")] + class DressGirlAIOnCollideWithPlayerPatch { + public static void Postfix(DressGirlAI __instance, Collider other) { + if (__instance.hauntingPlayer == null) { + Plugin.Instance.PluginLogger.LogWarning("Could not access player after death!"); + return; + } + if (__instance.hauntingPlayer.isPlayerDead) { + Plugin.Instance.PluginLogger.LogInfo("Player is now dead! Setting special cause of death..."); + AdvancedDeathTracker.SetCauseOfDeath(__instance.hauntingPlayer, AdvancedCauseOfDeath.Enemy_GhostGirl); + } else { + Plugin.Instance.PluginLogger.LogWarning("Player is somehow still alive! Skipping..."); + return; + } + } + } + + [HarmonyPatch(typeof(SandWormAI))] + [HarmonyPatch("EatPlayer")] + class SandWormAIEatPlayerPatch { + public static void Postfix(PlayerControllerB playerScript) { + if (playerScript == null) { + Plugin.Instance.PluginLogger.LogWarning("Could not access player after death!"); + return; + } + if (playerScript.isPlayerDead) { + Plugin.Instance.PluginLogger.LogInfo("Player is now dead! Setting special cause of death..."); + AdvancedDeathTracker.SetCauseOfDeath(playerScript, AdvancedCauseOfDeath.Enemy_EarthLeviathan); + } else { + Plugin.Instance.PluginLogger.LogWarning("Player is somehow still alive! Skipping..."); + return; + } + } + } + + [HarmonyPatch(typeof(BaboonBirdAI))] + [HarmonyPatch("OnCollideWithPlayer")] + class BaboonBirdAIOnCollideWithPlayerPatch { + public static void Postfix(BaboonBirdAI __instance, Collider other) { + var doingKillAnimation = Traverse.Create(__instance).Field("doingKillAnimation").GetValue(); + PlayerControllerB playerControllerB = __instance.MeetsStandardPlayerCollisionConditions(other, __instance.inSpecialAnimation || doingKillAnimation); + if (playerControllerB == null) { + Plugin.Instance.PluginLogger.LogWarning("Could not access player after death!"); + return; + } + + if (playerControllerB.isPlayerDead) { + Plugin.Instance.PluginLogger.LogInfo("Player is now dead! Setting special cause of death..."); + AdvancedDeathTracker.SetCauseOfDeath(playerControllerB, AdvancedCauseOfDeath.Enemy_BaboonHawk); + } else { + Plugin.Instance.PluginLogger.LogWarning("Player is somehow still alive! Skipping..."); + return; + } + } + } + + [HarmonyPatch(typeof(JesterAI))] + [HarmonyPatch("killPlayerAnimation")] + class JesterAIKillPlayerAnimationPatch { + public static void Postfix(JesterAI __instance, int playerId) { + PlayerControllerB playerControllerB = StartOfRound.Instance.allPlayerScripts[playerId]; + if (playerControllerB == null) { + Plugin.Instance.PluginLogger.LogWarning("Could not access player after death!"); + return; + } + + if (playerControllerB.isPlayerDead) { + Plugin.Instance.PluginLogger.LogInfo("Player is now dead! Setting special cause of death..."); + AdvancedDeathTracker.SetCauseOfDeath(playerControllerB, AdvancedCauseOfDeath.Enemy_Jester); + } else { + Plugin.Instance.PluginLogger.LogWarning("Player is somehow still alive! Skipping..."); + return; + } + } + } +} \ No newline at end of file diff --git a/Coroner/Patch/HUDManagerPatch.cs b/Coroner/Patch/HUDManagerPatch.cs new file mode 100644 index 0000000..0d06f75 --- /dev/null +++ b/Coroner/Patch/HUDManagerPatch.cs @@ -0,0 +1,84 @@ +using System; +using GameNetcodeStuff; +using TMPro; + +namespace Coroner.Patch { + [HarmonyLib.HarmonyPatch(typeof(HUDManager))] + [HarmonyLib.HarmonyPatch("FillEndGameStats")] + class HUDManagerFillEndGameStatsPatch { + /* + * A list of possible funny strings. + * Displayed when the player has no notes (and hasn't died). + */ + public static readonly string[] FUNNY_NOTES = { + "* The goofiest goober.\n", + "* The cutest employee.\n", + "* Had the most fun.\n", + "* Had the least fun.\n", + "* The bravest employee.\n", + "* Did a sick flip.\n", + "* Stubbed their toe.\n", + "* The most likely to succeed.\n", + "* The least likely to succeed.\n", + "* The most likely to die.\n", + "* The least likely to die.\n", + "* A team player.\n", + "* A real go-getter.\n", + "* Passed GO and collected $200.\n", + "* Getting freaky on a Friday night.\n", + "* I think this one's a serial killer.\n", + }; + + static readonly Random RANDOM = new Random(); + + public static void Postfix(HUDManager __instance) { + Plugin.Instance.PluginLogger.LogInfo("Applying Coroner patches to player notes..."); + + for (int playerIndex = 0; playerIndex < __instance.statsUIElements.playerNotesText.Length; playerIndex++) { + PlayerControllerB playerController = __instance.playersManager.allPlayerScripts[playerIndex]; + if (!playerController.disconnectedMidGame && !playerController.isPlayerDead && !playerController.isPlayerControlled) { + Plugin.Instance.PluginLogger.LogInfo("Player " + playerIndex + " is not controlled by a player. Skipping..."); + continue; + } + + TextMeshProUGUI textMesh = __instance.statsUIElements.playerNotesText[playerIndex]; + if (__instance.playersManager.allPlayerScripts[playerIndex].isPlayerDead) { + if (Plugin.Instance.PluginConfig.ShouldDisplayCauseOfDeath()) { + var causeOfDeath = AdvancedDeathTracker.GetCauseOfDeath(playerController); + var causeOfDeathStr = AdvancedDeathTracker.StringifyCauseOfDeath(causeOfDeath); + + if (Plugin.Instance.PluginConfig.ShouldDeathReplaceNotes()) { + Plugin.Instance.PluginLogger.LogInfo("Player " + playerIndex + " is dead! Replacing notes with Cause of Death..."); + textMesh.text = "* " + causeOfDeathStr + "\n"; + } else { + Plugin.Instance.PluginLogger.LogInfo("Player " + playerIndex + " is dead! Appending notes with Cause of Death..."); + textMesh.text += "* " + causeOfDeathStr + "\n"; + } + } else { + Plugin.Instance.PluginLogger.LogInfo("Player " + playerIndex + " is dead, but Config says leave it be..."); + } + } else { + Plugin.Instance.PluginLogger.LogInfo("Player " + playerIndex + " is not dead!"); + if (textMesh.text == "") { + if (Plugin.Instance.PluginConfig.ShouldDisplayFunnyNotes()) { + Plugin.Instance.PluginLogger.LogInfo("Player " + playerIndex + " has no notes! Injecting something funny..."); + textMesh.text = ChooseFunnyNote(); + } else { + Plugin.Instance.PluginLogger.LogInfo("Player " + playerIndex + " has no notes, but Config says leave it be..."); + } + } else { + Plugin.Instance.PluginLogger.LogInfo("Player " + playerIndex + " has notes! Let's leave it be..."); + } + } + } + + // We are done with the death tracker, so clear it. + AdvancedDeathTracker.ClearDeathTracker(); + } + + static string ChooseFunnyNote() { + // Choose a random entry from the list. + return FUNNY_NOTES[RANDOM.Next(FUNNY_NOTES.Length)]; + } + } +} \ No newline at end of file diff --git a/Coroner/Plugin.cs b/Coroner/Plugin.cs new file mode 100644 index 0000000..b5f447f --- /dev/null +++ b/Coroner/Plugin.cs @@ -0,0 +1,47 @@ +using BepInEx; + +using HarmonyLib; +using BepInEx.Logging; + +namespace Coroner +{ + public static class PluginInfo + { + public const string PLUGIN_ID = "Coroner"; + public const string PLUGIN_NAME = "Coroner"; + public const string PLUGIN_VERSION = "1.0.0"; + public const string PLUGIN_GUID = "com.elitemastereric.coroner"; + } + + [BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)] + public class Plugin : BaseUnityPlugin + { + public static Plugin Instance { get; private set; } + + public ManualLogSource PluginLogger; + + public PluginConfig PluginConfig; + + private void Awake() + { + Instance = this; + + PluginLogger = Logger; + + // Apply Harmony patches (if any exist) + Harmony harmony = new Harmony(PluginInfo.PLUGIN_GUID); + harmony.PatchAll(); + + // Plugin startup logic + PluginLogger.LogInfo($"Plugin {PluginInfo.PLUGIN_NAME} ({PluginInfo.PLUGIN_GUID}) is loaded!"); + + LoadConfig(); + } + + private void LoadConfig() + { + PluginConfig = new PluginConfig(); + PluginConfig.BindConfig(Config); + } + } +} diff --git a/Coroner/PluginConfig.cs b/Coroner/PluginConfig.cs new file mode 100644 index 0000000..17107b9 --- /dev/null +++ b/Coroner/PluginConfig.cs @@ -0,0 +1,39 @@ +using BepInEx.Configuration; + +namespace Coroner +{ + public class PluginConfig + { + ConfigEntry DisplayCauseOfDeath; + ConfigEntry DisplayFunnyNotes; + ConfigEntry DeathReplacesNotes; + + // Constructor + public PluginConfig() + { + } + + // Bind config values to fields + public void BindConfig(ConfigFile _config) + { + DisplayCauseOfDeath = _config.Bind("General", "DisplayCauseOfDeath", true, "Display the cause of death in the player notes."); + DisplayFunnyNotes = _config.Bind("General", "DisplayFunnyNotes", true, "Display a random note when the player has no notes."); + DeathReplacesNotes = _config.Bind("General", "DeathReplacesNotes", true, "True to replace notes when the player dies, false to append."); + } + + public bool ShouldDisplayCauseOfDeath() + { + return DisplayCauseOfDeath.Value; + } + + public bool ShouldDisplayFunnyNotes() + { + return DisplayFunnyNotes.Value; + } + + public bool ShouldDeathReplaceNotes() + { + return DeathReplacesNotes.Value; + } + } +} diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..13f96f0 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3214421 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Coroner + +A plugin which overhauls the end-of-mission Performance Report with new information, including cause of death for any deceased players. There are some fun easter eggs thrown in too. + +## Demonstration +![](https://raw.githubusercontent.com/EliteMasterEric/Coroner/master/Art/StungByBees.png) + +## Notice +This should support MoreCompany's overhauled results screen! If you have any issues please report them on Discord. + +## Upcoming +- More detailed information on cause of death. +- Additional bug fixes. + +## Credits +- EliteMasterEric: Programming diff --git a/Releases/build.bat b/Releases/build.bat new file mode 100644 index 0000000..579048a --- /dev/null +++ b/Releases/build.bat @@ -0,0 +1,11 @@ +REM Copy ../Art/icon.png to the current directory +copy /y ..\Art\icon.png . +REM Copy ../Art/manifest.json to the current directory +copy /y ..\Art\manifest.json . +REM Copy ../README.md to the current directory +copy /y ..\README.md . +REM Copy all files from ../Coroner/build/bin/Debug to the current directory +xcopy /s /y /q ..\Coroner\build\bin\Debug\* . + +REM Create a zip file named Coroner.zip containing all files (except build.bat) in the current directory +"C:\Program Files\7-Zip\7z.exe" a Coroner.zip * -x!build.bat diff --git a/global.json b/global.json new file mode 100644 index 0000000..3162c33 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "7.0.404" + } +} \ No newline at end of file