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.
///