Merge pull request #31 from All-Of-Us-Mods/voting

Voting API
This commit is contained in:
angxl wtf authored and GitHub committed 2025-03-15 21:10:19 -04:00
commit 144a95fa3b
17 files changed
+878 -25

No files matched your search

+45 -6
View File
@@ -1,9 +1,14 @@
using MiraAPI.Events;
using System.Linq;
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 +34,41 @@ public static class ExampleEventHandlers
});
}
// If you want to add extra votes to a player, do something like this.
[RegisterEvent]
public static void StartMeetingEvent(StartMeetingEvent _)
{
foreach (var plr in PlayerControl.AllPlayerControls)
{
if (plr.Data.Role is MayorRole)
{
plr.GetVoteData().IncreaseRemainingVotes(1);
}
}
}
// 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)
{
if (@event.VoteData.Owner.Data.Role is not NeutralKillerRole) return;
@event.VoteData.SetRemainingVotes(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().Votes.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 +82,10 @@ public static class ExampleEventHandlers
{
Logger<ExamplePlugin>.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
+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,26 @@
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// Checks if voting is complete. If canceled, the default end voting logic will be skipped.
/// </summary>
public class CheckForEndVotingEvent : MiraCancelableEvent
{
/// <summary>
/// Gets a value indicating whether default voting logic determines that voting is complete.
/// </summary>
public bool IsVotingComplete { get; }
/// <summary>
/// Gets or sets a value indicating whether the voting should be forced to end, regardless of the default logic.
/// </summary>
public bool ForceEndVoting { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="CheckForEndVotingEvent"/> class.
/// </summary>
/// <param name="isVotingComplete">>True if voting is complete, false otherwise.</param>
public CheckForEndVotingEvent(bool isVotingComplete)
{
IsVotingComplete = isVotingComplete;
}
}
@@ -0,0 +1,44 @@
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,43 @@
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.
/// </summary>
public class MeetingSelectEvent : MiraEvent
{
/// <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>
/// Gets or sets a value indicating whether the player is allowed to select the target.
/// </summary>
public bool AllowSelect { get; set; }
/// <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>
/// <param name="allowSelect">>Whether the player is allowed to select the target.</param>
public MeetingSelectEvent(PlayerVoteData playerVoteData, int targetId, bool allowSelect)
{
VoteData = playerVoteData;
TargetId = targetId;
TargetPlayerInfo = GameData.Instance.GetPlayerById((byte)targetId);
AllowSelect = allowSelect;
}
}
@@ -0,0 +1,25 @@
using System.Collections.Generic;
using MiraAPI.Voting;
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// 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.
/// </summary>
public class PopulateResultsEvent : MiraCancelableEvent
{
/// <summary>
/// Gets a list of networked votes.
/// </summary>
public List<CustomVote> Votes { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PopulateResultsEvent"/> class.
/// </summary>
/// <param name="votes">The list of votes.</param>
public PopulateResultsEvent(List<CustomVote> votes)
{
Votes = votes;
}
}
@@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Linq;
using MiraAPI.Voting;
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// Ran after calculating votes and before displaying the results. Only ran on the host.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="ProcessVotesEvent"/> class.
/// </remarks>
/// <param name="votes">>The list of votes that are being processed.</param>
/// <param name="exiledPlayer">The player to be exiled. Will be null if no player is to be exiled.</param>
public class ProcessVotesEvent(List<CustomVote> votes, NetworkedPlayerInfo? exiledPlayer = null) : MiraEvent
{
private readonly List<CustomVote> _originalVotes = [.. votes];
private NetworkedPlayerInfo? _exiledPlayer = exiledPlayer;
/// <summary>
/// Gets a value indicating whether the exiled player has been modified by the event.
/// </summary>
public bool ExiledPlayerModified { get; private set; }
/// <summary>
/// Gets a value indicating whether the votes have been modified by the event.
/// </summary>
public bool VotesModified => !Votes.SequenceEqual(_originalVotes);
/// <summary>
/// Gets the list of votes that are being processed.
/// </summary>
public List<CustomVote> Votes { get; } = votes;
/// <summary>
/// Gets or sets the player to be exiled. Will be null if no player is to be exiled.
/// </summary>
public NetworkedPlayerInfo? ExiledPlayer
{
get => _exiledPlayer;
set
{
_exiledPlayer = value;
ExiledPlayerModified = true;
}
}
}
+5
View File
@@ -39,4 +39,9 @@ public enum MiraRpc : uint
/// Custom Game Over RPC.
/// </summary>
CustomGameOver,
/// <summary>
/// Custom RPC to undo a player's vote.
/// </summary>
RemoveVote,
}
@@ -5,10 +5,12 @@ using MiraAPI.Utilities;
namespace MiraAPI.Patches.Events;
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdReportDeadBody))]
[HarmonyPatch]
public static class ReportBodyEventPatch
{
public static bool Prefix(PlayerControl __instance, NetworkedPlayerInfo? target)
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdReportDeadBody))]
public static bool PlayerControlCmdReportDeadBodyPrefix(PlayerControl __instance, NetworkedPlayerInfo? target)
{
var body = target != null ? Helpers.GetBodyById(target.PlayerId) : null;
@@ -1,15 +0,0 @@
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Meeting;
namespace MiraAPI.Patches.Events;
[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))]
public static class StartMeetingEventPatch
{
public static void Postfix(MeetingHud __instance)
{
var @event = new StartMeetingEvent(__instance);
MiraEventManager.InvokeEvent(@event);
}
}
+9 -2
View File
@@ -6,6 +6,7 @@ using MiraAPI.Events.Vanilla.Player;
using MiraAPI.Hud;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
using MiraAPI.Voting;
using Reactor.Utilities.Extensions;
namespace MiraAPI.Patches;
@@ -24,12 +25,18 @@ public static class PlayerControlPatches
[HarmonyPatch(nameof(PlayerControl.Start))]
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<PlayerVoteData>();
}
/// <summary>
@@ -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;
}
}
}
+237
View File
@@ -0,0 +1,237 @@
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;
namespace MiraAPI.Patches.Voting;
[HarmonyPatch(typeof(MeetingHud))]
internal 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.Votes.Clear();
voteData.SetRemainingVotes(1);
if (plr.Data.IsDead || plr.Data.Disconnected)
{
voteData.SetRemainingVotes(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 hasVotes = voteData.VotesRemaining > 0;
var hasVotedFor = voteData.VotedFor((byte)suspectStateIdx);
var @event = new MeetingSelectEvent(voteData, suspectStateIdx, hasVotes && !hasVotedFor);
MiraEventManager.InvokeEvent(@event);
return @event.AllowSelect;
}
[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.VotedFor(pc.PlayerId))
{
continue;
}
voteData.Votes.RemoveAll(x=>x.Suspect==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.VotedFor(suspectPlayerId))
{
return false;
}
VotingUtils.HandleVote(voteData, suspectPlayerId);
__instance.SetDirtyBit(1U);
__instance.CheckForEndVoting();
if (voteData.VotesRemaining != 0)
{
return false;
}
__instance.playerStates.First(x => x.TargetPlayerId == srcPlayerId).SetVote(suspectPlayerId);
PlayerControl.LocalPlayer.RpcSendChatNote(srcPlayerId, ChatNoteTypes.DidVote);
return false;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(MeetingHud.CmdCastVote))]
public static void CmdCastVotePatch(MeetingHud __instance, byte playerId, byte suspectIdx)
{
var plr = GameData.Instance.GetPlayerById(playerId);
var voteData = plr.Object.GetVoteData();
if (!AmongUsClient.Instance.AmHost)
{
if (voteData == null || voteData.VotesRemaining == 0 ||
voteData.VotedFor(suspectIdx))
{
return;
}
VotingUtils.HandleVote(voteData, suspectIdx);
if (voteData.VotesRemaining == 0)
{
__instance.playerStates.First(x => x.TargetPlayerId == playerId).SetVote(suspectIdx);
}
}
if (PlayerControl.LocalPlayer.PlayerId != playerId)
{
return;
}
SoundManager.Instance.PlaySound(__instance.VoteLockinSound, false);
foreach (var playerVoteArea in __instance.playerStates)
{
playerVoteArea.ClearButtons();
}
__instance.SkipVoteButton.ClearButtons();
var localVoteData = PlayerControl.LocalPlayer.GetVoteData();
if (localVoteData == null || localVoteData.VotesRemaining != 0) return;
__instance.SkipVoteButton.voteComplete = true;
__instance.SkipVoteButton.gameObject.SetActive(false);
}
[HarmonyPrefix]
[HarmonyPatch(nameof(MeetingHud.CheckForEndVoting))]
public static bool EndCheck(MeetingHud __instance)
{
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;
}
var votes = VotingUtils.CalculateVotes();
var exiled = VotingUtils.GetExiled(votes, out var isTie);
var @event = new ProcessVotesEvent(votes, exiled);
MiraEventManager.InvokeEvent(@event);
if (@event.VotesModified)
{
votes = @event.Votes;
exiled = VotingUtils.GetExiled(votes, out isTie);
}
if (@event.ExiledPlayerModified)
{
exiled = @event.ExiledPlayer;
}
var voterStates = new Il2CppStructArray<MeetingHud.VoterState>([
.. 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, ref Il2CppStructArray<MeetingHud.VoterState> states)
{
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;
}
VotingUtils.HandlePopulateResults(votes);
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
@@ -7,6 +7,7 @@ using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.Networking;
using MiraAPI.Roles;
using MiraAPI.Voting;
using Reactor.Utilities;
using TMPro;
using UnityEngine;
@@ -74,6 +75,71 @@ public static class Extensions
return Mathf.Approximately(number, Mathf.Round(number));
}
/// <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>
/// Gets the best constructor for a type based on the specified arguments.
/// </summary>
+8
View File
@@ -0,0 +1,8 @@
namespace MiraAPI.Voting;
/// <summary>
/// Represents a custom vote.
/// </summary>
/// <param name="Voter">The player that voted.</param>
/// <param name="Suspect">The suspect being voted for.</param>
public record struct CustomVote(byte Voter, byte Suspect);
+108
View File
@@ -0,0 +1,108 @@
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 cppPtr) : MonoBehaviour(cppPtr)
{
/// <summary>
/// Gets the owner of this component.
/// </summary>
public PlayerControl Owner { get; private set; } = null!;
/// <summary>
/// Gets the list of votes the owner has.
/// </summary>
public List<CustomVote> Votes { 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>
/// Returns whether the owner has voted for the specified player.
/// </summary>
/// <param name="playerId">The target's playerId.</param>
/// <returns>True if the owner voted for the specified player, false otherwise.</returns>
public bool VotedFor(byte playerId)
{
return Votes.Exists(x => x.Suspect == playerId);
}
/// <summary>
/// Adds the voted player to the list.
/// </summary>
/// <param name="playerId">The target's playerId.</param>
public void VoteForPlayer(byte playerId)
{
Votes.Add(new CustomVote(Owner.PlayerId, playerId));
}
/// <summary>
/// Removes the specified vote.
/// </summary>
/// <param name="vote">The vote you would like to remove.</param>
public void RemovePlayerVote(CustomVote vote)
{
Votes.Remove(vote);
}
/// <summary>
/// Removes a single vote from the owner.
/// </summary>
/// <param name="playerId">The target's playerId.</param>
public void RemovePlayerVote(byte playerId)
{
Votes.Remove(Votes.Find(x=>x.Suspect==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 SetRemainingVotes(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 IncreaseRemainingVotes(int amount)
{
SetRemainingVotes(VotesRemaining + amount);
}
/// <summary>
/// Removes votes from the owner.
/// </summary>
/// <param name="amount">The amount of votes you would like to remove.</param>
public void DecreaseRemainingVotes(int amount)
{
SetRemainingVotes(VotesRemaining - amount);
}
}
+180
View File
@@ -0,0 +1,180 @@
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>
/// Gets the exiled player from the list of votes. Returns null if no player is to be exiled.
/// </summary>
/// <param name="votes">>The list of votes to check.</param>
/// <param name="isTie">Whether the vote is a tie.</param>
/// <returns>The player to be exiled. Will be null if no player is to be exiled.</returns>
public static NetworkedPlayerInfo? GetExiled(List<CustomVote> 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;
}
/// <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)
{
var @event = new HandleVoteEvent(voteData, suspectIdx);
MiraEventManager.InvokeEvent(@event);
if (@event.IsCancelled)
{
return;
}
voteData.DecreaseRemainingVotes(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.DecreaseRemainingVotes(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, float> CalculateNumVotes(IEnumerable<CustomVote> votes)
{
var dictionary = new Dictionary<byte, float>();
foreach (var vote in votes.Select(v=>v.Suspect))
{
if (!dictionary.TryAdd(vote, 1))
{
dictionary[vote] += 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
[
.. Helpers.GetAlivePlayers()
.SelectMany(player => player.GetVoteData().Votes)
];
}
/// <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)
{
PopulateResultsEvent @event = new PopulateResultsEvent(votes);
MiraEventManager.InvokeEvent(@event);
if (@event.IsCancelled)
{
return;
}
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);
}
}
}
}
}