From 6b774500ddd0ebd649d1925931078b7bddf2b7a6 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Tue, 18 Aug 2026 23:07:39 -0500
Subject: [PATCH] Fix reimplemented Judge for good
---
.../Vanilla/Meeting/Voting/HandleVoteEvent.cs | 21 +++++
MiraAPI/MiraApiPlugin.cs | 2 +
MiraAPI/Networking/MiraRpc.cs | 5 ++
MiraAPI/Patches/Voting/MeetingHudPatches.cs | 11 +++
MiraAPI/VanillaEvents/JudgeEvents.cs | 8 +-
MiraAPI/Voting/VotingUtils.cs | 77 ++++++++++++++++++-
6 files changed, 119 insertions(+), 5 deletions(-)
diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs
index 2145b32..8f29544 100644
--- a/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs
+++ b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs
@@ -34,6 +34,11 @@ public class HandleVoteEvent : MiraCancelableEvent
///
public NetworkedPlayerInfo TargetPlayerInfo { get; }
+ ///
+ /// Gets or sets a value indicating whether the vote event is an Overrule caused by a Judge.
+ ///
+ public bool IsOverruling { get; set; }
+
///
/// Initializes a new instance of the class.
///
@@ -47,4 +52,20 @@ public class HandleVoteEvent : MiraCancelableEvent
PreventVote = false;
TargetPlayerInfo = GameData.Instance.GetPlayerById(targetId);
}
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The voter's .
+ /// The target's playerId.
+ /// Whether the vote is actually for a Judge.
+ public HandleVoteEvent(PlayerVoteData playerVoteData, byte targetId, bool isOverruling)
+ {
+ VoteData = playerVoteData;
+ Player = playerVoteData.Owner;
+ TargetId = targetId;
+ PreventVote = false;
+ IsOverruling = isOverruling;
+ TargetPlayerInfo = GameData.Instance.GetPlayerById(targetId);
+ }
}
diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs
index 594d927..2ce6c88 100644
--- a/MiraAPI/MiraApiPlugin.cs
+++ b/MiraAPI/MiraApiPlugin.cs
@@ -4,6 +4,7 @@ using BepInEx;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using MiraAPI.PluginLoading;
+using MiraAPI.VanillaEvents;
using Reactor;
using Reactor.Networking;
using Reactor.Networking.Attributes;
@@ -49,6 +50,7 @@ public partial class MiraApiPlugin : BasePlugin
public override void Load()
{
Harmony.PatchAll();
+ JudgeEvents.Initialize();
ReactorCredits.Register("Mira API", Version, IsDevBuild, ReactorCredits.AlwaysShow);
diff --git a/MiraAPI/Networking/MiraRpc.cs b/MiraAPI/Networking/MiraRpc.cs
index 25312e0..7228904 100644
--- a/MiraAPI/Networking/MiraRpc.cs
+++ b/MiraAPI/Networking/MiraRpc.cs
@@ -74,4 +74,9 @@ public enum MiraRpc : uint
/// Custom RPC to replace Among Us CastVote.
///
CastVote,
+
+ ///
+ /// Custom RPC to replace Among Us QueueOverruleVotes.
+ ///
+ QueueOverruleVotes,
}
diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs
index ed71003..d88c109 100644
--- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs
+++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs
@@ -2,6 +2,7 @@
using System.Linq;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
+using InnerNet;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Meeting;
using MiraAPI.Events.Vanilla.Meeting.Voting;
@@ -223,6 +224,16 @@ internal static class MeetingHudPatches
return false;
}
+ // TODO: figure out a way to do host-authorization since right now any player can send RpcQueueOverruleVotes
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(MeetingHud.CmdQueueOverruleVotes))]
+ // Although this method is inlined in MeetingHud.Confirm, the next patch fixes that.
+ public static bool CmdQueueOverruleVotesPatch(MeetingHud __instance, PlayerId judgePlayerId, PlayerId targetPlayerId, ushort overruleNonce)
+ {
+ VotingUtils.RpcQueueOverruleVotes(PlayerControl.LocalPlayer, judgePlayerId.Value, targetPlayerId.Value, overruleNonce);
+ return false;
+ }
+
[HarmonyPrefix]
[HarmonyPatch(nameof(MeetingHud.Confirm))]
public static bool ConfirmPatch(MeetingHud __instance, [HarmonyArgument(0)] byte suspect)
diff --git a/MiraAPI/VanillaEvents/JudgeEvents.cs b/MiraAPI/VanillaEvents/JudgeEvents.cs
index 639642e..4a20c6e 100644
--- a/MiraAPI/VanillaEvents/JudgeEvents.cs
+++ b/MiraAPI/VanillaEvents/JudgeEvents.cs
@@ -6,11 +6,17 @@ namespace MiraAPI.VanillaEvents;
public static class JudgeEvents
{
- [RegisterEvent(100000)]
+ public static void Initialize()
+ {
+ // This is required because MiraAPI isn't an IMiraPlugin (besides in the gamemodes branch)
+ MiraEventManager.RegisterEventHandler(@event => ProcessVotesEventHandler(@event), -1000);
+ }
+
public static void ProcessVotesEventHandler(ProcessVotesEvent @event)
{
if (MeetingHud.Instance.TryGetWinningOverrule(out var judgeOverrule, out var networkedPlayerInfo, out var networkedPlayerInfo2))
{
+ Error("Judge has overruled votes!");
@event.OverruledVote = true;
@event.OverruledNonce = judgeOverrule.OverruleNonce;
diff --git a/MiraAPI/Voting/VotingUtils.cs b/MiraAPI/Voting/VotingUtils.cs
index fdbdce1..9744546 100644
--- a/MiraAPI/Voting/VotingUtils.cs
+++ b/MiraAPI/Voting/VotingUtils.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
+using InnerNet;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Meeting.Voting;
using MiraAPI.Networking;
@@ -41,9 +42,9 @@ public static class VotingUtils
/// The player's .
/// Who the player voted for.
/// Whether you want the vote to commence or not.
- private static void HandleVote(PlayerVoteData voteData, byte suspectIdx, out bool cancelVote)
+ private static void HandleVote(PlayerVoteData voteData, byte suspectIdx, bool byJudge, out bool cancelVote)
{
- var @event = new HandleVoteEvent(voteData, suspectIdx);
+ var @event = new HandleVoteEvent(voteData, suspectIdx, byJudge);
MiraEventManager.InvokeEvent(@event);
cancelVote = @event.PreventVote;
@@ -60,7 +61,14 @@ public static class VotingUtils
return;
}
- voteData.DecreaseRemainingVotes(1);
+ if (byJudge)
+ {
+ voteData.SetRemainingVotes(0);
+ }
+ else
+ {
+ voteData.DecreaseRemainingVotes(1);
+ }
voteData.VoteForPlayer(suspectIdx);
}
@@ -100,6 +108,67 @@ public static class VotingUtils
MeetingHud.Instance.SkipVoteButton.gameObject.SetActive(true);
}
+ ///
+ /// Networks the casting of a vote. We replace the vanilla solution with a custom version that works for Judge specifically.
+ ///
+ /// The who sent this RPC.
+ /// The id of the player who casted the vote.
+ /// The voted player's id.
+ /// Data that is checked by the Judge role to determine which Judge takes priority.
+ [MethodRpc((uint)MiraRpc.QueueOverruleVotes)]
+ public static void RpcQueueOverruleVotes(PlayerControl source, byte srcPlayerId, byte suspectPlayerId, ushort overruleNonce)
+ {
+ CustomCastJudgeVote(srcPlayerId, suspectPlayerId, overruleNonce);
+ }
+
+ private static void CustomCastJudgeVote(PlayerId judgePlayerId, PlayerId targetPlayerId, ushort overruleNonce)
+ {
+ var plr = GameData.Instance.GetPlayerById(judgePlayerId.Value);
+ if (!plr) return;
+
+ var voteData = plr.Object.GetVoteData();
+ if (!voteData) return;
+
+ HandleVote(voteData, targetPlayerId.Value, true, out var cancelVote);
+
+ // Handle local behaviour for the voter (for some reason checking AmOwner does not work for host)
+ if (PlayerControl.LocalPlayer.PlayerId == judgePlayerId.Value)
+ {
+ if (!cancelVote) SoundManager.Instance.PlaySound(MeetingHud.Instance.VoteLockinSound, false);
+
+ foreach (var playerVoteArea in MeetingHud.Instance.playerStates)
+ {
+ playerVoteArea.ClearButtons();
+ }
+
+ MeetingHud.Instance.SkipVoteButton.ClearButtons();
+
+ var localVoteData = PlayerControl.LocalPlayer.GetVoteData();
+ if (!localVoteData || localVoteData.VotesRemaining != 0) return;
+
+ MeetingHud.Instance.SkipVoteButton.VoteComplete = true;
+ MeetingHud.Instance.SkipVoteButton.gameObject.SetActive(false);
+ }
+
+ if (cancelVote) return;
+
+ // If player has no more votes, then make it show that the player has used all their votes.
+ if (voteData.VotesRemaining == 0)
+ {
+ MeetingHud.Instance.AddOrUpdateJudgeOverrule(judgePlayerId, targetPlayerId, overruleNonce);
+ MeetingHud.Instance.playerStates.First(x => x.PlayerId == judgePlayerId.Value).SetVote(targetPlayerId.Value);
+ }
+
+ // If host, then check end voting, and set votes/send chat if applicable
+ if (!PlayerControl.LocalPlayer.IsHost()) return;
+
+ MeetingHud.Instance.SetDirtyBit(1U);
+ MeetingHud.Instance.CheckForEndVoting();
+
+ if (voteData.VotesRemaining != 0) return;
+ PlayerControl.LocalPlayer.RpcSendChatNote(judgePlayerId.Value, ChatNoteTypes.DidVote);
+ }
+
///
/// Networks the casting of a vote. We replace the vanilla solution with a custom version that works for the use case.
///
@@ -120,7 +189,7 @@ public static class VotingUtils
var voteData = plr.Object.GetVoteData();
if (!voteData) return;
- HandleVote(voteData, suspectPlayerId, out var cancelVote);
+ HandleVote(voteData, suspectPlayerId, false, out var cancelVote);
// Handle local behaviour for the voter (for some reason checking AmOwner does not work for host)
if (PlayerControl.LocalPlayer.PlayerId == srcPlayerId)