diff --git a/MiraAPI.Example/ExampleEventHandlers.cs b/MiraAPI.Example/ExampleEventHandlers.cs new file mode 100644 index 0000000..21904e4 --- /dev/null +++ b/MiraAPI.Example/ExampleEventHandlers.cs @@ -0,0 +1,42 @@ +using MiraAPI.Events; +using MiraAPI.Events.Mira; +using MiraAPI.Events.Vanilla; +using MiraAPI.Example.Buttons.Freezer; +using Reactor.Utilities; + +namespace MiraAPI.Example; + +public static class ExampleEventHandlers +{ + public static void Initialize() + { + // Register event handlers here + MiraEventManager.RegisterEventHandler>(FreezeButtonClickHandler, 1); + MiraEventManager.RegisterEventHandler>(FreezeButtonCancelledHandler); + MiraEventManager.RegisterEventHandler(UpdateSystemEventHandler); + } + + public static void UpdateSystemEventHandler(UpdateSystemEvent @event) + { + Logger.Error(@event.SystemType.ToString()); + } + + // Example event handler + public static void FreezeButtonClickHandler(MiraButtonClickEvent @event) + { + Logger.Warning("Freeze button clicked!"); + + if (PlayerControl.LocalPlayer.Data.PlayerName == "stupid") + { + @event.Cancel(); + @event.Button.SetTimer(15f); + } + } + + // Example event handler + public static void FreezeButtonCancelledHandler(MiraButtonCancelledEvent @event) + { + Logger.Warning("Freeze button cancelled!"); + @event.Button.OverrideName("Freeze Canceled"); + } +} diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs index 89ba051..4a62da2 100644 --- a/MiraAPI.Example/ExamplePlugin.cs +++ b/MiraAPI.Example/ExamplePlugin.cs @@ -2,10 +2,14 @@ using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Mira; +using MiraAPI.Example.Buttons.Freezer; using MiraAPI.PluginLoading; using Reactor; using Reactor.Networking; using Reactor.Networking.Attributes; +using Reactor.Utilities; namespace MiraAPI.Example; @@ -21,6 +25,7 @@ public partial class ExamplePlugin : BasePlugin, IMiraPlugin public ConfigFile GetConfigFile() => Config; public override void Load() { + ExampleEventHandlers.Initialize(); Harmony.PatchAll(); } } diff --git a/MiraAPI/Events/Mira/MiraButtonCancelledEvent.cs b/MiraAPI/Events/Mira/MiraButtonCancelledEvent.cs new file mode 100644 index 0000000..f4a6ebf --- /dev/null +++ b/MiraAPI/Events/Mira/MiraButtonCancelledEvent.cs @@ -0,0 +1,44 @@ +using MiraAPI.Hud; + +namespace MiraAPI.Events.Mira; + +/// +/// Invoked when a Mira Button click is cancelled. Do not use for vanilla buttons. +/// +/// The Mira Button type. +public sealed class MiraButtonCancelledEvent : MiraEvent where T : CustomActionButton +{ + /// + /// Gets Mira Button whose click was cancelled. + /// + public T Button { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The Mira Button whose click was cancelled. + public MiraButtonCancelledEvent(T button) + { + Button = button; + } +} + +/// +/// Invoked when a Mira Button click is cancelled. Do not use for vanilla buttons. +/// +public sealed class MiraButtonCancelledEvent : MiraEvent +{ + /// + /// Gets Mira Button whose click was cancelled. + /// + public CustomActionButton Button { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The Mira Button whose click was cancelled. + public MiraButtonCancelledEvent(CustomActionButton button) + { + Button = button; + } +} diff --git a/MiraAPI/Events/Mira/MiraButtonClickEvent.cs b/MiraAPI/Events/Mira/MiraButtonClickEvent.cs new file mode 100644 index 0000000..c070383 --- /dev/null +++ b/MiraAPI/Events/Mira/MiraButtonClickEvent.cs @@ -0,0 +1,51 @@ +using MiraAPI.Hud; + +namespace MiraAPI.Events.Mira; + +/// +/// Button click event for Mira Buttons only. Do not use for vanilla buttons. +/// +/// The Mira Button type. +public sealed class MiraButtonClickEvent : MiraCancelableEvent where T : CustomActionButton +{ + /// + /// Gets Mira Button that was clicked. + /// + public T Button { get; } + + /// + /// Gets the generic click event that is invoked before this button-specific event. + /// + public MiraButtonClickEvent GenericClickEvent { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The Mira Button that was clicked. + /// The generic click event invoked before button-specific events. + public MiraButtonClickEvent(T button, MiraButtonClickEvent genericClickEvent) + { + GenericClickEvent = genericClickEvent; + Button = button; + } +} + +/// +/// Button click event for Mira Buttons only. Do not use for vanilla buttons. +/// +public sealed class MiraButtonClickEvent : MiraCancelableEvent +{ + /// + /// Gets Mira Button that was clicked. + /// + public CustomActionButton Button { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The Mira Button that was clicked. + public MiraButtonClickEvent(CustomActionButton button) + { + Button = button; + } +} diff --git a/MiraAPI/Events/MiraCancelableEvent.cs b/MiraAPI/Events/MiraCancelableEvent.cs new file mode 100644 index 0000000..b4d3f05 --- /dev/null +++ b/MiraAPI/Events/MiraCancelableEvent.cs @@ -0,0 +1,28 @@ +namespace MiraAPI.Events; + +/// +/// Abstract class for Mira Events that can be cancelled. +/// +public abstract class MiraCancelableEvent : MiraEvent +{ + /// + /// Gets a value indicating whether the event is cancelled. + /// + public bool IsCancelled { get; private set; } + + /// + /// Cancels the event. + /// + public void Cancel() + { + IsCancelled = true; + } + + /// + /// Uncancels the event. + /// + public void UnCancel() + { + IsCancelled = false; + } +} diff --git a/MiraAPI/Events/MiraEvent.cs b/MiraAPI/Events/MiraEvent.cs new file mode 100644 index 0000000..f0f49bd --- /dev/null +++ b/MiraAPI/Events/MiraEvent.cs @@ -0,0 +1,6 @@ +namespace MiraAPI.Events; + +/// +/// Abstract class for Mira Events. +/// +public abstract class MiraEvent; diff --git a/MiraAPI/Events/MiraEventManager.cs b/MiraAPI/Events/MiraEventManager.cs new file mode 100644 index 0000000..7d6486c --- /dev/null +++ b/MiraAPI/Events/MiraEventManager.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using BepInEx.Unity.IL2CPP; +using Reactor.Utilities; + +namespace MiraAPI.Events; + +/// +/// Mira Event manager. +/// +public static class MiraEventManager +{ + private static readonly Dictionary> EventWrappers = []; + + /// + /// Invoke an event. + /// + /// The event instance. + /// Type of Event. + /// If there was an event handler invoked for this event, return true. Otherwise, return false. + public static bool InvokeEvent(T eventInstance) where T : MiraEvent + { + EventWrappers.TryGetValue(typeof(T), out var handlers); + if (handlers == null) + { + Logger.Warning("No handlers for event " + typeof(T).Name); + return false; + } + + foreach (var handler in handlers) + { + ((Action)handler.EventHandler).Invoke(eventInstance); + } + + return true; + } + + /// + /// Invoke an event and use a specific type to find the handlers. + /// + /// The event instance. + /// The type to use for handler lookup. + /// If there was an event handler invoked for this event, return true. Otherwise, return false. + public static bool InvokeEvent(MiraEvent eventInstance, Type type) + { + EventWrappers.TryGetValue(type, out var handlers); + if (handlers == null) + { + Logger.Warning("No handlers for event " + type.Name); + return false; + } + + foreach (var handler in handlers) + { + handler.EventHandler.DynamicInvoke(eventInstance); + } + + return true; + } + + /// + /// Register an event. + /// + /// The callback method/handler for the event. + /// The priority of the event handler. Higher values are called first. + /// Type of event. + public static void RegisterEventHandler(Action handler, int priority = 0) where T : MiraEvent + { + if (!EventWrappers.ContainsKey(typeof(T))) + { + EventWrappers.Add(typeof(T), []); + } + + var handlers = EventWrappers[typeof(T)]; + handlers.Add(new MiraEventWrapper(handler, priority)); + + Logger.Info("Registered event handler for " + typeof(T).Name); + } + + internal static void SortAllHandlers() + { + foreach (var handlers in EventWrappers.Values) + { + handlers.Sort((a, b) => a.Priority.CompareTo(b.Priority)); + } + } +} diff --git a/MiraAPI/Events/MiraEventWrapper.cs b/MiraAPI/Events/MiraEventWrapper.cs new file mode 100644 index 0000000..0251ea9 --- /dev/null +++ b/MiraAPI/Events/MiraEventWrapper.cs @@ -0,0 +1,30 @@ +using System; + +namespace MiraAPI.Events; + +/// +/// Wrapper for event handlers. +/// +public class MiraEventWrapper +{ + /// + /// Gets the event handler delegate. + /// + public Delegate EventHandler { get; } + + /// + /// Gets the priority of the handler. Higher values are called first. + /// + public int Priority { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The action for the event handler. + /// The priority of the handler. + public MiraEventWrapper(Delegate eventHandler, int priority) + { + EventHandler = eventHandler; + Priority = priority; + } +} diff --git a/MiraAPI/Events/Vanilla/AdminButtonClickEvent.cs b/MiraAPI/Events/Vanilla/AdminButtonClickEvent.cs new file mode 100644 index 0000000..c37b933 --- /dev/null +++ b/MiraAPI/Events/Vanilla/AdminButtonClickEvent.cs @@ -0,0 +1,21 @@ +namespace MiraAPI.Events.Vanilla; + +/// +/// Button click event for from Vanilla Among Us. +/// +public class AdminButtonClickEvent : MiraCancelableEvent +{ + /// + /// Gets the instance of that was clicked. + /// + public AdminButton Button { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The AdminButton. + public AdminButtonClickEvent(AdminButton button) + { + Button = button; + } +} diff --git a/MiraAPI/Events/Vanilla/AfterMurderEvent.cs b/MiraAPI/Events/Vanilla/AfterMurderEvent.cs new file mode 100644 index 0000000..a425cd2 --- /dev/null +++ b/MiraAPI/Events/Vanilla/AfterMurderEvent.cs @@ -0,0 +1,28 @@ +namespace MiraAPI.Events.Vanilla; + +/// +/// Event that is invoked after a player is murdered. Only called after a successful murder. This event is not cancelable. +/// +public class AfterMurderEvent : MiraEvent +{ + /// + /// Gets the player that killed the target. + /// + public PlayerControl Source { get; } + + /// + /// Gets the player that was killed. + /// + public PlayerControl Target { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The killer. + /// The killed player. + public AfterMurderEvent(PlayerControl source, PlayerControl target) + { + Source = source; + Target = target; + } +} diff --git a/MiraAPI/Events/Vanilla/BeforeMurderEvent.cs b/MiraAPI/Events/Vanilla/BeforeMurderEvent.cs new file mode 100644 index 0000000..f4f2be3 --- /dev/null +++ b/MiraAPI/Events/Vanilla/BeforeMurderEvent.cs @@ -0,0 +1,28 @@ +namespace MiraAPI.Events.Vanilla; + +/// +/// Event that is invoked before a player is murdered. This event is cancelable. +/// +public sealed class BeforeMurderEvent : MiraCancelableEvent +{ + /// + /// Gets the player that is killing the target. + /// + public PlayerControl Source { get; } + + /// + /// Gets the player that is being killed. + /// + public PlayerControl Target { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The player that is killing the target. + /// The player that is being killed. + public BeforeMurderEvent(PlayerControl source, PlayerControl target) + { + Source = source; + Target = target; + } +} diff --git a/MiraAPI/Events/Vanilla/CloseDoorsEvent.cs b/MiraAPI/Events/Vanilla/CloseDoorsEvent.cs new file mode 100644 index 0000000..8d0b9bf --- /dev/null +++ b/MiraAPI/Events/Vanilla/CloseDoorsEvent.cs @@ -0,0 +1,21 @@ +namespace MiraAPI.Events.Vanilla; + +/// +/// Event fired when a player closes the doors in a room. +/// +public class CloseDoorsEvent : MiraCancelableEvent +{ + /// + /// Gets the room that the doors were closed in. + /// + public SystemTypes Room { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The room that was closed. + public CloseDoorsEvent(SystemTypes room) + { + Room = room; + } +} diff --git a/MiraAPI/Events/Vanilla/EnterVentEvent.cs b/MiraAPI/Events/Vanilla/EnterVentEvent.cs new file mode 100644 index 0000000..63e872c --- /dev/null +++ b/MiraAPI/Events/Vanilla/EnterVentEvent.cs @@ -0,0 +1,28 @@ +namespace MiraAPI.Events.Vanilla; + +/// +/// Event that is invoked when a player enters a vent. This event is cancelable. +/// +public class EnterVentEvent : MiraCancelableEvent +{ + /// + /// Gets the player that is entering the vent. + /// + public PlayerControl Player { get; } + + /// + /// Gets the vent that the player is entering. + /// + public Vent? Vent { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The player that is entering the vent. + /// The vent being entered. + public EnterVentEvent(PlayerControl player, Vent? vent) + { + Player = player; + Vent = vent; + } +} diff --git a/MiraAPI/Events/Vanilla/ExitVentEvent.cs b/MiraAPI/Events/Vanilla/ExitVentEvent.cs new file mode 100644 index 0000000..1269b8a --- /dev/null +++ b/MiraAPI/Events/Vanilla/ExitVentEvent.cs @@ -0,0 +1,28 @@ +namespace MiraAPI.Events.Vanilla; + +/// +/// Event that is invoked when a player exits a vent. This event is cancelable. +/// +public class ExitVentEvent : MiraCancelableEvent +{ + /// + /// Gets the player that is exiting the vent. + /// + public PlayerControl Player { get; } + + /// + /// Gets the vent that the player is exiting. + /// + public Vent? Vent { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The player who is exiting the vent. + /// The vent being exited from. + public ExitVentEvent(PlayerControl player, Vent? vent) + { + Player = player; + Vent = vent; + } +} diff --git a/MiraAPI/Events/Vanilla/SabotageButtonClickEvent.cs b/MiraAPI/Events/Vanilla/SabotageButtonClickEvent.cs new file mode 100644 index 0000000..b657486 --- /dev/null +++ b/MiraAPI/Events/Vanilla/SabotageButtonClickEvent.cs @@ -0,0 +1,21 @@ +namespace MiraAPI.Events.Vanilla; + +/// +/// Button click event for from Vanilla Among Us. +/// +public class SabotageButtonClickEvent : MiraCancelableEvent +{ + /// + /// Gets the instance of that was clicked. + /// + public SabotageButton Button { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The SabotageButton. + public SabotageButtonClickEvent(SabotageButton button) + { + Button = button; + } +} diff --git a/MiraAPI/Events/Vanilla/StartMeetingEvent.cs b/MiraAPI/Events/Vanilla/StartMeetingEvent.cs new file mode 100644 index 0000000..2e3d8c2 --- /dev/null +++ b/MiraAPI/Events/Vanilla/StartMeetingEvent.cs @@ -0,0 +1,35 @@ +namespace MiraAPI.Events.Vanilla; + +/// +/// The event that is invoked when a meeting is called +/// +public class StartMeetingEvent : MiraCancelableEvent +{ + /// + /// Gets the player that is calling the meeting. + /// + 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 StartMeetingEvent(PlayerControl reporter, NetworkedPlayerInfo? target, DeadBody? body) + { + Reporter = reporter; + Target = target; + Body = body; + } +} diff --git a/MiraAPI/Events/Vanilla/UpdateSystemEvent.cs b/MiraAPI/Events/Vanilla/UpdateSystemEvent.cs new file mode 100644 index 0000000..1ac8bda --- /dev/null +++ b/MiraAPI/Events/Vanilla/UpdateSystemEvent.cs @@ -0,0 +1,34 @@ +namespace MiraAPI.Events.Vanilla; + +/// +/// Cancelable event that is invoked before a system is updated. Usually used for sabotage events. +/// +public class UpdateSystemEvent : MiraCancelableEvent +{ + /// + /// Gets the type of system that will be updated. + /// + public SystemTypes SystemType { get; } + + /// + /// Gets the player that is updating the system. + /// + public PlayerControl Player { get; } + + /// + /// Gets the byte amount the system is being updated by. + /// + public byte Amount { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The SystemType being updated. + /// Amount to update System to. + public UpdateSystemEvent(SystemTypes systemType, PlayerControl player, byte amount) + { + SystemType = systemType; + Player = player; + Amount = amount; + } +} diff --git a/MiraAPI/Events/Vanilla/UseButtonClickEvent.cs b/MiraAPI/Events/Vanilla/UseButtonClickEvent.cs new file mode 100644 index 0000000..043b166 --- /dev/null +++ b/MiraAPI/Events/Vanilla/UseButtonClickEvent.cs @@ -0,0 +1,21 @@ +namespace MiraAPI.Events.Vanilla; + +/// +/// Button click event for from Vanilla Among Us. +/// +public class UseButtonClickEvent : MiraCancelableEvent +{ + /// + /// Gets the instance of that was clicked. + /// + public UseButton Button { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The UseButton. + public UseButtonClickEvent(UseButton button) + { + Button = button; + } +} diff --git a/MiraAPI/Events/Vanilla/VentButtonClickEvent.cs b/MiraAPI/Events/Vanilla/VentButtonClickEvent.cs new file mode 100644 index 0000000..a46601f --- /dev/null +++ b/MiraAPI/Events/Vanilla/VentButtonClickEvent.cs @@ -0,0 +1,21 @@ +namespace MiraAPI.Events.Vanilla; + +/// +/// Button click event for from Vanilla Among Us. +/// +public class VentButtonClickEvent : MiraCancelableEvent +{ + /// + /// Gets the instance of that was clicked. + /// + public VentButton Button { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The VentButton. + public VentButtonClickEvent(VentButton button) + { + Button = button; + } +} diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index c80adef..0c89062 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -1,7 +1,11 @@ -using MiraAPI.Utilities.Assets; +using System; +using MiraAPI.Events; +using MiraAPI.Events.Mira; +using MiraAPI.Utilities.Assets; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; +using Object = UnityEngine.Object; namespace MiraAPI.Hud; @@ -95,7 +99,42 @@ public abstract class CustomActionButton var pb = Button.GetComponent(); pb.OnClick = new Button.ButtonClickedEvent(); - pb.OnClick.AddListener((UnityAction)ClickHandler); + pb.OnClick.AddListener((UnityAction)(() => + { + // Invoke the generic button click event. + var genericEvent = new MiraButtonClickEvent(this); + MiraEventManager.InvokeEvent(genericEvent); + if (genericEvent.IsCancelled) + { + MiraEventManager.InvokeEvent(new MiraButtonCancelledEvent(this)); + } + + // Invoke the button click event for specific button. + var eventType = CustomButtonManager.ButtonEventTypes[GetType()]; + var @event = (MiraCancelableEvent)Activator.CreateInstance(eventType, this)!; + var specificInvoked = MiraEventManager.InvokeEvent(@event, eventType); + if (@event.IsCancelled) + { + var cancelEventType = CustomButtonManager.ButtonCancelledEventTypes[GetType()]; + var cancelEvent = (MiraEvent)Activator.CreateInstance(cancelEventType, this)!; + MiraEventManager.InvokeEvent(cancelEvent, cancelEventType); + } + + if (specificInvoked) + { + if (!@event.IsCancelled) + { + ClickHandler(); + } + } + else + { + if (!genericEvent.IsCancelled) + { + ClickHandler(); + } + } + })); } /// diff --git a/MiraAPI/Hud/CustomButtonManager.cs b/MiraAPI/Hud/CustomButtonManager.cs index 61c5ab6..d2accef 100644 --- a/MiraAPI/Hud/CustomButtonManager.cs +++ b/MiraAPI/Hud/CustomButtonManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using MiraAPI.Events.Mira; using MiraAPI.PluginLoading; using Reactor.Utilities; @@ -9,6 +10,8 @@ namespace MiraAPI.Hud; internal static class CustomButtonManager { internal static readonly List CustomButtons = []; + internal static readonly Dictionary ButtonEventTypes = []; + internal static readonly Dictionary ButtonCancelledEventTypes = []; internal static void RegisterButton(Type buttonType, MiraPluginInfo pluginInfo) { @@ -29,5 +32,8 @@ internal static class CustomButtonManager typeof(CustomButtonSingleton<>).MakeGenericType(buttonType) .GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! .SetValue(null, button); + + ButtonEventTypes.Add(buttonType, typeof(MiraButtonClickEvent<>).MakeGenericType(buttonType)); + ButtonCancelledEventTypes.Add(buttonType, typeof(MiraButtonCancelledEvent<>).MakeGenericType(buttonType)); } } diff --git a/MiraAPI/Networking/CustomMurderRpc.cs b/MiraAPI/Networking/CustomMurderRpc.cs index 51c0c52..df565c2 100644 --- a/MiraAPI/Networking/CustomMurderRpc.cs +++ b/MiraAPI/Networking/CustomMurderRpc.cs @@ -3,6 +3,9 @@ using System.Linq; using AmongUs.GameOptions; using Assets.CoreScripts; using BepInEx.Unity.IL2CPP.Utils; +using Discord; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla; using Reactor.Networking.Attributes; using Reactor.Networking.Rpc; using Reactor.Utilities; @@ -39,6 +42,15 @@ public static class CustomMurderRpc bool playKillSound = true) { var murderResultFlags = didSucceed ? MurderResultFlags.Succeeded : MurderResultFlags.FailedError; + + var beforeMurderEvent = new BeforeMurderEvent(source, target); + MiraEventManager.InvokeEvent(beforeMurderEvent); + + if (beforeMurderEvent.IsCancelled) + { + murderResultFlags = MurderResultFlags.FailedError; + } + var murderResultFlags2 = MurderResultFlags.DecisionByHost | murderResultFlags; source.CustomMurder( @@ -268,5 +280,8 @@ public static class CustomMurderRpc PlayerControl.LocalPlayer.isKilling = false; source.isKilling = false; + + var afterMurderEvent = new AfterMurderEvent(source, target); + MiraEventManager.InvokeEvent(afterMurderEvent); } } diff --git a/MiraAPI/Patches/Events/HudButtonEventPatches.cs b/MiraAPI/Patches/Events/HudButtonEventPatches.cs new file mode 100644 index 0000000..8ea7c5b --- /dev/null +++ b/MiraAPI/Patches/Events/HudButtonEventPatches.cs @@ -0,0 +1,49 @@ +using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla; +using Reactor.Utilities; + +namespace MiraAPI.Patches.Events; + +/// +/// Patch for button related MiraEvents. +/// +[HarmonyPatch] +public static class HudButtonEventPatches +{ + [HarmonyPrefix] + [HarmonyPatch(typeof(AdminButton), nameof(AdminButton.DoClick))] + public static bool AdminButtonDoClickPrefix(AdminButton __instance) + { + var @event = new AdminButtonClickEvent(__instance); + MiraEventManager.InvokeEvent(@event); + return !@event.IsCancelled; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(SabotageButton), nameof(SabotageButton.DoClick))] + public static bool SabotageButtonDoClickPrefix(SabotageButton __instance) + { + var @event = new SabotageButtonClickEvent(__instance); + MiraEventManager.InvokeEvent(@event); + return !@event.IsCancelled; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(UseButton), nameof(UseButton.DoClick))] + public static bool UseButtonDoClickPrefix(UseButton __instance) + { + var @event = new UseButtonClickEvent(__instance); + MiraEventManager.InvokeEvent(@event); + return !@event.IsCancelled; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(VentButton), nameof(VentButton.DoClick))] + public static bool VentButtonDoClickPrefix(VentButton __instance) + { + var @event = new VentButtonClickEvent(__instance); + MiraEventManager.InvokeEvent(@event); + return !@event.IsCancelled; + } +} diff --git a/MiraAPI/Patches/Events/SabotageEventPatches.cs b/MiraAPI/Patches/Events/SabotageEventPatches.cs new file mode 100644 index 0000000..b8a1fd2 --- /dev/null +++ b/MiraAPI/Patches/Events/SabotageEventPatches.cs @@ -0,0 +1,30 @@ +using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla; + +namespace MiraAPI.Patches.Events; + +/// +/// Used for patching sabotage/system related MiraEvents. +/// +[HarmonyPatch] +public static class SabotageEventPatches +{ + [HarmonyPrefix] + [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.RpcUpdateSystem), typeof(SystemTypes), typeof(byte))] + public static bool ShipStatusUpdateSystemPrefix(ShipStatus __instance, SystemTypes systemType, byte amount) + { + var @event = new UpdateSystemEvent(systemType, PlayerControl.LocalPlayer, amount); + MiraEventManager.InvokeEvent(@event); + return !@event.IsCancelled; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.RpcCloseDoorsOfType))] + public static bool ShipStatusCloseDoorsOfTypePrefix(ShipStatus __instance, SystemTypes type) + { + var @event = new CloseDoorsEvent(type); + MiraEventManager.InvokeEvent(@event); + return !@event.IsCancelled; + } +} diff --git a/MiraAPI/Patches/Events/StartMeetingEventPatch.cs b/MiraAPI/Patches/Events/StartMeetingEventPatch.cs new file mode 100644 index 0000000..00f8ba4 --- /dev/null +++ b/MiraAPI/Patches/Events/StartMeetingEventPatch.cs @@ -0,0 +1,21 @@ +using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla; +using MiraAPI.Utilities; + +namespace MiraAPI.Patches.Events; + +[HarmonyPatch] +public static class StartMeetingEventPatch +{ + [HarmonyPrefix] + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdReportDeadBody))] + public static bool PlayerControlCmdReportDeadBodyPrefix(PlayerControl __instance, NetworkedPlayerInfo? target) + { + var body = target != null ? Helpers.GetBodyById(target.PlayerId) : null; + + var @event = new StartMeetingEvent(__instance, target, body); + MiraEventManager.InvokeEvent(@event); + return !@event.IsCancelled; + } +} diff --git a/MiraAPI/Patches/Events/VentEventPatches.cs b/MiraAPI/Patches/Events/VentEventPatches.cs new file mode 100644 index 0000000..f978f15 --- /dev/null +++ b/MiraAPI/Patches/Events/VentEventPatches.cs @@ -0,0 +1,60 @@ +using System.Linq; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla; +using MiraAPI.Utilities; +using Reactor.Utilities; + +namespace MiraAPI.Patches.Events; + +/// +/// Patches to invoke vent related MiraEvents. +/// +[HarmonyPatch] +public static class VentEventPatches +{ + // necessary because Vent.Use is inlined in il2cpp. + private static bool _showButtons; + + [HarmonyPostfix] + [HarmonyPatch(typeof(VentButton), nameof(VentButton.DoClick))] + public static void VentUsePostfix(VentButton __instance) + { + __instance.currentTarget?.SetButtons(_showButtons); + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.RpcEnterVent))] + public static bool PlayerPhysicsRpcEnterVentPrefix(PlayerPhysics __instance, int id) + { + var pc = __instance.myPlayer; + var vent = ShipStatus.Instance.AllVents.FirstOrDefault(v => v.Id == id); + + var @event = new EnterVentEvent(pc, vent); + MiraEventManager.InvokeEvent(@event); + + if (pc.AmOwner) + { + _showButtons = !@event.IsCancelled; + } + return !@event.IsCancelled; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.RpcExitVent))] + public static bool PlayerPhysicsRpcExitVentPrefix(PlayerPhysics __instance, int id) + { + var pc = __instance.myPlayer; + var vent = ShipStatus.Instance.AllVents.First(v => v.Id == id); + + var @event = new ExitVentEvent(pc, vent); + MiraEventManager.InvokeEvent(@event); + + if (pc.AmOwner) + { + _showButtons = @event.IsCancelled; + } + return !@event.IsCancelled; + } +} diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index feeba48..acb6ebe 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using BepInEx.Unity.IL2CPP; using MiraAPI.Colors; +using MiraAPI.Events; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Hud; @@ -51,6 +52,7 @@ public sealed class MiraPluginManager Logger.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API."); }; IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors; + IL2CPPChainloader.Instance.Finished += MiraEventManager.SortAllHandlers; } /// diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index 0df4297..5bd29bb 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -15,6 +15,8 @@ public struct CustomRoleConfiguration /// The role in which you are configuring. public CustomRoleConfiguration(ICustomRole role) { + var roleBehaviour = role as RoleBehaviour; + MaxRoleCount = 15; DefaultRoleCount = 0; DefaultChance = 0; @@ -29,13 +31,12 @@ public struct CustomRoleConfiguration }; RoleHintType = RoleHintType.RoleTab; GhostRole = role.Team is ModdedRoleTeams.Impostor ? RoleTypes.ImpostorGhost : RoleTypes.CrewmateGhost; - CanGetKilled = !IsGhostRole && role.Team is not ModdedRoleTeams.Impostor; + CanGetKilled = roleBehaviour?.IsDead == false && role.Team is not ModdedRoleTeams.Impostor; UseVanillaKillButton = role.Team is ModdedRoleTeams.Impostor; CanUseVent = role.Team is ModdedRoleTeams.Impostor; CanUseSabotage = role.Team is ModdedRoleTeams.Impostor; TasksCountForProgress = role.Team is ModdedRoleTeams.Crewmate; - IsGhostRole = false; - HideSettings = IsGhostRole; + HideSettings = roleBehaviour?.IsDead == true; CanModifyChance = true; } @@ -99,11 +100,6 @@ public struct CustomRoleConfiguration /// public bool TasksCountForProgress; - /// - /// Gets a value indicating whether the role is a Ghost. - /// - public bool IsGhostRole; - /// /// Gets a value indicating whether the role should show up in the Role Options menu. /// diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index 8d7f48a..eb291d8 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -93,7 +93,7 @@ public static class CustomRoleManager roleBehaviour.MaxCount = customRole.Configuration.MaxRoleCount; roleBehaviour.RoleScreenshot = customRole.Configuration.OptionsScreenshot.LoadAsset(); - if (customRole.Configuration.IsGhostRole) + if (roleBehaviour.IsDead) { RoleManager.GhostRoles.Add(roleBehaviour.Role); } diff --git a/MiraAPI/Utilities/Assets/SpriteTools.cs b/MiraAPI/Utilities/Assets/SpriteTools.cs index d9750ae..d8da0d0 100644 --- a/MiraAPI/Utilities/Assets/SpriteTools.cs +++ b/MiraAPI/Utilities/Assets/SpriteTools.cs @@ -13,26 +13,6 @@ namespace MiraAPI.Utilities.Assets; /// public static class SpriteTools { - private delegate bool DLoadImage(IntPtr tex, IntPtr data, bool markNonReadable); - - private static DLoadImage? _iCallLoadImage; - - /// - /// Load an image into a texture. - /// - /// The texture. - /// Byte data of image. - /// Mark nonreadable. - /// True if succeeded. - public static bool LoadImage(Texture2D tex, byte[] data, bool markNonReadable) - { - _iCallLoadImage ??= IL2CPP.ResolveICall("UnityEngine.ImageConversion::LoadImage"); - - var il2CPPArray = (Il2CppStructArray)data; - - return _iCallLoadImage.Invoke(tex.Pointer, il2CPPArray.Pointer, markNonReadable); - } - /// /// Load a sprite from a resource path. /// @@ -46,7 +26,7 @@ public static class SpriteTools if (myStream != null) { var buttonTexture = myStream.ReadFully(); - LoadImage(tex, buttonTexture, false); + tex.LoadImage(buttonTexture, false); } else { diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index fda3e03..24ad24f 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -1,4 +1,5 @@ -using MiraAPI.Roles; +using System.Collections; +using MiraAPI.Roles; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -14,6 +15,15 @@ namespace MiraAPI.Utilities; /// public static class Helpers { + /// + /// Returns an empty coroutine. + /// + /// Empty coroutine. + public static IEnumerator EmptyCoroutine() + { + yield break; + } + /// /// Creates a ContactFilter2D from a layer mask. ///