diff --git a/AmongUs.props b/AmongUs.props index 3c3ea71..0819a88 100644 --- a/AmongUs.props +++ b/AmongUs.props @@ -2,7 +2,7 @@ - + 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/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs index a61cc51..8e4894a 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs @@ -33,6 +33,16 @@ public class ProcessVotesEvent(List votes, NetworkedPlayerInfo? exil /// public List Votes { get; } = votes; + /// + /// Gets or sets a value indicating whether an overruling was done. Normally set by judge but can be overwritten. + /// + public bool OverruledVote { get; set; } + + /// + /// Gets or sets a value specifically to determine which Judge (if any) caused the overruling. Normally set by judge but can be overwritten. + /// + public ushort OverruledNonce { get; set; } + /// /// Gets or sets the player to be exiled. Will be if no player is to be exiled. /// diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index 661a30a..799a40e 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -5,6 +5,7 @@ using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; using MiraAPI.PluginLoading; +using MiraAPI.VanillaEvents; using Reactor; using Reactor.Networking; using Reactor.Networking.Attributes; @@ -62,6 +63,7 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin 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 392bd46..836e822 100644 --- a/MiraAPI/Networking/MiraRpc.cs +++ b/MiraAPI/Networking/MiraRpc.cs @@ -75,6 +75,11 @@ public enum MiraRpc : uint /// CastVote, + /// + /// Custom RPC to replace Among Us QueueOverruleVotes. + /// + QueueOverruleVotes, + /// /// Custom RPC to sync the Gamemode option. /// diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs index 7a4338a..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; @@ -77,7 +78,7 @@ internal static class MeetingHudPatches [HarmonyPatch(nameof(MeetingHud.Update))] public static void ForceSkipPatch(MeetingHud __instance) { - if (__instance.state is not (MeetingHud.VoteStates.NotVoted or MeetingHud.VoteStates.Voted)) + if (__instance.state is not (MeetingHud.MeetingStates.NotVoted or MeetingHud.MeetingStates.Voted)) { return; } @@ -127,13 +128,13 @@ internal static class MeetingHudPatches return false; } - var playerVoteArea = __instance.playerStates.First(pv => pv.TargetPlayerId == pc.PlayerId); + var playerVoteArea = __instance.playerStates.First(pv => pv.PlayerId == pc.PlayerId); playerVoteArea.AmDead = true; playerVoteArea.Overlay.gameObject.SetActive(true); foreach (var player in Helpers.GetAlivePlayers()) { - var pva = __instance.playerStates.First(pv => pv.TargetPlayerId == player.PlayerId); + var pva = __instance.playerStates.First(pv => pv.PlayerId == player.PlayerId); var voteData = player.GetVoteData(); if (pva.AmDead || !voteData.VotedFor(pc.PlayerId)) @@ -150,7 +151,7 @@ internal static class MeetingHudPatches __instance.SetDirtyBit(1U); __instance.CheckForEndVoting(); - if (__instance.state == MeetingHud.VoteStates.Results) + if (__instance.state == MeetingHud.MeetingStates.Results) { __instance.SetupProceedButton(); } @@ -199,7 +200,7 @@ internal static class MeetingHudPatches }) ]); - __instance.RpcVotingComplete(voterStates, exiled, isTie); + __instance.RpcVotingComplete(voterStates, exiled, isTie, @event.OverruledVote, @event.OverruledNonce); return false; } @@ -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/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index fca6892..6c9d432 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -165,7 +165,7 @@ public static class Extensions /// /// The . /// The player's . - public static PlayerControl? GetPlayer(this PlayerVoteArea state) => GameData.Instance.GetPlayerById(state.TargetPlayerId)?.Object; + public static PlayerControl? GetPlayer(this PlayerVoteArea state) => GameData.Instance.GetPlayerById(state.PlayerId)?.Object; /// /// Gets an representing the amount of tasks a player has left. diff --git a/MiraAPI/VanillaEvents/JudgeEvents.cs b/MiraAPI/VanillaEvents/JudgeEvents.cs new file mode 100644 index 0000000..4a20c6e --- /dev/null +++ b/MiraAPI/VanillaEvents/JudgeEvents.cs @@ -0,0 +1,28 @@ +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Meeting.Voting; +using MiraAPI.Roles; + +namespace MiraAPI.VanillaEvents; + +public static class JudgeEvents +{ + 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; + + @event.ExiledPlayer = networkedPlayerInfo2.Role is ICustomRole { Team: not ModdedRoleTeams.Crewmate } || networkedPlayerInfo2.Role.TeamType == RoleTeamTypes.Impostor + ? GameData.Instance.GetPlayerById(judgeOverrule.OverruledPlayerId) + : networkedPlayerInfo; + } + } +} diff --git a/MiraAPI/Voting/VotingUtils.cs b/MiraAPI/Voting/VotingUtils.cs index fb117e4..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); } @@ -75,14 +83,14 @@ public static class VotingUtils { if (!source.IsHost()) return; - MeetingHud.Instance.playerStates.First(state => state.TargetPlayerId == voterId).UnsetVote(); + MeetingHud.Instance.playerStates.First(state => state.PlayerId == voterId).UnsetVote(); if (PlayerControl.LocalPlayer.PlayerId != voterId) { return; } - MeetingHud.Instance.playerStates.First(state => state.TargetPlayerId == votedFor).ThumbsDown.enabled = false; + MeetingHud.Instance.playerStates.First(state => state.PlayerId == votedFor).ThumbsDown.enabled = false; if (!AmongUsClient.Instance.AmHost) { @@ -93,13 +101,74 @@ public static class VotingUtils foreach (var t in MeetingHud.Instance.playerStates) { - t.voteComplete = false; + t.VoteComplete = false; } - MeetingHud.Instance.SkipVoteButton.voteComplete = false; + MeetingHud.Instance.SkipVoteButton.VoteComplete = false; 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) @@ -137,7 +206,7 @@ public static class VotingUtils var localVoteData = PlayerControl.LocalPlayer.GetVoteData(); if (!localVoteData || localVoteData.VotesRemaining != 0) return; - MeetingHud.Instance.SkipVoteButton.voteComplete = true; + MeetingHud.Instance.SkipVoteButton.VoteComplete = true; MeetingHud.Instance.SkipVoteButton.gameObject.SetActive(false); } @@ -146,7 +215,7 @@ public static class VotingUtils // If player has no more votes, then make it show that the player has used all their votes. if (voteData.VotesRemaining == 0) { - MeetingHud.Instance.playerStates.First(x => x.TargetPlayerId == srcPlayerId).SetVote(suspectPlayerId); + MeetingHud.Instance.playerStates.First(x => x.PlayerId == srcPlayerId).SetVote(suspectPlayerId); } // If host, then check end voting, and set votes/send chat if applicable @@ -233,7 +302,7 @@ public static class VotingUtils MeetingHud.Instance.BloopAVoteIcon(playerById, num, MeetingHud.Instance.SkippedVoting.transform); num++; } - else if (vote.Suspect == playerVoteArea.TargetPlayerId) + else if (vote.Suspect == playerVoteArea.PlayerId) { if (!delays.TryAdd(vote.Suspect, 0)) {