Voting "API"

This commit is contained in:
ang-xd committed 2025-02-28 23:19:34 -05:00
1 parent 26afd47d2a
commit 989b511276
15 files changed
+807 -31

No files matched your search

+44 -6
View File
@@ -1,9 +1,15 @@
using MiraAPI.Events; using System.Linq;
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Mira; using MiraAPI.Events.Mira;
using MiraAPI.Events.Vanilla.Gameplay; using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Events.Vanilla.Map; using MiraAPI.Events.Vanilla.Map;
using MiraAPI.Events.Vanilla.Meeting;
using MiraAPI.Events.Vanilla.Meeting.Voting;
using MiraAPI.Events.Vanilla.Player; using MiraAPI.Events.Vanilla.Player;
using MiraAPI.Example.Buttons.Freezer; using MiraAPI.Example.Buttons.Freezer;
using MiraAPI.Example.Roles;
using MiraAPI.Utilities;
using Reactor.Utilities; using Reactor.Utilities;
namespace MiraAPI.Example; 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. // Events can be registered using an attribute as well.
[RegisterEvent] [RegisterEvent]
public static void UpdateSystemEventHandler(UpdateSystemEvent @event) public static void UpdateSystemEventHandler(UpdateSystemEvent @event)
@@ -42,11 +81,10 @@ public static class ExampleEventHandlers
{ {
Logger<ExamplePlugin>.Warning("Freeze button clicked!"); Logger<ExamplePlugin>.Warning("Freeze button clicked!");
if (PlayerControl.LocalPlayer.Data.PlayerName == "stupid") if (PlayerControl.LocalPlayer.Data.PlayerName != "stupid") return;
{
@event.Cancel(); @event.Cancel();
@event.Button.SetTimer(15f); @event.Button.SetTimer(15f);
}
} }
// Example event handler // Example event handler
+14
View File
@@ -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);
}
@@ -0,0 +1,37 @@
namespace MiraAPI.Events.Vanilla.Meeting;
/// <summary>
/// The event that is invoked when a body is reported.
/// This is invoked in CmdReportBody.
/// </summary>
public class ReportBodyEvent : MiraCancelableEvent
{
/// <summary>
/// Gets the player that is reporting the body.
/// </summary>
public PlayerControl Reporter { get; }
/// <summary>
/// Gets the player that is being reported. Will be null for emergency meeting.
/// </summary>
public NetworkedPlayerInfo? Target { get; }
/// <summary>
/// Gets the body that is being reported. Will be null for emergency meeting.
/// </summary>
public DeadBody? Body { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ReportBodyEvent"/> class.
/// </summary>
/// <param name="reporter">The player who reported the body.</param>
/// <param name="target">The player being reported.</param>
/// <param name="body">The body being reported.</param>
public ReportBodyEvent(PlayerControl reporter, NetworkedPlayerInfo? target, DeadBody? body)
{
Reporter = reporter;
Target = target;
Body = body;
}
}
@@ -1,35 +1,22 @@
namespace MiraAPI.Events.Vanilla.Meeting; namespace MiraAPI.Events.Vanilla.Meeting;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public class StartMeetingEvent : MiraCancelableEvent public class StartMeetingEvent : MiraEvent
{ {
/// <summary> /// <summary>
/// Gets the player that is calling the meeting. /// Gets the MeetingHud instance.
/// </summary> /// </summary>
public PlayerControl Reporter { get; } public MeetingHud MeetingHud { get; }
/// <summary>
/// Gets the player that is being reported. Will be null for emergency meeting.
/// </summary>
public NetworkedPlayerInfo? Target { get; }
/// <summary>
/// Gets the body that is being reported. Will be null for emergency meeting.
/// </summary>
public DeadBody? Body { get; }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StartMeetingEvent"/> class. /// Initializes a new instance of the <see cref="StartMeetingEvent"/> class.
/// </summary> /// </summary>
/// <param name="reporter">The player who reported the body.</param> /// <param name="meetingHud">The MeetingHud instance.</param>
/// <param name="target">The player being reported.</param> public StartMeetingEvent(MeetingHud meetingHud)
/// <param name="body">The body being reported.</param>
public StartMeetingEvent(PlayerControl reporter, NetworkedPlayerInfo? target, DeadBody? body)
{ {
Reporter = reporter; MeetingHud = meetingHud;
Target = target;
Body = body;
} }
} }
@@ -0,0 +1,45 @@
using MiraAPI.Utilities;
using MiraAPI.Voting;
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// 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.
/// </summary>
public class HandleVoteEvent : MiraCancelableEvent
{
/// <summary>
/// Gets the instance of the voter's vote data.
/// </summary>
public PlayerVoteData VoteData { get; }
/// <summary>
/// Gets the player who voted.
/// </summary>
public PlayerControl? Player { get; }
/// <summary>
/// Gets the player id of the target.
/// </summary>
public byte TargetId { get; }
/// <summary>
/// Gets the player info of the target.
/// </summary>
public NetworkedPlayerInfo TargetPlayerInfo { get; }
/// <summary>
/// Initializes a new instance of the <see cref="HandleVoteEvent"/> class.
/// </summary>
/// <param name="playerVoteData">The voter's data.</param>
/// <param name="targetId">The target's playerId.</param>
public HandleVoteEvent(PlayerVoteData playerVoteData, byte targetId)
{
VoteData = playerVoteData;
Player = playerVoteData.Owner;
TargetId = targetId;
TargetPlayerInfo = GameData.Instance.GetPlayerById(targetId);
}
}
@@ -0,0 +1,37 @@
using MiraAPI.Utilities;
using MiraAPI.Voting;
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// 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.
/// </summary>
public class MeetingSelectEvent : MiraCancelableEvent
{
/// <summary>
/// Gets the instance of the voter's vote data.
/// </summary>
public PlayerVoteData VoteData { get; }
/// <summary>
/// Gets the player id of the target.
/// </summary>
public int TargetId { get; }
/// <summary>
/// Gets the player info of the target.
/// </summary>
public NetworkedPlayerInfo TargetPlayerInfo { get; }
/// <summary>
/// Initializes a new instance of the <see cref="MeetingSelectEvent"/> class.
/// </summary>
/// <param name="playerVoteData">The voter's data.</param>
/// <param name="targetId">The target's playerId.</param>
public MeetingSelectEvent(PlayerVoteData playerVoteData, int targetId)
{
VoteData = playerVoteData;
TargetId = targetId;
TargetPlayerInfo = GameData.Instance.GetPlayerById((byte)targetId);
}
}
+2
View File
@@ -39,4 +39,6 @@ public enum MiraRpc : uint
/// Custom Game Over RPC. /// Custom Game Over RPC.
/// </summary> /// </summary>
CustomGameOver, CustomGameOver,
PopulateResults,
RemoveVote,
} }
@@ -6,7 +6,7 @@ using MiraAPI.Utilities;
namespace MiraAPI.Patches.Events; namespace MiraAPI.Patches.Events;
[HarmonyPatch] [HarmonyPatch]
public static class StartMeetingEventPatch public static class ReportBodyEventPatch
{ {
[HarmonyPrefix] [HarmonyPrefix]
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdReportDeadBody))] [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdReportDeadBody))]
@@ -14,7 +14,7 @@ public static class StartMeetingEventPatch
{ {
var body = target != null ? Helpers.GetBodyById(target.PlayerId) : null; 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); MiraEventManager.InvokeEvent(@event);
return !@event.IsCancelled; return !@event.IsCancelled;
} }
+10 -2
View File
@@ -6,6 +6,8 @@ using MiraAPI.Events.Vanilla.Player;
using MiraAPI.Hud; using MiraAPI.Hud;
using MiraAPI.Modifiers; using MiraAPI.Modifiers;
using MiraAPI.Roles; using MiraAPI.Roles;
using MiraAPI.Utilities;
using MiraAPI.Voting;
using Reactor.Utilities.Extensions; using Reactor.Utilities.Extensions;
namespace MiraAPI.Patches; namespace MiraAPI.Patches;
@@ -24,12 +26,18 @@ public static class PlayerControlPatches
[HarmonyPatch(nameof(PlayerControl.Start))] [HarmonyPatch(nameof(PlayerControl.Start))]
public static void PlayerControlStartPostfix(PlayerControl __instance) public static void PlayerControlStartPostfix(PlayerControl __instance)
{ {
if (__instance.gameObject.TryGetComponent<ModifierComponent>(out var comp)) if (__instance.gameObject.TryGetComponent<ModifierComponent>(out var modifierComp))
{ {
comp.DestroyImmediate(); modifierComp.DestroyImmediate();
}
if (__instance.gameObject.TryGetComponent<PlayerVoteData>(out var voteComp))
{
voteComp.DestroyImmediate();
} }
__instance.gameObject.AddComponent<ModifierComponent>(); __instance.gameObject.AddComponent<ModifierComponent>();
__instance.gameObject.AddComponent<PlayerVoteData>();
} }
/// <summary> /// <summary>
+219
View File
@@ -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<PopulateResultsRpc>.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;
}
}
+66
View File
@@ -6,6 +6,7 @@ using HarmonyLib;
using MiraAPI.GameOptions; using MiraAPI.GameOptions;
using MiraAPI.Networking; using MiraAPI.Networking;
using MiraAPI.Roles; using MiraAPI.Roles;
using MiraAPI.Voting;
using Reactor.Utilities; using Reactor.Utilities;
using TMPro; using TMPro;
using UnityEngine; using UnityEngine;
@@ -39,6 +40,71 @@ public static class Extensions
BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value))); BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value)));
} }
/// <summary>
/// Gets a cache of player's vote data components to improve performance.
/// </summary>
public static Dictionary<PlayerControl, PlayerVoteData> VoteDataComponents { get; } = [];
/// <summary>
/// Gets the PlayerVoteData of a player.
/// </summary>
/// <param name="player">The PlayerControl object.</param>
/// <returns>A PlayerVoteData if there is one, null otherwise.</returns>
public static PlayerVoteData GetVoteData(this PlayerControl player)
{
if (VoteDataComponents.TryGetValue(player, out var component))
{
return component;
}
component = player.GetComponent<PlayerVoteData>();
if (!component)
{
throw new InvalidOperationException("PlayerVoteData is not attached to the player.");
}
VoteDataComponents[player] = component;
return component;
}
public static KeyValuePair<byte, int> MaxPair(this Dictionary<byte, int> self, out bool tie)
{
tie = true;
var result = new KeyValuePair<byte, int>(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<byte, float> MaxPair(this Dictionary<byte, float> self, out bool tie)
{
tie = true;
var result = new KeyValuePair<byte, float>(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;
}
/// <summary> /// <summary>
/// Gets the best constructor for a type based on the specified arguments. /// Gets the best constructor for a type based on the specified arguments.
/// </summary> /// </summary>
+19
View File
@@ -0,0 +1,19 @@
namespace MiraAPI.Voting;
/// <summary>
/// A custom implementation to keep track of votes.
/// </summary>
/// <param name="voter">The player who voted.</param>
/// <param name="suspect">The suspect.</param>
public readonly struct CustomVote(byte voter, byte suspect)
{
/// <summary>
/// Gets the playerId of the Voter.
/// </summary>
public byte Voter { get; } = voter;
/// <summary>
/// Gets the playerId of the Suspect.
/// </summary>
public byte Suspect { get; } = suspect;
}
+90
View File
@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using Reactor.Utilities.Attributes;
using UnityEngine;
namespace MiraAPI.Voting;
/// <summary>
/// Handles player votes, and removing/adding additional votes.
/// </summary>
[RegisterInIl2Cpp]
public class PlayerVoteData(nint ptr) : MonoBehaviour(ptr)
{
/// <summary>
/// Gets the owner of this component.
/// </summary>
public PlayerControl? Owner { get; private set; }
/// <summary>
/// Gets the players which the owner has voted for.
/// </summary>
public List<byte> VotedPlayers { get; private set; } = [];
/// <summary>
/// Gets or sets the amount of votes the owner has left.
/// </summary>
public int VotesRemaining
{
get => _votesRemaining;
set => _votesRemaining = Math.Max(0, value);
}
private int _votesRemaining = 1;
private void Awake()
{
Owner = GetComponent<PlayerControl>();
if (Owner == null)
{
Destroy(this);
}
}
/// <summary>
/// Adds the voted player to the list.
/// </summary>
/// <param name="playerId">The target's playerId.</param>
public void VoteForPlayer(byte playerId)
{
VotedPlayers.Add(playerId);
}
/// <summary>
/// Removes the voted player from the list.
/// </summary>
/// <param name="playerId">The target's playerId.</param>
public void RemovePlayerVote(byte playerId)
{
if (!VotedPlayers.Contains(playerId)) return;
VotedPlayers.Remove(playerId);
}
/// <summary>
/// Sets the player's votes remaining.
/// </summary>
/// <param name="votesRemaining">The amount of votes you would like to set it to.</param>
public void SetVotesRemaining(int votesRemaining)
{
VotesRemaining = votesRemaining;
}
/// <summary>
/// Adds votes to the owner.
/// </summary>
/// <param name="amount">The amount of votes you would like to add.</param>
public void AddVotes(int amount)
{
SetVotesRemaining(VotesRemaining + amount);
}
/// <summary>
/// Removes votes from the owner.
/// </summary>
/// <param name="amount">The amount of votes you would like to remove.</param>
public void RemoveVotes(int amount)
{
SetVotesRemaining(VotesRemaining - amount);
}
}
+63
View File
@@ -0,0 +1,63 @@
using System.Linq;
using Hazel;
using MiraAPI.Networking;
using Reactor.Networking.Attributes;
using Reactor.Networking.Rpc;
namespace MiraAPI.Voting;
/// <summary>
/// Handles the networking for populating results of a meeting. Custom implementation to work with Mira voting API.
/// </summary>
[RegisterCustomRpc((uint)MiraRpc.PopulateResults)]
public class PopulateResultsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<MiraApiPlugin, CustomVote[]>(plugin, id)
{
/// <inheritdoc />
public override RpcLocalHandling LocalHandling => RpcLocalHandling.Before;
/// <inheritdoc />
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);
}
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public override void Handle(PlayerControl innerNetObject, CustomVote[]? data)
{
if (AmongUsClient.Instance.HostId != innerNetObject.OwnerId)
{
return;
}
if (data == null)
{
return;
}
VotingUtils.HandlePopulateResults([.. data]);
}
}
+151
View File
@@ -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;
/// <summary>
/// Utilities used for the Mira voting system.
/// </summary>
public static class VotingUtils
{
private const byte SkipVoteId = 253;
/// <summary>
/// Handles when a vote is added and allows for other mods to override/modify.
/// </summary>
/// <param name="voteData">The player's vote data.</param>
/// <param name="suspectIdx">Who the player voted for.</param>
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);
}
/// <summary>
/// Networks the removal of votes. Used to remove votes when a player disconnects.
/// </summary>
/// <param name="source">The player who is sending the RPC. Should be the host.</param>
/// <param name="voterId">The player who voted.</param>
/// <param name="votedFor">The player who the voter voted for.</param>
[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);
}
/// <summary>
/// Calculates the total number of votes.
/// </summary>
/// <param name="votes">A list of calculated votes.</param>
/// <returns>The total votes.</returns>
public static Dictionary<byte, int> CalculateNumVotes(IEnumerable<CustomVote> votes)
{
var dictionary = new Dictionary<byte, int>();
foreach (var vote in votes)
{
if (!dictionary.TryAdd(vote.Suspect, 1))
{
dictionary[vote.Suspect] += 1;
}
}
return dictionary;
}
/// <summary>
/// Calculates votes to check if all players have voted.
/// </summary>
/// <returns>The list of votes.</returns>
public static List<CustomVote> CalculateVotes()
{
return (from player in Helpers.GetAlivePlayers()
from vote in player.GetVoteData()!.VotedPlayers
select new CustomVote(player.PlayerId, vote)).ToList();
}
/// <summary>
/// Handles the populating of results locally. Called by the PopulateResultsRpc.
/// </summary>
/// <param name="votes">The list of networked votes.</param>
public static void HandlePopulateResults(List<CustomVote> votes)
{
MeetingHud.Instance.TitleText.text =
DestroyableSingleton<TranslationController>.Instance.GetString(
StringNames.MeetingVotingResults,
Il2CppSystem.Array.Empty<Il2CppSystem.Object>());
var delays = new Dictionary<byte, int>();
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<MiraApiPlugin>.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);
}
}
}
}
}