diff --git a/CHANGELOG.md b/CHANGELOG.md index a224340..6b90cb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,15 @@ This update represents a major refactor of Coroner's codebase. - Split up causes of death for coworkers murdering with different weapons into different types (for example, Stop Signs vs Knives). - Added a documented `Coroner.API` class to make it easy for mods to add their own integrations. - NOTE: This feature is not currently 100% complete as mods cannot display their own custom causes of death right now. +- Added a `test` language which displays generic death messages for debugging purposes. ## Changed - Replaced `LC_API` with `StaticNetcodeLib` for more reliable, less bloated networking that doesn't depend on an outdated library. ## Removed - Removed all languages except for English, due to the other languages now having missing causes of death. A long-term solution for this problem will come later. ## Fixed - Fixed a bug where leaving the game before the Performance Report and then joining a new lobby would not clear causes of death, resulting in incorrect causes of death being displayed later. +## Known Issues +- Coroner may sometimes fail to distinguish between the driver and passenger of the Company Cruiser. # 1.6.2 ## Fixed diff --git a/Coroner/AdvancedCauseOfDeath.cs b/Coroner/AdvancedCauseOfDeath.cs index 278e9b7..df52e64 100644 --- a/Coroner/AdvancedCauseOfDeath.cs +++ b/Coroner/AdvancedCauseOfDeath.cs @@ -141,7 +141,7 @@ public static AdvancedCauseOfDeath GuessCauseOfDeath(PlayerControllerB playerCon } } - static GrabbableObject? GetHeldObject(PlayerControllerB playerController) + public static GrabbableObject? GetHeldObject(PlayerControllerB playerController) { var heldObjectServer = playerController.currentlyHeldObjectServer; if (heldObjectServer == null) return null; @@ -174,7 +174,7 @@ public static bool IsHoldingShovel(PlayerControllerB playerController) if (heldObject is Shovel) { - if (heldObject.gameObject.name == "Shovel") + if (heldObject.gameObject.name.StartsWith("Shovel")) { return true; } @@ -189,7 +189,7 @@ public static bool IsHoldingStopSign(PlayerControllerB playerController) if (heldObject is Shovel) { - if (heldObject.gameObject.name == "StopSign") + if (heldObject.gameObject.name.StartsWith("StopSign")) { return true; } @@ -205,7 +205,7 @@ public static bool IsHoldingYieldSign(PlayerControllerB playerController) if (heldObject is Shovel) { - if (heldObject.gameObject.name == "YieldSign") + if (heldObject.gameObject.name.StartsWith("YieldSign")) { return true; } diff --git a/Coroner/Patch/CauseOfDeathPatch.cs b/Coroner/Patch/CauseOfDeathPatch.cs index d02e643..4eeda59 100644 --- a/Coroner/Patch/CauseOfDeathPatch.cs +++ b/Coroner/Patch/CauseOfDeathPatch.cs @@ -1047,85 +1047,128 @@ public static void Prefix(PlayerControllerB __instance, ref CauseOfDeath causeOf [HarmonyPatch("DamagePlayerFromOtherClientClientRpc")] class PlayerControllerBDamagePlayerFromOtherClientClientRpcPatch { - public static void Postfix(PlayerControllerB __instance, int damageAmount, Vector3 hitDirection, int playerWhoHit, int newHealthAmount) + // IL_021c: callvirt instance void GameNetcodeStuff.PlayerControllerB::DamagePlayer(int32, bool, bool, valuetype CauseOfDeath, int32, bool, valuetype [UnityEngine.CoreModule]UnityEngine.Vector3) + const string DAMAGE_PLAYER_SIGNATURE = "Void DamagePlayer(Int32, Boolean, Boolean, CauseOfDeath, Int32, Boolean, UnityEngine.Vector3)"; + + static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator, MethodBase method) { - try + var code = new List(instructions); + + var codeToInjectDamage = BuildInstructionsToInsert(method); + if (codeToInjectDamage == null) { - Plugin.Instance.PluginLogger.LogDebug("Handling friendly fire damage..."); - if (__instance == null) + Plugin.Instance.PluginLogger.LogError("Could not build instructions to insert in PlayerControllerBDamagePlayerFromOtherClientClientRpcPatch! Safely aborting..."); + return instructions; + } + + // Search for where PlayerControllerB.DamagePlayer is called. + // Do this after the first insection is done. + int insertionIndexDamage = -1; + for (int i = 0; i < code.Count; i++) + { + CodeInstruction instruction = code[i]; + if (instruction.opcode == OpCodes.Call && instruction.operand.ToString() == DAMAGE_PLAYER_SIGNATURE) { - Plugin.Instance.PluginLogger.LogWarning("Could not access victim after death!"); - return; + insertionIndexDamage = i; } + } - if (__instance.isPlayerDead) - { - if (__instance.causeOfDeath == CauseOfDeath.Bludgeoning) - { - // NOTE: Bludgeoning is used for melee damage on all clients. - PlayerControllerB murderer = StartOfRound.Instance.allPlayerScripts[playerWhoHit]; + if (insertionIndexDamage == -1) + { + Plugin.Instance.PluginLogger.LogError("Could not find PlayerControllerB.DamagePlayer call in PlayerControllerBDamagePlayerFromOtherClientClientRpcPatch! Safely aborting..."); + return instructions; + } + else + { + // Moment of truth. + Plugin.Instance.PluginLogger.LogDebug("Injecting patch into PlayerControllerB.DamagePlayerFromOtherClientClientRpc..."); + code.InsertRange(insertionIndexDamage + 1, codeToInjectDamage); + Plugin.Instance.PluginLogger.LogDebug("Done."); - if (murderer == null) - { - Plugin.Instance.PluginLogger.LogWarning("Player was murdered by another player but couldn't access the killer!"); - AdvancedDeathTracker.SetCauseOfDeath(__instance, AdvancedCauseOfDeath.Bludgeoning); - return; - } - else - { + } - if (AdvancedDeathTracker.IsHoldingKnife(murderer)) - { - Plugin.Instance.PluginLogger.LogDebug("Player was murdered by Knife! Setting special cause of death..."); - AdvancedDeathTracker.SetCauseOfDeath(__instance, AdvancedCauseOfDeath.Player_Murder_Knife); - } - else if (AdvancedDeathTracker.IsHoldingShovel(murderer)) - { - Plugin.Instance.PluginLogger.LogDebug("Player was murdered by Shovel! Setting special cause of death..."); - AdvancedDeathTracker.SetCauseOfDeath(__instance, AdvancedCauseOfDeath.Player_Murder_Shovel); - } - else if (AdvancedDeathTracker.IsHoldingStopSign(murderer)) - { - Plugin.Instance.PluginLogger.LogDebug("Player was murdered by Stop Sign! Setting special cause of death..."); - AdvancedDeathTracker.SetCauseOfDeath(__instance, AdvancedCauseOfDeath.Player_Murder_Stop_Sign); - } - else if (AdvancedDeathTracker.IsHoldingYieldSign(murderer)) - { - Plugin.Instance.PluginLogger.LogDebug("Player was murdered by Yield Sign! Setting special cause of death..."); - AdvancedDeathTracker.SetCauseOfDeath(__instance, AdvancedCauseOfDeath.Player_Murder_Yield_Sign); - } - else - { - Plugin.Instance.PluginLogger.LogWarning("Player was killed by someone else but we don't know what weapon! " + __instance.causeOfDeath); - AdvancedDeathTracker.SetCauseOfDeath(__instance, AdvancedCauseOfDeath.Unknown); - } - } - } - else if (__instance.causeOfDeath == CauseOfDeath.Gunshots) - { - Plugin.Instance.PluginLogger.LogDebug("Player was murdered by gunfire! Setting special cause of death..."); - AdvancedDeathTracker.SetCauseOfDeath(__instance, AdvancedCauseOfDeath.Player_Murder_Shotgun); - } - else - { - Plugin.Instance.PluginLogger.LogWarning("Player was killed by someone else but we don't know how! " + __instance.causeOfDeath); - AdvancedDeathTracker.SetCauseOfDeath(__instance, AdvancedCauseOfDeath.Unknown); - } + Plugin.Instance.PluginLogger.LogDebug("Done with all PlayerControllerBDamagePlayerFromOtherClientClientRpcPatch patches."); + return code; + } + + static List? BuildInstructionsToInsert(MethodBase method) + { + var result = new List(); + + var argumentIndex_self = 0; // Instance functions are just static functions where the first argument is `self` + var argumentIndex_damageAmount = 1; + var argumentIndex_hitDirection = 2; + var argumentIndex_playerWhoHit = 3; + var argumentIndex_newHealthAmount = 4; + + result.Add(new CodeInstruction(OpCodes.Ldarg, argumentIndex_self)); + result.Add(new CodeInstruction(OpCodes.Ldarg, argumentIndex_damageAmount)); + result.Add(new CodeInstruction(OpCodes.Ldarg, argumentIndex_hitDirection)); + result.Add(new CodeInstruction(OpCodes.Ldarg, argumentIndex_playerWhoHit)); + result.Add(new CodeInstruction(OpCodes.Ldarg, argumentIndex_newHealthAmount)); + + // IL_0180: call void [Coroner]Coroner.Patch.PlayerControllerBDamagePlayerFromOtherClientClientRpcPatch::MaybeRewriteCauseOfDeath(class GameNetcodeStuff.PlayerControllerB, float32) + result.Add(new CodeInstruction(OpCodes.Call, typeof(PlayerControllerBDamagePlayerFromOtherClientClientRpcPatch).GetMethod(nameof(MaybeRewriteCauseOfDeath)))); + + return result; + } + + public static void MaybeRewriteCauseOfDeath(PlayerControllerB targetPlayer, int damageAmount, Vector3 hitDirection, int playerWhoHit, int newHealthAmount) { + Plugin.Instance.PluginLogger.LogDebug($"Player damaged another player ${targetPlayer}({damageAmount}, {hitDirection}, {playerWhoHit}, {newHealthAmount})"); + // Called when the player is DAMAGED by an explosion, but not necessarily killed. + if (targetPlayer.isPlayerDead) { + Plugin.Instance.PluginLogger.LogDebug($"Player died from friendly fire damage"); + RewriteCauseOfDeath(targetPlayer, playerWhoHit); + } else { + Plugin.Instance.PluginLogger.LogDebug($"Player did not die from friendly fire (left at ${targetPlayer.health} health)"); + } + } + + public static void RewriteCauseOfDeath(PlayerControllerB targetPlayer, int playerWhoHitIndex) + { + PlayerControllerB playerWhoHit = StartOfRound.Instance.allPlayerScripts[playerWhoHitIndex]; + + if (targetPlayer == null) { + Plugin.Instance.PluginLogger.LogError("Damage from other client: victim is null!"); + } else if (playerWhoHit == null) { + Plugin.Instance.PluginLogger.LogError("Damage from other client: attacker is null!"); + } else { + Plugin.Instance.PluginLogger.LogDebug($"Player died from murder ({targetPlayer.causeOfDeath}), determining special cause of death..."); + + if (AdvancedDeathTracker.IsHoldingShotgun(playerWhoHit)) { + Plugin.Instance.PluginLogger.LogDebug("Player was murdered by Shotgun! Setting special cause of death..."); + AdvancedDeathTracker.SetCauseOfDeath(targetPlayer, AdvancedCauseOfDeath.Player_Murder_Shotgun); + } + else if (AdvancedDeathTracker.IsHoldingKnife(playerWhoHit)) + { + Plugin.Instance.PluginLogger.LogDebug("Player was murdered by Knife! Setting special cause of death..."); + AdvancedDeathTracker.SetCauseOfDeath(targetPlayer, AdvancedCauseOfDeath.Player_Murder_Knife); + } + else if (AdvancedDeathTracker.IsHoldingShovel(playerWhoHit)) + { + Plugin.Instance.PluginLogger.LogDebug("Player was murdered by Shovel! Setting special cause of death..."); + AdvancedDeathTracker.SetCauseOfDeath(targetPlayer, AdvancedCauseOfDeath.Player_Murder_Shovel); + } + else if (AdvancedDeathTracker.IsHoldingStopSign(playerWhoHit)) + { + Plugin.Instance.PluginLogger.LogDebug("Player was murdered by Stop Sign! Setting special cause of death..."); + AdvancedDeathTracker.SetCauseOfDeath(targetPlayer, AdvancedCauseOfDeath.Player_Murder_Stop_Sign); + } + else if (AdvancedDeathTracker.IsHoldingYieldSign(playerWhoHit)) + { + Plugin.Instance.PluginLogger.LogDebug("Player was murdered by Yield Sign! Setting special cause of death..."); + AdvancedDeathTracker.SetCauseOfDeath(targetPlayer, AdvancedCauseOfDeath.Player_Murder_Yield_Sign); } else { - Plugin.Instance.PluginLogger.LogDebug("Player is somehow still alive! Skipping..."); - return; + Plugin.Instance.PluginLogger.LogWarning($"Player was killed by someone else, holding an unknown item {AdvancedDeathTracker.GetHeldObject(playerWhoHit)}! " + targetPlayer.causeOfDeath); + AdvancedDeathTracker.SetCauseOfDeath(targetPlayer, targetPlayer.causeOfDeath); } } - catch (System.Exception e) - { - Plugin.Instance.PluginLogger.LogError("Error in PlayerControllerBDamagePlayerFromOtherClientClientRpcPatch.Postfix: " + e); - Plugin.Instance.PluginLogger.LogError(e.StackTrace); - } } } + // Player_Ladder [HarmonyPatch(typeof(ExtensionLadderItem))] [HarmonyPatch("StartLadderAnimation")] @@ -1221,7 +1264,8 @@ static IEnumerable Transpiler(IEnumerable inst { // Moment of truth. Plugin.Instance.PluginLogger.LogDebug("Injecting patch into VehicleController.DestroyCar..."); - code.InsertRange(insertionIndex + 1, codeToInject); + // Inject BEFORE the KillPlayer rather than after, so they're still in the vehicle. + code.InsertRange(insertionIndex, codeToInject); Plugin.Instance.PluginLogger.LogDebug("Done."); return code; @@ -1250,6 +1294,7 @@ public static void RewriteCauseOfDeath(VehicleController vehicle) Plugin.Instance.PluginLogger.LogWarning("Could not get reference to vehicle..."); AdvancedDeathTracker.SetCauseOfDeath(GameNetworkManager.Instance.localPlayerController, AdvancedCauseOfDeath.Player_Cruiser_Explode_Bystander); } else { + Plugin.Instance.PluginLogger.LogDebug($"Got vehicle controller. ({vehicle.localPlayerInControl}, {vehicle.localPlayerInPassengerSeat})"); if (vehicle.localPlayerInControl) { @@ -1264,6 +1309,15 @@ public static void RewriteCauseOfDeath(VehicleController vehicle) } } + [HarmonyPatch(typeof(VehicleController))] + [HarmonyPatch("RemovePlayerControlOfVehicleClientRpc")] + class VehicleControllerRemovePlayerControlOfVehicleClientRpcPatch { + + static void Postfix(VehicleController __instance, int playerId, bool setIgnitionStarted) { + Plugin.Instance.PluginLogger.LogDebug($"Removed player control of vehicle: {playerId} ({setIgnitionStarted})"); + } + } + // Player_Cruiser_Driver // Player_Cruiser_Passenger [HarmonyPatch(typeof(VehicleController))] @@ -1377,7 +1431,7 @@ public static void MaybeRewriteCauseOfDeath(VehicleController vehicle) { // Called when the player is DAMAGED by a crash, but not necessarily killed. var targetPlayer = GameNetworkManager.Instance.localPlayerController; if (targetPlayer.isPlayerDead) { - Plugin.Instance.PluginLogger.LogDebug($"Player died from non-lethal car accident damage"); + Plugin.Instance.PluginLogger.LogDebug($"Player died from car accident damage"); RewriteCauseOfDeath(vehicle); } else { Plugin.Instance.PluginLogger.LogDebug($"Player did not die from car accident (left at ${targetPlayer.health} health)"); @@ -1407,7 +1461,6 @@ public static void RewriteCauseOfDeath(VehicleController vehicle) } } - // Player_Cruiser_Ran_Over [HarmonyPatch(typeof(VehicleCollisionTrigger))] [HarmonyPatch("OnTriggerEnter")] @@ -1522,7 +1575,7 @@ public static void MaybeRewriteCauseOfDeath() { var targetPlayer = GameNetworkManager.Instance.localPlayerController; if (targetPlayer.isPlayerDead) { if (targetPlayer.causeOfDeath == CauseOfDeath.Crushing) { - Plugin.Instance.PluginLogger.LogDebug($"Player died from non-lethal car accident damage"); + Plugin.Instance.PluginLogger.LogDebug($"Player died from car accident damage"); RewriteCauseOfDeath(); } else { Plugin.Instance.PluginLogger.LogWarning($"Player was hit by a car but died of something else? {targetPlayer.causeOfDeath}"); @@ -1541,7 +1594,6 @@ public static void RewriteCauseOfDeath() } } - // ========= // Other // ========= @@ -1729,7 +1781,7 @@ static IEnumerable Transpiler(IEnumerable inst public static void MaybeRewriteCauseOfDeath(PlayerControllerB targetPlayer, float killRange) { // Called when the player is DAMAGED by an explosion, but not necessarily killed. if (targetPlayer.isPlayerDead) { - Plugin.Instance.PluginLogger.LogDebug($"Player died from non-lethal landmine damage"); + Plugin.Instance.PluginLogger.LogDebug($"Player died from landmine damage"); RewriteCauseOfDeath(targetPlayer, killRange); } else { Plugin.Instance.PluginLogger.LogDebug($"Player did not die from landmine (left at ${targetPlayer.health} health)"); diff --git a/LanguageData/Strings_en.xml b/LanguageData/Strings_en.xml index 64776d2..94c42b7 100644 --- a/LanguageData/Strings_en.xml +++ b/LanguageData/Strings_en.xml @@ -274,13 +274,13 @@ - + - + - + diff --git a/README.md b/README.md index e623b2c..22ae3ea 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Report any issues on the [Lethal Company Modding Discord](https://discord.com/ch [X] Language strings [X] Code implementation [X] Fix Old Bird deaths - [] Implement cruiser + [X] Implement cruiser [X] Implement Kidnapper Fox [X] Implement Barber [] Implement localization modules