diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs index 93005de..48ec40f 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs @@ -24,6 +24,11 @@ public class HandleVoteEvent : MiraCancelableEvent /// public byte TargetId { get; } + /// + /// Gets a value indicating whether to prevent the vote from commencing. + /// + public bool PreventVote { get; } + /// /// Gets the player info of the target. /// @@ -39,6 +44,7 @@ public class HandleVoteEvent : MiraCancelableEvent VoteData = playerVoteData; Player = playerVoteData.Owner; TargetId = targetId; + PreventVote = false; TargetPlayerInfo = GameData.Instance.GetPlayerById(targetId); } } diff --git a/MiraAPI/Networking/MiraRpc.cs b/MiraAPI/Networking/MiraRpc.cs index 9ee1088..29703d7 100644 --- a/MiraAPI/Networking/MiraRpc.cs +++ b/MiraAPI/Networking/MiraRpc.cs @@ -44,4 +44,9 @@ public enum MiraRpc : uint /// Custom RPC to undo a player's vote. /// RemoveVote, + + /// + /// Custom RPC to replace Among Us CastVote + /// + CastVote, } diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs index 316fc7f..809485e 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -8,6 +8,7 @@ using MiraAPI.Events.Vanilla.Meeting.Voting; using MiraAPI.Modifiers; using MiraAPI.Utilities; using MiraAPI.Voting; +using Reactor.Utilities; namespace MiraAPI.Patches.Voting; @@ -140,84 +141,6 @@ internal static class MeetingHudPatches return false; } - [HarmonyPrefix] - [HarmonyPatch(nameof(MeetingHud.CastVote))] - public static bool CastVotePatch(MeetingHud __instance, byte srcPlayerId, byte suspectPlayerId) - { - var plr = GameData.Instance.GetPlayerById(srcPlayerId); - - if (plr == null) - { - return false; - } - - var voteData = plr.Object.GetVoteData(); - if (voteData == null || voteData.VotesRemaining == 0 || - voteData.VotedFor(suspectPlayerId)) - { - return false; - } - - VotingUtils.HandleVote(voteData, suspectPlayerId); - - __instance.SetDirtyBit(1U); - __instance.CheckForEndVoting(); - - if (voteData.VotesRemaining != 0) - { - return false; - } - - __instance.playerStates.First(x => x.TargetPlayerId == srcPlayerId).SetVote(suspectPlayerId); - PlayerControl.LocalPlayer.RpcSendChatNote(srcPlayerId, ChatNoteTypes.DidVote); - - return false; - } - - [HarmonyPostfix] - [HarmonyPatch(nameof(MeetingHud.CmdCastVote))] - public static void CmdCastVotePatch(MeetingHud __instance, byte playerId, byte suspectIdx) - { - var plr = GameData.Instance.GetPlayerById(playerId); - var voteData = plr.Object.GetVoteData(); - - if (!AmongUsClient.Instance.AmHost) - { - if (voteData == null || voteData.VotesRemaining == 0 || - voteData.VotedFor(suspectIdx)) - { - return; - } - - VotingUtils.HandleVote(voteData, suspectIdx); - - if (voteData.VotesRemaining == 0) - { - __instance.playerStates.First(x => x.TargetPlayerId == playerId).SetVote(suspectIdx); - } - } - - if (PlayerControl.LocalPlayer.PlayerId != playerId) - { - return; - } - - SoundManager.Instance.PlaySound(__instance.VoteLockinSound, false); - - foreach (var playerVoteArea in __instance.playerStates) - { - playerVoteArea.ClearButtons(); - } - - __instance.SkipVoteButton.ClearButtons(); - - var localVoteData = PlayerControl.LocalPlayer.GetVoteData(); - if (localVoteData == null || localVoteData.VotesRemaining != 0) return; - - __instance.SkipVoteButton.voteComplete = true; - __instance.SkipVoteButton.gameObject.SetActive(false); - } - [HarmonyPrefix] [HarmonyPatch(nameof(MeetingHud.CheckForEndVoting))] public static bool EndCheck(MeetingHud __instance) @@ -280,12 +203,20 @@ internal static class MeetingHudPatches return false; } + // TODO: figure out a way to do host-authorization since right now any player can send RpcCastVote + [HarmonyPrefix] + [HarmonyPatch(nameof(MeetingHud.CmdCastVote))] + public static bool CmdCastVoteOverridePatch(MeetingHud __instance, byte playerId, byte suspectIdx) + { + VotingUtils.RpcCastVote(PlayerControl.LocalPlayer, playerId, suspectIdx); + return false; + } + [HarmonyPrefix] [HarmonyPatch(nameof(MeetingHud.Confirm))] public static bool ConfirmPatch(MeetingHud __instance, [HarmonyArgument(0)] byte suspect) { __instance.CmdCastVote(PlayerControl.LocalPlayer.PlayerId, suspect); - return false; } } diff --git a/MiraAPI/Voting/VotingUtils.cs b/MiraAPI/Voting/VotingUtils.cs index 56833a6..38fbf16 100644 --- a/MiraAPI/Voting/VotingUtils.cs +++ b/MiraAPI/Voting/VotingUtils.cs @@ -41,16 +41,26 @@ public static class VotingUtils /// /// The player's vote data. /// Who the player voted for. - public static void HandleVote(PlayerVoteData voteData, byte suspectIdx) + /// Whether you want the vote to commence or not. + private static void HandleVote(PlayerVoteData voteData, byte suspectIdx, out bool cancelVote) { var @event = new HandleVoteEvent(voteData, suspectIdx); MiraEventManager.InvokeEvent(@event); + cancelVote = @event.PreventVote; + if (@event.IsCancelled) { return; } + if (voteData.VotesRemaining == 0 || + voteData.VotedFor(suspectIdx)) + { + cancelVote = true; + return; + } + voteData.DecreaseRemainingVotes(1); voteData.VoteForPlayer(suspectIdx); } @@ -64,10 +74,7 @@ public static class VotingUtils [MethodRpc((uint)MiraRpc.RemoveVote)] public static void RpcRemoveVote(PlayerControl source, byte voterId, byte votedFor) { - if (source.OwnerId != AmongUsClient.Instance.HostId) - { - return; - } + if (!source.IsHost()) return; MeetingHud.Instance.playerStates.First(state => state.TargetPlayerId == voterId).UnsetVote(); @@ -94,6 +101,65 @@ 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 the use case. + /// + /// The player who sent this RPC. + /// The id of the player who casted the vote. + /// The voted player's id. + [MethodRpc((uint)MiraRpc.CastVote)] + public static void RpcCastVote(PlayerControl source, byte srcPlayerId, byte suspectPlayerId) + { + CustomCastVote(srcPlayerId, suspectPlayerId); + } + + private static void CustomCastVote(byte srcPlayerId, byte suspectPlayerId) + { + var plr = GameData.Instance.GetPlayerById(srcPlayerId); + if (!plr) return; + + var voteData = plr.Object.GetVoteData(); + if (!voteData) return; + + HandleVote(voteData, suspectPlayerId, out var cancelVote); + + // Handle local behaviour for the voter (for some reason checking AmOwner does not work for host) + if (PlayerControl.LocalPlayer.PlayerId == srcPlayerId) + { + 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.playerStates.First(x => x.TargetPlayerId == srcPlayerId).SetVote(suspectPlayerId); + } + + // 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(srcPlayerId, ChatNoteTypes.DidVote); + } + /// /// Calculates the total number of votes. ///