From e381de033e5c86bdc6989d18737830d7b6799683 Mon Sep 17 00:00:00 2001 From: wanderingpix Date: Thu, 20 Aug 2026 22:41:26 +0100 Subject: [PATCH] WIP Targeted meeting abilities API --- MiraAPI.Example/ExampleAssets.cs | 1 + .../MeetingAbilities/ShootButton.cs | 28 ++++ .../Resources/SharpshootButton.png | Bin 0 -> 5841 bytes .../MeetingAbilityBehaviour.cs | 77 ++++++++++ .../MeetingAbilities/MeetingButtonUsesMode.cs | 7 + .../TargetedMeetingAbilityManager.cs | 30 ++++ .../MeetingAbilities/TargetedMeetingButton.cs | 140 ++++++++++++++++++ MiraAPI/Patches/IntroCutscenePatches.cs | 9 +- MiraAPI/Patches/Voting/MeetingHudPatches.cs | 65 +++++++- MiraAPI/PluginLoading/MiraPluginInfo.cs | 3 + MiraAPI/PluginLoading/MiraPluginManager.cs | 19 +++ 11 files changed, 376 insertions(+), 3 deletions(-) create mode 100644 MiraAPI.Example/MeetingAbilities/ShootButton.cs create mode 100644 MiraAPI.Example/Resources/SharpshootButton.png create mode 100644 MiraAPI/MeetingAbilities/MeetingAbilityBehaviour.cs create mode 100644 MiraAPI/MeetingAbilities/MeetingButtonUsesMode.cs create mode 100644 MiraAPI/MeetingAbilities/TargetedMeetingAbilityManager.cs create mode 100644 MiraAPI/MeetingAbilities/TargetedMeetingButton.cs diff --git a/MiraAPI.Example/ExampleAssets.cs b/MiraAPI.Example/ExampleAssets.cs index 903afd8..b0ca484 100644 --- a/MiraAPI.Example/ExampleAssets.cs +++ b/MiraAPI.Example/ExampleAssets.cs @@ -9,5 +9,6 @@ public static class ExampleAssets // Credit to EpicHorrors for the teleport button asset. public static LoadableResourceAsset TeleportButton { get; } = new("MiraAPI.Example.Resources.TeleportButton.png"); + public static LoadableResourceAsset SharpshootButton { get; } = new("MiraAPI.Example.Resources.SharpshootButton.png"); public static LoadableResourceAsset Banner { get; } = new("MiraAPI.Example.Resources.FortniteBanner.jpeg"); } diff --git a/MiraAPI.Example/MeetingAbilities/ShootButton.cs b/MiraAPI.Example/MeetingAbilities/ShootButton.cs new file mode 100644 index 0000000..76b4596 --- /dev/null +++ b/MiraAPI.Example/MeetingAbilities/ShootButton.cs @@ -0,0 +1,28 @@ +using MiraAPI.MeetingAbilities; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.Example.MeetingAbilities; + +public class ShootButton : TargetedMeetingButton +{ + public override string Name => "Shooooooot"; + + public override int MaxUses => 0; + + public override float Cooldown => 10; + + public override LoadableAsset Sprite => ExampleAssets.SharpshootButton; + + public override MeetingButtonUsesMode ButtonUsesMode => MeetingButtonUsesMode.PerGame; + + public override bool Enabled(RoleBehaviour r) + { + return true; + } + + protected override void OnClick(PlayerVoteArea playerVoteArea) + { + playerVoteArea.SetDisabled(); + } +} \ No newline at end of file diff --git a/MiraAPI.Example/Resources/SharpshootButton.png b/MiraAPI.Example/Resources/SharpshootButton.png new file mode 100644 index 0000000000000000000000000000000000000000..f8ccae6e98138f2e0f06d621270a7cdb4681afc2 GIT binary patch literal 5841 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 + SetFillUp(1, 1); + if (!TryGetComponent(out Button)) + { + Error($"Could not initialize MeetingButtonBehaviour for {Button.GetType().Name}, Destroying..."); + Destroy(this); + } + _init = true; + CooldownText = Instantiate(HudManager.Instance.KillButton.cooldownTimerText, transform); + CooldownText.GetComponent().Destroy(); + CooldownText.transform.localPosition = new Vector3(0, 0, -10); + CooldownText.m_maxFontSize = 3f; + CooldownText.color = new Color(1, 1, 1, 0.7f); + UsesText = Instantiate(CooldownText, transform); + UsesText.transform.localPosition = new Vector3(0.25f, 0.25f, -10); + UsesText.transform.localScale = Vector3.one * 0.7f; + } + + private void OnEnable() + { + if (!_init) return; + Button.enabled = true; + Renderer.enabled = true; + if (_button.Enabled(PlayerControl.LocalPlayer.Data.Role) && _button.IsTargetValid(VoteArea)) return; + Button.enabled = false; + Renderer.enabled = false; + } + + public void Update() + { + if (!_init) return; + if (_button.Timer < 0) + { + Renderer.material.SetFloat("_Desat", 0.0f); + CooldownText.text = string.Empty; + UsesText.text = _button.LimitedUses ? _button.UsesRemaining.ToString(CultureInfo.InvariantCulture) : string.Empty; + } + else + { + Renderer.material.SetFloat("_Desat", 0.5f); + CooldownText.text = _button.Timer.ToString(_button.CooldownTimerFormatString, NumberFormatInfo.InvariantInfo); + UsesText.text = string.Empty; + } + SetFillUp(_button.Timer, _button.Cooldown); + } + + public void SetFillUp(float timer, float maxTimer) + { + float percentCool = Mathf.Clamp(timer / maxTimer, 0.001f, 1f); + if (percentCool <= 0) percentCool = 1f; + Renderer.material.SetFloat("_Percent", percentCool); + } +} diff --git a/MiraAPI/MeetingAbilities/MeetingButtonUsesMode.cs b/MiraAPI/MeetingAbilities/MeetingButtonUsesMode.cs new file mode 100644 index 0000000..a4ddb40 --- /dev/null +++ b/MiraAPI/MeetingAbilities/MeetingButtonUsesMode.cs @@ -0,0 +1,7 @@ +namespace MiraAPI.MeetingAbilities; + +public enum MeetingButtonUsesMode +{ + PerGame, + PerMeeting +} \ No newline at end of file diff --git a/MiraAPI/MeetingAbilities/TargetedMeetingAbilityManager.cs b/MiraAPI/MeetingAbilities/TargetedMeetingAbilityManager.cs new file mode 100644 index 0000000..fa40a46 --- /dev/null +++ b/MiraAPI/MeetingAbilities/TargetedMeetingAbilityManager.cs @@ -0,0 +1,30 @@ +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; + +public static class TargetedMeetingAbilityManager +{ + /// + /// 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/TargetedMeetingButton.cs b/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs new file mode 100644 index 0000000..6e831e8 --- /dev/null +++ b/MiraAPI/MeetingAbilities/TargetedMeetingButton.cs @@ -0,0 +1,140 @@ +using System.Collections.Generic; +using MiraAPI.Utilities.Assets; +using UnityEngine; +using UnityEngine.UI; + +namespace MiraAPI.MeetingAbilities; + +/// +/// Abstract class for creating custom meeting abilities. +/// +public abstract class TargetedMeetingButton +{ + /// + /// Gets the name of the . + /// + public abstract string Name { get; } + + /// + /// Gets the Max Uses of the button. + /// Set it to 0 for Infinite uses. + /// + public abstract int MaxUses { get; } + + /// + /// Gets or sets the amount of uses left for this button. + /// + public int UsesRemaining { get; set; } + + /// + /// Gets a value indicating whether the has limited uses. + /// + public bool LimitedUses => MaxUses != 0; + + /// + /// Gets the cooldown time for this button. + /// + public abstract float Cooldown { get; } + + /// + /// Gets or sets the current cooldown timer for this button. + /// + public float Timer { get; set; } + + /// + /// Gets the cooldown time for this button. + /// + public virtual string CooldownTimerFormatString => "0"; + + /// + /// Gets the sprite used for this button. + /// + public abstract LoadableAsset Sprite { get; } + + /// + /// Gets the for this button. + /// + public virtual MeetingButtonUsesMode ButtonUsesMode { get; } = MeetingButtonUsesMode.PerGame; + + /// + /// Determines if the button is enabled. + /// + /// The local player's . + /// Whether the button is enabled or not. + public abstract bool Enabled(RoleBehaviour r); + + /// + /// Callback method for when the button is clicked. + /// + /// The target . + protected abstract void OnClick(PlayerVoteArea playerVoteArea); + + /// + /// Handles the button creation. + /// + /// The target . + /// The instance. + public virtual MeetingAbilityBehaviour CreateButton(PlayerVoteArea playerVoteArea) + { + if (MeetingHud.Instance == null) return null!; + + var btn = Object.Instantiate(MeetingHud.Instance.PlayerButtonPrefab.ConfirmButton).GetComponent(); + btn.gameObject.name = Name + "Button"; + + var spriteRenderer = btn.GetComponent(); + spriteRenderer.sprite = Sprite.LoadAsset(); + spriteRenderer.material = new Material(HudManager.Instance.KillButton.graphic.material); + spriteRenderer.SetCooldownNormalizedUvs(); + btn.gameObject.SetActive(true); + + btn.OnClick = new Button.ButtonClickedEvent(); + btn.OnClick.AddListener(new System.Action(() => ClickHandler(playerVoteArea))); + var abilityBehaviour = btn.gameObject.AddComponent(); + abilityBehaviour.Initialize(this, playerVoteArea); + Timer = Cooldown; + return abilityBehaviour; + } + + /// + /// Called every . + /// Handles the timer logic. + /// + public virtual void UpdateHandler() + { + Timer -= Time.deltaTime; + Timer = Mathf.Clamp(Timer, 0, int.MaxValue); + FixedUpdate(); + } + + /// + /// Callback method for handling logic every frame. + /// + public virtual void FixedUpdate() + { + } + + /// + /// Called when the button is clicked + /// Handles increasing cooldown and decreasing uses remaining. + /// + /// The target . + public virtual void ClickHandler(PlayerVoteArea playerVoteArea) + { + if (UsesRemaining <= 0 && LimitedUses) return; + if (Timer > 0) return; + Timer = Cooldown; + UsesRemaining--; + playerVoteArea.Cancel(); + OnClick(playerVoteArea); + } + + /// + /// Determines whether the target is valid. + /// + /// The target . + /// Whether the target is valid or not. + public virtual bool IsTargetValid(PlayerVoteArea playerVoteArea) + { + return playerVoteArea != null && playerVoteArea.PlayerId.Value != PlayerControl.LocalPlayer.Data.PlayerId; + } +} diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs index 4ee990c..c611728 100644 --- a/MiraAPI/Patches/IntroCutscenePatches.cs +++ b/MiraAPI/Patches/IntroCutscenePatches.cs @@ -1,8 +1,10 @@ -using System.Reflection; +using System.Linq; +using System.Reflection; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.MeetingAbilities; using MiraAPI.Roles; using MiraAPI.Utilities; @@ -128,6 +130,11 @@ public static class IntroCutscenePatches var @event = new BeforeRoundStartEvent(true); MiraEventManager.InvokeEvent(@event); + foreach (var ability in TargetedMeetingAbilityManager.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 ed71003..b0e084a 100644 --- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs +++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs @@ -1,13 +1,17 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; 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.MeetingAbilities; using MiraAPI.Modifiers; using MiraAPI.Utilities; using MiraAPI.Voting; +using UnityEngine; namespace MiraAPI.Patches.Voting; @@ -33,6 +37,54 @@ internal static class MeetingHudPatches return true; } + [HarmonyPrefix] + [HarmonyPatch(typeof(PlayerVoteArea), nameof(PlayerVoteArea.Start))] + public static void VoteAreaStartPostfix(PlayerVoteArea __instance) + { + foreach (var targetedMeetingAbility in TargetedMeetingAbilityManager.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) + { + if (PlayerControl.LocalPlayer.Data.IsDead || __instance.AmDead || !(bool) (UnityEngine.Object) __instance.Parent) + return false; + JudgeRole? judgeRole = PlayerControl.LocalPlayer.Data.Role.TryCast(); + bool flag = judgeRole && PlayerControl.LocalPlayer.PlayerId != __instance.PlayerId; + __instance.JudgeOverruleButton.gameObject.SetActive(flag); + if (__instance.VoteComplete || !__instance.Parent.Select((byte) __instance.PlayerId)) + return false; + __instance.Buttons.SetActive(true); + + float startPos = __instance.AnimateButtonsFromLeft ? 0.2f : 1.95f; + + Il2CppSystem.Collections.Generic.List selectableElements = new Il2CppSystem.Collections.Generic.List(); + foreach (var btn in __instance.Buttons.GetComponentsInChildren()) + { + selectableElements.Add(btn); + } + + for (int i = 0; i < selectableElements.Count; i++) + { + var button = selectableElements[i]; + float endPos = 1.3f - 0.65f * i; + float duration = 0.25f + 0.1f * i; + __instance.StartCoroutine(Effects.All(Effects.Lerp(duration, (Action) (t => + button.transform.localPosition = Vector2.Lerp(Vector2.right * startPos, Vector2.right * endPos, Effects.ExpOut(t)))))); + } + + ControllerManager.Instance.OpenOverlayMenu(__instance.name, __instance.CancelButton, selectableElements[1], selectableElements); + + return false; + } + [HarmonyPostfix] [HarmonyPatch(nameof(MeetingHud.Start))] public static void MeetingHudStartPatch(MeetingHud __instance) @@ -54,6 +106,10 @@ internal static class MeetingHudPatches } } + foreach (var ability in TargetedMeetingAbilityManager.Buttons.Where(x => x.ButtonUsesMode == MeetingButtonUsesMode.PerMeeting)) + { + ability.UsesRemaining = ability.MaxUses; + } var @event = new StartMeetingEvent(__instance); MiraEventManager.InvokeEvent(@event); } @@ -77,6 +133,11 @@ internal static class MeetingHudPatches [HarmonyPatch(nameof(MeetingHud.Update))] public static void ForceSkipPatch(MeetingHud __instance) { + foreach (var targetedMeetingAbility in TargetedMeetingAbilityManager.Buttons) + { + targetedMeetingAbility.UpdateHandler(); + } + if (__instance.state is not (MeetingHud.MeetingStates.NotVoted or MeetingHud.MeetingStates.Voted)) { return; @@ -105,7 +166,7 @@ internal static class MeetingHudPatches [HarmonyPrefix] [HarmonyPatch(nameof(MeetingHud.Select))] - public static bool SelectPatch(MeetingHud __instance, int suspectStateIdx) + public static bool MeetingHudSelectPatch(MeetingHud __instance, int suspectStateIdx) { var voteData = PlayerControl.LocalPlayer.GetVoteData(); diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index db030b0..ba4d74e 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -5,6 +5,7 @@ using BepInEx.Configuration; using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Hud; +using MiraAPI.MeetingAbilities; using MiraAPI.Modifiers; using MiraAPI.Presets; @@ -78,6 +79,8 @@ public class MiraPluginInfo internal List InternalButtons { get; } = []; + internal List InternalMeetingAbilities { 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 f9439b9..13ec3e8 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -16,6 +16,7 @@ using MiraAPI.Hud; using MiraAPI.Keybinds; using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; +using MiraAPI.MeetingAbilities; using MiraAPI.Modifiers; using MiraAPI.Patches.Options; using MiraAPI.Presets; @@ -117,6 +118,11 @@ public sealed class MiraPluginManager { continue; } + + if (RegisterMeetingAbility(type, info)) + { + continue; + } RegisterColorClasses(type); RegisterKeybinds(type, plugin); @@ -443,4 +449,17 @@ public sealed class MiraPluginManager Error($"Failed to register keybind class {type.Name}: {e}"); } } + + private static bool RegisterMeetingAbility(Type type, MiraPluginInfo info) + { + try + { + return TargetedMeetingAbilityManager.RegisterMeetingAbility(type, info); + } + catch (Exception e) + { + Error($"Failed to register meeting ability {type.Name}: {e}"); + return false; + } + } }