From 7bbbd7597df18e04155ca42cf8f61faf227ac2c9 Mon Sep 17 00:00:00 2001 From: wanderingpix Date: Sat, 22 Aug 2026 13:32:14 +0100 Subject: [PATCH] Refactor + untargeted meeting abilities --- .../MeetingAbilities/EndMeetingButton.cs | 33 ++ ...tButton.cs => MultiTargetMeetingButton.cs} | 2 +- .../MeetingAbilityBehaviour.cs | 2 +- .../MeetingAbilities/MeetingActionButton.cs | 321 ++++++++++++++++++ .../MeetingAbilities/MeetingButtonManager.cs | 117 +++++++ .../MultiTargetMeetingButton.cs | 4 +- .../MeetingAbilities/TargetedMeetingButton.cs | 8 +- .../TargetedMeetingButtonManager.cs | 33 -- .../TargetedMeetingButtonSingleton.cs | 2 +- .../Patches/Events/FreeplayRoundStartPatch.cs | 5 +- MiraAPI/Patches/IntroCutscenePatches.cs | 6 +- MiraAPI/Patches/Voting/MeetingHudPatches.cs | 27 +- MiraAPI/PluginLoading/MiraPluginInfo.cs | 16 +- MiraAPI/PluginLoading/MiraPluginManager.cs | 21 +- .../Components/CenteredGridArrange.cs | 76 +++++ 15 files changed, 600 insertions(+), 73 deletions(-) create mode 100644 MiraAPI.Example/MeetingAbilities/EndMeetingButton.cs rename MiraAPI/Hud/{MultiTargetButton.cs => MultiTargetMeetingButton.cs} (96%) create mode 100644 MiraAPI/MeetingAbilities/MeetingActionButton.cs create mode 100644 MiraAPI/MeetingAbilities/MeetingButtonManager.cs delete mode 100644 MiraAPI/MeetingAbilities/TargetedMeetingButtonManager.cs create mode 100644 MiraAPI/Utilities/Components/CenteredGridArrange.cs diff --git a/MiraAPI.Example/MeetingAbilities/EndMeetingButton.cs b/MiraAPI.Example/MeetingAbilities/EndMeetingButton.cs new file mode 100644 index 0000000..0d39027 --- /dev/null +++ b/MiraAPI.Example/MeetingAbilities/EndMeetingButton.cs @@ -0,0 +1,33 @@ +using MiraAPI.Example.Modifiers; +using MiraAPI.Example.Roles; +using MiraAPI.Hud; +using MiraAPI.Keybinds; +using MiraAPI.MeetingAbilities; +using MiraAPI.Modifiers; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.Example.MeetingAbilities; + +public class EndMeetingButton : MeetingActionButton +{ + public override string Name => "End Meeting"; + + public override float Cooldown => 15; + + public override float InitialCooldown => 30; + + public override int MaxUses => 3; + + public override LoadableAsset Sprite => ExampleAssets.ExampleButton; + + public override bool Enabled(RoleBehaviour? role) + { + return PlayerControl.LocalPlayer != null && role is MayorRole; + } + + protected override void OnClick() + { + MeetingHud.Instance.RpcClose(); + } +} diff --git a/MiraAPI/Hud/MultiTargetButton.cs b/MiraAPI/Hud/MultiTargetMeetingButton.cs similarity index 96% rename from MiraAPI/Hud/MultiTargetButton.cs rename to MiraAPI/Hud/MultiTargetMeetingButton.cs index d52d845..2a36219 100644 --- a/MiraAPI/Hud/MultiTargetButton.cs +++ b/MiraAPI/Hud/MultiTargetMeetingButton.cs @@ -8,7 +8,7 @@ namespace MiraAPI.Hud; /// that has multiple target objects. /// /// The type of the target objects. -public abstract class MultiTargetButton : CustomActionButton where T : MonoBehaviour +public abstract class MultiTargetMeetingButton : CustomActionButton where T : MonoBehaviour { /// /// Gets or sets the list of targets of the button. diff --git a/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs b/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs index e390b8d..2df5139 100644 --- a/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs +++ b/MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs @@ -96,7 +96,7 @@ public class MeetingAbilityBehaviour : MonoBehaviour { Renderer.material.SetFloat("_Desat", 0.0f); CooldownText.text = string.Empty; - UsesText.text = _button.LimitedUses ? _button.UsesRemaining.ToString(CultureInfo.InvariantCulture) : string.Empty; + UsesText.text = _button.LimitedUses ? _button.UsesLeft.ToString(CultureInfo.InvariantCulture) : string.Empty; } else { diff --git a/MiraAPI/MeetingAbilities/MeetingActionButton.cs b/MiraAPI/MeetingAbilities/MeetingActionButton.cs new file mode 100644 index 0000000..88cc799 --- /dev/null +++ b/MiraAPI/MeetingAbilities/MeetingActionButton.cs @@ -0,0 +1,321 @@ +using System; +using MiraAPI.Events; +using MiraAPI.Events.Mira; +using MiraAPI.Hud; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.UI; +using Object = UnityEngine.Object; + +namespace MiraAPI.MeetingAbilities; + +/// +/// Class for making custom meeting action buttons. +/// +public abstract class MeetingActionButton +{ + /// + /// Gets the name and text of the button. + /// + public abstract string Name { get; } + + /// + /// Gets the initial cooldown duration in seconds. + /// + public virtual float InitialCooldown => Cooldown; + + /// + /// Gets the button's cooldown duration in seconds. + /// + public abstract float Cooldown { get; } + + /// + /// Gets the sprite of the button. Use to load a sprite from a resource path. Use to load a sprite from an asset bundle. + /// + public abstract LoadableAsset Sprite { get; } + + /// + /// Gets the format string for the cooldown timer. + /// + public virtual string CooldownTimerFormatString => "0"; + + /// + /// Gets a value indicating whether the button has limited uses. + /// + public bool LimitedUses => ZeroIsInfinite ? MaxUses > 0 : MaxUses >= 0; + + /// + /// Gets the maximum number of uses the button has. If the button has infinite uses, set to 0 or -1 based on what value that ZeroIsInfinite is set to. + /// + public virtual int MaxUses => ZeroIsInfinite ? 0 : -1; + + /// + /// Gets the value indicating uses mode. + /// + public virtual MeetingButtonUsesMode UsesMode => MeetingButtonUsesMode.PerGame; + + /// + /// Gets the button's text outline . + /// + public virtual Color TextOutlineColor => Color.clear; + + /// + /// Gets or sets a value indicating whether limited uses are determined via zero or a negative number of uses. + /// + public virtual bool ZeroIsInfinite { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the timer is currently active. + /// + public bool TimerPaused { get; set; } + + /// + /// Gets or sets the amount of uses left. + /// + public int UsesLeft { get; set; } + + /// + /// Gets or sets the timer variable to measure cooldowns and effects. + /// + public float Timer { get; set; } + + /// + /// Gets or sets the object in game. This is created by Mira API automatically. + /// + public ActionButton? Button { get; set; } + + /// + /// The method used to create the button. + /// + /// The parent of the button. + public virtual void CreateButton(Transform parent) + { + if (Button) + { + return; + } + + Timer = AmongUsClient.Instance?.NetworkMode == NetworkModes.FreePlay ? 0 : InitialCooldown; + TimerPaused = false; + + Button = Object.Instantiate(MeetingHud.Instance.MeetingAbilityButton, parent); + Button.name = Name + "Button"; + Button.OverrideText(Name.ToUpperInvariant()); + + Button.graphic.sprite = Sprite.LoadAsset(); + + Button.SetUsesRemaining(UsesLeft); + if (MaxUses <= 0) + { + Button.SetInfiniteUses(); + } + + if (TextOutlineColor != Color.clear) + { + SetTextOutline(TextOutlineColor); + } + + var pb = Button.GetComponent(); + pb.OnClick = new Button.ButtonClickedEvent(); + pb.OnClick.AddListener((UnityAction)(() => + { + ClickHandler(); + })); + Button.gameObject.SetActive(Enabled(PlayerControl.LocalPlayer.Data.Role)); + } + + /// + /// A utility function to change the outline of the button's text. + /// + /// The new . + public virtual void SetTextOutline(Color color) + { + Button?.buttonLabelText.SetOutlineColor(color); + } + + /// + /// A utility function to override the of the button. + /// + /// The new to override with. + public virtual void OverrideSprite(Sprite sprite) + { + if (Button != null) + { + Button.graphic.sprite = sprite; + } + } + + /// + /// A utility function to override the name of the button. + /// + /// The new name to override with. + public virtual void OverrideName(string name) + { + Button?.OverrideText(name); + } + + /// + /// Set the button's timer. + /// + /// The time you want to set to. + public virtual void SetTimer(float time) + { + Timer = Mathf.Clamp(time, -1, float.MaxValue); + } + + /// + /// Increase the button's timer. + /// + /// The amount you want to increase by. + public virtual void IncreaseTimer(float amount) + { + SetTimer(Timer + amount); + } + + /// + /// Decrease the button's timer. + /// + /// The amount you want to decrease by. + public virtual void DecreaseTimer(float amount) + { + SetTimer(Timer - amount); + } + + /// + /// Sets whether the timer is paused or not. + /// + /// Whether you want to pause/resume the timer. + public virtual void SetTimerPaused(bool val) + { + TimerPaused = val; + } + + /// + /// Set the amount of uses this button has left. + /// + /// The amount you want to set to. + public virtual void SetUses(int amount) + { + UsesLeft = Mathf.Clamp(amount, 0, int.MaxValue); + Button?.SetUsesRemaining(UsesLeft); + + if (Button != null) + { + Button.usesRemainingSprite.color = UsesLeft == 0 ? Color.red : Color.white; + } + } + + /// + /// Increase the amount of uses this button has left. + /// + /// The amount you want to increase by. Default: 1. + public virtual void IncreaseUses(int amount = 1) + { + SetUses(UsesLeft + amount); + } + + /// + /// Decrease the amount of uses this button has left. + /// + /// The amount you want to decrease by. Default: 1. + public virtual void DecreaseUses(int amount = 1) + { + SetUses(UsesLeft - amount); + } + + /// + /// A utility function that runs with the local player's if the button is enabled. + /// + /// the instance. + protected virtual void FixedUpdate(MeetingHud hud) + { + } + + /// + /// Callback method for the button click event. + /// + protected abstract void OnClick(); + + /// + /// This method determines if the button should be active or not. + /// means the button is active, means the button is disabled. + /// + /// The of the local player. + /// if the button is enabled, otherwise. + public abstract bool Enabled(RoleBehaviour? role); + + /// + /// When the button is usable, this method is called to determine if the button can be clicked. + /// By default, it takes into account the timer, effect, and uses. + /// You can override it to change the behavior. + /// + /// A value that represents whether the button can be clicked or not. + public virtual bool CanClick() + { + return Timer <= 0 && CanUse(); + } + + /// + /// Whether the button should light up or not. This is also the base for CanClick. + /// You can override it to change the behaviour. Do not include timer in here, that is for CanClick. + /// + /// A value that represents whether the button should light up or not. + public virtual bool CanUse() + { + return !LimitedUses || UsesLeft > 0; + } + + /// + /// This method handles the button click event. It is a wrapper for the method. + /// This method takes into account cooldowns, effects, and uses, before calling the method. + /// It can be overridden for custom behavior. + /// + public virtual void ClickHandler() + { + if (!CanClick()) + { + return; + } + + if (LimitedUses) + { + UsesLeft--; + Button?.SetUsesRemaining(UsesLeft); + } + + Timer = Cooldown; + OnClick(); + } + + /// + /// This method is called on the method. It is a wrapper for the method. + /// By default, it handles the cooldown and effect timers, and sets the button to enabled or disabled. + /// It can be overridden for custom behavior. + /// + /// The instance. + public virtual void UpdateHandler(MeetingHud hud) + { + if (Timer >= 0 && !TimerPaused) + { + Timer -= Time.deltaTime; + } + + if (Button) + { + if (CanUse()) + { + Button!.SetEnabled(); + } + else + { + Button!.SetDisabled(); + } + Button.SetCooldownFormat(Timer, Cooldown, CooldownTimerFormatString); + Button.SetCoolDown(Timer, Cooldown); + } + + FixedUpdate(hud); + } +} diff --git a/MiraAPI/MeetingAbilities/MeetingButtonManager.cs b/MiraAPI/MeetingAbilities/MeetingButtonManager.cs new file mode 100644 index 0000000..b45e00b --- /dev/null +++ b/MiraAPI/MeetingAbilities/MeetingButtonManager.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MiraAPI.Modifiers; +using MiraAPI.Modifiers.Types; +using MiraAPI.PluginLoading; +using MiraAPI.Utilities.Assets; +using MiraAPI.Utilities.Components; +using UnityEngine; + +namespace MiraAPI.MeetingAbilities; + +/// +/// Class for managing all registered meeting abilities. +/// +public static class MeetingButtonManager +{ + /// + /// Gets a list if==of all registered . + /// + public static List TargetedButtons { get; internal set; } = new(); + + /// + /// Gets a list if==of all registered . + /// + public static List UntargetedButtons { get; internal set; } = new(); + + internal static bool RegisterTargetedMeetingButton(Type type, MiraPluginInfo info) + { + if (!typeof(TargetedMeetingButton).IsAssignableFrom(type)) + { + return false; + } + + var button = Activator.CreateInstance(type) as TargetedMeetingButton; + info.InternalTargetedMeetingButtons.Add(button); + TargetedButtons.Add(button); + return true; + } + + internal static bool RegisterMeetingButton(Type type, MiraPluginInfo info) + { + if (!typeof(MeetingActionButton).IsAssignableFrom(type)) + { + return false; + } + + var button = Activator.CreateInstance(type) as MeetingActionButton; + info.InternalMeetingButtons.Add(button); + UntargetedButtons.Add(button); + return true; + } + + private static CenteredGridArrange? actionButtonsParent = null!; + + public static void OnGameStart() + { + foreach (var ability in TargetedButtons.Where(x => x.UsesMode == MeetingButtonUsesMode.PerGame)) + { + ability.UsesLeft = ability.MaxUses; + } + foreach (var ability in UntargetedButtons.Where(x => x.UsesMode == MeetingButtonUsesMode.PerGame)) + { + ability.UsesLeft = ability.MaxUses; + } + } + + /// + /// Called on . Handles button creation, and ability uses. + /// + /// The instance. + public static void OnMeetingStart(MeetingHud meetingHud) + { + // Untargeted buttons parent. + actionButtonsParent = new GameObject("ActionsButtonParent").AddComponent(); + actionButtonsParent.transform.SetParent(meetingHud.MeetingAbilityButton.transform.parent, meetingHud.MeetingAbilityButton.transform.parent); + actionButtonsParent.CellSize = new Vector2(0.7f, 0.7f); + actionButtonsParent.transform.localPosition = new Vector3(0, -2.1f, -10); + meetingHud.MeetingAbilityButton.transform.SetParent(actionButtonsParent.transform); + + // Untargeted buttons creation. + foreach (var ability in UntargetedButtons) + { + try + { + ability.CreateButton(actionButtonsParent.transform); + if (ability.UsesMode == MeetingButtonUsesMode.PerMeeting) ability.UsesLeft = ability.MaxUses; + ability.Timer = ability.InitialCooldown; + } + catch (Exception e) + { + Error($"Failed to create meeting button {ability.Name}: {e.Message}"); + } + } + + // Targeted buttons creation. + foreach (var ability in TargetedButtons) + { + try + { + foreach (var playerVoteArea in meetingHud.playerStates) + { + var btn = ability.CreateButton(playerVoteArea); + btn.transform.SetParent(playerVoteArea.Buttons.transform); + playerVoteArea.CancelButton.transform.SetAsFirstSibling(); + } + + if (ability.UsesMode == MeetingButtonUsesMode.PerMeeting) ability.UsesLeft = ability.MaxUses; + if (ability is MultiTargetMeetingButton multiAbility) multiAbility.Targets = new(); + } + catch (Exception e) + { + Error($"Failed to create targeted meeting button {ability.Name}: {e.Message}"); + } + } + } +} diff --git a/MiraAPI/MeetingAbilities/MultiTargetMeetingButton.cs b/MiraAPI/MeetingAbilities/MultiTargetMeetingButton.cs index 34ee068..b4dcffa 100644 --- a/MiraAPI/MeetingAbilities/MultiTargetMeetingButton.cs +++ b/MiraAPI/MeetingAbilities/MultiTargetMeetingButton.cs @@ -30,7 +30,7 @@ public abstract class MultiTargetMeetingButton : TargetedMeetingButton /// public override void ClickHandler(MeetingAbilityBehaviour button, PlayerVoteArea playerVoteArea) { - if (UsesRemaining <= 0 && LimitedUses) return; + if (UsesLeft <= 0 && LimitedUses) return; if (Timer > 0) return; var toggle = !Targets.Remove(playerVoteArea); if (toggle) Targets.Add(playerVoteArea, button); @@ -55,7 +55,7 @@ public abstract class MultiTargetMeetingButton : TargetedMeetingButton button.Renderer.sprite = Sprite.LoadAsset(); } Timer = Cooldown; - UsesRemaining -= 1; + UsesLeft -= 1; OnFinish(); Targets = new(); } diff --git a/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs b/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs index 4b60f3e..324e7c7 100644 --- a/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs +++ b/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs @@ -24,7 +24,7 @@ public abstract class TargetedMeetingButton /// /// Gets or sets the amount of uses left for this button. /// - public int UsesRemaining { get; set; } + public int UsesLeft { get; set; } /// /// Gets a value indicating whether the has limited uses. @@ -65,7 +65,7 @@ public abstract class TargetedMeetingButton /// /// Gets the for this button. /// - public virtual MeetingButtonUsesMode ButtonUsesMode { get; } = MeetingButtonUsesMode.PerGame; + public virtual MeetingButtonUsesMode UsesMode { get; } = MeetingButtonUsesMode.PerGame; /// /// Determines if the button is enabled. @@ -135,10 +135,10 @@ public abstract class TargetedMeetingButton /// The target . public virtual void ClickHandler(MeetingAbilityBehaviour button, PlayerVoteArea playerVoteArea) { - if (UsesRemaining <= 0 && LimitedUses) return; + if (UsesLeft <= 0 && LimitedUses) return; if (Timer > 0) return; Timer = Cooldown; - UsesRemaining--; + UsesLeft--; playerVoteArea.Cancel(); OnClick(playerVoteArea); } diff --git a/MiraAPI/MeetingAbilities/TargetedMeetingButtonManager.cs b/MiraAPI/MeetingAbilities/TargetedMeetingButtonManager.cs deleted file mode 100644 index 9d6e4b3..0000000 --- a/MiraAPI/MeetingAbilities/TargetedMeetingButtonManager.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using MiraAPI.Modifiers; -using MiraAPI.Modifiers.Types; -using MiraAPI.PluginLoading; -using MiraAPI.Utilities.Assets; -using UnityEngine; - -namespace MiraAPI.MeetingAbilities; - -/// -/// Class for managing all registered meeting abilities. -/// -public static class TargetedMeetingButtonManager -{ - /// - /// Gets a list if==of all registered . - /// - public static List Buttons { get; internal set; } = new(); - - internal static bool RegisterMeetingAbility(Type type, MiraPluginInfo info) - { - if (!typeof(TargetedMeetingButton).IsAssignableFrom(type)) - { - return false; - } - - var button = Activator.CreateInstance(type) as TargetedMeetingButton; - info.InternalMeetingAbilities.Add(button); - Buttons.Add(button); - return true; - } -} diff --git a/MiraAPI/MeetingAbilities/TargetedMeetingButtonSingleton.cs b/MiraAPI/MeetingAbilities/TargetedMeetingButtonSingleton.cs index eab3968..47dc791 100644 --- a/MiraAPI/MeetingAbilities/TargetedMeetingButtonSingleton.cs +++ b/MiraAPI/MeetingAbilities/TargetedMeetingButtonSingleton.cs @@ -15,5 +15,5 @@ public static class TargetedMeetingButtonSingleton where T : TargetedMeetingB /// Gets the instance of the button. /// [SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a utility class to get the instance of a targeted meeting button.")] - public static T Instance => _instance ??= TargetedMeetingButtonManager.Buttons.OfType().Single(); + public static T Instance => _instance ??= MeetingButtonManager.TargetedButtons.OfType().Single(); } diff --git a/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs b/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs index 36d4b18..0f582e1 100644 --- a/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs +++ b/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs @@ -20,10 +20,7 @@ public static class FreeplayRoundStartPatch { if (!__result) { - foreach (var ability in TargetedMeetingButtonManager.Buttons.Where(x => x.ButtonUsesMode == MeetingButtonUsesMode.PerGame)) - { - ability.UsesRemaining = ability.MaxUses; - } + MeetingButtonManager.OnGameStart(); MiraEventManager.InvokeEvent(new RoundStartEvent(true)); } } diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs index e12de85..f90f7ce 100644 --- a/MiraAPI/Patches/IntroCutscenePatches.cs +++ b/MiraAPI/Patches/IntroCutscenePatches.cs @@ -125,16 +125,12 @@ public static class IntroCutscenePatches Info("IntroCutscene ended"); + MeetingButtonManager.OnGameStart(); MiraEventManager.InvokeEvent(new IntroEndEvent(introCutscene)); var @event = new BeforeRoundStartEvent(true); MiraEventManager.InvokeEvent(@event); - foreach (var ability in TargetedMeetingButtonManager.Buttons.Where(x => x.ButtonUsesMode == MeetingButtonUsesMode.PerGame)) - { - ability.UsesRemaining = ability.MaxUses; - } - if (@event.IsCancelled) return; MiraEventManager.InvokeEvent(new RoundStartEvent(true)); } diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs index efe167d..4158955 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -10,6 +10,7 @@ using MiraAPI.Events.Vanilla.Meeting.Voting; using MiraAPI.MeetingAbilities; using MiraAPI.Modifiers; using MiraAPI.Utilities; +using MiraAPI.Utilities.Components; using MiraAPI.Voting; using UnityEngine; @@ -37,19 +38,6 @@ internal static class MeetingHudPatches return true; } - [HarmonyPrefix] - [HarmonyPatch(typeof(PlayerVoteArea), nameof(PlayerVoteArea.Start))] - public static void VoteAreaStartPostfix(PlayerVoteArea __instance) - { - foreach (var targetedMeetingAbility in TargetedMeetingButtonManager.Buttons) - { - var btn = targetedMeetingAbility.CreateButton(__instance); - btn.transform.SetParent(__instance.Buttons.transform); - btn.gameObject.SetActive(targetedMeetingAbility.Enabled(PlayerControl.LocalPlayer.Data.Role) && targetedMeetingAbility.IsTargetValid(__instance)); - } - __instance.CancelButton.transform.SetAsFirstSibling(); - } - [HarmonyPrefix] [HarmonyPatch(typeof(PlayerVoteArea), nameof(PlayerVoteArea.Select))] public static bool VoteAreaSelectPatch(PlayerVoteArea __instance) @@ -89,6 +77,7 @@ internal static class MeetingHudPatches [HarmonyPatch(nameof(MeetingHud.Start))] public static void MeetingHudStartPatch(MeetingHud __instance) { + MeetingButtonManager.OnMeetingStart(__instance); foreach (var plr in PlayerControl.AllPlayerControls) { var voteData = plr.GetVoteData(); @@ -106,11 +95,6 @@ internal static class MeetingHudPatches } } - foreach (var ability in TargetedMeetingButtonManager.Buttons) - { - if (ability.ButtonUsesMode == MeetingButtonUsesMode.PerMeeting) ability.UsesRemaining = ability.MaxUses; - if (ability is MultiTargetMeetingButton multiAbility) multiAbility.Targets = new(); - } var @event = new StartMeetingEvent(__instance); MiraEventManager.InvokeEvent(@event); } @@ -134,11 +118,16 @@ internal static class MeetingHudPatches [HarmonyPatch(nameof(MeetingHud.Update))] public static void ForceSkipPatch(MeetingHud __instance) { - foreach (var targetedMeetingAbility in TargetedMeetingButtonManager.Buttons.Where(x => x.Enabled(PlayerControl.LocalPlayer.Data.Role))) + foreach (var targetedMeetingAbility in MeetingButtonManager.TargetedButtons.Where(x => x.Enabled(PlayerControl.LocalPlayer.Data.Role))) { targetedMeetingAbility.UpdateHandler(); } + foreach (var meetingAbility in MeetingButtonManager.UntargetedButtons.Where(x => x.Enabled(PlayerControl.LocalPlayer.Data.Role))) + { + meetingAbility.UpdateHandler(__instance); + } + if (__instance.state is not (MeetingHud.MeetingStates.NotVoted or MeetingHud.MeetingStates.Voted)) { return; diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index ba4d74e..17e5940 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -50,6 +50,16 @@ public class MiraPluginInfo /// public IReadOnlyCollection Buttons { get; private set; } = null!; + /// + /// Gets a of this 's . + /// + public IReadOnlyCollection TargetedMeetingButtons { get; private set; } = null!; + + /// + /// Gets a of this 's . + /// + public IReadOnlyCollection MeetingButtons { get; private set; } = null!; + /// /// Gets a of this 's s. /// @@ -63,6 +73,8 @@ public class MiraPluginInfo Options = [..InternalOptions]; Roles = new ReadOnlyDictionary(InternalRoles); Buttons = [..InternalButtons]; + MeetingButtons = [..InternalMeetingButtons]; + TargetedMeetingButtons = [..InternalTargetedMeetingButtons]; } internal List InternalPresets { get; } = []; @@ -79,7 +91,9 @@ public class MiraPluginInfo internal List InternalButtons { get; } = []; - internal List InternalMeetingAbilities { get; } = []; + internal List InternalTargetedMeetingButtons { get; } = []; + + internal List InternalMeetingButtons { get; } = []; /// /// Gets the plugin's ID, as defined in the plugin's BepInEx metadata. diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 67d97e2..2ce1641 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -118,12 +118,17 @@ public sealed class MiraPluginManager { continue; } - + if (RegisterMeetingAbility(type, info)) { continue; } + if (RegisterTargetedMeetingAbility(type, info)) + { + continue; + } + RegisterColorClasses(type); RegisterKeybinds(type, plugin); } @@ -454,7 +459,7 @@ public sealed class MiraPluginManager { try { - return TargetedMeetingButtonManager.RegisterMeetingAbility(type, info); + return MeetingButtonManager.RegisterMeetingButton(type, info); } catch (Exception e) { @@ -462,4 +467,16 @@ public sealed class MiraPluginManager return false; } } + private static bool RegisterTargetedMeetingAbility(Type type, MiraPluginInfo info) + { + try + { + return MeetingButtonManager.RegisterTargetedMeetingButton(type, info); + } + catch (Exception e) + { + Error($"Failed to register targeted meeting ability {type.Name}: {e}"); + return false; + } + } } diff --git a/MiraAPI/Utilities/Components/CenteredGridArrange.cs b/MiraAPI/Utilities/Components/CenteredGridArrange.cs new file mode 100644 index 0000000..8daa8b1 --- /dev/null +++ b/MiraAPI/Utilities/Components/CenteredGridArrange.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI.Utilities.Components; + +[RegisterInIl2Cpp] +public class CenteredGridArrange(IntPtr ptr) : MonoBehaviour(ptr) +{ + public Vector2 CellSize; + public int MaxColumns = 3; + + private List cells; + private static List currentChildren = new List(); + + private void Start() + { + cells = new List(); + GetChildsActive(); + CheckCurrentChildren(); + } + + private void FixedUpdate() => CheckCurrentChildren(); + + private void CheckCurrentChildren() + { + GetChildsActive(); + if (cells.SequenceEqual(currentChildren)) + return; + cells.Clear(); + foreach (Transform currentChild in currentChildren) + cells.Add(currentChild); + ArrangeChilds(); + } + + private void GetChildsActive() + { + currentChildren.Clear(); + foreach (var obj in transform) + { + var child = obj.TryCast(); + if (child == null) continue; + if (child.gameObject.activeSelf) + currentChildren.Add(child); + } + } + + private void ArrangeChilds() + { + if (cells.Count == 0) + return; + + int totalRows = Mathf.CeilToInt((float)cells.Count / MaxColumns); + float totalHeight = (totalRows - 1) * CellSize.y; + float startY = transform.position.y + totalHeight * 0.5f; + + for (int index = 0; index < cells.Count; ++index) + { + int row = index / MaxColumns; + int rowStartIndex = row * MaxColumns; + int itemsInRow = Mathf.Min(MaxColumns, cells.Count - rowStartIndex); + int col = index - rowStartIndex; + + float rowWidth = (itemsInRow - 1) * CellSize.x; + float startX = transform.position.x - rowWidth * 0.5f; + + float x = startX + col * CellSize.x; + float y = startY - row * CellSize.y; + + Transform cell = cells[index]; + cell.position = new Vector3(x, y, cell.position.z); + } + } +} \ No newline at end of file