From 22716b29abe0df7e600c651b55cbc237fe674a9d Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Thu, 13 Aug 2026 09:20:26 -0500 Subject: [PATCH] Add Framed and Advanced Murder RPCs --- MiraAPI/Networking/CustomMurderRpc.cs | 410 +++++++++++++++++++++++++- MiraAPI/Networking/MiraRpc.cs | 20 ++ 2 files changed, 424 insertions(+), 6 deletions(-) diff --git a/MiraAPI/Networking/CustomMurderRpc.cs b/MiraAPI/Networking/CustomMurderRpc.cs index 7009819..b53fd85 100644 --- a/MiraAPI/Networking/CustomMurderRpc.cs +++ b/MiraAPI/Networking/CustomMurderRpc.cs @@ -164,6 +164,397 @@ public static class CustomMurderRpc playKillSound); } + /// + /// Networked Custom Murder method, which checks for meetings as well, ran by clients for the host to confirm kills. + /// + /// The killer. + /// The player to murder. + /// Whether the murder is intended to be triggered in a meeting. + /// Should the kill be indirect. + /// Should the kill ignore defense. + /// Whether the murder was successful or not. + /// Should the kill timer be reset. + /// Should a dead body be created. + /// Should the killer be snapped to the dead player. + /// Should the kill animation be shown. + /// Should the kill sound be played. + [MethodRpc((uint)MiraRpc.AdvancedCustomMurder, LocalHandling = RpcLocalHandling.Before)] + public static void RpcAdvancedCustomMurder( + this PlayerControl source, + PlayerControl target, + MeetingCheck inMeeting, + bool isIndirect, + bool ignoreDefense = false, + bool didSucceed = true, + bool resetKillTimer = true, + bool createDeadBody = true, + bool teleportMurderer = true, + bool showKillAnim = true, + bool playKillSound = true) + { + if (LobbyBehaviour.Instance) + { + return; + } + var murderResultFlags = didSucceed ? MurderResultFlags.Succeeded : MurderResultFlags.FailedError; + + var beforeMurderEvent = new BeforeMurderEvent(source, target, isIndirect, ignoreDefense, inMeeting); + MiraEventManager.InvokeEvent(beforeMurderEvent); + var isMeetingActive = MeetingHud.Instance != null || ExileController.Instance != null; + if ((inMeeting is MeetingCheck.ForMeeting && !isMeetingActive) || (inMeeting is MeetingCheck.OutsideMeeting && isMeetingActive)) + { + beforeMurderEvent.Cancel(); + } + + if (!ignoreDefense && target.ProtectedByGa()) + { + beforeMurderEvent.Cancel(); + murderResultFlags = MurderResultFlags.FailedProtected; + } + else if (beforeMurderEvent.IsCancelled) + { + murderResultFlags = MurderResultFlags.FailedError; + } + + if (beforeMurderEvent.IsCancelled && source.AmOwner) + { + source.isKilling = true; + } + + if (!PlayerControl.LocalPlayer.IsHost()) + { + return; + } + + RpcConfirmAdvancedCustomMurder( + PlayerControl.LocalPlayer, + source, + target, + isIndirect, + ignoreDefense, + murderResultFlags, + resetKillTimer, + createDeadBody, + teleportMurderer, + showKillAnim, + playKillSound); + } + + /// + /// Networked Custom Murder method, which checks for meetings as well, ran by clients for the host to confirm kills. + /// + /// The game host. + /// The killer. + /// The player to murder. + /// Should the kill be indirect. + /// Should the kill ignore defense. + /// End result for the murder. + /// Should the kill timer be reset. + /// Should a dead body be created. + /// Should the killer be snapped to the dead player. + /// Should the kill animation be shown. + /// Should the kill sound be played. + [MethodRpc((uint)MiraRpc.ConfirmAdvancedCustomMurder, LocalHandling = RpcLocalHandling.After)] + public static void RpcConfirmAdvancedCustomMurder( + this PlayerControl host, + PlayerControl source, + PlayerControl target, + bool isIndirect, + bool ignoreDefense, + MurderResultFlags murderResultFlags, + bool resetKillTimer = true, + bool createDeadBody = true, + bool teleportMurderer = true, + bool showKillAnim = true, + bool playKillSound = true) + { + if (LobbyBehaviour.Instance || !host.IsHost() || target.Data.IsDead || target.Data.Disconnected) + { + source.isKilling = false; + return; + } + + var murderResultFlags2 = MurderResultFlags.DecisionByHost | murderResultFlags; + + source.CustomMurder( + target, + null, + isIndirect, + ignoreDefense, + murderResultFlags2, + resetKillTimer, + createDeadBody, + teleportMurderer, + showKillAnim, + playKillSound); + } + + /// + /// Networked Custom Murder method, which checks for meetings as well, ran by clients for the host to confirm kills. + /// + /// The killer. + /// The player to murder. + /// The player to frame. + /// Whether the murder is intended to be triggered in a meeting. + /// Should the kill be indirect. + /// Should the kill ignore defense. + /// Whether the murder was successful or not. + /// Should the kill timer be reset. + /// Should a dead body be created. + /// Should the framed player be snapped to the dead player. + /// Should the kill animation be shown. + /// Should the kill sound be played. + [MethodRpc((uint)MiraRpc.FramedCustomMurder, LocalHandling = RpcLocalHandling.Before)] + public static void RpcFramedCustomMurder( + this PlayerControl source, + PlayerControl target, + PlayerControl framed, + MeetingCheck inMeeting, + bool isIndirect = true, + bool ignoreDefense = false, + bool didSucceed = true, + bool resetKillTimer = true, + bool createDeadBody = true, + bool teleportMurderer = true, + bool showKillAnim = true, + bool playKillSound = true) + { + if (LobbyBehaviour.Instance) + { + return; + } + var murderResultFlags = didSucceed ? MurderResultFlags.Succeeded : MurderResultFlags.FailedError; + + var beforeMurderEvent = new BeforeMurderEvent(source, target, framed, isIndirect, ignoreDefense, inMeeting); + MiraEventManager.InvokeEvent(beforeMurderEvent); + var isMeetingActive = MeetingHud.Instance != null || ExileController.Instance != null; + if ((inMeeting is MeetingCheck.ForMeeting && !isMeetingActive) || (inMeeting is MeetingCheck.OutsideMeeting && isMeetingActive)) + { + beforeMurderEvent.Cancel(); + } + + if (!ignoreDefense && target.ProtectedByGa()) + { + beforeMurderEvent.Cancel(); + murderResultFlags = MurderResultFlags.FailedProtected; + } + else if (beforeMurderEvent.IsCancelled) + { + murderResultFlags = MurderResultFlags.FailedError; + } + + if (beforeMurderEvent.IsCancelled && source.AmOwner) + { + source.isKilling = true; + } + + if (!PlayerControl.LocalPlayer.IsHost()) + { + return; + } + + RpcConfirmFramedCustomMurder( + PlayerControl.LocalPlayer, + source, + target, + framed, + isIndirect, + ignoreDefense, + murderResultFlags, + resetKillTimer, + createDeadBody, + teleportMurderer, + showKillAnim, + playKillSound); + } + + /// + /// Networked Custom Murder method, which checks for meetings as well, ran by clients for the host to confirm kills. + /// + /// The game host. + /// The killer. + /// The player to murder. + /// The player to frame. + /// Should the kill be indirect. + /// Should the kill ignore defense. + /// End result for the murder. + /// Should the kill timer be reset. + /// Should a dead body be created. + /// Should the framed player be snapped to the dead player. + /// Should the kill animation be shown. + /// Should the kill sound be played. + [MethodRpc((uint)MiraRpc.ConfirmFramedCustomMurder, LocalHandling = RpcLocalHandling.After)] + public static void RpcConfirmFramedCustomMurder( + this PlayerControl host, + PlayerControl source, + PlayerControl target, + PlayerControl framed, + bool isIndirect, + bool ignoreDefense, + MurderResultFlags murderResultFlags, + bool resetKillTimer = true, + bool createDeadBody = true, + bool teleportMurderer = true, + bool showKillAnim = true, + bool playKillSound = true) + { + if (LobbyBehaviour.Instance || !host.IsHost() || target.Data.IsDead || target.Data.Disconnected) + { + source.isKilling = false; + return; + } + + var murderResultFlags2 = MurderResultFlags.DecisionByHost | murderResultFlags; + + source.CustomMurder( + target, + framed, + isIndirect, + ignoreDefense, + murderResultFlags2, + resetKillTimer, + createDeadBody, + teleportMurderer, + showKillAnim, + playKillSound); + } + + /// + /// Custom Murder method without networking. If you need a networked version, use . + /// + /// The killer. + /// The player to murder. + /// The player to frame, if any. + /// Should the kill be indirect. + /// Should the kill ignore defense. + /// Murder result flags. + /// Should the kill timer be reset. + /// Should a dead body be created. + /// Should the killer be snapped to the dead player. + /// Should the kill animation be shown. + /// Should the kill sound be played. + public static void CustomMurder( + this PlayerControl source, + PlayerControl target, + PlayerControl? framed, + bool isIndirect, + bool ignoreDefense, + MurderResultFlags resultFlags, + bool resetKillTimer = true, + bool createDeadBody = true, + bool teleportMurderer = true, + bool showKillAnim = true, + bool playKillSound = true) + { + source.isKilling = false; + if (resultFlags.HasFlag(MurderResultFlags.FailedError) || LobbyBehaviour.Instance) + { + return; + } + + if (!ignoreDefense && (resultFlags.HasFlag(MurderResultFlags.FailedProtected) || + (resultFlags.HasFlag(MurderResultFlags.DecisionByHost) && + target.protectedByGuardianId > -1))) + { + target.protectedByGuardianThisRound = true; + var flag = PlayerControl.LocalPlayer.Data.Role.Role == RoleTypes.GuardianAngel; + if (flag && PlayerControl.LocalPlayer.Data.PlayerId == target.protectedByGuardianId) + { + DataManager.Player.Stats.IncrementStat(StatID.Role_GuardianAngel_CrewmatesProtected); + AchievementManager.Instance.OnProtectACrewmate(); + } + + if (source.AmOwner || flag) + { + target.ShowFailedMurder(); + + if (resetKillTimer) + { + source.SetKillTimer( + GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown) / 2f); + } + } + else + { + target.RemoveProtection(); + } + + return; + } + + if (!resultFlags.HasFlag(MurderResultFlags.Succeeded) && + !resultFlags.HasFlag(MurderResultFlags.DecisionByHost)) + { + return; + } + + DebugAnalytics.Instance.Analytics.Kill(target.Data, source.Data); + if (source.AmOwner) + { + DataManager.Player.Stats.IncrementStat( + GameManager.Instance.IsHideAndSeek() + ? StatID.HideAndSeek_ImpostorKills + : StatID.ImpostorKills); + + if (source.CurrentOutfitType == PlayerOutfitType.Shapeshifted) + { + DataManager.Player.Stats.IncrementStat(StatID.Role_Shapeshifter_ShiftedKills); + } + + if (Constants.ShouldPlaySfx() && playKillSound) + { + SoundManager.Instance.PlaySound(source.KillSfx, false, 0.8f); + } + + if (resetKillTimer) + { + source.SetKillTimer(GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown)); + } + } + + UnityTelemetry.Instance.WriteMurder(); + target.gameObject.layer = LayerMask.NameToLayer("Ghost"); + if (target.AmOwner) + { + DataManager.Player.Stats.IncrementStat(StatID.TimesMurdered); + if (Minigame.Instance) + { + try + { + Minigame.Instance.Close(); + Minigame.Instance.Close(); + } + catch + { + // ignored + } + } + + if (showKillAnim) + { + try + { + HudManager.Instance.KillOverlay.ShowKillAnimation(source.Data, target.Data); + } + catch (Exception e) + { + Error($"Error with kill animation: {e.ToString()}"); + } + } + + target.cosmetics.SetNameMask(false); + target.RpcSetScanner(false); + } + + AchievementManager.Instance.OnMurder( + source.AmOwner, + target.AmOwner, + source.CurrentOutfitType == PlayerOutfitType.Shapeshifted, + source.shapeshiftTargetPlayerId, + target.PlayerId); + source.MyPhysics.StartCoroutine(source.KillAnimations.RandomSnapshot().CoPerformCustomKill(source, target, framed, isIndirect, ignoreDefense, createDeadBody, teleportMurderer)); + } + /// /// Custom Murder method without networking. If you need a networked version, use . /// @@ -290,7 +681,7 @@ public static class CustomMurderRpc source.CurrentOutfitType == PlayerOutfitType.Shapeshifted, source.shapeshiftTargetPlayerId, target.PlayerId); - source.MyPhysics.StartCoroutine(source.KillAnimations.RandomSnapshot().CoPerformCustomKill(source, target, createDeadBody, teleportMurderer)); + source.MyPhysics.StartCoroutine(source.KillAnimations.RandomSnapshot().CoPerformCustomKill(source, target, null, false, false, createDeadBody, teleportMurderer)); } /// @@ -299,6 +690,9 @@ public static class CustomMurderRpc /// The kill animation. /// The murderer. /// The murdered player. + /// The framed player, if any. + /// Was the kill indirect. + /// Would the kill ignore defense. /// Should a dead body be created. /// Should the murder be teleported. /// Coroutine. @@ -306,6 +700,9 @@ public static class CustomMurderRpc this KillAnimation anim, PlayerControl source, PlayerControl target, + PlayerControl? framed, + bool isIndirect, + bool ignoreDefense, bool createDeadBody = true, bool teleportMurderer = true) { @@ -314,12 +711,13 @@ public static class CustomMurderRpc yield break; } var cam = Camera.main?.GetComponent(); - var isParticipant = source.AmOwner || target.AmOwner; - var sourcePhys = source.MyPhysics; + var playerToLunge = framed ?? source; + var isParticipant = playerToLunge.AmOwner || target.AmOwner; + var sourcePhys = playerToLunge.MyPhysics; if (teleportMurderer) { - KillAnimation.SetMovement(source, false); + KillAnimation.SetMovement(playerToLunge, false); } KillAnimation.SetMovement(target, false); @@ -373,7 +771,7 @@ public static class CustomMurderRpc yield return source.MyPhysics.Animations.CoPlayCustomAnimation(anim.BlurAnim); sourcePhys.Animations.PlayIdleAnimation(); source.NetTransform.SnapTo(target.transform.position); - KillAnimation.SetMovement(source, true); + KillAnimation.SetMovement(playerToLunge, true); } KillAnimation.SetMovement(target, true); @@ -383,7 +781,7 @@ public static class CustomMurderRpc deadBody.enabled = true; } - var afterMurderEvent = new AfterMurderEvent(source, target, deadBody); + var afterMurderEvent = framed != null ? new AfterMurderEvent(source, target, framed, deadBody, isIndirect, ignoreDefense) : new AfterMurderEvent(source, target, deadBody, isIndirect, ignoreDefense); MiraEventManager.InvokeEvent(afterMurderEvent); if (!isParticipant) diff --git a/MiraAPI/Networking/MiraRpc.cs b/MiraAPI/Networking/MiraRpc.cs index edc3a94..25312e0 100644 --- a/MiraAPI/Networking/MiraRpc.cs +++ b/MiraAPI/Networking/MiraRpc.cs @@ -40,6 +40,26 @@ public enum MiraRpc : uint /// ConfirmCustomMurder, + /// + /// Advanced Custom Murder RPC. + /// + AdvancedCustomMurder, + + /// + /// Advanced Custom Murder RPC Confirmation. + /// + ConfirmAdvancedCustomMurder, + + /// + /// Framed Custom Murder RPC. + /// + FramedCustomMurder, + + /// + /// Framed Custom Murder RPC Confirmation. + /// + ConfirmFramedCustomMurder, + /// /// Custom Game Over RPC. ///