mirror of
https://github.com/All-Of-Us-Mods/MiraAPI.git
synced 2026-10-19 15:51:48 +00:00
Voting "API"
This commit is contained in:
1 parent
26afd47d2a
commit
989b511276
15 files changed
+807
-31
No files matched your search
@@ -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<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
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <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>
|
||||
public class StartMeetingEvent : MiraCancelableEvent
|
||||
public class StartMeetingEvent : MiraEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the player that is calling the meeting.
|
||||
/// Gets the MeetingHud instance.
|
||||
/// </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; }
|
||||
public MeetingHud MeetingHud { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StartMeetingEvent"/> 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 StartMeetingEvent(PlayerControl reporter, NetworkedPlayerInfo? target, DeadBody? body)
|
||||
/// <param name="meetingHud">The MeetingHud instance.</param>
|
||||
public StartMeetingEvent(MeetingHud meetingHud)
|
||||
{
|
||||
Reporter = reporter;
|
||||
Target = target;
|
||||
Body = body;
|
||||
MeetingHud = meetingHud;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -39,4 +39,6 @@ public enum MiraRpc : uint
|
||||
/// Custom Game Over RPC.
|
||||
/// </summary>
|
||||
CustomGameOver,
|
||||
PopulateResults,
|
||||
RemoveVote,
|
||||
}
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
@@ -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<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,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;
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
/// <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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user