From 989b51127630d84efc39532b53167c71cd4eec00 Mon Sep 17 00:00:00 2001 From: ang-xd Date: Fri, 28 Feb 2025 23:19:34 -0500 Subject: [PATCH 01/38] 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/38] 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/38] 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/38] 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 886ef0e31fe0925a7c87142e99f4f794bb0f37f9 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:26:24 -0400 Subject: [PATCH 05/38] fix ToAdd/ToRemove modifier bug --- MiraAPI/Modifiers/ModifierComponent.cs | 58 ++++++++++++++------------ 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 8c7f6f2..53a8ac4 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Linq; using System.Text; using Il2CppInterop.Runtime.Attributes; @@ -25,7 +24,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// Gets the active modifiers on the player. /// [HideFromIl2Cpp] - public ImmutableList ActiveModifiers => Modifiers.ToImmutableList(); + public ImmutableList ActiveModifiers { get; private set; } = ImmutableList.Empty; [HideFromIl2Cpp] private List Modifiers { get; set; } = []; @@ -59,13 +58,13 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) private void FixedUpdate() { - foreach (var modifier in _toRemove) + foreach (var modifier in _toRemove.ToArray()) { modifier.OnDeactivate(); Modifiers.Remove(modifier); } - foreach (var modifier in _toAdd) + foreach (var modifier in _toAdd.ToArray()) { Modifiers.Add(modifier); modifier.Initialized = true; @@ -85,9 +84,11 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) } _toAdd.Clear(); _toRemove.Clear(); + + ActiveModifiers = Modifiers.ToImmutableList(); } - foreach (var modifier in Modifiers) + foreach (var modifier in ActiveModifiers) { modifier.FixedUpdate(); } @@ -95,7 +96,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) private void Update() { - foreach (var modifier in Modifiers) + foreach (var modifier in ActiveModifiers) { modifier.Update(); } @@ -105,21 +106,26 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) return; } - var filteredModifiers = Modifiers.Where(mod => !mod.HideOnUi).ToArray(); + if (MeetingHud.Instance) + { + return; + } - if (filteredModifiers.Length != 0 && !MeetingHud.Instance) + var showText = false; + var builder = new StringBuilder("Modifiers:\n"); + + foreach (var modifier in ActiveModifiers) { - var stringBuild = new StringBuilder(); - foreach (var mod in filteredModifiers) + if (modifier.HideOnUi) { - stringBuild.Append(CultureInfo.InvariantCulture, $"\n{mod.GetHudString()}"); + continue; } - _modifierText!.text = $"Modifiers:{stringBuild}"; - } - else if (_modifierText!.text != string.Empty) - { - _modifierText.text = string.Empty; + + showText = true; + builder.AppendLine(modifier.GetHudString()); } + + _modifierText!.text = showText ? builder.ToString() : string.Empty; } /// @@ -131,7 +137,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public IEnumerable GetModifiers(Func? predicate=null) where T : BaseModifier { - return Modifiers.OfType().Where(x => predicate == null || predicate(x)); + return ActiveModifiers.OfType().Where(x => predicate == null || predicate(x)); } /// @@ -143,7 +149,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public IEnumerable GetModifiers(Type type, Func? predicate=null) { - return Modifiers.Where(x => x.GetType() == type && (predicate == null || predicate(x))); + return ActiveModifiers.Where(x => x.GetType() == type && (predicate == null || predicate(x))); } /// @@ -263,7 +269,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public BaseModifier? GetModifier(Guid modifierGuid) { - return Modifiers.Find(x => x.UniqueId == modifierGuid); + return ActiveModifiers.Find(x => x.UniqueId == modifierGuid); } /// @@ -285,7 +291,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public void RemoveModifier(Type type, Func? predicate = null) { - var modifiers = Modifiers.Where(x => x.GetType() == type && (predicate == null || predicate(x))).ToList(); + var modifiers = ActiveModifiers.Where(x => x.GetType() == type && (predicate == null || predicate(x))).ToList(); if (modifiers.Count > 1) { throw new InvalidOperationException($"Cannot remove modifier {type.Name} because there are multiple instances of that modifier."); @@ -308,7 +314,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public void RemoveModifier(BaseModifier modifier) { - if (!Modifiers.Contains(modifier)) + if (!ActiveModifiers.Contains(modifier)) { Logger.Error($"Cannot remove modifier {modifier.ModifierName} because it is not active on this player."); return; @@ -374,13 +380,13 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) } var id = modifier.TypeId; - if (modifier.Unique && Modifiers.Find(x => x.TypeId == id) != null) + if (modifier.Unique && ActiveModifiers.Find(x => x.TypeId == id) != null) { Logger.Error($"Player already has modifier with id {id}!"); return null; } - if (Modifiers.Contains(modifier)) + if (ActiveModifiers.Contains(modifier)) { Logger.Error($"Player already has this modifier!"); return null; @@ -446,7 +452,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public bool HasModifier(Func? predicate=null) where T : BaseModifier { - return Modifiers.Exists(x => x is T modifier && (predicate == null || predicate(modifier))); + return ActiveModifiers.Exists(x => x is T modifier && (predicate == null || predicate(modifier))); } /// @@ -458,7 +464,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public bool HasModifier(Type type, Func? predicate=null) { - return Modifiers.Exists(x => x.GetType() == type && (predicate == null || predicate(x))); + return ActiveModifiers.Exists(x => x.GetType() == type && (predicate == null || predicate(x))); } /// @@ -484,6 +490,6 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] public bool HasModifier(Guid id) { - return Modifiers.Exists(x => x.UniqueId == id); + return ActiveModifiers.Exists(x => x.UniqueId == id); } } 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 06/38] 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 08d8a487014f25ee44aaff1d7114a0acb88d758e 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 07/38] 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 0dd2149..7b3dc3f 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -12,7 +12,7 @@ public abstract class BaseModifier : IOptionable /// /// 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 6ab283d39cb13d50222c7ed6e73d48db0f7aec1f Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:04:26 -0400 Subject: [PATCH 08/38] increase accessibility of properties in CustomActionButton.cs --- MiraAPI/Hud/CustomActionButton.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 4136e99..8e5815c 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -52,9 +52,9 @@ public abstract class CustomActionButton public virtual Color TextOutlineColor => Color.clear; /// - /// Gets the location of the button on the screen. + /// Gets or sets the location of the button on the screen. /// - public virtual ButtonLocation Location { get; private set; } = ButtonLocation.BottomLeft; + public virtual ButtonLocation Location { get; set; } = ButtonLocation.BottomLeft; /// /// Gets a value indicating whether the button has an effect ability. @@ -69,27 +69,27 @@ public abstract class CustomActionButton /// /// Gets or sets a value indicating whether the effect is currently active, if there is one. /// - public bool EffectActive { get; protected set; } + public bool EffectActive { get; set; } /// /// Gets or sets a value indicating whether the timer is currently active. /// - public bool TimerPaused { get; protected set; } + public bool TimerPaused { get; set; } /// /// Gets or sets the amount of uses left. /// - public int UsesLeft { get; protected set; } + public int UsesLeft { get; set; } /// /// Gets or sets the timer variable to measure cooldowns and effects. /// - public float Timer { get; protected set; } + public float Timer { get; set; } /// - /// Gets the button object in game. This is created by Mira API automatically. + /// Gets or sets the button object in game. This is created by Mira API automatically. /// - protected ActionButton? Button { get; private set; } + public ActionButton? Button { get; set; } /// /// The method used to create the button. @@ -463,7 +463,7 @@ public abstract class CustomActionButton : CustomActionButton where T : MonoB /// /// Gets or sets the target object of the button. /// - public T? Target { get; protected set; } + public T? Target { get; set; } /// /// Gets the distance the player must be from the target object to use the button. From 529fc89fc5821a73a010e1b402456f11d5c16eda Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:45:29 -0400 Subject: [PATCH 09/38] add WCAG 2.1 compliant alternate color --- .../Patches/Options/GameOptionsMenuPatch.cs | 6 +- .../Patches/Options/LobbyViewPanePatches.cs | 2 +- .../Patches/Options/RoleSettingMenuPatches.cs | 6 +- MiraAPI/Utilities/ColorExtensions.cs | 89 +++++++++++++++++++ MiraAPI/Utilities/Extensions.cs | 1 + 5 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 MiraAPI/Utilities/ColorExtensions.cs diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index aca0f2e..b792767 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -202,7 +202,7 @@ public static class GameOptionsMenuPatch { categoryHeaderMasked.Background.color = group.GroupColor; categoryHeaderMasked.Divider.color = group.GroupColor; - categoryHeaderMasked.Title.color = group.GroupColor.GetAlternateColor(); + categoryHeaderMasked.Title.color = group.GroupColor.FindAlternateColor(); } categoryHeaderMasked.Background.size = new Vector2( @@ -232,10 +232,10 @@ public static class GameOptionsMenuPatch { if (group.GroupColor != Color.clear) { - renderer.color = group.GroupColor.GetAlternateColor(); + renderer.color = group.GroupColor.FindAlternateColor(); if (renderer.transform.parent.TryGetComponent(out var btn)) { - btn.interactableColor = group.GroupColor.GetAlternateColor(); + btn.interactableColor = group.GroupColor.FindAlternateColor(); btn.interactableHoveredColor = Color.white; } } diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index 9b2a934..5172ba0 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -340,7 +340,7 @@ public static class LobbyViewPanePatches viewSettingsInfoPanelRoleVariant.chanceTitle.color = viewSettingsInfoPanelRoleVariant.chanceBackground.color = viewSettingsInfoPanelRoleVariant.background.color = - customRole.RoleColor.GetAlternateColor(); + customRole.RoleColor.FindAlternateColor(); instance.settingsInfo.Add(viewSettingsInfoPanelRoleVariant.gameObject); num -= 0.664f; } diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 126f0b5..b21f387 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -384,7 +384,7 @@ public static class RoleSettingMenuPatches } __instance.roleHeaderSprite.color = customRole.RoleColor; - __instance.roleHeaderText.color = customRole.RoleColor.GetAlternateColor(); + __instance.roleHeaderText.color = customRole.RoleColor.FindAlternateColor(); var categoryHeaderMasked = __instance.AdvancedRolesSettings.transform.Find("CategoryHeaderMasked").GetComponent(); @@ -465,7 +465,7 @@ public static class RoleSettingMenuPatches __instance.roleChances.Add(roleOptionSetting); roleOptionSetting.titleText.transform.localPosition = new Vector3(-0.5376f, -0.2923f, 0f); - roleOptionSetting.titleText.color = customRole.RoleColor.GetAlternateColor(); + roleOptionSetting.titleText.color = customRole.RoleColor.FindAlternateColor(); roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left; if (GameSettingMenuPatches.SelectedMod is null || @@ -483,7 +483,7 @@ public static class RoleSettingMenuPatches var passiveButton = newButton.GetComponent(); passiveButton.OnClick = new ButtonClickedEvent(); - passiveButton.interactableColor = btnRend.color = customRole.RoleColor.GetAlternateColor(); + passiveButton.interactableColor = btnRend.color = customRole.RoleColor.FindAlternateColor(); passiveButton.interactableHoveredColor = Color.white; passiveButton.OnClick.AddListener((UnityAction)(() => { ChangeTab(role, __instance); })); diff --git a/MiraAPI/Utilities/ColorExtensions.cs b/MiraAPI/Utilities/ColorExtensions.cs new file mode 100644 index 0000000..74c5a22 --- /dev/null +++ b/MiraAPI/Utilities/ColorExtensions.cs @@ -0,0 +1,89 @@ +using UnityEngine; + +namespace MiraAPI.Utilities; + +/// +/// Extension methods for the Color struct. +/// +public static class ColorExtensions +{ + /// + /// Gets the relative luminance of a color (WCAG 2.1 specification). + /// + /// Color to calculate luminance for. + /// Relative luminance value. + public static float GetRelativeLuminance(this Color color) + { + var r = color.r <= 0.03928f ? color.r / 12.92f : Mathf.Pow((color.r + 0.055f) / 1.055f, 2.4f); + var g = color.g <= 0.03928f ? color.g / 12.92f : Mathf.Pow((color.g + 0.055f) / 1.055f, 2.4f); + var b = color.b <= 0.03928f ? color.b / 12.92f : Mathf.Pow((color.b + 0.055f) / 1.055f, 2.4f); + return 0.2126f * r + 0.7152f * g + 0.0722f * b; + } + + /// + /// Calculates the contrast ratio between two colors (WCAG 2.1 specification). + /// + /// First color. + /// Second color. + /// Contrast ratio between the two colors. + public static float GetContrastRatio(this Color colorA, Color colorB) + { + var luminanceA = colorA.GetRelativeLuminance(); + var luminanceB = colorB.GetRelativeLuminance(); + return (Mathf.Max(luminanceA, luminanceB) + 0.05f) / (Mathf.Min(luminanceA, luminanceB) + 0.05f); + } + + /// + /// Generates an accessible alternate color with sufficient contrast. + /// + /// Base color. + /// Target contrast ratio (default 4.5f for AA). + /// Alternate color with sufficient contrast. + public static Color FindAlternateColor(this Color color, float desiredRatio = 4.5f) + { + var lightColor = FindMinimumContrastColor(color, Color.white, desiredRatio); + var darkColor = FindMinimumContrastColor(color, Color.black, desiredRatio); + + return lightColor.GetContrastRatio(color) >= darkColor.GetContrastRatio(color) ? lightColor : darkColor; + } + + /// + /// Finds the color with the minimum contrast ratio to the target color. + /// + /// The base color to start from. + /// The target color to compare against. + /// The desired contrast ratio. + /// The color with the minimum contrast ratio to the target color. + public static Color FindMinimumContrastColor(Color baseColor, Color targetColor, float desiredRatio) + { + var baseLuminance = baseColor.GetRelativeLuminance(); + var isLightening = targetColor == Color.white; + + var targetLuminance = isLightening + ? desiredRatio * (baseLuminance + 0.05f) - 0.05f + : (baseLuminance + 0.05f) / desiredRatio - 0.05f; + + if (isLightening && targetLuminance > 1f) return Color.white; + if (!isLightening && targetLuminance < 0f) return Color.black; + + float low = 0f, high = 1f; + var bestColor = targetColor; + for (var i = 0; i < 10; i++) + { + var t = (low + high) / 2f; + var testColor = Color.Lerp(baseColor, targetColor, t); + var testLuminance = testColor.GetRelativeLuminance(); + + if ((isLightening && testLuminance >= targetLuminance) || (!isLightening && testLuminance <= targetLuminance)) + { + high = t; + bestColor = testColor; + } + else + { + low = t; + } + } + return bestColor; + } +} diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index a85da1d..47a05f7 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -245,6 +245,7 @@ public static class Extensions /// The original color. /// The amount to darken or lighten the original color by between 0.0 and 1.0. /// An alternate color that has been darkened or lightened. + [Obsolete("Use FindAlternateColor for WACG compliance.")] public static Color GetAlternateColor(this Color color, float amount = 0.45f) { return color.IsColorDark() ? LightenColor(color, amount) : DarkenColor(color, amount); From 33fd657eb364029ef65d8f75ac2b66ba8ee9a2c9 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:48:04 -0400 Subject: [PATCH 10/38] add "OptionsMenuColor" to ICustomRole --- MiraAPI/Patches/Options/LobbyViewPanePatches.cs | 4 ++-- MiraAPI/Patches/Options/RoleSettingMenuPatches.cs | 10 +++++----- MiraAPI/Roles/ICustomRole.cs | 5 +++++ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index 5172ba0..89a6a24 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -330,7 +330,7 @@ public static class LobbyViewPanePatches numPerGame, chancePerGame, 61, - customRole.RoleColor, + customRole.OptionsMenuColor, customRole.Configuration.Icon.LoadAsset(), true); viewSettingsInfoPanelRoleVariant.iconSprite.transform.localScale = new Vector3(0.365f, 0.365f, 1f); @@ -340,7 +340,7 @@ public static class LobbyViewPanePatches viewSettingsInfoPanelRoleVariant.chanceTitle.color = viewSettingsInfoPanelRoleVariant.chanceBackground.color = viewSettingsInfoPanelRoleVariant.background.color = - customRole.RoleColor.FindAlternateColor(); + customRole.OptionsMenuColor.FindAlternateColor(); instance.settingsInfo.Add(viewSettingsInfoPanelRoleVariant.gameObject); num -= 0.664f; } diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index b21f387..011857d 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -383,8 +383,8 @@ public static class RoleSettingMenuPatches __instance.roleScreenshot.drawMode = SpriteDrawMode.Sliced; } - __instance.roleHeaderSprite.color = customRole.RoleColor; - __instance.roleHeaderText.color = customRole.RoleColor.FindAlternateColor(); + __instance.roleHeaderSprite.color = customRole.OptionsMenuColor; + __instance.roleHeaderText.color = customRole.OptionsMenuColor.FindAlternateColor(); var categoryHeaderMasked = __instance.AdvancedRolesSettings.transform.Find("CategoryHeaderMasked").GetComponent(); @@ -459,13 +459,13 @@ public static class RoleSettingMenuPatches roleOptionSetting.transform.localPosition = new Vector3(-0.1f, ScrollerNum, -2f); roleOptionSetting.SetRole(GameOptionsManager.Instance.CurrentGameOptions.RoleOptions, role, 20); - roleOptionSetting.labelSprite.color = customRole.RoleColor; + roleOptionSetting.labelSprite.color = customRole.OptionsMenuColor; roleOptionSetting.OnValueChanged = new Action(ValueChanged); roleOptionSetting.SetClickMask(__instance.ButtonClickMask); __instance.roleChances.Add(roleOptionSetting); roleOptionSetting.titleText.transform.localPosition = new Vector3(-0.5376f, -0.2923f, 0f); - roleOptionSetting.titleText.color = customRole.RoleColor.FindAlternateColor(); + roleOptionSetting.titleText.color = customRole.OptionsMenuColor.FindAlternateColor(); roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left; if (GameSettingMenuPatches.SelectedMod is null || @@ -483,7 +483,7 @@ public static class RoleSettingMenuPatches var passiveButton = newButton.GetComponent(); passiveButton.OnClick = new ButtonClickedEvent(); - passiveButton.interactableColor = btnRend.color = customRole.RoleColor.FindAlternateColor(); + passiveButton.interactableColor = btnRend.color = customRole.OptionsMenuColor.FindAlternateColor(); passiveButton.interactableHoveredColor = Color.white; passiveButton.OnClick.AddListener((UnityAction)(() => { ChangeTab(role, __instance); })); diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 27df3cd..a1c0e5d 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -34,6 +34,11 @@ public interface ICustomRole : IOptionable /// Color RoleColor { get; } + /// + /// Gets the color that should be used in the options menu. + /// + Color OptionsMenuColor => RoleColor; + /// /// Gets the team of the role. /// 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 11/38] 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 645d1dad1cf600d89385c55498936cb8970a2805 Mon Sep 17 00:00:00 2001 From: ang-xd Date: Wed, 12 Mar 2025 18:01:58 -0400 Subject: [PATCH 12/38] Broken thing --- MiraAPI/MiraApiPlugin.cs | 9 ++- MiraAPI/Patches/MainMenuManagerPatch.cs | 26 +++++++ .../Patches/Options/LobbyViewPanePatches.cs | 8 +-- .../Options/NotificationPopperPatch.cs | 36 +++++++++- MiraAPI/Roles/CustomRoleConfiguration.cs | 5 +- MiraAPI/Roles/CustomRoleManager.cs | 16 +++++ MiraAPI/Utilities/Helpers.cs | 67 +++++++++++++++++++ 7 files changed, 157 insertions(+), 10 deletions(-) create mode 100644 MiraAPI/Patches/MainMenuManagerPatch.cs diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index 3380820..fa601ce 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -1,12 +1,19 @@ -using BepInEx; +using System; +using BepInEx; using BepInEx.Unity.IL2CPP; using HarmonyLib; +using Il2CppInterop.Runtime; using MiraAPI.PluginLoading; +using MiraAPI.Roles; +using MonoMod.Utils; using Reactor; using Reactor.Networking; using Reactor.Networking.Attributes; using Reactor.Utilities; +using TMPro; using UnityEngine; +using UnityEngine.ProBuilder; +using Delegate = Il2CppSystem.Delegate; namespace MiraAPI; diff --git a/MiraAPI/Patches/MainMenuManagerPatch.cs b/MiraAPI/Patches/MainMenuManagerPatch.cs new file mode 100644 index 0000000..a54efe0 --- /dev/null +++ b/MiraAPI/Patches/MainMenuManagerPatch.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq; +using HarmonyLib; +using Il2CppInterop.Runtime; +using MiraAPI.Roles; +using MiraAPI.Utilities; +using TMPro; + +namespace MiraAPI.Patches; + +[HarmonyPatch(typeof(MainMenuManager))] + +public class MainMenuManagerPatch +{ + [HarmonyPostfix] + [HarmonyPatch(nameof(MainMenuManager.Start))] + public static void StartPatch() + { + TMP_Settings.defaultSpriteAsset.fallbackSpriteAssets.Add(CustomRoleManager.SpriteAsset); + /*TMP_Text.OnSpriteAssetRequest += DelegateSupport.ConvertDelegate>(new Func((index, str) => + { + var asset = Helpers.RegisteredSpriteAssets.First(x => x.name.Contains(str)); + return asset != null ? asset : null; + }));*/ + } +} diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index 89a6a24..c8e782a 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -330,8 +330,8 @@ public static class LobbyViewPanePatches numPerGame, chancePerGame, 61, - customRole.OptionsMenuColor, - customRole.Configuration.Icon.LoadAsset(), + customRole.RoleColor, + customRole.Configuration.Icon != null ? customRole.Configuration.Icon.LoadAsset() : MiraAssets.Empty.LoadAsset(), true); viewSettingsInfoPanelRoleVariant.iconSprite.transform.localScale = new Vector3(0.365f, 0.365f, 1f); viewSettingsInfoPanelRoleVariant.iconSprite.transform.localPosition = new Vector3(0.7144f, -0.028f, -2); @@ -340,7 +340,7 @@ public static class LobbyViewPanePatches viewSettingsInfoPanelRoleVariant.chanceTitle.color = viewSettingsInfoPanelRoleVariant.chanceBackground.color = viewSettingsInfoPanelRoleVariant.background.color = - customRole.OptionsMenuColor.FindAlternateColor(); + customRole.RoleColor.GetAlternateColor(); instance.settingsInfo.Add(viewSettingsInfoPanelRoleVariant.gameObject); num -= 0.664f; } @@ -418,7 +418,7 @@ public static class LobbyViewPanePatches role.StringName, maskLayer, role.TeamType == RoleTeamTypes.Crewmate, - customRole.Configuration.Icon.LoadAsset()); + customRole.Configuration.Icon != null ? customRole.Configuration.Icon.LoadAsset() : MiraAssets.Empty.LoadAsset()); viewPanel.header.icon.transform.localScale = new Vector3(0.465f, 0.465f, 1f); viewPanel.divider.material.SetInt(PlayerMaterial.MaskLayer, maskLayer); diff --git a/MiraAPI/Patches/Options/NotificationPopperPatch.cs b/MiraAPI/Patches/Options/NotificationPopperPatch.cs index 02efe2b..522bc2b 100644 --- a/MiraAPI/Patches/Options/NotificationPopperPatch.cs +++ b/MiraAPI/Patches/Options/NotificationPopperPatch.cs @@ -2,6 +2,10 @@ using HarmonyLib; using Il2CppSystem; using MiraAPI.Roles; +using Reactor.Utilities; +using UnityEngine; +using Action = System.Action; +using Object = Il2CppSystem.Object; namespace MiraAPI.Patches.Options; @@ -18,11 +22,14 @@ public static class NotificationPopperPatch [HarmonyArgument(3)] RoleTeamTypes teamType, [HarmonyArgument(4)] bool playSound) { - if (CustomRoleManager.CustomRoles.Values.FirstOrDefault(x=>x.StringName==key) is not ICustomRole customRole) + var role = CustomRoleManager.CustomRoles.Values.FirstOrDefault(x => x.StringName == key); + if (!role || role is not ICustomRole) { return true; } + Logger.Error("UPDATING ROLE TEXT"); + var text = teamType == RoleTeamTypes.Crewmate ? Palette.CrewmateSettingChangeText.ToTextColor() : Palette.ImpostorRed.ToTextColor(); @@ -30,6 +37,7 @@ public static class NotificationPopperPatch var item = TranslationController.Instance.GetString( StringNames.LobbyChangeSettingNotificationRole, string.Concat( + $"", "", text, TranslationController.Instance.GetString(key, Array.Empty()), @@ -39,7 +47,31 @@ public static class NotificationPopperPatch "" + roleChance + "%" ); - __instance.SettingsChangeMessageLogic(key, item, playSound); + CustomMessageLogic(__instance, role, item, playSound); return false; } + private static void CustomMessageLogic(NotificationPopper __instance, RoleBehaviour role, string item, bool playSound) + { + if (__instance.lastMessageKey == (int)role.StringName && __instance.activeMessages.Count > 0) + { + __instance.activeMessages.ToArray()[__instance.activeMessages.Count - 1].UpdateMessage(item); + } + else + { + __instance.lastMessageKey = (int)role.StringName; + var newMessage = UnityEngine.Object.Instantiate(__instance.notificationMessageOrigin, Vector3.zero, Quaternion.identity, __instance.transform); + newMessage.transform.localPosition = new Vector3(0f, 0f, -2f); + newMessage.SetUp(item, __instance.settingsChangeSprite, __instance.settingsChangeColor, new Action(delegate + { + __instance.OnMessageDestroy(newMessage); + })); + __instance.ShiftMessages(); + __instance.AddMessageToQueue(newMessage); + } + if (playSound) + { + SoundManager.Instance.PlaySoundImmediate(__instance.settingsChangeSound, false, 1f, 1f, null); + } + } } + diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index efaf444..85094a9 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -32,7 +32,6 @@ public record struct CustomRoleConfiguration TasksCountForProgress = role.Team is ModdedRoleTeams.Crewmate; HideSettings = roleBehaviour?.IsDead == true; ShowInFreeplay = roleBehaviour?.IsDead == false; - OptionsScreenshot = null; } /// @@ -58,12 +57,12 @@ public record struct CustomRoleConfiguration /// /// Gets or sets the Sprite used for the Role Options menu screenshot. /// - public LoadableAsset? OptionsScreenshot { get; set; } + public LoadableAsset? OptionsScreenshot { get; set; } = null; /// /// Gets or sets the Sprite used for the Role Icon. /// - public LoadableAsset Icon { get; set; } = MiraAssets.Empty; + public LoadableAsset? Icon { get; set; } = null; /// /// Gets or sets a value indicating whether the role is affected by light affectors on Airship. diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index a778a09..1484e26 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -13,6 +13,7 @@ using Reactor.Utilities; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; +using UnityEngine.TextCore; using Object = UnityEngine.Object; namespace MiraAPI.Roles; @@ -24,7 +25,9 @@ public static class CustomRoleManager { internal static readonly Dictionary CustomRoles = []; internal static readonly Dictionary RoleIds = []; + internal static TMP_SpriteAsset SpriteAsset; + private static Dictionary _roleIcons = []; private static ushort _roleId = 100; private static ushort GetNextRoleId() @@ -58,6 +61,8 @@ public static class CustomRoleManager pluginInfo.CustomRoles.Add((ushort)role.Role, role); } + + SpriteAsset = Helpers.CreateSpriteAsset(_roleIcons, "RoleIcons"); } private static RoleBehaviour? RegisterRole(Type roleType, MiraPluginInfo parentMod) @@ -93,6 +98,17 @@ public static class CustomRoleManager roleBehaviour.MaxCount = customRole.Configuration.MaxRoleCount; roleBehaviour.RoleScreenshot = customRole.Configuration.OptionsScreenshot?.LoadAsset(); + if (customRole.Configuration.Icon != null) + { + var asset = customRole.Configuration.Icon.LoadAsset(); + if (asset != null) + { + roleBehaviour.RoleIconSolid = asset; + roleBehaviour.RoleIconWhite = asset; + _roleIcons.Add(roleBehaviour.NiceName, asset); + } + } + if (roleBehaviour.IsDead) { RoleManager.GhostRoles.Add(roleBehaviour.Role); diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 6b3b79f..7e5e812 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -1,11 +1,15 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Text; +using HarmonyLib; using MiraAPI.Roles; +using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; +using UnityEngine.TextCore; using Object = UnityEngine.Object; namespace MiraAPI.Utilities; @@ -24,6 +28,69 @@ public static class Helpers return PlayerControl.AllPlayerControls.ToArray().Where(x => !x.Data.IsDead).ToList(); } + internal static readonly List RegisteredSpriteAssets = []; + + /// + /// Allows you to create a custom sprite asset. Normally used for TextMeshPro. + /// Ensure you set the TMP object's sprite asset to this one and use the sprite name tag. + /// https://docs.unity3d.com/Packages/com.unity.textmeshpro@4.0/manual/RichTextSprite.html + /// + /// The dictionary of sprites you want to add to the asset. String is the name of the sprite you'd like to use for rich text. + /// The name of the sprite asset. + /// The created sprite asset. + public static TMP_SpriteAsset CreateSpriteAsset(Dictionary sprites, string spriteAssetName) + { + var textures = sprites.Values.Select(x => x.texture).ToArray(); + var asset = ScriptableObject.CreateInstance(); + var image = new Texture2D(2048, 2048) { name = spriteAssetName }; + var rects = image.PackTextures(textures, 2); + + for (var i = 0; i < rects.Length; i++) + { + var rect = rects[i]; + var pair = sprites.ElementAt(i); + + var glyph = new TMP_SpriteGlyph + { + glyphRect = new GlyphRect + { + x = (int)(rect.x * image.width), + y = (int)(rect.y * image.height), + width = (int)(rect.width * image.width), + height = (int)(rect.height * image.height), + }, + metrics = new GlyphMetrics + { + width = (int)(rect.width * image.width), + height = (int)(rect.height * image.height), + horizontalBearingY = (int)(rect.height * image.height) * 0.75f, + horizontalBearingX = 0, + horizontalAdvance = (int)(rect.width * image.width), + }, + index = (uint)i, + sprite = pair.Value, + }; + + var character = new TMP_SpriteCharacter(0, asset, glyph) + { + name = pair.Key, + glyphIndex = (uint)i, + }; + + asset.spriteGlyphTable.Add(glyph); + asset.spriteCharacterTable.Add(character); + } + + asset.name = spriteAssetName; + asset.hashCode = TMP_TextUtilities.GetSimpleHashCode(asset.name); + asset.material = new Material(Shader.Find("TextMeshPro/Sprite")); + AccessTools.Property(asset.GetType(), "version").SetValue(asset, "1.1.0"); + asset.material.mainTexture = asset.spriteSheet = image; + asset.UpdateLookupTables(); + RegisteredSpriteAssets.Add(asset); + return asset.DontDestroyOnLoad().DontUnload(); + } + /// /// Returns an empty coroutine. /// From ab2f03ee4dee948b62a6ff12669c0ee9eed120b2 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 12 Mar 2025 20:13:20 -0400 Subject: [PATCH 13/38] still broken --- MiraAPI/Patches/MainMenuManagerPatch.cs | 8 +--- .../Patches/Options/LobbyViewPanePatches.cs | 2 +- .../Options/NotificationPopperPatch.cs | 4 +- MiraAPI/Roles/CustomRoleManager.cs | 4 +- MiraAPI/Utilities/Assets/SpriteTools.cs | 5 ++- MiraAPI/Utilities/Helpers.cs | 40 +++++++++++++++---- 6 files changed, 43 insertions(+), 20 deletions(-) diff --git a/MiraAPI/Patches/MainMenuManagerPatch.cs b/MiraAPI/Patches/MainMenuManagerPatch.cs index a54efe0..126e1a0 100644 --- a/MiraAPI/Patches/MainMenuManagerPatch.cs +++ b/MiraAPI/Patches/MainMenuManagerPatch.cs @@ -1,9 +1,5 @@ -using System; -using System.Linq; -using HarmonyLib; -using Il2CppInterop.Runtime; +using HarmonyLib; using MiraAPI.Roles; -using MiraAPI.Utilities; using TMPro; namespace MiraAPI.Patches; @@ -16,7 +12,7 @@ public class MainMenuManagerPatch [HarmonyPatch(nameof(MainMenuManager.Start))] public static void StartPatch() { - TMP_Settings.defaultSpriteAsset.fallbackSpriteAssets.Add(CustomRoleManager.SpriteAsset); + TMP_Settings.instance.m_defaultSpriteAsset = CustomRoleManager.SpriteAsset; /*TMP_Text.OnSpriteAssetRequest += DelegateSupport.ConvertDelegate>(new Func((index, str) => { var asset = Helpers.RegisteredSpriteAssets.First(x => x.name.Contains(str)); diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index c8e782a..43c5719 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -331,7 +331,7 @@ public static class LobbyViewPanePatches chancePerGame, 61, customRole.RoleColor, - customRole.Configuration.Icon != null ? customRole.Configuration.Icon.LoadAsset() : MiraAssets.Empty.LoadAsset(), + customRole.Configuration.Icon?.LoadAsset() ?? MiraAssets.Empty.LoadAsset(), true); viewSettingsInfoPanelRoleVariant.iconSprite.transform.localScale = new Vector3(0.365f, 0.365f, 1f); viewSettingsInfoPanelRoleVariant.iconSprite.transform.localPosition = new Vector3(0.7144f, -0.028f, -2); diff --git a/MiraAPI/Patches/Options/NotificationPopperPatch.cs b/MiraAPI/Patches/Options/NotificationPopperPatch.cs index 522bc2b..78a8d38 100644 --- a/MiraAPI/Patches/Options/NotificationPopperPatch.cs +++ b/MiraAPI/Patches/Options/NotificationPopperPatch.cs @@ -23,7 +23,7 @@ public static class NotificationPopperPatch [HarmonyArgument(4)] bool playSound) { var role = CustomRoleManager.CustomRoles.Values.FirstOrDefault(x => x.StringName == key); - if (!role || role is not ICustomRole) + if (!role || role is not ICustomRole customRole) { return true; } @@ -37,7 +37,7 @@ public static class NotificationPopperPatch var item = TranslationController.Instance.GetString( StringNames.LobbyChangeSettingNotificationRole, string.Concat( - $"", + $"", "", text, TranslationController.Instance.GetString(key, Array.Empty()), diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index 1484e26..fa49d65 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -62,7 +62,7 @@ public static class CustomRoleManager pluginInfo.CustomRoles.Add((ushort)role.Role, role); } - SpriteAsset = Helpers.CreateSpriteAsset(_roleIcons, "RoleIcons"); + SpriteAsset = Helpers.CreateSpriteAsset(_roleIcons, "CustomRoleIcons"); } private static RoleBehaviour? RegisterRole(Type roleType, MiraPluginInfo parentMod) @@ -105,7 +105,7 @@ public static class CustomRoleManager { roleBehaviour.RoleIconSolid = asset; roleBehaviour.RoleIconWhite = asset; - _roleIcons.Add(roleBehaviour.NiceName, asset); + _roleIcons.Add(customRole.RoleName, asset); } } diff --git a/MiraAPI/Utilities/Assets/SpriteTools.cs b/MiraAPI/Utilities/Assets/SpriteTools.cs index fa49f12..5d438de 100644 --- a/MiraAPI/Utilities/Assets/SpriteTools.cs +++ b/MiraAPI/Utilities/Assets/SpriteTools.cs @@ -30,6 +30,9 @@ public static class SpriteTools throw new ArgumentException($"Resource not found: {resourcePath}"); } - return Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), pixelsPerUnit); + tex.name = resourcePath; + var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), pixelsPerUnit); + sprite.name = resourcePath; + return sprite; } } diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 7e5e812..0c65c90 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using HarmonyLib; using MiraAPI.Roles; +using Reactor.Utilities; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; @@ -30,6 +31,23 @@ public static class Helpers internal static readonly List RegisteredSpriteAssets = []; + public static void Dump(this Texture2D texture, string path) => File.WriteAllBytes(path, texture.Decompress().EncodeToPNG()); + + public static Texture2D Decompress(this Texture2D source) + { + var renderTex = RenderTexture.GetTemporary(source.width, source.height, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Linear); + Graphics.Blit(source, renderTex); + var previous = RenderTexture.active; + RenderTexture.active = renderTex; + var readableText = new Texture2D(source.width, source.height); + readableText.ReadPixels(new(0, 0, renderTex.width, renderTex.height), 0, 0); + readableText.Apply(); + RenderTexture.active = previous; + RenderTexture.ReleaseTemporary(renderTex); + readableText.name = source.name; + return readableText; + } + /// /// Allows you to create a custom sprite asset. Normally used for TextMeshPro. /// Ensure you set the TMP object's sprite asset to this one and use the sprite name tag. @@ -44,11 +62,15 @@ public static class Helpers var asset = ScriptableObject.CreateInstance(); var image = new Texture2D(2048, 2048) { name = spriteAssetName }; var rects = image.PackTextures(textures, 2); + Dump(image, $"{spriteAssetName}.png"); for (var i = 0; i < rects.Length; i++) { var rect = rects[i]; var pair = sprites.ElementAt(i); + var sprite = pair.Value; + var spriteRect = sprite.rect; + var pivot = sprite.pivot; var glyph = new TMP_SpriteGlyph { @@ -61,17 +83,19 @@ public static class Helpers }, metrics = new GlyphMetrics { - width = (int)(rect.width * image.width), - height = (int)(rect.height * image.height), - horizontalBearingY = (int)(rect.height * image.height) * 0.75f, - horizontalBearingX = 0, - horizontalAdvance = (int)(rect.width * image.width), + width = spriteRect.width, + height = spriteRect.height, + horizontalBearingX = pivot.x, + horizontalBearingY = spriteRect.height - pivot.y, // Top of the sprite relative to baseline + horizontalAdvance = spriteRect.width, }, index = (uint)i, - sprite = pair.Value, + sprite = sprite, }; - var character = new TMP_SpriteCharacter(0, asset, glyph) + // Assign unique Unicode from the Private Use Area + var unicode = 0xE000 + (uint)i; + var character = new TMP_SpriteCharacter(unicode, asset, glyph) { name = pair.Key, glyphIndex = (uint)i, @@ -84,7 +108,7 @@ public static class Helpers asset.name = spriteAssetName; asset.hashCode = TMP_TextUtilities.GetSimpleHashCode(asset.name); asset.material = new Material(Shader.Find("TextMeshPro/Sprite")); - AccessTools.Property(asset.GetType(), "version").SetValue(asset, "1.1.0"); + asset.version = "1.1.0"; asset.material.mainTexture = asset.spriteSheet = image; asset.UpdateLookupTables(); RegisteredSpriteAssets.Add(asset); 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 14/38] 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 15/38] 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 16/38] 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 17/38] 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 18/38] 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 19/38] 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 bc7b835dc8f1277fe8c7ecd46a4a959b4a32521e Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sat, 15 Mar 2025 15:04:57 -0400 Subject: [PATCH 20/38] Admit defeat to custom sprite assets --- MiraAPI/Patches/MainMenuManagerPatch.cs | 22 ----- .../Options/NotificationPopperPatch.cs | 36 +------- MiraAPI/Roles/CustomRoleManager.cs | 5 -- MiraAPI/Utilities/Helpers.cs | 86 ------------------- 4 files changed, 2 insertions(+), 147 deletions(-) delete mode 100644 MiraAPI/Patches/MainMenuManagerPatch.cs diff --git a/MiraAPI/Patches/MainMenuManagerPatch.cs b/MiraAPI/Patches/MainMenuManagerPatch.cs deleted file mode 100644 index 126e1a0..0000000 --- a/MiraAPI/Patches/MainMenuManagerPatch.cs +++ /dev/null @@ -1,22 +0,0 @@ -using HarmonyLib; -using MiraAPI.Roles; -using TMPro; - -namespace MiraAPI.Patches; - -[HarmonyPatch(typeof(MainMenuManager))] - -public class MainMenuManagerPatch -{ - [HarmonyPostfix] - [HarmonyPatch(nameof(MainMenuManager.Start))] - public static void StartPatch() - { - TMP_Settings.instance.m_defaultSpriteAsset = CustomRoleManager.SpriteAsset; - /*TMP_Text.OnSpriteAssetRequest += DelegateSupport.ConvertDelegate>(new Func((index, str) => - { - var asset = Helpers.RegisteredSpriteAssets.First(x => x.name.Contains(str)); - return asset != null ? asset : null; - }));*/ - } -} diff --git a/MiraAPI/Patches/Options/NotificationPopperPatch.cs b/MiraAPI/Patches/Options/NotificationPopperPatch.cs index 78a8d38..02efe2b 100644 --- a/MiraAPI/Patches/Options/NotificationPopperPatch.cs +++ b/MiraAPI/Patches/Options/NotificationPopperPatch.cs @@ -2,10 +2,6 @@ using HarmonyLib; using Il2CppSystem; using MiraAPI.Roles; -using Reactor.Utilities; -using UnityEngine; -using Action = System.Action; -using Object = Il2CppSystem.Object; namespace MiraAPI.Patches.Options; @@ -22,14 +18,11 @@ public static class NotificationPopperPatch [HarmonyArgument(3)] RoleTeamTypes teamType, [HarmonyArgument(4)] bool playSound) { - var role = CustomRoleManager.CustomRoles.Values.FirstOrDefault(x => x.StringName == key); - if (!role || role is not ICustomRole customRole) + if (CustomRoleManager.CustomRoles.Values.FirstOrDefault(x=>x.StringName==key) is not ICustomRole customRole) { return true; } - Logger.Error("UPDATING ROLE TEXT"); - var text = teamType == RoleTeamTypes.Crewmate ? Palette.CrewmateSettingChangeText.ToTextColor() : Palette.ImpostorRed.ToTextColor(); @@ -37,7 +30,6 @@ public static class NotificationPopperPatch var item = TranslationController.Instance.GetString( StringNames.LobbyChangeSettingNotificationRole, string.Concat( - $"", "", text, TranslationController.Instance.GetString(key, Array.Empty()), @@ -47,31 +39,7 @@ public static class NotificationPopperPatch "" + roleChance + "%" ); - CustomMessageLogic(__instance, role, item, playSound); + __instance.SettingsChangeMessageLogic(key, item, playSound); return false; } - private static void CustomMessageLogic(NotificationPopper __instance, RoleBehaviour role, string item, bool playSound) - { - if (__instance.lastMessageKey == (int)role.StringName && __instance.activeMessages.Count > 0) - { - __instance.activeMessages.ToArray()[__instance.activeMessages.Count - 1].UpdateMessage(item); - } - else - { - __instance.lastMessageKey = (int)role.StringName; - var newMessage = UnityEngine.Object.Instantiate(__instance.notificationMessageOrigin, Vector3.zero, Quaternion.identity, __instance.transform); - newMessage.transform.localPosition = new Vector3(0f, 0f, -2f); - newMessage.SetUp(item, __instance.settingsChangeSprite, __instance.settingsChangeColor, new Action(delegate - { - __instance.OnMessageDestroy(newMessage); - })); - __instance.ShiftMessages(); - __instance.AddMessageToQueue(newMessage); - } - if (playSound) - { - SoundManager.Instance.PlaySoundImmediate(__instance.settingsChangeSound, false, 1f, 1f, null); - } - } } - diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index fa49d65..5eba0de 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -25,9 +25,7 @@ public static class CustomRoleManager { internal static readonly Dictionary CustomRoles = []; internal static readonly Dictionary RoleIds = []; - internal static TMP_SpriteAsset SpriteAsset; - private static Dictionary _roleIcons = []; private static ushort _roleId = 100; private static ushort GetNextRoleId() @@ -61,8 +59,6 @@ public static class CustomRoleManager pluginInfo.CustomRoles.Add((ushort)role.Role, role); } - - SpriteAsset = Helpers.CreateSpriteAsset(_roleIcons, "CustomRoleIcons"); } private static RoleBehaviour? RegisterRole(Type roleType, MiraPluginInfo parentMod) @@ -105,7 +101,6 @@ public static class CustomRoleManager { roleBehaviour.RoleIconSolid = asset; roleBehaviour.RoleIconWhite = asset; - _roleIcons.Add(customRole.RoleName, asset); } } diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 0c65c90..ab12bb8 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -29,92 +29,6 @@ public static class Helpers return PlayerControl.AllPlayerControls.ToArray().Where(x => !x.Data.IsDead).ToList(); } - internal static readonly List RegisteredSpriteAssets = []; - - public static void Dump(this Texture2D texture, string path) => File.WriteAllBytes(path, texture.Decompress().EncodeToPNG()); - - public static Texture2D Decompress(this Texture2D source) - { - var renderTex = RenderTexture.GetTemporary(source.width, source.height, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Linear); - Graphics.Blit(source, renderTex); - var previous = RenderTexture.active; - RenderTexture.active = renderTex; - var readableText = new Texture2D(source.width, source.height); - readableText.ReadPixels(new(0, 0, renderTex.width, renderTex.height), 0, 0); - readableText.Apply(); - RenderTexture.active = previous; - RenderTexture.ReleaseTemporary(renderTex); - readableText.name = source.name; - return readableText; - } - - /// - /// Allows you to create a custom sprite asset. Normally used for TextMeshPro. - /// Ensure you set the TMP object's sprite asset to this one and use the sprite name tag. - /// https://docs.unity3d.com/Packages/com.unity.textmeshpro@4.0/manual/RichTextSprite.html - /// - /// The dictionary of sprites you want to add to the asset. String is the name of the sprite you'd like to use for rich text. - /// The name of the sprite asset. - /// The created sprite asset. - public static TMP_SpriteAsset CreateSpriteAsset(Dictionary sprites, string spriteAssetName) - { - var textures = sprites.Values.Select(x => x.texture).ToArray(); - var asset = ScriptableObject.CreateInstance(); - var image = new Texture2D(2048, 2048) { name = spriteAssetName }; - var rects = image.PackTextures(textures, 2); - Dump(image, $"{spriteAssetName}.png"); - - for (var i = 0; i < rects.Length; i++) - { - var rect = rects[i]; - var pair = sprites.ElementAt(i); - var sprite = pair.Value; - var spriteRect = sprite.rect; - var pivot = sprite.pivot; - - var glyph = new TMP_SpriteGlyph - { - glyphRect = new GlyphRect - { - x = (int)(rect.x * image.width), - y = (int)(rect.y * image.height), - width = (int)(rect.width * image.width), - height = (int)(rect.height * image.height), - }, - metrics = new GlyphMetrics - { - width = spriteRect.width, - height = spriteRect.height, - horizontalBearingX = pivot.x, - horizontalBearingY = spriteRect.height - pivot.y, // Top of the sprite relative to baseline - horizontalAdvance = spriteRect.width, - }, - index = (uint)i, - sprite = sprite, - }; - - // Assign unique Unicode from the Private Use Area - var unicode = 0xE000 + (uint)i; - var character = new TMP_SpriteCharacter(unicode, asset, glyph) - { - name = pair.Key, - glyphIndex = (uint)i, - }; - - asset.spriteGlyphTable.Add(glyph); - asset.spriteCharacterTable.Add(character); - } - - asset.name = spriteAssetName; - asset.hashCode = TMP_TextUtilities.GetSimpleHashCode(asset.name); - asset.material = new Material(Shader.Find("TextMeshPro/Sprite")); - asset.version = "1.1.0"; - asset.material.mainTexture = asset.spriteSheet = image; - asset.UpdateLookupTables(); - RegisteredSpriteAssets.Add(asset); - return asset.DontDestroyOnLoad().DontUnload(); - } - /// /// Returns an empty coroutine. /// 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 21/38] 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) From ce9864ae8b3ef189ebba07b3616e8a839e5082b0 Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sat, 15 Mar 2025 17:21:26 -0400 Subject: [PATCH 22/38] Lots of new Util methods, and fix opt header colors --- MiraAPI/GameOptions/AbstractOptionGroup.cs | 2 +- MiraAPI/Hud/CustomActionButton.cs | 5 +- MiraAPI/MiraApiPlugin.cs | 1 + MiraAPI/Modifiers/ModifierExtensions.cs | 24 +++ MiraAPI/Modifiers/ModifierUtils.cs | 33 ++++ .../Patches/Options/GameOptionsMenuPatch.cs | 25 ++- .../Patches/Options/RoleSettingMenuPatches.cs | 38 ++--- MiraAPI/Patches/Stubs/MinigameStubs.cs | 11 ++ MiraAPI/Resources/CategoryHeader.png | Bin 0 -> 1221 bytes MiraAPI/Roles/CustomRoleConfiguration.cs | 7 + MiraAPI/Roles/CustomRoleManager.cs | 5 + MiraAPI/Roles/CustomRoleUtils.cs | 49 ++++++ MiraAPI/Roles/ICustomRole.cs | 2 +- MiraAPI/Utilities/Assets/MiraAssets.cs | 1 + MiraAPI/Utilities/Assets/SpriteTools.cs | 17 +++ MiraAPI/Utilities/Extensions.cs | 36 +++++ MiraAPI/Utilities/Helpers.cs | 144 ++++++++++++++++-- 17 files changed, 355 insertions(+), 45 deletions(-) create mode 100644 MiraAPI/Modifiers/ModifierUtils.cs create mode 100644 MiraAPI/Resources/CategoryHeader.png create mode 100644 MiraAPI/Roles/CustomRoleUtils.cs diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs index 45f9bd4..db2b93c 100644 --- a/MiraAPI/GameOptions/AbstractOptionGroup.cs +++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs @@ -35,7 +35,7 @@ public abstract class AbstractOptionGroup /// /// Gets the group color. This is used to color the group in the options menu. /// - public virtual Color GroupColor => Color.clear; + public virtual Color GroupColor => MiraApiPlugin.DefaultHeaderColor; /// /// Gets the group priority. This is used to determine the order in which groups are displayed in the options menu. diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 8e5815c..6a721da 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -168,7 +168,7 @@ public abstract class CustomActionButton /// Allows you to change the button's location. /// /// The new location. - /// Whether the button's position should change ingame. + /// Whether the button's position should change in-game. public virtual void SetButtonLocation(ButtonLocation location, bool moveButton = true) { if (!HudManager.InstanceExists || Button == null) @@ -221,7 +221,6 @@ public abstract class CustomActionButton /// A utility function to change the outline color of the button's text. /// /// The new color. - /// The thickness of the outline. Set to 0 if you want to remove it. public virtual void SetTextOutline(Color color) { Button?.buttonLabelText.SetOutlineColor(color); @@ -364,7 +363,7 @@ public abstract class CustomActionButton /// A value that represents whether the button should light up or not. public virtual bool CanUse() { - return !LimitedUses || UsesLeft > 0; + return PlayerControl.LocalPlayer.moveable && (!LimitedUses || UsesLeft > 0); } /// diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index fa601ce..46f0a16 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -27,6 +27,7 @@ namespace MiraAPI; public partial class MiraApiPlugin : BasePlugin { public static Color MiraColor { get; } = new Color32(238, 154, 112, 255); + public static Color DefaultHeaderColor = new Color32(77, 77, 77, 255); private static MiraPluginManager? PluginManager { get; set; } internal Harmony Harmony { get; } = new(Id); diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs index 06d9f06..ca49808 100644 --- a/MiraAPI/Modifiers/ModifierExtensions.cs +++ b/MiraAPI/Modifiers/ModifierExtensions.cs @@ -180,6 +180,30 @@ public static class ModifierExtensions return player.GetModifierComponent().HasModifier(typeId, predicate); } + /// + /// Clears all modifiers from a player. + /// + /// The player you want to clear modifiers for. + public static void ClearModifiers(this PlayerControl plr) + { + foreach (var mod in plr.GetModifierComponent().ActiveModifiers) + { + plr.RemoveModifier(mod); + } + } + + /// + /// Clears all modifiers from a player. + /// + /// The player you want to clear modifiers for. + public static void RpcClearModifiers(this PlayerControl plr) + { + foreach (var mod in plr.GetModifierComponent().ActiveModifiers) + { + plr.RpcRemoveModifier(mod.UniqueId); + } + } + /// /// Checks if the player has a specific modifier by its GUID. /// diff --git a/MiraAPI/Modifiers/ModifierUtils.cs b/MiraAPI/Modifiers/ModifierUtils.cs new file mode 100644 index 0000000..b69e7bc --- /dev/null +++ b/MiraAPI/Modifiers/ModifierUtils.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiraAPI.Modifiers; + +/// +/// Utilities to make handling modifiers in-game easier. +/// +public static class ModifierUtils +{ + /// + /// Gets a list of all active in-game modifiers. + /// + /// Select if modifier is valid to be added to list. + /// The modifier type. + /// A list of modifiers. + public static IEnumerable GetActiveModifiers(Func? predicate = null) where T : BaseModifier + { + return PlayerControl.AllPlayerControls.ToArray().SelectMany(x => x.GetModifiers(predicate)).OfType(); + } + + /// + /// Gets all players with a certain modifier. + /// + /// Select if modifier is valid. + /// The modifier type. + /// A list of players with that modifier. + public static IEnumerable GetPlayersWithModifier(Func? predicate = null) where T : BaseModifier + { + return PlayerControl.AllPlayerControls.ToArray().Where(x => x.HasModifier(predicate)); + } +} diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index b792767..05de7f6 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -42,7 +42,7 @@ public static class GameOptionsMenuPatch { var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups - .Where(x => x.OptionableType == null) ?? []; + .Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? []; foreach (var group in filteredGroups) { @@ -144,7 +144,7 @@ public static class GameOptionsMenuPatch } var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups - .Where(x => x.OptionableType == null) ?? []; + .Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? []; foreach (var group in filteredGroups) { @@ -198,16 +198,16 @@ public static class GameOptionsMenuPatch menu.settingsContainer); categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister(group.GroupName), 20); - if (group.GroupColor != Color.clear) - { - categoryHeaderMasked.Background.color = group.GroupColor; - categoryHeaderMasked.Divider.color = group.GroupColor; - categoryHeaderMasked.Title.color = group.GroupColor.FindAlternateColor(); - } + categoryHeaderMasked.Background.color = group.GroupColor; + categoryHeaderMasked.Divider.color = group.GroupColor; + categoryHeaderMasked.Title.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) ? Color.white : group.GroupColor.FindAlternateColor(); + categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset(); + categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.55f, -0.1833f, 0); categoryHeaderMasked.Background.size = new Vector2( categoryHeaderMasked.Background.size.x + 1.5f, categoryHeaderMasked.Background.size.y); + categoryHeaderMasked.gameObject.SetActive(false); group.Header = categoryHeaderMasked; @@ -230,7 +230,7 @@ public static class GameOptionsMenuPatch SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true); foreach (var renderer in componentsInChildren) { - if (group.GroupColor != Color.clear) + if (group.GroupColor != MiraApiPlugin.DefaultHeaderColor) { renderer.color = group.GroupColor.FindAlternateColor(); if (renderer.transform.parent.TryGetComponent(out var btn)) @@ -245,7 +245,7 @@ public static class GameOptionsMenuPatch foreach (var textMeshPro in newOpt.GetComponentsInChildren(true)) { - if (group.GroupColor != Color.clear) + if (group.GroupColor != MiraApiPlugin.DefaultHeaderColor) { textMeshPro.color = group.GroupColor; } @@ -257,12 +257,11 @@ public static class GameOptionsMenuPatch if (newOpt is ToggleOption toggle) { toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset(); - toggle.CheckMark.color = - group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal; + toggle.CheckMark.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor ? group.GroupColor : MiraAssets.AcceptedTeal; var rend = toggle.CheckMark.transform.parent.FindChild("ActiveSprite") .GetComponent(); rend.sprite = MiraAssets.CheckmarkBox.LoadAsset(); - rend.color = group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal; + rend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor ? group.GroupColor : MiraAssets.AcceptedTeal; } menu.Children.Add(newOpt); diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 011857d..e4984fd 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -127,32 +127,34 @@ public static class RoleSettingMenuPatches chanceLabel.Destroy(); countLabel.Destroy(); - if (name is StringNames.CrewmateRolesHeader) - { - categoryHeaderMasked.Title.color = Palette.CrewmateRoleHeaderTextBlue; - categoryHeaderMasked.Background.color = Palette.CrewmateRoleHeaderBlue; - } - else if (name is StringNames.ImpostorRolesHeader) - { - categoryHeaderMasked.Title.color = Palette.ImpostorRoleHeaderTextRed; - categoryHeaderMasked.Background.color = Palette.ImpostorRoleHeaderRed; - } - else - { - categoryHeaderMasked.Title.color = group.Color; - categoryHeaderMasked.Divider.color = group.Color; - categoryHeaderMasked.Background.color = group.Color; - } + categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset(); + categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.55f, -0.1833f, 0); - categoryHeaderMasked.Title.color = categoryHeaderMasked.Title.color.IsColorDark() ? - categoryHeaderMasked.Title.color.LightenColor(0.3f) : categoryHeaderMasked.Title.color.LightenColor(0.1f); + switch (name) + { + case StringNames.CrewmateRolesHeader: + categoryHeaderMasked.Title.color = Palette.CrewmateRoleHeaderTextBlue; + categoryHeaderMasked.Background.color = Palette.CrewmateRoleHeaderBlue; + break; + case StringNames.ImpostorRolesHeader: + categoryHeaderMasked.Title.color = Palette.ImpostorRoleHeaderTextRed; + categoryHeaderMasked.Background.color = Palette.ImpostorRoleHeaderRed; + break; + default: + categoryHeaderMasked.Title.color = group.Color.Equals(MiraApiPlugin.DefaultHeaderColor) ? Color.white : group.Color.FindAlternateColor(); + categoryHeaderMasked.Divider.color = group.Color; + categoryHeaderMasked.Background.color = group.Color; + break; + } categoryHeaderMasked.Title.fontStyle = __instance.categoryHeaderEditRoleOrigin.Title.fontStyle; categoryHeaderMasked.Title.font = __instance.categoryHeaderEditRoleOrigin.Title.font; categoryHeaderMasked.Title.fontMaterial = __instance.categoryHeaderEditRoleOrigin.Title.fontMaterial; categoryHeaderMasked.Divider.color = categoryHeaderMasked.Background.color; + categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.55f, -0.1833f, 0); categoryHeaderMasked.Background.size = new Vector2(categoryHeaderMasked.Background.size.x + 1.5f, categoryHeaderMasked.Background.size.y); + categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; categoryHeaderMasked.transform.localPosition = new Vector3(-0.44f, ScrollerNum, -2f); categoryHeaderMasked.gameObject.SetActive(true); diff --git a/MiraAPI/Patches/Stubs/MinigameStubs.cs b/MiraAPI/Patches/Stubs/MinigameStubs.cs index 3818d09..11e085e 100644 --- a/MiraAPI/Patches/Stubs/MinigameStubs.cs +++ b/MiraAPI/Patches/Stubs/MinigameStubs.cs @@ -33,4 +33,15 @@ public static class MinigameStubs { // nothing needed } + + /// + /// Fixes the error when creating custom minigames. Credit to Submerged for discovering this fix. + /// + /// The Minigame instance. + [HarmonyPrefix] + [HarmonyPatch(typeof(Minigame), nameof(Minigame.Begin))] + public static void MinigameErrorFixPatch(Minigame __instance) + { + __instance.logger ??= new Logger("Minigame", Logger.Level.Info, Logger.Category.Gameplay); + } } diff --git a/MiraAPI/Resources/CategoryHeader.png b/MiraAPI/Resources/CategoryHeader.png new file mode 100644 index 0000000000000000000000000000000000000000..3d2a4bfaf12c74008a375e85c21720685b4bfaae GIT binary patch literal 1221 zcmeAS@N?(olHy`uVBq!ia0y~yV2S{;BRSZBylDJ>JydH zPiC^)b32sCcJq;cKPCscIv zkG9**|6^{uJDoT``Do=|);g=_d#|rOm|(!cz_2OOlbwM<$1Q=8Aw_Ki1A|fa&4Ww~ zCl=p+J1;ISZk{EGv*h~gzkBxY|G)77kn`l)+Gu^HM=>A`mtL0mDLtAA683=`APn@o ze2~(F^78MVUn5a8aBH3Zbo~|QApxM~?&#Rq z+8A3$2_|lagu>OUSO5L>AH-z*Tem-YzZ?(Hpbay+ySv}NzTOP7L2UEx-M>xda_TTN zoM91Ta7Y6w_DhYFTgv?oB>!yt)~#O+Vhi};E&w_9OyZq?p&URxKYMQP=YmVEF_qw9 zdnv%s@NwO`b^kK9-+nLdT3Az2F{AD+3((lG>C>m%_d+xn?B2R{>*bO#b)X$-TlVh# zYcuyMsw3DY-+K@z2=tQij4y28yHLVODr-}{DA1(~cB=gj2L}`=;AVmhV4L+Wz7?oA zW7$iH6aT=y)wM}yzUf`Cdktb&U;VWUs^X=Yxq1AO&7N?DO_BHNLqS#x@2UIyt3C4D zdMBW#eXapx?9xpWpuGylKtCHCvw>KCZ1&Ud?62XvTVB6@J^gbGe;d#h5^L|jx1aaC z$^a;PAmj6%c?*?*9v1p+XBz=Bzq7ZuSO585aI}2bwn?WyEG%rF*YnL8yll;mRxAt= zN%KWqKPt#BTzep}tIr1a?W_idNg=K&)+;iXU4Y6;F3K*paE=lWcsuvbyfBbFjvbPbw z${QFOmImzS%Ml6_1*JL(JJ}ygZf*7|U*qN)Iu{t;9UY*+|6BLJaQS7+mHHcw7aY%u z3IV08gv_OZq5IilN|tAZ0b}sT{mQSeuFikHfBye%-_G6Ax3;!E-}bn9Z_@jUTQ8@8 zV(#IaH*db2`#1Nu;K#eb&~VdR)Bic1{o4KK@f+?g4nBJkSoAP>y85}Sb4q9e0AN<~ AcK`qY literal 0 HcmV?d00001 diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index 85094a9..cf714fd 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -1,6 +1,8 @@ using AmongUs.GameOptions; +using Il2CppSystem.Web.Util; using MiraAPI.Utilities.Assets; using UnityEngine; +using Helpers = MiraAPI.Utilities.Helpers; namespace MiraAPI.Roles; @@ -64,6 +66,11 @@ public record struct CustomRoleConfiguration /// public LoadableAsset? Icon { get; set; } = null; + /// + /// Gets or sets the Intro sound for the Role. + /// + public LoadableAsset? IntroSound { get; set; } = null; + /// /// Gets or sets a value indicating whether the role is affected by light affectors on Airship. /// diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index 5eba0de..0c2c4ad 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -104,6 +104,11 @@ public static class CustomRoleManager } } + if (customRole.Configuration.IntroSound != null) + { + roleBehaviour.IntroSound = customRole.Configuration.IntroSound.LoadAsset(); + } + if (roleBehaviour.IsDead) { RoleManager.GhostRoles.Add(roleBehaviour.Role); diff --git a/MiraAPI/Roles/CustomRoleUtils.cs b/MiraAPI/Roles/CustomRoleUtils.cs new file mode 100644 index 0000000..8f2432d --- /dev/null +++ b/MiraAPI/Roles/CustomRoleUtils.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using AmongUs.GameOptions; +using UnityEngine; + +namespace MiraAPI.Roles; + +/// +/// Utilities to make handling roles in-game easier. +/// +public static class CustomRoleUtils +{ + /// + /// Gets all active in-game roles. + /// + /// A list of roles. + // ReSharper disable once MemberCanBePrivate.Global + public static IEnumerable GetActiveRoles() => PlayerControl.AllPlayerControls.ToArray().Select(x => x.Data.Role); + + /// + /// Gets all active in-game roles in a certain team. + /// + /// The team you would like to check for. + /// A list of roles with the team. + public static IEnumerable GetActiveRolesOfTeam(ModdedRoleTeams team) => GetActiveRoles().Where(x => x is ICustomRole customRole && customRole.Team == team); + + /// + /// Gets all active in-game roles of a certain type. + /// + /// The role Type you would like to check for. Must be a RoleBehaviour. + /// A list of roles with that specific type. + public static IEnumerable GetActiveRolesOfType() where T : RoleBehaviour => GetActiveRoles().OfType(); + + /// + /// Creates a string builder for the Role Tab. + /// + /// The ICustomRole object. + /// A StringBuilder. + public static StringBuilder CreateForRole(ICustomRole role) + { + var taskStringBuilder = new StringBuilder(); + taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleColor.ToTextColor()}You are a {role.RoleName}."); + taskStringBuilder.Append(""); + taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleLongDescription}"); + return taskStringBuilder; + } +} diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index a1c0e5d..1784702 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -206,7 +206,7 @@ public interface ICustomRole : IOptionable /// Get the custom Role Tab text for this role. /// /// A StringBuilder with the role tab text. - StringBuilder SetTabText() => Helpers.CreateForRole(this); + StringBuilder SetTabText() => CustomRoleUtils.CreateForRole(this); /// /// Determine whether a given modifier can be applied to this role. diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index 514bd25..d2e855f 100644 --- a/MiraAPI/Utilities/Assets/MiraAssets.cs +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -10,6 +10,7 @@ public static class MiraAssets public static readonly LoadableResourceAsset NextButtonActive = new("MiraAPI.Resources.NextButtonActive.png"); public static readonly LoadableResourceAsset Cog = new("MiraAPI.Resources.Cog.png"); public static readonly LoadableResourceAsset Checkmark = new("MiraAPI.Resources.Checkmark.png"); + public static readonly LoadableResourceAsset CategoryHeader = new("MiraAPI.Resources.CategoryHeader.png"); public static readonly LoadableResourceAsset CheckmarkBox = new("MiraAPI.Resources.CheckMarkBox.png"); public static readonly Color32 AcceptedTeal = new(43, 233, 198, 255); } diff --git a/MiraAPI/Utilities/Assets/SpriteTools.cs b/MiraAPI/Utilities/Assets/SpriteTools.cs index 5d438de..7cb9d63 100644 --- a/MiraAPI/Utilities/Assets/SpriteTools.cs +++ b/MiraAPI/Utilities/Assets/SpriteTools.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Reflection; using Reactor.Utilities.Extensions; using UnityEngine; @@ -35,4 +36,20 @@ public static class SpriteTools sprite.name = resourcePath; return sprite; } + public static void Dump(this Sprite sprite, string path) => File.WriteAllBytes(path, sprite.texture.Decompress().EncodeToPNG()); + + public static Texture2D Decompress(this Texture2D source) + { + var renderTex = RenderTexture.GetTemporary(source.width, source.height, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Linear); + Graphics.Blit(source, renderTex); + var previous = RenderTexture.active; + RenderTexture.active = renderTex; + var readableText = new Texture2D(source.width, source.height); + readableText.ReadPixels(new(0, 0, renderTex.width, renderTex.height), 0, 0); + readableText.Apply(); + RenderTexture.active = previous; + RenderTexture.ReleaseTemporary(renderTex); + readableText.name = source.name; + return readableText; + } } diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 47a05f7..009be7f 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; using HarmonyLib; using MiraAPI.GameOptions; using MiraAPI.Networking; @@ -39,6 +40,30 @@ public static class Extensions BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value))); } + /// + /// Gets a PlayerControl from their PlayerVoteArea in a meeting. + /// + /// The vote area. + /// The player's PlayerControl. + public static PlayerControl? GetPlayer(this PlayerVoteArea state) => GameData.Instance.GetPlayerById(state.TargetPlayerId)?.Object; + + /// + /// Gets an int representing the amount of tasks a player has left. + /// + /// The player. + /// A count of how many tasks the player has left. + public static int GetTasksLeft(this PlayerControl player) => player.Data.Tasks.ToArray().Count(x => !x.Complete); + + /// + /// Checks if a PlayerControl is the game's host. + /// + /// The player you're checking for. + /// If the player is the host, true, else false. + public static bool IsHost(this PlayerControl playerControl) + { + return TutorialManager.InstanceExists || AmongUsClient.Instance.HostId == playerControl.OwnerId; + } + /// /// Determines if a float is an integer. /// @@ -65,6 +90,17 @@ public static class Extensions .FirstOrDefault(); } + /// + /// Gets a proper string for an enum. (with spaces). + /// + /// The enum you would like to change. + /// A proper string for the enum. + public static string ToDisplayString(this Enum @enum) + { + var regex = new Regex(@"([^\^])([A-Z][a-z$])"); + return regex.Replace(@enum.ToString(), m => $"{m.Groups[1].Value} {m.Groups[2].Value}"); + } + /// /// Gets the constructors of a type that match the specified arguments. /// diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index ab12bb8..a54b6cf 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using AmongUs.GameOptions; using HarmonyLib; using MiraAPI.Roles; using Reactor.Utilities; @@ -29,6 +30,123 @@ public static class Helpers return PlayerControl.AllPlayerControls.ToArray().Where(x => !x.Data.IsDead).ToList(); } + /// + /// Determines whether a given probability check succeeds. + /// + /// An integer value representing the success probability (0-100). + /// True if the number falls in the range, false if not. + public static bool CheckChance(int probability) + { + switch (probability) + { + case 0: + return false; + case 100: + return true; + default: + { + var num = Random.RandomRangeInt(1, 101); + return num <= probability; + } + } + } + + /// + /// Retrieves a vent from the ID. + /// + /// The vent ID. + /// The vent. + public static Vent? GetVentById(int id) + { + return ShipStatus.Instance.AllVents.FirstOrDefault(vent => vent.Id == id); + } + + /// + /// Creates an arrow. + /// + /// The arrow gameObject's parent. + /// The color of the arrow. + /// The created ArrowBehaviour. + public static ArrowBehaviour CreateArrow(Transform parent, Color color) + { + var prefab = Object.FindObjectOfType(true); + var arrow = Object.Instantiate(prefab, parent); + arrow.image = arrow.gameObject.GetComponent(); + arrow.image.color = color; + arrow.gameObject.layer = 5; + arrow.gameObject.SetActive(true); + return arrow; + } + + /// + /// Get the closest object of a specific type. + /// + /// A list of all the objects you'd like to check the distance for. + /// The position of where you want to check from. For example: PlayerControl.LocalPlayer.transform.position. + /// The object type. + /// The closest object. + public static T? FindClosestObjectOfType(List objectList, Vector3 position) where T : MonoBehaviour + { + T? closest = null; + var closestDistanceSqr = Mathf.Infinity; + + foreach (var obj in objectList) + { + if (obj == null) + { + continue; + } + + var sqrDistance = (obj.transform.position - position).sqrMagnitude; + if (sqrDistance < closestDistanceSqr) + { + closestDistanceSqr = sqrDistance; + closest = obj; + } + } + + return closest; + } + + /// + /// Creates and shows a notification. + /// + /// The text you want to display. + /// The color of the text and image. + /// The sound you want to play with the notification. + /// The sprite beside the notification. + /// The created notification. + public static LobbyNotificationMessage CreateAndShowNotification(string text, Color color, AudioClip? clip = null, Sprite? spr = null) + { + return CreateAndShowNotification(text, color, new Vector3(0f, 0f, -2f), clip, spr); + } + + /// + /// Creates and shows a notification. + /// + /// The text you want to display. + /// The color of the text and image. + /// The position of the notification. + /// The sound you want to play with the notification. + /// The sprite beside the notification. + /// The created notification. + public static LobbyNotificationMessage CreateAndShowNotification(string text, Color color, Vector3 localPos, AudioClip? clip = null, Sprite? spr = null) + { + var popper = HudManager.Instance.Notifier; + var newMessage = Object.Instantiate(popper.notificationMessageOrigin, Vector3.zero, Quaternion.identity, popper.transform); + newMessage.transform.localPosition = localPos; + newMessage.SetUp(text, spr ?? null, color, new System.Action(() => popper.OnMessageDestroy(newMessage))); + popper.lastMessageKey = -1; + popper.ShiftMessages(); + popper.AddMessageToQueue(newMessage); + + if (clip == null) return newMessage; + SoundManager.Instance.StopSound(clip); + SoundManager.Instance.PlaySound(clip, false, 2f); + + return newMessage; + } + /// /// Returns an empty coroutine. /// @@ -270,16 +388,24 @@ public static class Helpers } /// - /// Creates a string builder for the Role Tab. + /// Converts the first letter of a string to uppercase. /// - /// The ICustomRole object. - /// A StringBuilder. - public static StringBuilder CreateForRole(ICustomRole role) + /// The string. + /// The fixed string. + public static string FirstLetterToUpper(string str) { - var taskStringBuilder = new StringBuilder(); - taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleColor.ToTextColor()}You are a {role.RoleName}."); - taskStringBuilder.Append(""); - taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleLongDescription}"); - return taskStringBuilder; + return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str.ToLower(CultureInfo.InvariantCulture)); + } + + /// + /// Gets a random string based on characters. + /// + /// The length of the string. + /// The characters in the random string. + /// The random string. + public static string RandomString(int length, string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + { + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[Random.RandomRangeInt(0, s.Length)]).ToArray()); } } From 19ba9da2c362cdcc9faa4705241ea277ef3c07da Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sat, 15 Mar 2025 17:22:01 -0400 Subject: [PATCH 23/38] Remove debug method --- MiraAPI/Utilities/Assets/SpriteTools.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/MiraAPI/Utilities/Assets/SpriteTools.cs b/MiraAPI/Utilities/Assets/SpriteTools.cs index 7cb9d63..6858dcc 100644 --- a/MiraAPI/Utilities/Assets/SpriteTools.cs +++ b/MiraAPI/Utilities/Assets/SpriteTools.cs @@ -36,20 +36,4 @@ public static class SpriteTools sprite.name = resourcePath; return sprite; } - public static void Dump(this Sprite sprite, string path) => File.WriteAllBytes(path, sprite.texture.Decompress().EncodeToPNG()); - - public static Texture2D Decompress(this Texture2D source) - { - var renderTex = RenderTexture.GetTemporary(source.width, source.height, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Linear); - Graphics.Blit(source, renderTex); - var previous = RenderTexture.active; - RenderTexture.active = renderTex; - var readableText = new Texture2D(source.width, source.height); - readableText.ReadPixels(new(0, 0, renderTex.width, renderTex.height), 0, 0); - readableText.Apply(); - RenderTexture.active = previous; - RenderTexture.ReleaseTemporary(renderTex); - readableText.name = source.name; - return readableText; - } } From bf795a2e366dada33d237ee75a6b817e07c50af4 Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sat, 15 Mar 2025 23:47:05 -0400 Subject: [PATCH 24/38] Bug fixes with option screen & modifiers options --- .../Patches/Options/GameOptionsMenuPatch.cs | 5 +- .../Patches/Options/GameSettingMenuPatches.cs | 295 +++++++++++++----- .../Patches/Options/RoleSettingMenuPatches.cs | 7 +- MiraAPI/Resources/CategoryHeader.png | Bin 1221 -> 872 bytes 4 files changed, 227 insertions(+), 80 deletions(-) diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 05de7f6..8034eff 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -203,7 +203,10 @@ public static class GameOptionsMenuPatch categoryHeaderMasked.Title.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) ? Color.white : group.GroupColor.FindAlternateColor(); categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset(); - categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.55f, -0.1833f, 0); + categoryHeaderMasked.Background.sprite.texture.filterMode = FilterMode.Bilinear; + categoryHeaderMasked.Background.sprite.texture.wrapMode = TextureWrapMode.Clamp; + + categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.5f, -0.1833f, 0); categoryHeaderMasked.Background.size = new Vector2( categoryHeaderMasked.Background.size.x + 1.5f, categoryHeaderMasked.Background.size.y); diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index 7781bf8..9e05f58 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -1,8 +1,10 @@ using System; +using System.Linq; using HarmonyLib; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Utilities.Assets; +using Reactor.Utilities; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; @@ -22,6 +24,8 @@ internal static class GameSettingMenuPatches private static TextMeshPro? _text; private static Vector3 _roleBtnOgPos; + private static Vector3 _smallRoleBtnOgPos; + private static Vector3 _modifierBtnOgPos; private static GameOptionsMenu? _modifiersTab; private static PassiveButton? _modifiersButton; @@ -33,9 +37,20 @@ internal static class GameSettingMenuPatches { if ((previewOnly && Controller.currentTouchType == Controller.TouchType.Joystick) || !previewOnly) { - if (_modifiersTab) _modifiersTab?.gameObject.SetActive(tabNum == 3); + if (_modifiersTab) _modifiersTab!.gameObject.SetActive(tabNum == 3); _modifiersButton?.SelectButton(tabNum == 3); _smallRolesButton?.SelectButton(tabNum == 2); + if (tabNum == 3) + { + if (__instance.RoleSettingsButton.gameObject.active) + { + __instance.RoleSettingsButton.SelectButton(true); + } + else + { + _modifiersButton?.SelectButton(true); + } + } } if (previewOnly) @@ -45,6 +60,17 @@ internal static class GameSettingMenuPatches _modifiersButton?.SelectButton(tabNum == 3); _smallRolesButton?.SelectButton(tabNum == 2); + + if (tabNum != 3) return; + + if (__instance.RoleSettingsButton.gameObject.active) + { + __instance.RoleSettingsButton.SelectButton(true); + } + else + { + _modifiersButton?.SelectButton(true); + } } /// @@ -117,6 +143,7 @@ internal static class GameSettingMenuPatches _modifiersTab.name = "MODIFIERS TAB"; // create button for modifiers + __instance.RoleSettingsButton.buttonText.gameObject.GetComponent().Destroy(); __instance.RoleSettingsButton.gameObject.SetActive(false); _smallRolesButton = Object.Instantiate( __instance.RoleSettingsButton, @@ -137,6 +164,8 @@ internal static class GameSettingMenuPatches __instance.ChangeTab(2, true); })); + _smallRoleBtnOgPos = _smallRolesButton.transform.localPosition; + var roleText = _smallRolesButton.buttonText; roleText.text = "Roles"; roleText.GetComponent().Destroy(); @@ -178,9 +207,45 @@ internal static class GameSettingMenuPatches _modifiersButton.transform.localPosition = pos; _modifiersButton.name = "ModifiersButton"; + _modifierBtnOgPos = _modifiersButton.transform.localPosition; + UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); } + private static void ChangeRoleSettingButton(bool replace, GameSettingMenu __instance) + { + __instance.RoleSettingsButton.OnClick = new ButtonClickedEvent(); + __instance.RoleSettingsButton.OnMouseOver = new UnityEvent(); + + if (replace) + { + __instance.RoleSettingsButton.buttonText.text = "Modifiers Settings"; + __instance.RoleSettingsButton.OnClick.AddListener( + (UnityAction)(() => + { + __instance.ChangeTab(3, false); + })); + __instance.RoleSettingsButton.OnMouseOver.AddListener( + (UnityAction)(() => + { + __instance.ChangeTab(3, true); + })); + } + else + { + __instance.RoleSettingsButton.buttonText.text = "Roles Settings"; + __instance.RoleSettingsButton.OnClick.AddListener( + (UnityAction)(() => + { + __instance.ChangeTab(2, false); + })); + __instance.RoleSettingsButton.OnMouseOver.AddListener( + (UnityAction)(() => + { + __instance.ChangeTab(2, true); + })); + } + } private static void UpdateText(GameSettingMenu menu, GameOptionsMenu settings, RolesSettingsMenu roles) { if (_text is not null && SelectedModIdx == 0) @@ -197,106 +262,182 @@ internal static class GameSettingMenuPatches _text.text = name[..Math.Min(name.Length, 25)]; } - _modifiersButton.gameObject.SetActive(false); - _smallRolesButton.gameObject.SetActive(false); - menu.RoleSettingsButton.gameObject.SetActive(false); - if (SelectedModIdx != 0 && - SelectedMod?.OptionGroups.Exists(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) == true) + bool replaceWithModifiers = true; + _smallRolesButton!.transform.localPosition = _smallRoleBtnOgPos; + _modifiersButton!.transform.localPosition = _modifierBtnOgPos; + menu.RoleSettingsButton.transform.localPosition = _roleBtnOgPos; + + if (SelectedModIdx != 0) { + var modHasRoles = SelectedMod!.CustomRoles.Count != 0; + var modHasModifiers = SelectedMod.OptionGroups.Exists(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true); + var modHasOptions = SelectedMod.OptionGroups.Exists(x => x.OptionableType == null && !x.ShowInModifiersMenu); + _modifiersButton.gameObject.SetActive(true); _smallRolesButton.gameObject.SetActive(true); + menu.GameSettingsButton.gameObject.SetActive(true); + menu.RoleSettingsButton.gameObject.SetActive(false); + + // If there are no registered custom roles in the selected mod, hide the button. + if (!modHasRoles) + { + _smallRolesButton.gameObject.SetActive(false); + + if (roles.gameObject.active) + { + menu.ChangeTab(0, false); + } + + // If the mod has modifiers, we can disable the smaller modifier button and enable the bigger one. + if (modHasModifiers) + { + _modifiersButton.gameObject.SetActive(false); + menu.RoleSettingsButton.gameObject.SetActive(true); + + if (_modifiersTab!.gameObject.active) + { + menu.RoleSettingsButton.SelectButton(true); + } + } + } + + // If there are no modifiers in the selected mod, hide the modifiers button. + if (!modHasModifiers) + { + replaceWithModifiers = false; + _modifiersButton.gameObject.SetActive(false); + + if (_modifiersTab!.gameObject.active) + { + menu.ChangeTab(0, false); + } + // If the mod has roles, we can enable the bigger role button and disable the small one. + if (modHasRoles) + { + menu.RoleSettingsButton.gameObject.SetActive(true); + _smallRolesButton.gameObject.SetActive(false); + } + } + + // If there are no custom game options registered (that aren't modifier options), hide game settings button. + if (!modHasOptions) + { + menu.GameSettingsButton.gameObject.SetActive(false); + + if (settings.gameObject.active) + { + menu.ChangeTab(0, false); + } + + // If the mod has roles and modifiers, we can move their buttons to the game settings button position, since nothing is there. + if (menu.RoleSettingsButton.gameObject.active) + { + menu.RoleSettingsButton.transform.localPosition = menu.GameSettingsButton.transform.localPosition; + } + else if (_modifiersButton.gameObject.active && _smallRolesButton.gameObject.active) + { + _modifiersButton.transform.localPosition = new Vector3( + _modifiersButton.transform.localPosition.x, + menu.GameSettingsButton.transform.localPosition.y, + _modifiersButton.transform.localPosition.z); + + _smallRolesButton.transform.localPosition = new Vector3( + _smallRolesButton.transform.localPosition.x, + menu.GameSettingsButton.transform.localPosition.y, + _smallRolesButton.transform.localPosition.z); + } + } } else { + _modifiersButton.gameObject.SetActive(false); + _smallRolesButton.gameObject.SetActive(false); menu.RoleSettingsButton.gameObject.SetActive(true); - if (SelectedModIdx == 0 && _modifiersTab.gameObject.active) + menu.GameSettingsButton.gameObject.SetActive(true); + replaceWithModifiers = false; + + if (_modifiersTab!.gameObject.active) { menu.ChangeTab(0, false); } } - //menu.RoleSettingsButton.transform.localPosition = _roleBtnOgPos; - //menu.GameSettingsButton.gameObject.SetActive(true); - //menu.RoleSettingsButton.gameObject.SetActive(true); - - // TODO: reimplement this - /* - if (SelectedModIdx != 0) + if (menu.RoleSettingsButton.gameObject.active) { - if (SelectedMod?.Options.Where(x=>x.AdvancedRole==null).ToList().Count == 0) - { - menu.GameSettingsButton.gameObject.SetActive(false); - } - - if (SelectedMod?.CustomRoles.Count == 0) - { - menu.RoleSettingsButton.gameObject.SetActive(false); - } - - if (!menu.GameSettingsButton.gameObject.active && menu.RoleSettingsButton.gameObject.active) - { - menu.RoleSettingsButton.transform.localPosition = menu.GameSettingsButton.transform.localPosition; - } - }*/ - - if (roles.roleChances != null && SelectedMod != null && SelectedMod.CustomRoles.Count != 0) - { - if (roles.advancedSettingChildren is not null) - { - foreach (var child in roles.advancedSettingChildren) - { - child.gameObject.DestroyImmediate(); - } - - roles.advancedSettingChildren.Clear(); - roles.advancedSettingChildren = null; - } - - foreach (var header in roles.RoleChancesSettings.transform - .GetComponentsInChildren()) - { - header.gameObject.DestroyImmediate(); - } - - foreach (var roleChance in roles.roleChances) - { - roleChance.gameObject.DestroyImmediate(); - } - - roles.roleChances.Clear(); - roles.AdvancedRolesSettings.gameObject.SetActive(false); - roles.RoleChancesSettings.gameObject.SetActive(true); - roles.SetQuotaTab(); - roles.scrollBar.ScrollToTop(); + ChangeRoleSettingButton(replaceWithModifiers, menu); } - if (settings.Children != null && SelectedMod?.OptionGroups.Count != 0) + CleanupTab(settings, roles); + } + + private static void ClearOptions(Il2CppSystem.Collections.Generic.List options) + { + foreach (var child in options) { - foreach (var child in settings.Children) + if (child.TryCast() || !child.gameObject) { - if (child.TryCast()) - { - continue; - } + continue; + } + child.gameObject.DestroyImmediate(); + } + options.Clear(); + } - if (!child.gameObject) - { - continue; - } - - child.gameObject.DestroyImmediate(); + private static void CleanupTab(GameOptionsMenu settings, RolesSettingsMenu roles) + { + void CleanupRoleSettings(RolesSettingsMenu rolesMenu) + { + if (rolesMenu.advancedSettingChildren != null) + { + ClearOptions(rolesMenu.advancedSettingChildren); + rolesMenu.advancedSettingChildren = null; } - foreach (var header in settings.settingsContainer.GetComponentsInChildren()) + rolesMenu.RoleChancesSettings + .transform + .GetComponentsInChildren() + .ToList() + .ForEach(header => header.gameObject.DestroyImmediate()); + + foreach (var role in rolesMenu.roleChances) { - header.gameObject.DestroyImmediate(); + role.gameObject.DestroyImmediate(); } + rolesMenu.roleChances?.Clear(); - settings.Children.Clear(); - settings.Children = null; + rolesMenu.AdvancedRolesSettings.gameObject.SetActive(false); + rolesMenu.RoleChancesSettings.gameObject.SetActive(true); + rolesMenu.SetQuotaTab(); + rolesMenu.scrollBar.ScrollToTop(); + } - settings.Initialize(); - settings.scrollBar.ScrollToTop(); + void CleanupSettings(GameOptionsMenu gameOptMenu) + { + ClearOptions(gameOptMenu.Children); + gameOptMenu.Children = null; + + gameOptMenu.settingsContainer + .GetComponentsInChildren() + .ToList() + .ForEach(header => header.gameObject.DestroyImmediate()); + + gameOptMenu.Initialize(); + gameOptMenu.scrollBar.ScrollToTop(); + } + + if (roles.roleChances != null && SelectedMod?.CustomRoles.Count > 0) + { + CleanupRoleSettings(roles); + } + + if (settings.Children != null && SelectedMod?.OptionGroups.Count > 0) + { + CleanupSettings(settings); + } + + if (_modifiersTab?.Children?.Count > 0) + { + CleanupSettings(_modifiersTab); } } } diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index e4984fd..9fe48d7 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -128,7 +128,10 @@ public static class RoleSettingMenuPatches countLabel.Destroy(); categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset(); - categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.55f, -0.1833f, 0); + categoryHeaderMasked.Background.sprite.texture.filterMode = FilterMode.Bilinear; + categoryHeaderMasked.Background.sprite.texture.wrapMode = TextureWrapMode.Clamp; + + categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.5f, -0.1833f, 0); switch (name) { @@ -243,7 +246,7 @@ public static class RoleSettingMenuPatches [HarmonyPatch(nameof(RolesSettingsMenu.OpenChancesTab))] public static void OpenChancesTabPostfix(RolesSettingsMenu __instance) { - if (GameSettingMenuPatches.SelectedModIdx == 0) + if (GameSettingMenuPatches.SelectedModIdx == 0 || !__instance.scrollBar) { return; } diff --git a/MiraAPI/Resources/CategoryHeader.png b/MiraAPI/Resources/CategoryHeader.png index 3d2a4bfaf12c74008a375e85c21720685b4bfaae..0fb58e8c6475343b637f141e7016da395d698206 100644 GIT binary patch literal 872 zcmeAS@N?(olHy`uVBq!ia0y~yV2S{;BRQCXiD!+d&cwv>oy3$Xq zg@+b(m?j1u+O7}^p0q`syuB2$7*+-#EGT9v-fg!udt4P z6&m4^Te}XZdh6enajmXb&K<836$>o4zp{FR$FA>nI|Ri%cfW6wPMm0BQ!XwRdAPVd zapG0U+!oh>?-g5w#T=Id&GfL6-Pq!K=_Syjreh#Gr1~QcDP`Csadt2G2ox+*a|7CG z{Oj#SyF|g_|8Fbwb&}-vNWO1%J$3lHu=ydS)JG!T5l?*P05#0{mf~|rDdiJT;D`xG z+a(F0*)&3D{-n?lJh*iv8c}6Y-zH!_9eJ$GY$+U|MAGboFyt=akR{0Q@4tng9R* literal 1221 zcmeAS@N?(olHy`uVBq!ia0y~yV2S{;BRSZBylDJ>JydH zPiC^)b32sCcJq;cKPCscIv zkG9**|6^{uJDoT``Do=|);g=_d#|rOm|(!cz_2OOlbwM<$1Q=8Aw_Ki1A|fa&4Ww~ zCl=p+J1;ISZk{EGv*h~gzkBxY|G)77kn`l)+Gu^HM=>A`mtL0mDLtAA683=`APn@o ze2~(F^78MVUn5a8aBH3Zbo~|QApxM~?&#Rq z+8A3$2_|lagu>OUSO5L>AH-z*Tem-YzZ?(Hpbay+ySv}NzTOP7L2UEx-M>xda_TTN zoM91Ta7Y6w_DhYFTgv?oB>!yt)~#O+Vhi};E&w_9OyZq?p&URxKYMQP=YmVEF_qw9 zdnv%s@NwO`b^kK9-+nLdT3Az2F{AD+3((lG>C>m%_d+xn?B2R{>*bO#b)X$-TlVh# zYcuyMsw3DY-+K@z2=tQij4y28yHLVODr-}{DA1(~cB=gj2L}`=;AVmhV4L+Wz7?oA zW7$iH6aT=y)wM}yzUf`Cdktb&U;VWUs^X=Yxq1AO&7N?DO_BHNLqS#x@2UIyt3C4D zdMBW#eXapx?9xpWpuGylKtCHCvw>KCZ1&Ud?62XvTVB6@J^gbGe;d#h5^L|jx1aaC z$^a;PAmj6%c?*?*9v1p+XBz=Bzq7ZuSO585aI}2bwn?WyEG%rF*YnL8yll;mRxAt= zN%KWqKPt#BTzep}tIr1a?W_idNg=K&)+;iXU4Y6;F3K*paE=lWcsuvbyfBbFjvbPbw z${QFOmImzS%Ml6_1*JL(JJ}ygZf*7|U*qN)Iu{t;9UY*+|6BLJaQS7+mHHcw7aY%u z3IV08gv_OZq5IilN|tAZ0b}sT{mQSeuFikHfBye%-_G6Ax3;!E-}bn9Z_@jUTQ8@8 zV(#IaH*db2`#1Nu;K#eb&~VdR)Bic1{o4KK@f+?g4nBJkSoAP>y85}Sb4q9e0AN<~ AcK`qY From 7917d4b5d9ded53044682c1e0cfa6840fde63a59 Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sun, 16 Mar 2025 19:05:12 -0400 Subject: [PATCH 25/38] Make some changes to role registration for sounds --- MiraAPI.Example/Roles/ChameleonRole.cs | 7 +++++-- MiraAPI/Patches/Roles/GameStartupPatch.cs | 9 +++++++++ MiraAPI/Patches/Roles/RoleBehaviourPatch.cs | 2 +- MiraAPI/PluginLoading/MiraPluginManager.cs | 4 ++-- MiraAPI/Roles/CustomRoleConfiguration.cs | 3 +++ MiraAPI/Roles/CustomRoleManager.cs | 13 +++++++++++++ MiraAPI/Roles/CustomRoleUtils.cs | 13 +++++++++++++ MiraAPI/Utilities/Assets/PreloadedAsset.cs | 19 +++++++++++++++++++ MiraAPI/Utilities/Helpers.cs | 8 -------- 9 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 MiraAPI/Utilities/Assets/PreloadedAsset.cs diff --git a/MiraAPI.Example/Roles/ChameleonRole.cs b/MiraAPI.Example/Roles/ChameleonRole.cs index 1f2f61f..edb8e64 100644 --- a/MiraAPI.Example/Roles/ChameleonRole.cs +++ b/MiraAPI.Example/Roles/ChameleonRole.cs @@ -1,4 +1,6 @@ -using MiraAPI.Roles; +using AmongUs.GameOptions; +using MiraAPI.Roles; +using MiraAPI.Utilities; using TMPro; using UnityEngine; @@ -12,9 +14,10 @@ public class ChameloenRole : CrewmateRole, ICustomRole public Color RoleColor => Palette.AcceptedGreen; public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate; - public CustomRoleConfiguration Configuration => new CustomRoleConfiguration(this) + public CustomRoleConfiguration Configuration => new(this) { OptionsScreenshot = ExampleAssets.Banner, + IntroSound = CustomRoleUtils.GetIntroSound(RoleTypes.Shapeshifter), }; public void PlayerControlFixedUpdate(PlayerControl playerControl) diff --git a/MiraAPI/Patches/Roles/GameStartupPatch.cs b/MiraAPI/Patches/Roles/GameStartupPatch.cs index b452791..9bb2a54 100644 --- a/MiraAPI/Patches/Roles/GameStartupPatch.cs +++ b/MiraAPI/Patches/Roles/GameStartupPatch.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using MiraAPI.PluginLoading; using MiraAPI.Roles; namespace MiraAPI.Patches.Roles; @@ -21,6 +22,14 @@ public static class GameStartupPatch } _runOnce = true; + + if (MiraPluginManager.Instance.QueuedRoleRegistrations.Count <= 0) return; + foreach (var queue in MiraPluginManager.Instance.QueuedRoleRegistrations) + { + CustomRoleManager.RegisterRoleTypes(queue.Value, queue.Key); + } + + MiraPluginManager.Instance.QueuedRoleRegistrations.Clear(); CustomRoleManager.RegisterInRoleManager(); } } diff --git a/MiraAPI/Patches/Roles/RoleBehaviourPatch.cs b/MiraAPI/Patches/Roles/RoleBehaviourPatch.cs index 55cc5a5..1caba49 100644 --- a/MiraAPI/Patches/Roles/RoleBehaviourPatch.cs +++ b/MiraAPI/Patches/Roles/RoleBehaviourPatch.cs @@ -22,4 +22,4 @@ public static class RoleBehaviourPatch __result = behaviour.RoleColor; return false; } -} \ No newline at end of file +} diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 15fe686..885414d 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -27,7 +27,7 @@ public sealed class MiraPluginManager private readonly Dictionary _registeredPlugins = []; internal MiraPluginInfo[] RegisteredPlugins() => [.. _registeredPlugins.Values]; - + internal Dictionary> QueuedRoleRegistrations { get; } = new(); internal static MiraPluginManager Instance { get; private set; } = new(); internal void Initialize() @@ -97,7 +97,7 @@ public sealed class MiraPluginManager } info.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority)); - CustomRoleManager.RegisterRoleTypes(roles, info); + QueuedRoleRegistrations.Add(info, roles); _registeredPlugins.Add(assembly, info); Logger.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API."); diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index cf714fd..ac382a8 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -34,6 +34,9 @@ public record struct CustomRoleConfiguration TasksCountForProgress = role.Team is ModdedRoleTeams.Crewmate; HideSettings = roleBehaviour?.IsDead == true; ShowInFreeplay = roleBehaviour?.IsDead == false; + IntroSound = role.Team is ModdedRoleTeams.Crewmate + ? CustomRoleManager.CrewmateIntroSound + : CustomRoleManager.ImpostorIntroSound; } /// diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index 0c2c4ad..6552641 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -7,6 +7,7 @@ using Il2CppInterop.Runtime.Injection; using MiraAPI.Networking; using MiraAPI.PluginLoading; using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; using Reactor.Networking.Rpc; using Reactor.Utilities; @@ -23,6 +24,18 @@ namespace MiraAPI.Roles; /// public static class CustomRoleManager { + /// + /// The default Among Us Crewmate Intro Sound. + /// + public static readonly LoadableAsset CrewmateIntroSound = + CustomRoleUtils.GetIntroSound(RoleTypes.Crewmate)!; + + /// + /// The default Among Us Impostor Intro Sound. + /// + public static readonly LoadableAsset ImpostorIntroSound = + CustomRoleUtils.GetIntroSound(RoleTypes.Impostor)!; + internal static readonly Dictionary CustomRoles = []; internal static readonly Dictionary RoleIds = []; diff --git a/MiraAPI/Roles/CustomRoleUtils.cs b/MiraAPI/Roles/CustomRoleUtils.cs index 8f2432d..acaaeb3 100644 --- a/MiraAPI/Roles/CustomRoleUtils.cs +++ b/MiraAPI/Roles/CustomRoleUtils.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Linq; using System.Text; using AmongUs.GameOptions; +using MiraAPI.Utilities.Assets; using UnityEngine; namespace MiraAPI.Roles; @@ -46,4 +47,16 @@ public static class CustomRoleUtils taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleLongDescription}"); return taskStringBuilder; } + + /// + /// Returns an intro sound from a role. + /// + /// The role type. + /// The intro sound. + public static LoadableAsset? GetIntroSound(RoleTypes roleType) + { + var role = RoleManager.Instance.AllRoles.FirstOrDefault(role => role.Role == roleType); + if (role is ICustomRole customRole) return customRole.Configuration.IntroSound; + return new PreloadedAsset(role!.IntroSound); + } } diff --git a/MiraAPI/Utilities/Assets/PreloadedAsset.cs b/MiraAPI/Utilities/Assets/PreloadedAsset.cs new file mode 100644 index 0000000..ed28ebf --- /dev/null +++ b/MiraAPI/Utilities/Assets/PreloadedAsset.cs @@ -0,0 +1,19 @@ +using System; + +namespace MiraAPI.Utilities.Assets; + +/// +/// An implementation for creating loadable assets from preloaded assets. +/// +/// +/// +public class PreloadedAsset(T asset) : LoadableAsset where T : UnityEngine.Object +{ + private readonly T loadedAsset = asset ?? throw new ArgumentNullException(nameof(asset)); + + /// + public override T LoadAsset() + { + return loadedAsset; + } +} diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index a54b6cf..513789e 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -1,17 +1,9 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; -using System.Text; -using AmongUs.GameOptions; -using HarmonyLib; -using MiraAPI.Roles; -using Reactor.Utilities; -using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; -using UnityEngine.TextCore; using Object = UnityEngine.Object; namespace MiraAPI.Utilities; From e9beb3b423782fd7293eae8bb5daf39478f90085 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Fri, 21 Mar 2025 15:24:48 -0400 Subject: [PATCH 26/38] fix weird behaviour --- MiraAPI/GameOptions/OptionTypes/ModdedOption.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs index 670fb3c..83048f7 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs @@ -79,17 +79,7 @@ public abstract class ModdedOption : IModdedOption /// /// Gets or sets the config definition of the option. /// - public ConfigDefinition? ConfigDefinition - { - get => _configDefinition; - set - { - if (_configDefinition is not null) return; - _configDefinition = value; - } - } - - private ConfigDefinition? _configDefinition; + public ConfigDefinition? ConfigDefinition { get; set; } /// /// Initializes a new instance of the class. From 2cfc5ac039dbd6f479f40b424a8a37c1a6cc9680 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Mar 2025 14:00:12 -0400 Subject: [PATCH 27/38] update example --- MiraAPI.Example/ExampleEventHandlers.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/MiraAPI.Example/ExampleEventHandlers.cs b/MiraAPI.Example/ExampleEventHandlers.cs index 2e8fdf2..d2a22d8 100644 --- a/MiraAPI.Example/ExampleEventHandlers.cs +++ b/MiraAPI.Example/ExampleEventHandlers.cs @@ -38,12 +38,9 @@ public static class ExampleEventHandlers [RegisterEvent] public static void StartMeetingEvent(StartMeetingEvent _) { - foreach (var plr in PlayerControl.AllPlayerControls) + foreach (var plr in PlayerControl.AllPlayerControls.ToArray().Where(player => player.Data.Role is MayorRole)) { - if (plr.Data.Role is MayorRole) - { - plr.GetVoteData().IncreaseRemainingVotes(1); - } + plr.GetVoteData().IncreaseRemainingVotes(1); } } From f48143610c55e0a7dd373f993516135b673cfbcd Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sun, 23 Mar 2025 15:15:21 -0400 Subject: [PATCH 28/38] fix incorrect chunking algorithm --- MiraAPI/Utilities/Extensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 4139aee..7a0d9a7 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -283,12 +283,13 @@ public static class Extensions if (count + length > chunkSize) { - chunks.Enqueue(current.ToArray()); + chunks.Enqueue([.. current]); current.Clear(); count = 0; } current.Add(netData); + count += length; } if (current.Count > 0) From bb0b678747e3f70afb359ff094c2f6940cd6531d Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Mon, 24 Mar 2025 18:57:06 -0400 Subject: [PATCH 29/38] fix VERY stupid issue with LobbyViewPanePatches.cs --- MiraAPI/Patches/Options/LobbyViewPanePatches.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index 43c5719..afbcdae 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -250,14 +250,9 @@ public static class LobbyViewPanePatches var list = new List(); - var roleGroups = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values.OfType() + var roleGroups = SelectedMod.CustomRoles.Values.OfType() .ToLookup(x => x.RoleOptionsGroup); - if (roleGroups is null) - { - return; - } - // sort the groups by priority var sortedRoleGroups = roleGroups .OrderBy(x => x.Key.Priority) From e6e4a865461e0c0d21b0b5af06bb9c4fcb3ba0d1 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Thu, 27 Mar 2025 18:09:27 -0400 Subject: [PATCH 30/38] fix stupid innerscuff code --- MiraAPI/Patches/Roles/RoleManagerPatches.cs | 38 +++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/MiraAPI/Patches/Roles/RoleManagerPatches.cs b/MiraAPI/Patches/Roles/RoleManagerPatches.cs index f326239..63ed61a 100644 --- a/MiraAPI/Patches/Roles/RoleManagerPatches.cs +++ b/MiraAPI/Patches/Roles/RoleManagerPatches.cs @@ -5,6 +5,7 @@ using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; using MiraAPI.Modifiers; using MiraAPI.Roles; +using UnityEngine; namespace MiraAPI.Patches.Roles; @@ -15,6 +16,43 @@ public static class RoleManagerPatches [HarmonyPatch(nameof(RoleManager.SetRole))] public static void SetRolePatch(RoleManager __instance, PlayerControl targetPlayer, RoleTypes roleType) { + if (!targetPlayer) + { + return; + } + var data = targetPlayer.Data; + if (data == null) + { + Debug.LogError("It shouldn't be possible, but " + targetPlayer.name + " still doesn't have PlayerData during role selection."); + return; + } + if (data.Role) + { + data.Role.Deinitialize(targetPlayer); + Object.Destroy(data.Role.gameObject); + } + var roleBehaviour = Object.Instantiate(__instance.AllRoles.First(r => r.Role == roleType), data.gameObject.transform); + roleBehaviour.Initialize(targetPlayer); + targetPlayer.Data.Role = roleBehaviour; + targetPlayer.Data.RoleType = roleType; + + // basically everything is the same except this one if statement + // innersloth decided to check the roleType manually instead of the role behaviour. + if (!roleBehaviour.IsDead) + { + targetPlayer.Data.RoleWhenAlive = new Il2CppSystem.Nullable(roleType); + } + roleBehaviour.AdjustTasks(targetPlayer); + switch (roleBehaviour.IsDead) + { + case true when !targetPlayer.Data.IsDead: + targetPlayer.Die(DeathReason.Kill, false); + return; + case false when targetPlayer.Data.IsDead: + targetPlayer.Revive(); + break; + } + var @event = new SetRoleEvent(targetPlayer, roleType); MiraEventManager.InvokeEvent(@event); } From 521e5a12fdceec888365dc72f5cd54611615958e Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 29 Mar 2025 00:27:07 -0400 Subject: [PATCH 31/38] more accurate doc --- MiraAPI/Utilities/ColorExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Utilities/ColorExtensions.cs b/MiraAPI/Utilities/ColorExtensions.cs index 74c5a22..d773b47 100644 --- a/MiraAPI/Utilities/ColorExtensions.cs +++ b/MiraAPI/Utilities/ColorExtensions.cs @@ -8,10 +8,10 @@ namespace MiraAPI.Utilities; public static class ColorExtensions { /// - /// Gets the relative luminance of a color (WCAG 2.1 specification). + /// Gets the relative luminance of a color (WCAG 2.1 specification). 0 is black, 1 is white. /// /// Color to calculate luminance for. - /// Relative luminance value. + /// Luminance value between 0 and 1. public static float GetRelativeLuminance(this Color color) { var r = color.r <= 0.03928f ? color.r / 12.92f : Mathf.Pow((color.r + 0.055f) / 1.055f, 2.4f); From c5e424e8ffd00ebdaca77278c56b7904a4c7999a Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 29 Mar 2025 12:26:38 -0400 Subject: [PATCH 32/38] hide empty role groups in settings --- MiraAPI/Patches/Options/LobbyViewPanePatches.cs | 2 +- MiraAPI/Patches/Options/RoleSettingMenuPatches.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index afbcdae..cd948c3 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -260,7 +260,7 @@ public static class LobbyViewPanePatches foreach (var grouping in sortedRoleGroups) { - if (!grouping.Any()) + if (!grouping.Any() || grouping.All(x => x.Configuration.HideSettings)) { continue; } diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 9fe48d7..8d171a1 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -83,10 +83,9 @@ public static class RoleSettingMenuPatches var quotaThing = __instance.categoryHeaderEditRoleOrigin.transform.FindChild("QuotaHeader"); var template = __instance.transform.parent.parent.GetComponent().GameSettingsTab.categoryHeaderOrigin; - foreach (var grouping in sortedRoleGroups) { - if (!grouping.Any()) + if (!grouping.Any() || grouping.All(x=>x.Configuration.HideSettings)) { continue; } From 57105f487b27f9cd435ff851f71bcb6f6eaf80c0 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 29 Mar 2025 13:11:15 -0400 Subject: [PATCH 33/38] cleanup --- MiraAPI.Example/Roles/ChameleonRole.cs | 1 - MiraAPI/MiraApiPlugin.cs | 19 ++++++++++--------- .../Patches/Options/GameSettingMenuPatches.cs | 1 - MiraAPI/Roles/CustomRoleConfiguration.cs | 2 -- MiraAPI/Roles/CustomRoleManager.cs | 1 - MiraAPI/Roles/ICustomRole.cs | 1 - MiraAPI/Utilities/Assets/SpriteTools.cs | 1 - 7 files changed, 10 insertions(+), 16 deletions(-) diff --git a/MiraAPI.Example/Roles/ChameleonRole.cs b/MiraAPI.Example/Roles/ChameleonRole.cs index edb8e64..52550a3 100644 --- a/MiraAPI.Example/Roles/ChameleonRole.cs +++ b/MiraAPI.Example/Roles/ChameleonRole.cs @@ -1,6 +1,5 @@ using AmongUs.GameOptions; using MiraAPI.Roles; -using MiraAPI.Utilities; using TMPro; using UnityEngine; diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index 46f0a16..72424af 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -1,19 +1,12 @@ -using System; -using BepInEx; +using BepInEx; using BepInEx.Unity.IL2CPP; using HarmonyLib; -using Il2CppInterop.Runtime; using MiraAPI.PluginLoading; -using MiraAPI.Roles; -using MonoMod.Utils; using Reactor; using Reactor.Networking; using Reactor.Networking.Attributes; using Reactor.Utilities; -using TMPro; using UnityEngine; -using UnityEngine.ProBuilder; -using Delegate = Il2CppSystem.Delegate; namespace MiraAPI; @@ -26,8 +19,16 @@ namespace MiraAPI; [ReactorModFlags(ModFlags.RequireOnAllClients)] public partial class MiraApiPlugin : BasePlugin { + /// + /// Gets the branding Mira API color. + /// public static Color MiraColor { get; } = new Color32(238, 154, 112, 255); - public static Color DefaultHeaderColor = new Color32(77, 77, 77, 255); + + /// + /// Gets the default color for headers in the options menu. + /// + public static Color DefaultHeaderColor { get; } = new Color32(77, 77, 77, 255); + private static MiraPluginManager? PluginManager { get; set; } internal Harmony Harmony { get; } = new(Id); diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index 9e05f58..fcc3722 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -4,7 +4,6 @@ using HarmonyLib; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Utilities.Assets; -using Reactor.Utilities; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index ac382a8..5b0158d 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -1,8 +1,6 @@ using AmongUs.GameOptions; -using Il2CppSystem.Web.Util; using MiraAPI.Utilities.Assets; using UnityEngine; -using Helpers = MiraAPI.Utilities.Helpers; namespace MiraAPI.Roles; diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index 6552641..ab890fb 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -14,7 +14,6 @@ using Reactor.Utilities; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; -using UnityEngine.TextCore; using Object = UnityEngine.Object; namespace MiraAPI.Roles; diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 1784702..abc0656 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -3,7 +3,6 @@ using BepInEx.Configuration; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; -using MiraAPI.Utilities; using Reactor.Utilities; using UnityEngine; diff --git a/MiraAPI/Utilities/Assets/SpriteTools.cs b/MiraAPI/Utilities/Assets/SpriteTools.cs index 6858dcc..5d438de 100644 --- a/MiraAPI/Utilities/Assets/SpriteTools.cs +++ b/MiraAPI/Utilities/Assets/SpriteTools.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Reflection; using Reactor.Utilities.Extensions; using UnityEngine; From ea9bf162ea82e0043ee33412fd8484cce8b36a05 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 29 Mar 2025 13:12:12 -0400 Subject: [PATCH 34/38] rework modifier component caching --- MiraAPI/Modifiers/ModifierComponent.cs | 29 ++++++++++++----- MiraAPI/Patches/PlayerControlPatches.cs | 41 ++++--------------------- 2 files changed, 28 insertions(+), 42 deletions(-) diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 53a8ac4..f8859e3 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -29,9 +29,9 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) [HideFromIl2Cpp] private List Modifiers { get; set; } = []; - private PlayerControl? _player; + private PlayerControl _player = null!; - private TextMeshPro? _modifierText; + private TextMeshPro _modifierText = null!; private readonly List _toRemove = []; @@ -42,9 +42,18 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) _toRemove.AddRange(Modifiers); } - private void Start() + private void Awake() { _player = GetComponent(); + if (_player == null) + { + Logger.Error("ModifierComponent could not find PlayerControl on the same GameObject."); + Destroy(this); + return; + } + + ModifierExtensions.ModifierComponents.Add(_player, this); + Modifiers = []; if (!_player.AmOwner) @@ -78,9 +87,9 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) if (_toAdd.Count > 0 || _toRemove.Count > 0) { - if (_player?.AmOwner == true) + if (_player.AmOwner && HudManager.InstanceExists) { - HudManager.Instance?.SetHudActive(_player, _player.Data.Role, HudManager.Instance.TaskPanel.isActiveAndEnabled); + HudManager.Instance.SetHudActive(_player, _player.Data.Role, HudManager.Instance.TaskPanel.isActiveAndEnabled); } _toAdd.Clear(); _toRemove.Clear(); @@ -101,7 +110,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) modifier.Update(); } - if (!_modifierText || _player?.AmOwner == false) + if (!_modifierText || !_player.AmOwner) { return; } @@ -125,7 +134,13 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) builder.AppendLine(modifier.GetHudString()); } - _modifierText!.text = showText ? builder.ToString() : string.Empty; + _modifierText.text = showText ? builder.ToString() : string.Empty; + } + + private void OnDestroy() + { + // Remove this component from the global list when destroyed + ModifierExtensions.ModifierComponents.Remove(_player); } /// diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index feb2921..0f53f9f 100644 --- a/MiraAPI/Patches/PlayerControlPatches.cs +++ b/MiraAPI/Patches/PlayerControlPatches.cs @@ -15,14 +15,11 @@ namespace MiraAPI.Patches; /// General patches for the PlayerControl class. /// [HarmonyPatch(typeof(PlayerControl))] -public static class PlayerControlPatches +internal static class PlayerControlPatches { - /// - /// Adds the modifier component to the player on start. - /// - /// PlayerControl instance. [HarmonyPostfix] [HarmonyPatch(nameof(PlayerControl.Start))] + // ReSharper disable once InconsistentNaming public static void PlayerControlStartPostfix(PlayerControl __instance) { if (__instance.gameObject.TryGetComponent(out var modifierComp)) @@ -39,12 +36,9 @@ public static class PlayerControlPatches __instance.gameObject.AddComponent(); } - /// - /// Calls the OnDeath method for all active modifiers. - /// - /// PlayerControl instance. [HarmonyPostfix] [HarmonyPatch(nameof(PlayerControl.Die))] + // ReSharper disable once InconsistentNaming public static void PlayerControlDiePostfix(PlayerControl __instance, DeathReason reason) { var deathEvent = new PlayerDeathEvent(__instance, reason); @@ -58,13 +52,9 @@ public static class PlayerControlPatches } } - /// - /// Used to trigger the . - /// - /// PlayerControl instance. - /// The task id. [HarmonyPostfix] [HarmonyPatch(nameof(PlayerControl.CompleteTask))] + // ReSharper disable once InconsistentNaming public static void PlayerCompleteTaskPostfix(PlayerControl __instance, uint idx) { var playerTask = __instance.myTasks.ToArray().First(playerTask => playerTask.Id == idx); @@ -75,14 +65,9 @@ public static class PlayerControlPatches } } - /// - /// Used to trigger the . - /// - /// The source player. - /// The target. - /// Whether the kill succeeded. [HarmonyPrefix] [HarmonyPatch(nameof(PlayerControl.RpcMurderPlayer))] + // ReSharper disable once InconsistentNaming public static void PlayerControlMurderPrefix(PlayerControl __instance, PlayerControl target, ref bool didSucceed) { var beforeMurderEvent = new BeforeMurderEvent(__instance, target); @@ -91,12 +76,9 @@ public static class PlayerControlPatches didSucceed = beforeMurderEvent.IsCancelled; } - /// - /// FixedUpdate handler for custom roles and custom buttons. - /// - /// PlayerControl instance. [HarmonyPostfix] [HarmonyPatch(nameof(PlayerControl.FixedUpdate))] + // ReSharper disable once InconsistentNaming public static void PlayerControlFixedUpdatePostfix(PlayerControl __instance) { if (__instance.Data?.Role is ICustomRole customRole) @@ -124,15 +106,4 @@ public static class PlayerControlPatches button.FixedUpdateHandler(__instance); } } - - /// - /// Clear from modifier component cache. - /// - /// PlayerControl instance. - [HarmonyPrefix] - [HarmonyPatch(nameof(PlayerControl.OnDestroy))] - public static void PlayerControlOnDestroyPrefix(PlayerControl __instance) - { - ModifierExtensions.ModifierComponents.Remove(__instance); - } } From 51b54d4340c14a6eca9a07bd669134c724423ba1 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 29 Mar 2025 13:12:19 -0400 Subject: [PATCH 35/38] reorganize vent patch --- .../Patches/{VentPatches.cs => VentCanUsePatch.cs} | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) rename MiraAPI/Patches/{VentPatches.cs => VentCanUsePatch.cs} (78%) diff --git a/MiraAPI/Patches/VentPatches.cs b/MiraAPI/Patches/VentCanUsePatch.cs similarity index 78% rename from MiraAPI/Patches/VentPatches.cs rename to MiraAPI/Patches/VentCanUsePatch.cs index 2f8991d..b285411 100644 --- a/MiraAPI/Patches/VentPatches.cs +++ b/MiraAPI/Patches/VentCanUsePatch.cs @@ -8,17 +8,13 @@ using UnityEngine; namespace MiraAPI.Patches; /// -/// Vent patches to make sure the player is able to use the vent. +/// Used to change vent behaviour for the event system, custom roles, and modifiers. /// -[HarmonyPatch(typeof(Vent))] -public static class VentPatches +[HarmonyPatch(typeof(Vent), nameof(Vent.CanUse))] +internal static class VentCanUsePatch { - /// - /// CanUse patch. - /// - [HarmonyPostfix] - [HarmonyPatch(nameof(Vent.CanUse))] - public static void VentCanUsePostfix(Vent __instance, ref float __result, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] ref bool canUse, [HarmonyArgument(2)] ref bool couldUse) + // ReSharper disable InconsistentNaming + public static void Postfix(Vent __instance, ref float __result, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] ref bool canUse, [HarmonyArgument(2)] ref bool couldUse) { var @event = new PlayerCanUseEvent(__instance.Cast()); MiraEventManager.InvokeEvent(@event); From b28a0a125374c6944324c970fe7367f42d4c1099 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 29 Mar 2025 13:35:16 -0400 Subject: [PATCH 36/38] cleanup CustomPlayerMenu.cs --- MiraAPI/Hud/CustomPlayerMenu.cs | 54 ++++++++++++++++----------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs index 27f52ac..56d9f0e 100644 --- a/MiraAPI/Hud/CustomPlayerMenu.cs +++ b/MiraAPI/Hud/CustomPlayerMenu.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using AmongUs.GameOptions; using Il2CppInterop.Runtime.Attributes; @@ -8,6 +9,8 @@ using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; using UnityEngine; using UnityEngine.Events; +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. namespace MiraAPI.Hud; @@ -16,22 +19,18 @@ namespace MiraAPI.Hud; /// /// Used by Il2Cpp. Do not use constructor, this is a MonoBehaviour. [RegisterInIl2Cpp] +[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention")] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Unity Convention")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")] public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) { - public ShapeshifterPanel PanelPrefab; - - public float XStart = -0.8f; - - public float YStart = 2.15f; - - public float XOffset = 1.95f; - - public float YOffset = -0.65f; - - public UiElement BackButton; - - public UiElement DefaultButtonSelected; - + public ShapeshifterPanel panelPrefab; + public float xStart = -0.8f; + public float yStart = 2.15f; + public float xOffset = 1.95f; + public float yOffset = -0.65f; + public UiElement backButton; + public UiElement defaultButtonSelected; public List potentialVictims; /// @@ -42,17 +41,17 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) { var shapeShifterRole = RoleManager.Instance.GetRole(RoleTypes.Shapeshifter); - var ogMenu = shapeShifterRole.TryCast().ShapeshifterMenu; + var ogMenu = shapeShifterRole.TryCast()!.ShapeshifterMenu; var newMenu = Instantiate(ogMenu); var customMenu = newMenu.gameObject.AddComponent(); - customMenu.PanelPrefab = newMenu.PanelPrefab; - customMenu.XStart = newMenu.XStart; - customMenu.YStart = newMenu.YStart; - customMenu.XOffset = newMenu.XOffset; - customMenu.YOffset = newMenu.YOffset; - customMenu.BackButton = newMenu.BackButton; - var back = customMenu.BackButton.GetComponent(); + customMenu.panelPrefab = newMenu.PanelPrefab; + customMenu.xStart = newMenu.XStart; + customMenu.yStart = newMenu.YStart; + customMenu.xOffset = newMenu.XOffset; + customMenu.yOffset = newMenu.YOffset; + customMenu.backButton = newMenu.BackButton; + var back = customMenu.backButton.GetComponent(); back.OnClick.RemoveAllListeners(); back.OnClick.AddListener((UnityAction)(() => { @@ -65,7 +64,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) newMenu.DestroyImmediate(); - customMenu.transform.SetParent(Camera.main.transform, false); + customMenu.transform.SetParent(Camera.main!.transform, false); customMenu.transform.localPosition = new Vector3(0f, 0f, -50f); return customMenu; } @@ -75,6 +74,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) ControllerManager.Instance.CloseOverlayMenu(name); } + /// public override void Begin(PlayerTask task) { throw new NotImplementedException("Use the other Begin method."); @@ -90,7 +90,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) { MinigameStubs.Begin(this, null); - var back = BackButton.GetComponent(); + var back = backButton.GetComponent(); back.OnClick.AddListener((UnityAction)(() => { onClick(null); @@ -107,13 +107,13 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) var num = i % 3; var num2 = i / 3; var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor; - var shapeshifterPanel = Instantiate(PanelPrefab, transform); - shapeshifterPanel.transform.localPosition = new Vector3(XStart + num * XOffset, YStart + num2 * YOffset, -1f); + var shapeshifterPanel = Instantiate(panelPrefab, transform); + shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f); shapeshifterPanel.SetPlayer(i, player.Data, (Il2CppSystem.Action)(() => { onClick(player); })); shapeshifterPanel.NameText.color = flag ? player.Data.Role.NameColor : Color.white; potentialVictims.Add(shapeshifterPanel); list2.Add(shapeshifterPanel.Button); } - ControllerManager.Instance.OpenOverlayMenu(name, BackButton, DefaultButtonSelected, list2); + ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2); } } From 6d51e6806aa6f779f37a2bd15d392129720197b1 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 29 Mar 2025 13:37:06 -0400 Subject: [PATCH 37/38] fix warning in role stubs --- MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs b/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs index 0248aaa..fe04962 100644 --- a/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs +++ b/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs @@ -4,6 +4,9 @@ using Il2CppSystem.Text; namespace MiraAPI.Patches.Stubs; +/// +/// Stub methods for the RoleBehaviour class. Needed because of Il2Cpp limitations on injected classes. +/// [HarmonyPatch] public static class RoleBehaviourStubs { @@ -57,7 +60,7 @@ public static class RoleBehaviourStubs [MethodImpl(MethodImplOptions.NoInlining)] public static bool CanUse(RoleBehaviour instance, IUsable console) { - return default; + return false; } /// @@ -84,7 +87,7 @@ public static class RoleBehaviourStubs [MethodImpl(MethodImplOptions.NoInlining)] public static bool DidWin(RoleBehaviour instance, GameOverReason gameOverReason) { - return default; + return false; } /// @@ -97,7 +100,7 @@ public static class RoleBehaviourStubs [MethodImpl(MethodImplOptions.NoInlining)] public static PlayerControl FindClosestTarget(RoleBehaviour instance) { - return null; + return null!; } /// @@ -110,7 +113,7 @@ public static class RoleBehaviourStubs [MethodImpl(MethodImplOptions.NoInlining)] public static float GetAbilityDistance(RoleBehaviour instance) { - return default; + return 0; } /// @@ -124,7 +127,7 @@ public static class RoleBehaviourStubs [MethodImpl(MethodImplOptions.NoInlining)] public static bool IsValidTarget(RoleBehaviour instance, NetworkedPlayerInfo target) { - return default; + return false; } /// From 4fad586aa78013de2bfbee987b8f3ffadbc8082a Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Sat, 29 Mar 2025 13:53:58 -0400 Subject: [PATCH 38/38] use new alternate color in lobby --- MiraAPI/Patches/Options/LobbyViewPanePatches.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index cd948c3..8182aa7 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -335,7 +335,7 @@ public static class LobbyViewPanePatches viewSettingsInfoPanelRoleVariant.chanceTitle.color = viewSettingsInfoPanelRoleVariant.chanceBackground.color = viewSettingsInfoPanelRoleVariant.background.color = - customRole.RoleColor.GetAlternateColor(); + customRole.RoleColor.FindAlternateColor(); instance.settingsInfo.Add(viewSettingsInfoPanelRoleVariant.gameObject); num -= 0.664f; }