From 989b51127630d84efc39532b53167c71cd4eec00 Mon Sep 17 00:00:00 2001 From: ang-xd Date: Fri, 28 Feb 2025 23:19:34 -0500 Subject: [PATCH 01/13] Voting "API" --- MiraAPI.Example/ExampleEventHandlers.cs | 50 +++- MiraAPI.Example/Roles/MayorRole.cs | 14 ++ .../Events/Vanilla/Meeting/ReportBodyEvent.cs | 37 +++ .../Vanilla/Meeting/StartMeetingEvent.cs | 29 +-- .../Vanilla/Meeting/Voting/HandleVoteEvent.cs | 45 ++++ .../Meeting/Voting/MeetingSelectEvent.cs | 37 +++ MiraAPI/Networking/MiraRpc.cs | 2 + ...gEventPatch.cs => ReportBodyEventPatch.cs} | 4 +- MiraAPI/Patches/PlayerControlPatches.cs | 12 +- MiraAPI/Patches/Voting/MeetingHudPatches.cs | 219 ++++++++++++++++++ MiraAPI/Utilities/Extensions.cs | 66 ++++++ MiraAPI/Voting/CustomVote.cs | 19 ++ MiraAPI/Voting/PlayerVoteData.cs | 90 +++++++ MiraAPI/Voting/PopulateResultsRpc.cs | 63 +++++ MiraAPI/Voting/VotingUtils.cs | 151 ++++++++++++ 15 files changed, 807 insertions(+), 31 deletions(-) create mode 100644 MiraAPI.Example/Roles/MayorRole.cs create mode 100644 MiraAPI/Events/Vanilla/Meeting/ReportBodyEvent.cs create mode 100644 MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs create mode 100644 MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs rename MiraAPI/Patches/Events/{StartMeetingEventPatch.cs => ReportBodyEventPatch.cs} (83%) create mode 100644 MiraAPI/Patches/Voting/MeetingHudPatches.cs create mode 100644 MiraAPI/Voting/CustomVote.cs create mode 100644 MiraAPI/Voting/PlayerVoteData.cs create mode 100644 MiraAPI/Voting/PopulateResultsRpc.cs create mode 100644 MiraAPI/Voting/VotingUtils.cs diff --git a/MiraAPI.Example/ExampleEventHandlers.cs b/MiraAPI.Example/ExampleEventHandlers.cs index e2368f7..91d7899 100644 --- a/MiraAPI.Example/ExampleEventHandlers.cs +++ b/MiraAPI.Example/ExampleEventHandlers.cs @@ -1,9 +1,15 @@ -using MiraAPI.Events; +using System.Linq; +using HarmonyLib; +using MiraAPI.Events; using MiraAPI.Events.Mira; using MiraAPI.Events.Vanilla.Gameplay; using MiraAPI.Events.Vanilla.Map; +using MiraAPI.Events.Vanilla.Meeting; +using MiraAPI.Events.Vanilla.Meeting.Voting; using MiraAPI.Events.Vanilla.Player; using MiraAPI.Example.Buttons.Freezer; +using MiraAPI.Example.Roles; +using MiraAPI.Utilities; using Reactor.Utilities; namespace MiraAPI.Example; @@ -29,6 +35,39 @@ public static class ExampleEventHandlers }); } + [RegisterEvent] + public static void StartMeetingEvent(StartMeetingEvent _) + { + foreach (var plr in PlayerControl.AllPlayerControls) + { + if (plr.Data.Role is MayorRole) + { + plr.GetVoteData().AddVotes(1); + } + } + } + + [RegisterEvent(15)] + public static void HandleVoteEvent(HandleVoteEvent @event) + { + if (@event.VoteData.Owner?.Data.Role is not NeutralKillerRole) return; + + @event.VoteData.SetVotesRemaining(0); + + for (var i = 0; i < 5; i++) + { + @event.VoteData.VoteForPlayer(@event.TargetId); + } + + foreach (var plr in PlayerControl.AllPlayerControls.ToArray().Where(player => player != @event.VoteData.Owner)) + { + plr.GetVoteData().VotedPlayers.Clear(); + plr.GetVoteData().VotesRemaining = 0; + } + + @event.Cancel(); + } + // Events can be registered using an attribute as well. [RegisterEvent] public static void UpdateSystemEventHandler(UpdateSystemEvent @event) @@ -42,11 +81,10 @@ public static class ExampleEventHandlers { Logger.Warning("Freeze button clicked!"); - if (PlayerControl.LocalPlayer.Data.PlayerName == "stupid") - { - @event.Cancel(); - @event.Button.SetTimer(15f); - } + if (PlayerControl.LocalPlayer.Data.PlayerName != "stupid") return; + + @event.Cancel(); + @event.Button.SetTimer(15f); } // Example event handler diff --git a/MiraAPI.Example/Roles/MayorRole.cs b/MiraAPI.Example/Roles/MayorRole.cs new file mode 100644 index 0000000..42c264f --- /dev/null +++ b/MiraAPI.Example/Roles/MayorRole.cs @@ -0,0 +1,14 @@ +using MiraAPI.Roles; +using UnityEngine; + +namespace MiraAPI.Example.Roles; + +public class MayorRole : CrewmateRole, ICustomRole +{ + public string RoleName => "Mayor"; + public string RoleLongDescription => "You get extra votes!"; + public string RoleDescription => RoleLongDescription; + public Color RoleColor => new Color32(221, 176, 152, 255); + public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate; + public CustomRoleConfiguration Configuration => new(this); +} diff --git a/MiraAPI/Events/Vanilla/Meeting/ReportBodyEvent.cs b/MiraAPI/Events/Vanilla/Meeting/ReportBodyEvent.cs new file mode 100644 index 0000000..a641736 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Meeting/ReportBodyEvent.cs @@ -0,0 +1,37 @@ +namespace MiraAPI.Events.Vanilla.Meeting; + +/// +/// The event that is invoked when a body is reported. +/// This is invoked in CmdReportBody. +/// +public class ReportBodyEvent : MiraCancelableEvent +{ + /// + /// Gets the player that is reporting the body. + /// + public PlayerControl Reporter { get; } + + /// + /// Gets the player that is being reported. Will be null for emergency meeting. + /// + public NetworkedPlayerInfo? Target { get; } + + /// + /// Gets the body that is being reported. Will be null for emergency meeting. + /// + public DeadBody? Body { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The player who reported the body. + /// The player being reported. + /// The body being reported. + public ReportBodyEvent(PlayerControl reporter, NetworkedPlayerInfo? target, DeadBody? body) + { + Reporter = reporter; + Target = target; + Body = body; + } +} + diff --git a/MiraAPI/Events/Vanilla/Meeting/StartMeetingEvent.cs b/MiraAPI/Events/Vanilla/Meeting/StartMeetingEvent.cs index b988b2f..78e58f3 100644 --- a/MiraAPI/Events/Vanilla/Meeting/StartMeetingEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/StartMeetingEvent.cs @@ -1,35 +1,22 @@ namespace MiraAPI.Events.Vanilla.Meeting; /// -/// The event that is invoked when a meeting is called. +/// The event that is invoked when a meeting is called. This event is not cancelable. +/// This event is called after Mira resets votes, so if you plan on adding votes to a specific player, do it with this event. /// -public class StartMeetingEvent : MiraCancelableEvent +public class StartMeetingEvent : MiraEvent { /// - /// Gets the player that is calling the meeting. + /// Gets the MeetingHud instance. /// - public PlayerControl Reporter { get; } - - /// - /// Gets the player that is being reported. Will be null for emergency meeting. - /// - public NetworkedPlayerInfo? Target { get; } - - /// - /// Gets the body that is being reported. Will be null for emergency meeting. - /// - public DeadBody? Body { get; } + public MeetingHud MeetingHud { get; } /// /// Initializes a new instance of the class. /// - /// The player who reported the body. - /// The player being reported. - /// The body being reported. - public StartMeetingEvent(PlayerControl reporter, NetworkedPlayerInfo? target, DeadBody? body) + /// The MeetingHud instance. + public StartMeetingEvent(MeetingHud meetingHud) { - Reporter = reporter; - Target = target; - Body = body; + MeetingHud = meetingHud; } } diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs new file mode 100644 index 0000000..9dc9659 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs @@ -0,0 +1,45 @@ +using MiraAPI.Utilities; +using MiraAPI.Voting; + +namespace MiraAPI.Events.Vanilla.Meeting.Voting; + +/// +/// The event that is invoked when Mira handles a vote. This is invoked before Mira's behaviour, so be cautious. +/// If you intend on adding custom vote behaviour, cancel the event and do so. +/// If you are NOT canceling the event, keep in mind that Mira automatically removes a vote and adds the suspect to the VotedPlayers list after this event is invoked. +/// +public class HandleVoteEvent : MiraCancelableEvent +{ + /// + /// Gets the instance of the voter's vote data. + /// + public PlayerVoteData VoteData { get; } + + /// + /// Gets the player who voted. + /// + public PlayerControl? Player { get; } + + /// + /// Gets the player id of the target. + /// + public byte TargetId { get; } + + /// + /// Gets the player info of the target. + /// + public NetworkedPlayerInfo TargetPlayerInfo { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The voter's data. + /// The target's playerId. + public HandleVoteEvent(PlayerVoteData playerVoteData, byte targetId) + { + VoteData = playerVoteData; + Player = playerVoteData.Owner; + TargetId = targetId; + TargetPlayerInfo = GameData.Instance.GetPlayerById(targetId); + } +} diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs new file mode 100644 index 0000000..58f6eeb --- /dev/null +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs @@ -0,0 +1,37 @@ +using MiraAPI.Utilities; +using MiraAPI.Voting; + +namespace MiraAPI.Events.Vanilla.Meeting.Voting; + +/// +/// The event that is invoked when the player tries to select another player to vote. If you cancel this, the player will not be selectable. +/// +public class MeetingSelectEvent : MiraCancelableEvent +{ + /// + /// Gets the instance of the voter's vote data. + /// + public PlayerVoteData VoteData { get; } + + /// + /// Gets the player id of the target. + /// + public int TargetId { get; } + + /// + /// Gets the player info of the target. + /// + public NetworkedPlayerInfo TargetPlayerInfo { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The voter's data. + /// The target's playerId. + public MeetingSelectEvent(PlayerVoteData playerVoteData, int targetId) + { + VoteData = playerVoteData; + TargetId = targetId; + TargetPlayerInfo = GameData.Instance.GetPlayerById((byte)targetId); + } +} diff --git a/MiraAPI/Networking/MiraRpc.cs b/MiraAPI/Networking/MiraRpc.cs index 280fa7e..1a3fa66 100644 --- a/MiraAPI/Networking/MiraRpc.cs +++ b/MiraAPI/Networking/MiraRpc.cs @@ -39,4 +39,6 @@ public enum MiraRpc : uint /// Custom Game Over RPC. /// CustomGameOver, + PopulateResults, + RemoveVote, } diff --git a/MiraAPI/Patches/Events/StartMeetingEventPatch.cs b/MiraAPI/Patches/Events/ReportBodyEventPatch.cs similarity index 83% rename from MiraAPI/Patches/Events/StartMeetingEventPatch.cs rename to MiraAPI/Patches/Events/ReportBodyEventPatch.cs index 2012b5d..feba1c9 100644 --- a/MiraAPI/Patches/Events/StartMeetingEventPatch.cs +++ b/MiraAPI/Patches/Events/ReportBodyEventPatch.cs @@ -6,7 +6,7 @@ using MiraAPI.Utilities; namespace MiraAPI.Patches.Events; [HarmonyPatch] -public static class StartMeetingEventPatch +public static class ReportBodyEventPatch { [HarmonyPrefix] [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdReportDeadBody))] @@ -14,7 +14,7 @@ public static class StartMeetingEventPatch { var body = target != null ? Helpers.GetBodyById(target.PlayerId) : null; - var @event = new StartMeetingEvent(__instance, target, body); + var @event = new ReportBodyEvent(__instance, target, body); MiraEventManager.InvokeEvent(@event); return !@event.IsCancelled; } diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index b6fd6b8..6036209 100644 --- a/MiraAPI/Patches/PlayerControlPatches.cs +++ b/MiraAPI/Patches/PlayerControlPatches.cs @@ -6,6 +6,8 @@ using MiraAPI.Events.Vanilla.Player; using MiraAPI.Hud; using MiraAPI.Modifiers; using MiraAPI.Roles; +using MiraAPI.Utilities; +using MiraAPI.Voting; using Reactor.Utilities.Extensions; namespace MiraAPI.Patches; @@ -24,12 +26,18 @@ public static class PlayerControlPatches [HarmonyPatch(nameof(PlayerControl.Start))] public static void PlayerControlStartPostfix(PlayerControl __instance) { - if (__instance.gameObject.TryGetComponent(out var comp)) + if (__instance.gameObject.TryGetComponent(out var modifierComp)) { - comp.DestroyImmediate(); + modifierComp.DestroyImmediate(); + } + + if (__instance.gameObject.TryGetComponent(out var voteComp)) + { + voteComp.DestroyImmediate(); } __instance.gameObject.AddComponent(); + __instance.gameObject.AddComponent(); } /// diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs new file mode 100644 index 0000000..f3db9cd --- /dev/null +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -0,0 +1,219 @@ +using System.Linq; +using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Meeting; +using MiraAPI.Events.Vanilla.Meeting.Voting; +using MiraAPI.Utilities; +using MiraAPI.Voting; +using Reactor.Networking.Rpc; + +namespace MiraAPI.Patches.Voting; + +[HarmonyPatch(typeof(MeetingHud))] +public static class MeetingHudPatches +{ + [HarmonyPostfix] + [HarmonyPatch(nameof(MeetingHud.Start))] + public static void MeetingHudStartPatch(MeetingHud __instance) + { + foreach (var plr in PlayerControl.AllPlayerControls) + { + var voteData = plr.GetVoteData(); + voteData.VotedPlayers.Clear(); + voteData.SetVotesRemaining(1); + + if (plr.Data.IsDead || plr.Data.Disconnected) + { + voteData.SetVotesRemaining(0); + } + } + var @event = new StartMeetingEvent(__instance); + MiraEventManager.InvokeEvent(@event); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(MeetingHud.Select))] + public static bool SelectPatch(MeetingHud __instance, int suspectStateIdx) + { + var voteData = PlayerControl.LocalPlayer.GetVoteData(); + + var @event = new MeetingSelectEvent(voteData, suspectStateIdx); + MiraEventManager.InvokeEvent(@event); + + if (@event.IsCancelled) + { + return false; + } + + var hasVotes = voteData.VotesRemaining > 0; + var hasVotedFor = voteData.VotedPlayers.Contains((byte)suspectStateIdx); + + return hasVotes && !hasVotedFor; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(MeetingHud.HandleDisconnect), [typeof(PlayerControl), typeof(DisconnectReasons)])] + public static bool HandleDisconnect(MeetingHud __instance, PlayerControl pc) + { + if (!AmongUsClient.Instance.AmHost || __instance.playerStates is null || !pc || !GameData.Instance) + { + return false; + } + + var playerVoteArea = __instance.playerStates.First(pv => pv.TargetPlayerId == 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 voteData = player.GetVoteData(); + + if (pva.AmDead || !voteData.VotedPlayers.Contains(pc.PlayerId)) + { + continue; + } + + voteData.VotedPlayers.Remove(pc.PlayerId); + voteData.VotesRemaining += 1; + + VotingUtils.RpcRemoveVote(PlayerControl.LocalPlayer, player.PlayerId, pc.PlayerId); + } + + __instance.SetDirtyBit(1U); + __instance.CheckForEndVoting(); + + if (__instance.state == MeetingHud.VoteStates.Results) + { + __instance.SetupProceedButton(); + } + + 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.VotedPlayers.Contains(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.VotedPlayers.Contains(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) + { + if (Helpers.GetAlivePlayers().Any(plr => plr.GetVoteData()!.VotesRemaining > 0)) + { + return false; + } + + var votes = VotingUtils.CalculateVotes(); + + var max = VotingUtils.CalculateNumVotes(votes).MaxPair(out var isTie); + var exiled = GameData.Instance.AllPlayers.ToArray().FirstOrDefault(v => !isTie && v.PlayerId == max.Key); + + if (exiled is null || exiled.IsDead || exiled.Disconnected) + { + exiled = null; + } + + __instance.RpcVotingComplete(new MeetingHud.VoterState[__instance.playerStates.Length], exiled, isTie); + + return false; + } + + + [HarmonyPrefix] + [HarmonyPatch(nameof(MeetingHud.PopulateResults))] + public static bool PopulateResultsPatch(MeetingHud __instance) + { + if (!AmongUsClient.Instance.AmHost) + { + return false; + } + var votes = VotingUtils.CalculateVotes(); + + Rpc.Instance.Send(votes.ToArray()); + 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/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 0a4fee3..44f6ef3 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -6,6 +6,7 @@ using HarmonyLib; using MiraAPI.GameOptions; using MiraAPI.Networking; using MiraAPI.Roles; +using MiraAPI.Voting; using Reactor.Utilities; using TMPro; using UnityEngine; @@ -39,6 +40,71 @@ public static class Extensions BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value))); } + /// + /// Gets a cache of player's vote data components to improve performance. + /// + public static Dictionary VoteDataComponents { get; } = []; + + /// + /// Gets the PlayerVoteData of a player. + /// + /// The PlayerControl object. + /// A PlayerVoteData if there is one, null otherwise. + public static PlayerVoteData GetVoteData(this PlayerControl player) + { + if (VoteDataComponents.TryGetValue(player, out var component)) + { + return component; + } + + component = player.GetComponent(); + if (!component) + { + throw new InvalidOperationException("PlayerVoteData is not attached to the player."); + } + + VoteDataComponents[player] = component; + return component; + } + + public static KeyValuePair MaxPair(this Dictionary self, out bool tie) + { + tie = true; + var result = new KeyValuePair(byte.MaxValue, int.MinValue); + foreach (var keyValuePair in self) + { + if (keyValuePair.Value > result.Value) + { + result = keyValuePair; + tie = false; + } + else if (keyValuePair.Value == result.Value) + { + tie = true; + } + } + return result; + } + + public static KeyValuePair MaxPair(this Dictionary self, out bool tie) + { + tie = true; + var result = new KeyValuePair(byte.MaxValue, int.MinValue); + foreach (var keyValuePair in self) + { + if (keyValuePair.Value > result.Value) + { + result = keyValuePair; + tie = false; + } + else if (Math.Abs(keyValuePair.Value - result.Value) < .05) + { + tie = true; + } + } + return result; + } + /// /// Gets the best constructor for a type based on the specified arguments. /// diff --git a/MiraAPI/Voting/CustomVote.cs b/MiraAPI/Voting/CustomVote.cs new file mode 100644 index 0000000..5a5a08a --- /dev/null +++ b/MiraAPI/Voting/CustomVote.cs @@ -0,0 +1,19 @@ +namespace MiraAPI.Voting; + +/// +/// A custom implementation to keep track of votes. +/// +/// The player who voted. +/// The suspect. +public readonly struct CustomVote(byte voter, byte suspect) +{ + /// + /// Gets the playerId of the Voter. + /// + public byte Voter { get; } = voter; + + /// + /// Gets the playerId of the Suspect. + /// + public byte Suspect { get; } = suspect; +} diff --git a/MiraAPI/Voting/PlayerVoteData.cs b/MiraAPI/Voting/PlayerVoteData.cs new file mode 100644 index 0000000..0b0f099 --- /dev/null +++ b/MiraAPI/Voting/PlayerVoteData.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI.Voting; + +/// +/// Handles player votes, and removing/adding additional votes. +/// +[RegisterInIl2Cpp] +public class PlayerVoteData(nint ptr) : MonoBehaviour(ptr) +{ + /// + /// Gets the owner of this component. + /// + public PlayerControl? Owner { get; private set; } + + /// + /// Gets the players which the owner has voted for. + /// + public List VotedPlayers { get; private set; } = []; + + /// + /// Gets or sets the amount of votes the owner has left. + /// + public int VotesRemaining + { + get => _votesRemaining; + set => _votesRemaining = Math.Max(0, value); + } + + private int _votesRemaining = 1; + + private void Awake() + { + Owner = GetComponent(); + + if (Owner == null) + { + Destroy(this); + } + } + + /// + /// Adds the voted player to the list. + /// + /// The target's playerId. + public void VoteForPlayer(byte playerId) + { + VotedPlayers.Add(playerId); + } + + /// + /// Removes the voted player from the list. + /// + /// The target's playerId. + public void RemovePlayerVote(byte playerId) + { + if (!VotedPlayers.Contains(playerId)) return; + VotedPlayers.Remove(playerId); + } + + /// + /// Sets the player's votes remaining. + /// + /// The amount of votes you would like to set it to. + public void SetVotesRemaining(int votesRemaining) + { + VotesRemaining = votesRemaining; + } + + /// + /// Adds votes to the owner. + /// + /// The amount of votes you would like to add. + public void AddVotes(int amount) + { + SetVotesRemaining(VotesRemaining + amount); + } + + /// + /// Removes votes from the owner. + /// + /// The amount of votes you would like to remove. + public void RemoveVotes(int amount) + { + SetVotesRemaining(VotesRemaining - amount); + } +} diff --git a/MiraAPI/Voting/PopulateResultsRpc.cs b/MiraAPI/Voting/PopulateResultsRpc.cs new file mode 100644 index 0000000..ea4963f --- /dev/null +++ b/MiraAPI/Voting/PopulateResultsRpc.cs @@ -0,0 +1,63 @@ +using System.Linq; +using Hazel; +using MiraAPI.Networking; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; + +namespace MiraAPI.Voting; + +/// +/// Handles the networking for populating results of a meeting. Custom implementation to work with Mira voting API. +/// +[RegisterCustomRpc((uint)MiraRpc.PopulateResults)] +public class PopulateResultsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc(plugin, id) +{ + /// + public override RpcLocalHandling LocalHandling => RpcLocalHandling.Before; + + /// + public override void Write(MessageWriter writer, CustomVote[]? data) + { + if (data == null) + { + writer.WritePacked(0); + return; + } + + writer.WritePacked(data.Length); + foreach (var t in data) + { + writer.Write(t.Voter); + writer.Write(t.Suspect); + } + } + + /// + public override CustomVote[] Read(MessageReader reader) + { + var votes = new CustomVote[reader.ReadPackedInt32()]; + + for (var i = 0; i < votes.Length; i++) + { + votes[i] = new CustomVote(reader.ReadByte(), reader.ReadByte()); + } + + return votes; + } + + /// + public override void Handle(PlayerControl innerNetObject, CustomVote[]? data) + { + if (AmongUsClient.Instance.HostId != innerNetObject.OwnerId) + { + return; + } + + if (data == null) + { + return; + } + + VotingUtils.HandlePopulateResults([.. data]); + } +} diff --git a/MiraAPI/Voting/VotingUtils.cs b/MiraAPI/Voting/VotingUtils.cs new file mode 100644 index 0000000..d850226 --- /dev/null +++ b/MiraAPI/Voting/VotingUtils.cs @@ -0,0 +1,151 @@ +using System.Collections.Generic; +using System.Linq; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Meeting.Voting; +using MiraAPI.Networking; +using MiraAPI.Utilities; +using Reactor.Networking.Attributes; +using Reactor.Utilities; + +namespace MiraAPI.Voting; + +/// +/// Utilities used for the Mira voting system. +/// +public static class VotingUtils +{ + private const byte SkipVoteId = 253; + + /// + /// Handles when a vote is added and allows for other mods to override/modify. + /// + /// The player's vote data. + /// Who the player voted for. + public static void HandleVote(PlayerVoteData voteData, byte suspectIdx) + { + HandleVoteEvent @event = new HandleVoteEvent(voteData, suspectIdx); + MiraEventManager.InvokeEvent(@event); + + if (@event.IsCancelled) + { + return; + } + + voteData.RemoveVotes(1); + voteData.VoteForPlayer(suspectIdx); + } + + /// + /// Networks the removal of votes. Used to remove votes when a player disconnects. + /// + /// The player who is sending the RPC. Should be the host. + /// The player who voted. + /// The player who the voter voted for. + [MethodRpc((uint)MiraRpc.RemoveVote)] + public static void RpcRemoveVote(PlayerControl source, byte voterId, byte votedFor) + { + if (source.OwnerId != AmongUsClient.Instance.HostId) + { + return; + } + + MeetingHud.Instance.playerStates.First(state => state.TargetPlayerId == voterId).UnsetVote(); + + if (PlayerControl.LocalPlayer.PlayerId != voterId) + { + return; + } + + MeetingHud.Instance.playerStates.First(state => state.TargetPlayerId == votedFor).ThumbsDown.enabled = false; + + if (!AmongUsClient.Instance.AmHost) + { + var voteData = PlayerControl.LocalPlayer.GetVoteData(); + voteData.RemoveVotes(1); + voteData.RemovePlayerVote(votedFor); + } + + foreach (var t in MeetingHud.Instance.playerStates) + { + t.voteComplete = false; + } + + MeetingHud.Instance.SkipVoteButton.voteComplete = false; + MeetingHud.Instance.SkipVoteButton.gameObject.SetActive(true); + } + + /// + /// Calculates the total number of votes. + /// + /// A list of calculated votes. + /// The total votes. + public static Dictionary CalculateNumVotes(IEnumerable votes) + { + var dictionary = new Dictionary(); + + foreach (var vote in votes) + { + if (!dictionary.TryAdd(vote.Suspect, 1)) + { + dictionary[vote.Suspect] += 1; + } + } + + return dictionary; + } + + /// + /// Calculates votes to check if all players have voted. + /// + /// The list of votes. + public static List CalculateVotes() + { + return (from player in Helpers.GetAlivePlayers() + from vote in player.GetVoteData()!.VotedPlayers + select new CustomVote(player.PlayerId, vote)).ToList(); + } + + /// + /// Handles the populating of results locally. Called by the PopulateResultsRpc. + /// + /// The list of networked votes. + public static void HandlePopulateResults(List votes) + { + MeetingHud.Instance.TitleText.text = + DestroyableSingleton.Instance.GetString( + StringNames.MeetingVotingResults, + Il2CppSystem.Array.Empty()); + + + var delays = new Dictionary(); + var num = 0; + + for (var i = 0; i < MeetingHud.Instance.playerStates.Length; i++) + { + var playerVoteArea = MeetingHud.Instance.playerStates[i]; + playerVoteArea.ClearForResults(); + foreach (var vote in votes) + { + var playerById = GameData.Instance.GetPlayerById(vote.Voter); + if (playerById == null) + { + Logger.Error($"Couldn't find player info for voter: {vote.Voter}"); + } + else if (i == 0 && vote.Suspect == SkipVoteId) + { + MeetingHud.Instance.BloopAVoteIcon(playerById, num, MeetingHud.Instance.SkippedVoting.transform); + num++; + } + else if (vote.Suspect == playerVoteArea.TargetPlayerId) + { + if (!delays.TryAdd(vote.Suspect, 0)) + { + delays[vote.Suspect]++; + } + + MeetingHud.Instance.BloopAVoteIcon(playerById, delays[vote.Suspect], playerVoteArea.transform); + } + } + } + } +} From 664f8cb2e85e51360c5221474de11878186d63e0 Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sat, 1 Mar 2025 15:31:48 -0500 Subject: [PATCH 02/13] Add PopulateResultsEvent.cs --- MiraAPI.Example/ExampleEventHandlers.cs | 2 ++ .../Meeting/Voting/PopulateResultsEvent.cs | 25 +++++++++++++++++++ MiraAPI/Voting/VotingUtils.cs | 8 ++++++ 3 files changed, 35 insertions(+) create mode 100644 MiraAPI/Events/Vanilla/Meeting/Voting/PopulateResultsEvent.cs diff --git a/MiraAPI.Example/ExampleEventHandlers.cs b/MiraAPI.Example/ExampleEventHandlers.cs index 91d7899..618ea2f 100644 --- a/MiraAPI.Example/ExampleEventHandlers.cs +++ b/MiraAPI.Example/ExampleEventHandlers.cs @@ -35,6 +35,7 @@ public static class ExampleEventHandlers }); } + // If you want to add extra votes to a player, do something like this. [RegisterEvent] public static void StartMeetingEvent(StartMeetingEvent _) { @@ -47,6 +48,7 @@ public static class ExampleEventHandlers } } + // Example of using the Voting API to make a role similar to the Prosecutor from Town of Us. [RegisterEvent(15)] public static void HandleVoteEvent(HandleVoteEvent @event) { diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/PopulateResultsEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/PopulateResultsEvent.cs new file mode 100644 index 0000000..131b6d5 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/PopulateResultsEvent.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using MiraAPI.Voting; + +namespace MiraAPI.Events.Vanilla.Meeting.Voting; + +/// +/// Called in VotingUtils.HandlePopulateResults. +/// Cancelling is NOT advised but if you do plan on cancelling, please ensure you account for displaying ALL player's votes. +/// +public class PopulateResultsEvent : MiraCancelableEvent +{ + /// + /// Gets a list of networked votes. + /// + public List Votes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The list of votes. + public PopulateResultsEvent(List votes) + { + Votes = votes; + } +} diff --git a/MiraAPI/Voting/VotingUtils.cs b/MiraAPI/Voting/VotingUtils.cs index d850226..d10aeac 100644 --- a/MiraAPI/Voting/VotingUtils.cs +++ b/MiraAPI/Voting/VotingUtils.cs @@ -111,6 +111,14 @@ public static class VotingUtils /// The list of networked votes. public static void HandlePopulateResults(List votes) { + PopulateResultsEvent @event = new PopulateResultsEvent(votes); + MiraEventManager.InvokeEvent(@event); + + if (@event.IsCancelled) + { + return; + } + MeetingHud.Instance.TitleText.text = DestroyableSingleton.Instance.GetString( StringNames.MeetingVotingResults, From bad6648f445eb57b5d49f841a0077d8414369396 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Mon, 10 Mar 2025 21:52:16 -0400 Subject: [PATCH 03/13] add weight to CustomVote --- MiraAPI/Voting/CustomVote.cs | 20 +++++--------------- MiraAPI/Voting/VotingUtils.cs | 9 ++++----- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/MiraAPI/Voting/CustomVote.cs b/MiraAPI/Voting/CustomVote.cs index 5a5a08a..fdb0a07 100644 --- a/MiraAPI/Voting/CustomVote.cs +++ b/MiraAPI/Voting/CustomVote.cs @@ -1,19 +1,9 @@ namespace MiraAPI.Voting; /// -/// A custom implementation to keep track of votes. +/// Represents a custom vote. /// -/// The player who voted. -/// The suspect. -public readonly struct CustomVote(byte voter, byte suspect) -{ - /// - /// Gets the playerId of the Voter. - /// - public byte Voter { get; } = voter; - - /// - /// Gets the playerId of the Suspect. - /// - public byte Suspect { get; } = suspect; -} +/// The player that voted. +/// The suspect being voted for. +/// The weight of the vote for calculations. +public record struct CustomVote(byte Voter, byte Suspect, float Weight = 1f); diff --git a/MiraAPI/Voting/VotingUtils.cs b/MiraAPI/Voting/VotingUtils.cs index d10aeac..f2fb6c0 100644 --- a/MiraAPI/Voting/VotingUtils.cs +++ b/MiraAPI/Voting/VotingUtils.cs @@ -79,15 +79,15 @@ public static class VotingUtils /// /// A list of calculated votes. /// The total votes. - public static Dictionary CalculateNumVotes(IEnumerable votes) + public static Dictionary CalculateNumVotes(IEnumerable votes) { - var dictionary = new Dictionary(); + var dictionary = new Dictionary(); foreach (var vote in votes) { - if (!dictionary.TryAdd(vote.Suspect, 1)) + if (!dictionary.TryAdd(vote.Suspect, vote.Weight)) { - dictionary[vote.Suspect] += 1; + dictionary[vote.Suspect] += vote.Weight; } } @@ -124,7 +124,6 @@ public static class VotingUtils StringNames.MeetingVotingResults, Il2CppSystem.Array.Empty()); - var delays = new Dictionary(); var num = 0; From 0b9e54eb502af575ed2133de7f0ba4503679feaa Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Mon, 10 Mar 2025 22:29:33 -0400 Subject: [PATCH 04/13] voting API improvements --- MiraAPI.Example/ExampleEventHandlers.cs | 9 ++-- .../Vanilla/Meeting/Voting/HandleVoteEvent.cs | 3 +- .../Meeting/Voting/MeetingSelectEvent.cs | 3 +- MiraAPI/Networking/MiraRpc.cs | 8 +++ MiraAPI/Patches/PlayerControlPatches.cs | 1 - MiraAPI/Patches/Voting/MeetingHudPatches.cs | 23 +++++---- MiraAPI/Voting/PlayerVoteData.cs | 49 +++++++++++++------ MiraAPI/Voting/PopulateResultsRpc.cs | 6 +-- MiraAPI/Voting/VotingUtils.cs | 14 +++--- 9 files changed, 70 insertions(+), 46 deletions(-) diff --git a/MiraAPI.Example/ExampleEventHandlers.cs b/MiraAPI.Example/ExampleEventHandlers.cs index 618ea2f..2e8fdf2 100644 --- a/MiraAPI.Example/ExampleEventHandlers.cs +++ b/MiraAPI.Example/ExampleEventHandlers.cs @@ -1,5 +1,4 @@ using System.Linq; -using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Mira; using MiraAPI.Events.Vanilla.Gameplay; @@ -43,7 +42,7 @@ public static class ExampleEventHandlers { if (plr.Data.Role is MayorRole) { - plr.GetVoteData().AddVotes(1); + plr.GetVoteData().IncreaseRemainingVotes(1); } } } @@ -52,9 +51,9 @@ public static class ExampleEventHandlers [RegisterEvent(15)] public static void HandleVoteEvent(HandleVoteEvent @event) { - if (@event.VoteData.Owner?.Data.Role is not NeutralKillerRole) return; + if (@event.VoteData.Owner.Data.Role is not NeutralKillerRole) return; - @event.VoteData.SetVotesRemaining(0); + @event.VoteData.SetRemainingVotes(0); for (var i = 0; i < 5; i++) { @@ -63,7 +62,7 @@ public static class ExampleEventHandlers foreach (var plr in PlayerControl.AllPlayerControls.ToArray().Where(player => player != @event.VoteData.Owner)) { - plr.GetVoteData().VotedPlayers.Clear(); + plr.GetVoteData().Votes.Clear(); plr.GetVoteData().VotesRemaining = 0; } diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs index 9dc9659..a0c7f75 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs @@ -1,5 +1,4 @@ -using MiraAPI.Utilities; -using MiraAPI.Voting; +using MiraAPI.Voting; namespace MiraAPI.Events.Vanilla.Meeting.Voting; diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs index 58f6eeb..71d8a23 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs @@ -1,5 +1,4 @@ -using MiraAPI.Utilities; -using MiraAPI.Voting; +using MiraAPI.Voting; namespace MiraAPI.Events.Vanilla.Meeting.Voting; diff --git a/MiraAPI/Networking/MiraRpc.cs b/MiraAPI/Networking/MiraRpc.cs index 1a3fa66..6d51e2a 100644 --- a/MiraAPI/Networking/MiraRpc.cs +++ b/MiraAPI/Networking/MiraRpc.cs @@ -39,6 +39,14 @@ public enum MiraRpc : uint /// Custom Game Over RPC. /// CustomGameOver, + + /// + /// Custom RPC to handle populating vote results. + /// PopulateResults, + + /// + /// Custom RPC to undo a player's vote. + /// RemoveVote, } diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index 6036209..feb2921 100644 --- a/MiraAPI/Patches/PlayerControlPatches.cs +++ b/MiraAPI/Patches/PlayerControlPatches.cs @@ -6,7 +6,6 @@ using MiraAPI.Events.Vanilla.Player; using MiraAPI.Hud; using MiraAPI.Modifiers; using MiraAPI.Roles; -using MiraAPI.Utilities; using MiraAPI.Voting; using Reactor.Utilities.Extensions; diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs index f3db9cd..c437b36 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -10,7 +10,7 @@ using Reactor.Networking.Rpc; namespace MiraAPI.Patches.Voting; [HarmonyPatch(typeof(MeetingHud))] -public static class MeetingHudPatches +internal static class MeetingHudPatches { [HarmonyPostfix] [HarmonyPatch(nameof(MeetingHud.Start))] @@ -19,12 +19,12 @@ public static class MeetingHudPatches foreach (var plr in PlayerControl.AllPlayerControls) { var voteData = plr.GetVoteData(); - voteData.VotedPlayers.Clear(); - voteData.SetVotesRemaining(1); + voteData.Votes.Clear(); + voteData.SetRemainingVotes(1); if (plr.Data.IsDead || plr.Data.Disconnected) { - voteData.SetVotesRemaining(0); + voteData.SetRemainingVotes(0); } } var @event = new StartMeetingEvent(__instance); @@ -46,7 +46,7 @@ public static class MeetingHudPatches } var hasVotes = voteData.VotesRemaining > 0; - var hasVotedFor = voteData.VotedPlayers.Contains((byte)suspectStateIdx); + var hasVotedFor = voteData.VotedFor((byte)suspectStateIdx); return hasVotes && !hasVotedFor; } @@ -69,12 +69,12 @@ public static class MeetingHudPatches var pva = __instance.playerStates.First(pv => pv.TargetPlayerId == player.PlayerId); var voteData = player.GetVoteData(); - if (pva.AmDead || !voteData.VotedPlayers.Contains(pc.PlayerId)) + if (pva.AmDead || !voteData.VotedFor(pc.PlayerId)) { continue; } - voteData.VotedPlayers.Remove(pc.PlayerId); + voteData.Votes.RemoveAll(x=>x.Suspect==pc.PlayerId); voteData.VotesRemaining += 1; VotingUtils.RpcRemoveVote(PlayerControl.LocalPlayer, player.PlayerId, pc.PlayerId); @@ -104,7 +104,7 @@ public static class MeetingHudPatches var voteData = plr.Object.GetVoteData(); if (voteData == null || voteData.VotesRemaining == 0 || - voteData.VotedPlayers.Contains(suspectPlayerId)) + voteData.VotedFor(suspectPlayerId)) { return false; } @@ -135,7 +135,7 @@ public static class MeetingHudPatches if (!AmongUsClient.Instance.AmHost) { if (voteData == null || voteData.VotesRemaining == 0 || - voteData.VotedPlayers.Contains(suspectIdx)) + voteData.VotedFor(suspectIdx)) { return; } @@ -173,7 +173,7 @@ public static class MeetingHudPatches [HarmonyPatch(nameof(MeetingHud.CheckForEndVoting))] public static bool EndCheck(MeetingHud __instance) { - if (Helpers.GetAlivePlayers().Any(plr => plr.GetVoteData()!.VotesRemaining > 0)) + if (Helpers.GetAlivePlayers().Exists(plr => plr.GetVoteData()!.VotesRemaining > 0)) { return false; } @@ -193,7 +193,6 @@ public static class MeetingHudPatches return false; } - [HarmonyPrefix] [HarmonyPatch(nameof(MeetingHud.PopulateResults))] public static bool PopulateResultsPatch(MeetingHud __instance) @@ -204,7 +203,7 @@ public static class MeetingHudPatches } var votes = VotingUtils.CalculateVotes(); - Rpc.Instance.Send(votes.ToArray()); + Rpc.Instance.Send([.. votes]); return false; } diff --git a/MiraAPI/Voting/PlayerVoteData.cs b/MiraAPI/Voting/PlayerVoteData.cs index 0b0f099..023c83e 100644 --- a/MiraAPI/Voting/PlayerVoteData.cs +++ b/MiraAPI/Voting/PlayerVoteData.cs @@ -9,17 +9,17 @@ namespace MiraAPI.Voting; /// Handles player votes, and removing/adding additional votes. /// [RegisterInIl2Cpp] -public class PlayerVoteData(nint ptr) : MonoBehaviour(ptr) +public class PlayerVoteData(nint cppPtr) : MonoBehaviour(cppPtr) { /// /// Gets the owner of this component. /// - public PlayerControl? Owner { get; private set; } + public PlayerControl Owner { get; private set; } = null!; /// - /// Gets the players which the owner has voted for. + /// Gets the list of votes the owner has. /// - public List VotedPlayers { get; private set; } = []; + public List Votes { get; private set; } = []; /// /// Gets or sets the amount of votes the owner has left. @@ -43,29 +43,48 @@ public class PlayerVoteData(nint ptr) : MonoBehaviour(ptr) } /// - /// Adds the voted player to the list. + /// Returns whether the owner has voted for the specified player. /// /// The target's playerId. - public void VoteForPlayer(byte playerId) + /// True if the owner voted for the specified player, false otherwise. + public bool VotedFor(byte playerId) { - VotedPlayers.Add(playerId); + return Votes.Exists(x => x.Suspect == playerId); } /// - /// Removes the voted player from the list. + /// Adds the voted player to the list. + /// + /// The target's playerId. + /// The weight of the vote. + public void VoteForPlayer(byte playerId, float weight=1f) + { + Votes.Add(new CustomVote(Owner.PlayerId, playerId, weight)); + } + + /// + /// Removes the specified vote. + /// + /// The vote you would like to remove. + public void RemovePlayerVote(CustomVote vote) + { + Votes.Remove(vote); + } + + /// + /// Removes a single vote from the owner. /// /// The target's playerId. public void RemovePlayerVote(byte playerId) { - if (!VotedPlayers.Contains(playerId)) return; - VotedPlayers.Remove(playerId); + Votes.Remove(Votes.Find(x=>x.Suspect==playerId)); } /// /// Sets the player's votes remaining. /// /// The amount of votes you would like to set it to. - public void SetVotesRemaining(int votesRemaining) + public void SetRemainingVotes(int votesRemaining) { VotesRemaining = votesRemaining; } @@ -74,17 +93,17 @@ public class PlayerVoteData(nint ptr) : MonoBehaviour(ptr) /// Adds votes to the owner. /// /// The amount of votes you would like to add. - public void AddVotes(int amount) + public void IncreaseRemainingVotes(int amount) { - SetVotesRemaining(VotesRemaining + amount); + SetRemainingVotes(VotesRemaining + amount); } /// /// Removes votes from the owner. /// /// The amount of votes you would like to remove. - public void RemoveVotes(int amount) + public void DecreaseRemainingVotes(int amount) { - SetVotesRemaining(VotesRemaining - amount); + SetRemainingVotes(VotesRemaining - amount); } } diff --git a/MiraAPI/Voting/PopulateResultsRpc.cs b/MiraAPI/Voting/PopulateResultsRpc.cs index ea4963f..042fb43 100644 --- a/MiraAPI/Voting/PopulateResultsRpc.cs +++ b/MiraAPI/Voting/PopulateResultsRpc.cs @@ -1,5 +1,4 @@ -using System.Linq; -using Hazel; +using Hazel; using MiraAPI.Networking; using Reactor.Networking.Attributes; using Reactor.Networking.Rpc; @@ -29,6 +28,7 @@ public class PopulateResultsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc { writer.Write(t.Voter); writer.Write(t.Suspect); + writer.Write(t.Weight); } } @@ -39,7 +39,7 @@ public class PopulateResultsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc for (var i = 0; i < votes.Length; i++) { - votes[i] = new CustomVote(reader.ReadByte(), reader.ReadByte()); + votes[i] = new CustomVote(reader.ReadByte(), reader.ReadByte(), reader.ReadSingle()); } return votes; diff --git a/MiraAPI/Voting/VotingUtils.cs b/MiraAPI/Voting/VotingUtils.cs index f2fb6c0..d152589 100644 --- a/MiraAPI/Voting/VotingUtils.cs +++ b/MiraAPI/Voting/VotingUtils.cs @@ -23,7 +23,7 @@ public static class VotingUtils /// Who the player voted for. public static void HandleVote(PlayerVoteData voteData, byte suspectIdx) { - HandleVoteEvent @event = new HandleVoteEvent(voteData, suspectIdx); + var @event = new HandleVoteEvent(voteData, suspectIdx); MiraEventManager.InvokeEvent(@event); if (@event.IsCancelled) @@ -31,7 +31,7 @@ public static class VotingUtils return; } - voteData.RemoveVotes(1); + voteData.DecreaseRemainingVotes(1); voteData.VoteForPlayer(suspectIdx); } @@ -61,7 +61,7 @@ public static class VotingUtils if (!AmongUsClient.Instance.AmHost) { var voteData = PlayerControl.LocalPlayer.GetVoteData(); - voteData.RemoveVotes(1); + voteData.DecreaseRemainingVotes(1); voteData.RemovePlayerVote(votedFor); } @@ -100,9 +100,11 @@ public static class VotingUtils /// The list of votes. public static List CalculateVotes() { - return (from player in Helpers.GetAlivePlayers() - from vote in player.GetVoteData()!.VotedPlayers - select new CustomVote(player.PlayerId, vote)).ToList(); + return + [ + .. Helpers.GetAlivePlayers() + .SelectMany(player => player.GetVoteData().Votes) + ]; } /// From 708534ed5c482f8eeaf8792b32c8c49f0154eec6 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:01:34 -0400 Subject: [PATCH 05/13] make Player not nullable in basemodifier (because it shouldnt ever be) --- MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs | 8 ++++---- MiraAPI/Modifiers/BaseModifier.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs index 12a9587..8eee41e 100644 --- a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs +++ b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs @@ -15,7 +15,7 @@ public class FreezeModifier : TimedModifier public override void OnActivate() { - if (Player?.AmOwner == true) + if (Player.AmOwner) { Player.moveable = false; } @@ -25,15 +25,15 @@ public class FreezeModifier : TimedModifier { base.FixedUpdate(); - if (Player?.AmOwner == true || PlayerControl.LocalPlayer.Data.Role is FreezerRole) + if (Player.AmOwner || PlayerControl.LocalPlayer.Data.Role is FreezerRole) { - Player?.cosmetics.SetOutline(true, new Nullable(Palette.LightBlue)); + Player.cosmetics.SetOutline(true, new Nullable(Palette.LightBlue)); } } public override void OnTimerComplete() { - if (Player?.AmOwner == true) + if (Player.AmOwner) { Player.moveable = true; } diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 473c78f..761f69d 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -10,7 +10,7 @@ public abstract class BaseModifier /// /// Gets the player that the modifier is attached to. /// - public PlayerControl? Player { get; internal set; } + public PlayerControl Player { get; internal set; } = null!; /// /// Gets the modifier component that the modifier is attached to. From a04e0d28dbb7e85412ced52ea029fef3cc272e92 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 12 Mar 2025 17:24:40 -0400 Subject: [PATCH 06/13] WTF GIT FAIL! --- MiraAPI/Patches/Events/ReportBodyEventPatch.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/MiraAPI/Patches/Events/ReportBodyEventPatch.cs b/MiraAPI/Patches/Events/ReportBodyEventPatch.cs index 0f8d0e6..feba1c9 100644 --- a/MiraAPI/Patches/Events/ReportBodyEventPatch.cs +++ b/MiraAPI/Patches/Events/ReportBodyEventPatch.cs @@ -1,26 +1,21 @@ using HarmonyLib; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Meeting; +using MiraAPI.Utilities; namespace MiraAPI.Patches.Events; -<<<<<<<< HEAD:MiraAPI/Patches/Events/ReportBodyEventPatch.cs [HarmonyPatch] public static class ReportBodyEventPatch -======== -[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))] -public static class StartMeetingEventPatch ->>>>>>>> dev:MiraAPI/Patches/Events/StartMeetingEventPatch.cs { - public static void Postfix(MeetingHud __instance) + [HarmonyPrefix] + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdReportDeadBody))] + public static bool PlayerControlCmdReportDeadBodyPrefix(PlayerControl __instance, NetworkedPlayerInfo? target) { -<<<<<<<< HEAD:MiraAPI/Patches/Events/ReportBodyEventPatch.cs var body = target != null ? Helpers.GetBodyById(target.PlayerId) : null; var @event = new ReportBodyEvent(__instance, target, body); -======== - var @event = new StartMeetingEvent(__instance); ->>>>>>>> dev:MiraAPI/Patches/Events/StartMeetingEventPatch.cs MiraEventManager.InvokeEvent(@event); + return !@event.IsCancelled; } } From 3cba870980ec253b41341808ad74b1044a2c2c42 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 12 Mar 2025 22:30:54 -0400 Subject: [PATCH 07/13] add ProcessVotesEvent.cs and remove "weighting" --- .../Vanilla/Meeting/Voting/HandleVoteEvent.cs | 2 +- .../Meeting/Voting/ProcessVotesEvent.cs | 48 +++++++++++++++++++ MiraAPI/Patches/Voting/MeetingHudPatches.cs | 18 ++++--- MiraAPI/Voting/CustomVote.cs | 3 +- MiraAPI/Voting/PlayerVoteData.cs | 5 +- MiraAPI/Voting/PopulateResultsRpc.cs | 3 +- MiraAPI/Voting/VotingUtils.cs | 26 ++++++++-- 7 files changed, 88 insertions(+), 17 deletions(-) create mode 100644 MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs index a0c7f75..93005de 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/HandleVoteEvent.cs @@ -17,7 +17,7 @@ public class HandleVoteEvent : MiraCancelableEvent /// /// Gets the player who voted. /// - public PlayerControl? Player { get; } + public PlayerControl Player { get; } /// /// Gets the player id of the target. diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs new file mode 100644 index 0000000..9ecaab0 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/ProcessVotesEvent.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; +using MiraAPI.Voting; + +namespace MiraAPI.Events.Vanilla.Meeting.Voting; + +/// +/// Ran after calculating votes and before displaying the results. Only ran on the host. +/// +/// +/// Initializes a new instance of the class. +/// +/// >The list of votes that are being processed. +/// The player to be exiled. Will be null if no player is to be exiled. +public class ProcessVotesEvent(List votes, NetworkedPlayerInfo? exiledPlayer = null) : MiraEvent +{ + private readonly List _originalVotes = [.. votes]; + + private NetworkedPlayerInfo? _exiledPlayer = exiledPlayer; + + /// + /// Gets a value indicating whether the exiled player has been modified by the event. + /// + public bool ExiledPlayerModified { get; private set; } + + /// + /// Gets a value indicating whether the votes have been modified by the event. + /// + public bool VotesModified => !Votes.SequenceEqual(_originalVotes); + + /// + /// Gets the list of votes that are being processed. + /// + public List Votes { get; } = votes; + + /// + /// Gets or sets the player to be exiled. Will be null if no player is to be exiled. + /// + public NetworkedPlayerInfo? ExiledPlayer + { + get => _exiledPlayer; + set + { + _exiledPlayer = value; + ExiledPlayerModified = true; + } + } +} diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs index c437b36..c7babda 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -173,23 +173,29 @@ internal static class MeetingHudPatches [HarmonyPatch(nameof(MeetingHud.CheckForEndVoting))] public static bool EndCheck(MeetingHud __instance) { - if (Helpers.GetAlivePlayers().Exists(plr => plr.GetVoteData()!.VotesRemaining > 0)) + if (Helpers.GetAlivePlayers().Exists(plr => plr.GetVoteData().VotesRemaining > 0)) { return false; } var votes = VotingUtils.CalculateVotes(); + var exiled = VotingUtils.GetExiled(votes, out var isTie); - var max = VotingUtils.CalculateNumVotes(votes).MaxPair(out var isTie); - var exiled = GameData.Instance.AllPlayers.ToArray().FirstOrDefault(v => !isTie && v.PlayerId == max.Key); + var @event = new ProcessVotesEvent(votes, exiled); + MiraEventManager.InvokeEvent(@event); - if (exiled is null || exiled.IsDead || exiled.Disconnected) + if (@event.VotesModified) { - exiled = null; + votes = @event.Votes; + exiled = VotingUtils.GetExiled(votes, out isTie); + } + + if (@event.ExiledPlayerModified) + { + exiled = @event.ExiledPlayer; } __instance.RpcVotingComplete(new MeetingHud.VoterState[__instance.playerStates.Length], exiled, isTie); - return false; } diff --git a/MiraAPI/Voting/CustomVote.cs b/MiraAPI/Voting/CustomVote.cs index fdb0a07..b36796a 100644 --- a/MiraAPI/Voting/CustomVote.cs +++ b/MiraAPI/Voting/CustomVote.cs @@ -5,5 +5,4 @@ /// /// The player that voted. /// The suspect being voted for. -/// The weight of the vote for calculations. -public record struct CustomVote(byte Voter, byte Suspect, float Weight = 1f); +public record struct CustomVote(byte Voter, byte Suspect); diff --git a/MiraAPI/Voting/PlayerVoteData.cs b/MiraAPI/Voting/PlayerVoteData.cs index 023c83e..b7a1b84 100644 --- a/MiraAPI/Voting/PlayerVoteData.cs +++ b/MiraAPI/Voting/PlayerVoteData.cs @@ -56,10 +56,9 @@ public class PlayerVoteData(nint cppPtr) : MonoBehaviour(cppPtr) /// Adds the voted player to the list. /// /// The target's playerId. - /// The weight of the vote. - public void VoteForPlayer(byte playerId, float weight=1f) + public void VoteForPlayer(byte playerId) { - Votes.Add(new CustomVote(Owner.PlayerId, playerId, weight)); + Votes.Add(new CustomVote(Owner.PlayerId, playerId)); } /// diff --git a/MiraAPI/Voting/PopulateResultsRpc.cs b/MiraAPI/Voting/PopulateResultsRpc.cs index 042fb43..24d21ff 100644 --- a/MiraAPI/Voting/PopulateResultsRpc.cs +++ b/MiraAPI/Voting/PopulateResultsRpc.cs @@ -28,7 +28,6 @@ public class PopulateResultsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc { writer.Write(t.Voter); writer.Write(t.Suspect); - writer.Write(t.Weight); } } @@ -39,7 +38,7 @@ public class PopulateResultsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc for (var i = 0; i < votes.Length; i++) { - votes[i] = new CustomVote(reader.ReadByte(), reader.ReadByte(), reader.ReadSingle()); + votes[i] = new CustomVote(reader.ReadByte(), reader.ReadByte()); } return votes; diff --git a/MiraAPI/Voting/VotingUtils.cs b/MiraAPI/Voting/VotingUtils.cs index d152589..56833a6 100644 --- a/MiraAPI/Voting/VotingUtils.cs +++ b/MiraAPI/Voting/VotingUtils.cs @@ -16,6 +16,26 @@ public static class VotingUtils { private const byte SkipVoteId = 253; + /// + /// Gets the exiled player from the list of votes. Returns null if no player is to be exiled. + /// + /// >The list of votes to check. + /// Whether the vote is a tie. + /// The player to be exiled. Will be null if no player is to be exiled. + public static NetworkedPlayerInfo? GetExiled(List votes, out bool isTie) + { + var max = CalculateNumVotes(votes).MaxPair(out var tie); + isTie = tie; + var exiled = GameData.Instance.AllPlayers.ToArray().FirstOrDefault(v => !tie && v.PlayerId == max.Key); + + if (exiled is null || exiled.IsDead || exiled.Disconnected) + { + exiled = null; + } + + return exiled; + } + /// /// Handles when a vote is added and allows for other mods to override/modify. /// @@ -83,11 +103,11 @@ public static class VotingUtils { var dictionary = new Dictionary(); - foreach (var vote in votes) + foreach (var vote in votes.Select(v=>v.Suspect)) { - if (!dictionary.TryAdd(vote.Suspect, vote.Weight)) + if (!dictionary.TryAdd(vote, 1)) { - dictionary[vote.Suspect] += vote.Weight; + dictionary[vote] += 1; } } From fb40664bd0a868df043d01cf78c1ea98dac9504a Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 12 Mar 2025 22:56:41 -0400 Subject: [PATCH 08/13] add CheckForEndVotingEvent.cs --- .../Meeting/Voting/CheckForEndVotingEvent.cs | 26 +++++++++++++++++++ MiraAPI/Patches/Voting/MeetingHudPatches.cs | 8 +++++- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 MiraAPI/Events/Vanilla/Meeting/Voting/CheckForEndVotingEvent.cs diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/CheckForEndVotingEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/CheckForEndVotingEvent.cs new file mode 100644 index 0000000..b0bdae5 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/CheckForEndVotingEvent.cs @@ -0,0 +1,26 @@ +namespace MiraAPI.Events.Vanilla.Meeting.Voting; + +/// +/// Checks if voting is complete. If canceled, the default end voting logic will be skipped. +/// +public class CheckForEndVotingEvent : MiraCancelableEvent +{ + /// + /// Gets a value indicating whether default voting logic determines that voting is complete. + /// + public bool IsVotingComplete { get; } + + /// + /// Gets or sets a value indicating whether the voting should be forced to end, regardless of the default logic. + /// + public bool ForceEndVoting { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// >True if voting is complete, false otherwise. + public CheckForEndVotingEvent(bool isVotingComplete) + { + IsVotingComplete = isVotingComplete; + } +} diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs index c7babda..25bb2f0 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -173,7 +173,13 @@ internal static class MeetingHudPatches [HarmonyPatch(nameof(MeetingHud.CheckForEndVoting))] public static bool EndCheck(MeetingHud __instance) { - if (Helpers.GetAlivePlayers().Exists(plr => plr.GetVoteData().VotesRemaining > 0)) + var shouldEnd = Helpers.GetAlivePlayers().Exists(plr => plr.GetVoteData().VotesRemaining > 0); + + var checkEndEvent = new CheckForEndVotingEvent(shouldEnd); + MiraEventManager.InvokeEvent(checkEndEvent); + shouldEnd = checkEndEvent.ForceEndVoting || checkEndEvent is { IsCancelled: false, IsVotingComplete: true }; + + if (!shouldEnd) { return false; } From 1462061df94f371a4300c474ba9b0961e686707d Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Thu, 13 Mar 2025 09:23:26 -0400 Subject: [PATCH 09/13] fix REALLY stupid bug --- MiraAPI/Patches/Voting/MeetingHudPatches.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs index 25bb2f0..d38c075 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -173,7 +173,7 @@ internal static class MeetingHudPatches [HarmonyPatch(nameof(MeetingHud.CheckForEndVoting))] public static bool EndCheck(MeetingHud __instance) { - var shouldEnd = Helpers.GetAlivePlayers().Exists(plr => plr.GetVoteData().VotesRemaining > 0); + var shouldEnd = !Helpers.GetAlivePlayers().Exists(plr => plr.GetVoteData().VotesRemaining > 0); var checkEndEvent = new CheckForEndVotingEvent(shouldEnd); MiraEventManager.InvokeEvent(checkEndEvent); From db327604c58451eeca47a1f34e0f3f3a25285e1c Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Thu, 13 Mar 2025 12:19:08 -0400 Subject: [PATCH 10/13] Remove PopulateResultsRpc and simplify. --- MiraAPI/Networking/MiraRpc.cs | 5 -- MiraAPI/Patches/Voting/MeetingHudPatches.cs | 24 ++++++-- MiraAPI/Voting/PopulateResultsRpc.cs | 62 --------------------- 3 files changed, 18 insertions(+), 73 deletions(-) delete mode 100644 MiraAPI/Voting/PopulateResultsRpc.cs diff --git a/MiraAPI/Networking/MiraRpc.cs b/MiraAPI/Networking/MiraRpc.cs index 6d51e2a..9ee1088 100644 --- a/MiraAPI/Networking/MiraRpc.cs +++ b/MiraAPI/Networking/MiraRpc.cs @@ -40,11 +40,6 @@ public enum MiraRpc : uint /// CustomGameOver, - /// - /// Custom RPC to handle populating vote results. - /// - PopulateResults, - /// /// Custom RPC to undo a player's vote. /// diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs index d38c075..b375914 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -1,11 +1,11 @@ using System.Linq; using HarmonyLib; +using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Meeting; using MiraAPI.Events.Vanilla.Meeting.Voting; using MiraAPI.Utilities; using MiraAPI.Voting; -using Reactor.Networking.Rpc; namespace MiraAPI.Patches.Voting; @@ -201,21 +201,33 @@ internal static class MeetingHudPatches exiled = @event.ExiledPlayer; } - __instance.RpcVotingComplete(new MeetingHud.VoterState[__instance.playerStates.Length], exiled, isTie); + var voterStates = new Il2CppStructArray([ + .. votes.Select( + v=> new MeetingHud.VoterState + { + VoterId = v.Voter, + VotedForId = v.Suspect, + }) + ]); + + __instance.RpcVotingComplete(voterStates, exiled, isTie); return false; } [HarmonyPrefix] [HarmonyPatch(nameof(MeetingHud.PopulateResults))] - public static bool PopulateResultsPatch(MeetingHud __instance) + public static bool PopulateResultsPatch(MeetingHud __instance, ref Il2CppStructArray states) { - if (!AmongUsClient.Instance.AmHost) + var votes = states.Select(x=> new CustomVote(x.VoterId, x.VotedForId)).ToList(); + var @event = new PopulateResultsEvent(votes); + MiraEventManager.InvokeEvent(@event); + + if (@event.IsCancelled) { return false; } - var votes = VotingUtils.CalculateVotes(); - Rpc.Instance.Send([.. votes]); + VotingUtils.HandlePopulateResults(votes); return false; } diff --git a/MiraAPI/Voting/PopulateResultsRpc.cs b/MiraAPI/Voting/PopulateResultsRpc.cs deleted file mode 100644 index 24d21ff..0000000 --- a/MiraAPI/Voting/PopulateResultsRpc.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Hazel; -using MiraAPI.Networking; -using Reactor.Networking.Attributes; -using Reactor.Networking.Rpc; - -namespace MiraAPI.Voting; - -/// -/// Handles the networking for populating results of a meeting. Custom implementation to work with Mira voting API. -/// -[RegisterCustomRpc((uint)MiraRpc.PopulateResults)] -public class PopulateResultsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc(plugin, id) -{ - /// - public override RpcLocalHandling LocalHandling => RpcLocalHandling.Before; - - /// - public override void Write(MessageWriter writer, CustomVote[]? data) - { - if (data == null) - { - writer.WritePacked(0); - return; - } - - writer.WritePacked(data.Length); - foreach (var t in data) - { - writer.Write(t.Voter); - writer.Write(t.Suspect); - } - } - - /// - public override CustomVote[] Read(MessageReader reader) - { - var votes = new CustomVote[reader.ReadPackedInt32()]; - - for (var i = 0; i < votes.Length; i++) - { - votes[i] = new CustomVote(reader.ReadByte(), reader.ReadByte()); - } - - return votes; - } - - /// - public override void Handle(PlayerControl innerNetObject, CustomVote[]? data) - { - if (AmongUsClient.Instance.HostId != innerNetObject.OwnerId) - { - return; - } - - if (data == null) - { - return; - } - - VotingUtils.HandlePopulateResults([.. data]); - } -} From 25eafe664bd7b9eb947e240f7119580f95ed3f00 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Thu, 13 Mar 2025 12:53:00 -0400 Subject: [PATCH 11/13] improve MeetingSelectEvent --- .../Vanilla/Meeting/Voting/MeetingSelectEvent.cs | 13 ++++++++++--- MiraAPI/Patches/Voting/MeetingHudPatches.cs | 13 ++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs index 71d8a23..bde0592 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs @@ -3,9 +3,9 @@ namespace MiraAPI.Events.Vanilla.Meeting.Voting; /// -/// The event that is invoked when the player tries to select another player to vote. If you cancel this, the player will not be selectable. +/// The event that is invoked when the player tries to select another player to vote. /// -public class MeetingSelectEvent : MiraCancelableEvent +public class MeetingSelectEvent : MiraEvent { /// /// Gets the instance of the voter's vote data. @@ -22,15 +22,22 @@ public class MeetingSelectEvent : MiraCancelableEvent /// public NetworkedPlayerInfo TargetPlayerInfo { get; } + /// + /// Gets or sets a value indicating whether the player is allowed to vote for the target. + /// + public bool AllowVote { get; set; } + /// /// Initializes a new instance of the class. /// /// The voter's data. /// The target's playerId. - public MeetingSelectEvent(PlayerVoteData playerVoteData, int targetId) + /// >Whether the player is allowed to vote for the target. + public MeetingSelectEvent(PlayerVoteData playerVoteData, int targetId, bool allowVote) { VoteData = playerVoteData; TargetId = targetId; TargetPlayerInfo = GameData.Instance.GetPlayerById((byte)targetId); + AllowVote = allowVote; } } diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs index b375914..0e65e3a 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -37,18 +37,13 @@ internal static class MeetingHudPatches { var voteData = PlayerControl.LocalPlayer.GetVoteData(); - var @event = new MeetingSelectEvent(voteData, suspectStateIdx); - MiraEventManager.InvokeEvent(@event); - - if (@event.IsCancelled) - { - return false; - } - var hasVotes = voteData.VotesRemaining > 0; var hasVotedFor = voteData.VotedFor((byte)suspectStateIdx); - return hasVotes && !hasVotedFor; + var @event = new MeetingSelectEvent(voteData, suspectStateIdx, hasVotes && !hasVotedFor); + MiraEventManager.InvokeEvent(@event); + + return @event.AllowVote; } [HarmonyPrefix] From f0b2f9ee2a343791ca378634583e3de9f5dbf76d Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Fri, 14 Mar 2025 17:39:27 -0400 Subject: [PATCH 12/13] fix bad naming --- .../Vanilla/Meeting/Voting/MeetingSelectEvent.cs | 10 +++++----- MiraAPI/Patches/Voting/MeetingHudPatches.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs index bde0592..6ea8eff 100644 --- a/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/Voting/MeetingSelectEvent.cs @@ -23,21 +23,21 @@ public class MeetingSelectEvent : MiraEvent public NetworkedPlayerInfo TargetPlayerInfo { get; } /// - /// Gets or sets a value indicating whether the player is allowed to vote for the target. + /// Gets or sets a value indicating whether the player is allowed to select the target. /// - public bool AllowVote { get; set; } + public bool AllowSelect { get; set; } /// /// Initializes a new instance of the class. /// /// The voter's data. /// The target's playerId. - /// >Whether the player is allowed to vote for the target. - public MeetingSelectEvent(PlayerVoteData playerVoteData, int targetId, bool allowVote) + /// >Whether the player is allowed to select the target. + public MeetingSelectEvent(PlayerVoteData playerVoteData, int targetId, bool allowSelect) { VoteData = playerVoteData; TargetId = targetId; TargetPlayerInfo = GameData.Instance.GetPlayerById((byte)targetId); - AllowVote = allowVote; + AllowSelect = allowSelect; } } diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs index 0e65e3a..42712e4 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -43,7 +43,7 @@ internal static class MeetingHudPatches var @event = new MeetingSelectEvent(voteData, suspectStateIdx, hasVotes && !hasVotedFor); MiraEventManager.InvokeEvent(@event); - return @event.AllowVote; + return @event.AllowSelect; } [HarmonyPrefix] From 8f76f21cb11b159c6e210db523df021c7a12d210 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 15 Mar 2025 16:12:05 -0400 Subject: [PATCH 13/13] fix dummy voting in freeplay --- MiraAPI/Patches/Voting/DummyBehaviourPatch.cs | 16 ++++++++++++++++ MiraAPI/Patches/Voting/MeetingHudPatches.cs | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 MiraAPI/Patches/Voting/DummyBehaviourPatch.cs diff --git a/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs b/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs new file mode 100644 index 0000000..a276946 --- /dev/null +++ b/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs @@ -0,0 +1,16 @@ +using HarmonyLib; +using MiraAPI.Utilities; + +namespace MiraAPI.Patches.Voting; + +[HarmonyPatch(typeof(DummyBehaviour), nameof(DummyBehaviour.Update))] +internal static class DummyBehaviourPatch +{ + public static void Postfix(DummyBehaviour __instance) + { + if (MeetingHud.Instance && __instance.myPlayer.GetVoteData()) + { + __instance.voted = __instance.myPlayer.GetVoteData().VotesRemaining == 0; + } + } +} diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs index 42712e4..6070b1e 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -47,7 +47,7 @@ internal static class MeetingHudPatches } [HarmonyPrefix] - [HarmonyPatch(nameof(MeetingHud.HandleDisconnect), [typeof(PlayerControl), typeof(DisconnectReasons)])] + [HarmonyPatch(nameof(MeetingHud.HandleDisconnect), typeof(PlayerControl), typeof(DisconnectReasons))] public static bool HandleDisconnect(MeetingHud __instance, PlayerControl pc) { if (!AmongUsClient.Instance.AmHost || __instance.playerStates is null || !pc || !GameData.Instance)