diff --git a/MiraAPI.Example/Buttons/ExampleButton.cs b/MiraAPI.Example/Buttons/ExampleButton.cs index 3a06661..4c9c7d6 100644 --- a/MiraAPI.Example/Buttons/ExampleButton.cs +++ b/MiraAPI.Example/Buttons/ExampleButton.cs @@ -1,4 +1,6 @@ -using MiraAPI.Hud; +using MiraAPI.Example.Modifiers; +using MiraAPI.Hud; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Utilities; using UnityEngine; @@ -22,6 +24,6 @@ public class ExampleButton : CustomActionButton public override bool Enabled(RoleBehaviour role) { - return true; + return PlayerControl.LocalPlayer.HasModifier(); } } \ No newline at end of file diff --git a/MiraAPI.Example/ExamplePlugin.cs b/MiraAPI.Example/ExamplePlugin.cs index 881242c..79ddabd 100644 --- a/MiraAPI.Example/ExamplePlugin.cs +++ b/MiraAPI.Example/ExamplePlugin.cs @@ -22,5 +22,6 @@ public partial class ExamplePlugin : BasePlugin, IMiraPlugin public override void Load() { Harmony.PatchAll(); + AddComponent(); } } \ No newline at end of file diff --git a/MiraAPI.Example/MiraDebugWindow.cs b/MiraAPI.Example/MiraDebugWindow.cs new file mode 100644 index 0000000..56718e8 --- /dev/null +++ b/MiraAPI.Example/MiraDebugWindow.cs @@ -0,0 +1,34 @@ +using MiraAPI.Utilities; +using Reactor.Utilities.Attributes; +using Reactor.Utilities.ImGui; +using System; +using MiraAPI.Example.Modifiers; +using UnityEngine; + +namespace MiraAPI.Example; +[RegisterInIl2Cpp] +public class MiraDebugWindow : MonoBehaviour +{ + public DragWindow DebuggingWindow; + public MiraDebugWindow(IntPtr ptr) : base(ptr) + { + DebuggingWindow = new DragWindow(new Rect(10, 10, 0, 0), "MIRA API DEBUGGING", () => + { + if (GUILayout.Button("Test modifier")) + { + PlayerControl.LocalPlayer.AddModifier(); + } + if (GUILayout.Button("Remove modifier")) + { + PlayerControl.LocalPlayer.RemoveModifier(); + } + }); + + DebuggingWindow.Enabled = true; + } + + public void OnGUI() + { + DebuggingWindow.OnGUI(); + } +} \ No newline at end of file diff --git a/MiraAPI.Example/ModifierExample.cs b/MiraAPI.Example/ModifierExample.cs deleted file mode 100644 index 03df100..0000000 --- a/MiraAPI.Example/ModifierExample.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MiraAPI.Example; - -public class ModifierExample -{ - //name, role specific, etc - - // mono behaviour stuff here (ModdedPlayerModifier inherits monobehaviour) -} \ No newline at end of file diff --git a/MiraAPI.Example/Modifiers/GameModifierExample.cs b/MiraAPI.Example/Modifiers/GameModifierExample.cs new file mode 100644 index 0000000..2b491ae --- /dev/null +++ b/MiraAPI.Example/Modifiers/GameModifierExample.cs @@ -0,0 +1,25 @@ +using MiraAPI.Modifiers; +using MiraAPI.Modifiers.Types; + +namespace MiraAPI.Example.Modifiers; + +[RegisterModifier] +public class GameModifierExample : GameModifier +{ + public override string ModifierName => "Example Game Modifier"; + + public override bool CanVent() + { + return true; + } + + public override int GetAmountPerGame() + { + return 1; + } + + public override int GetAssignmentChance() + { + return 100; + } +} \ No newline at end of file diff --git a/MiraAPI.Example/Modifiers/ModifierTimerExample.cs b/MiraAPI.Example/Modifiers/ModifierTimerExample.cs new file mode 100644 index 0000000..e4b7832 --- /dev/null +++ b/MiraAPI.Example/Modifiers/ModifierTimerExample.cs @@ -0,0 +1,21 @@ +using MiraAPI.Modifiers; +using MiraAPI.Modifiers.Types; + +namespace MiraAPI.Example.Modifiers; + +[RegisterModifier] +public class ModifierTimerExample : TimedModifier +{ + public override string ModifierName => "Color Changer"; + public override bool HideOnUi => true; + public override float Duration => 10f; + public override bool AutoStart => true; + + private System.Random _rand = new(); + + public override void OnTimerComplete() + { + Player.SetColor((byte)_rand.Next(0, Palette.PlayerColors.Count)); + StartTimer(); + } +} diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 1836eb1..56d6d39 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -12,7 +12,7 @@ using Reactor.Networking.Rpc; namespace MiraAPI.GameOptions; -public class ModdedOptionsManager +public static class ModdedOptionsManager { private static readonly Dictionary OptionAttributes = new(); private static readonly Dictionary TypeToGroup = new(); diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs new file mode 100644 index 0000000..2e2f107 --- /dev/null +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -0,0 +1,14 @@ +namespace MiraAPI.Modifiers; +public abstract class BaseModifier +{ + public abstract string ModifierName { get; } + public virtual bool HideOnUi => false; + public PlayerControl Player { get; internal set; } + public uint ModifierId { get; internal set; } + public virtual void OnActivate() { } + public virtual void OnDeactivate() { } + public virtual void Update() { } + public virtual void FixedUpdate() { } + public virtual void OnDeath() { } + public virtual bool CanVent() => Player.Data.Role.CanVent; +} \ No newline at end of file diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs new file mode 100644 index 0000000..34a5cab --- /dev/null +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -0,0 +1,146 @@ +using MiraAPI.Modifiers.Types; +using MiraAPI.Networking; +using MiraAPI.Utilities; +using Reactor.Networking.Attributes; +using Reactor.Utilities; +using Reactor.Utilities.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using TMPro; +using UnityEngine; + +namespace MiraAPI.Modifiers; + +[RegisterInIl2Cpp] +public class ModifierComponent(IntPtr ptr) : MonoBehaviour(ptr) +{ + public List ActiveModifiers { get; private set; } + + public PlayerControl player; + + private TextMeshPro _modifierText; + + public void Start() + { + player = GetComponent(); + ActiveModifiers = []; + + if (!player.AmOwner) return; + + _modifierText = Helpers.CreateTextLabel("ModifierText", HudManager.Instance.transform, AspectPosition.EdgeAlignments.RightTop, new Vector3(10.1f, 3.5f, 0), textAlignment: TextAlignmentOptions.Right); + _modifierText.verticalAlignment = VerticalAlignmentOptions.Top; + } + + public void FixedUpdate() + { + foreach (var modifier in ActiveModifiers) + { + modifier.FixedUpdate(); + } + } + + public void Update() + { + foreach (var modifier in ActiveModifiers) + { + modifier.Update(); + } + + var filteredModifiers = ActiveModifiers.Where(mod => !mod.HideOnUi); + + var baseModifiers = filteredModifiers as BaseModifier[] ?? filteredModifiers.ToArray(); + + if (player.AmOwner && baseModifiers.Any()) + { + var stringBuild = new StringBuilder(); + foreach (var mod in baseModifiers) + { + stringBuild.Append($"\n{mod.ModifierName}"); + if (mod is TimedModifier timer) + { + stringBuild.Append($" ({Math.Round(timer.Duration - timer.TimeRemaining, 0)}s/{timer.Duration}s)"); + } + } + _modifierText.text = $"Modifiers:{stringBuild}"; + } + else if (player.AmOwner && !baseModifiers.Any() && _modifierText.text != string.Empty) + { + _modifierText.text = string.Empty; + } + } + + public static void RemoveModifier(PlayerControl target, uint modifierId) + { + if (!ModifierManager.IdToTypeModifiers.TryGetValue(modifierId, out var type)) + { + Logger.Error($"Cannot remove modifier with id {modifierId} because it is not registered."); + return; + } + + var modifierComponent = target.GetModifierComponent(); + + var modifier = modifierComponent.ActiveModifiers.Find(x => x.GetType() == type); + + if (modifier is null) + { + Logger.Error($"Cannot remove modifier {type.Name} because it is not active."); + return; + } + + modifier.OnDeactivate(); + modifierComponent.ActiveModifiers.Remove(modifier); + + if (target.AmOwner) + { + HudManager.Instance.SetHudActive(true); + } + } + + public static void AddModifier(PlayerControl target, uint modifierId) + { + if (!ModifierManager.IdToTypeModifiers.TryGetValue(modifierId, out var type)) + { + Logger.Error($"Cannot add modifier with id {modifierId} because it is not registered."); + return; + } + + if (target.HasModifier(modifierId)) + { + Logger.Error($"Player already has modifier with id {modifierId}!"); + return; + } + + var modifier = (BaseModifier)Activator.CreateInstance(type); + + var modifierComponent = target.GetModifierComponent(); + + if (modifier is null) + { + Logger.Error($"Cannot add modifier {type.Name} because it is null."); + return; + } + + modifierComponent.ActiveModifiers.Add(modifier); + modifier.Player = modifierComponent.player; + modifier.ModifierId = modifierId; + modifier.OnActivate(); + + if (target.AmOwner) + { + if (modifier is TimedModifier { AutoStart: true } timer) + { + timer.StartTimer(); + } + + HudManager.Instance.SetHudActive(true); + } + } + + [MethodRpc((uint)MiraRpc.RemoveModifier)] + public static void RpcRemoveModifier(PlayerControl target, uint modifierId) => RemoveModifier(target, modifierId); + + [MethodRpc((uint)MiraRpc.AddModifier)] + public static void RpcAddModifier(PlayerControl target, uint modifierId) => AddModifier(target, modifierId); +} \ No newline at end of file diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs new file mode 100644 index 0000000..afe81b1 --- /dev/null +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -0,0 +1,151 @@ +using MiraAPI.Modifiers.Types; +using MiraAPI.Networking; +using MiraAPI.Roles; +using MiraAPI.Utilities; +using Reactor.Networking.Rpc; +using Reactor.Utilities.Extensions; +using System; +using System.Collections.Generic; +using Random = System.Random; + +namespace MiraAPI.Modifiers; + +public static class ModifierManager +{ + public static readonly Dictionary IdToTypeModifiers = []; + public static readonly Dictionary TypeToIdModifiers = []; + + private static uint _nextId; + + private static uint GetNextId() + { + _nextId++; + return _nextId; + } + + public static void RegisterModifier(Type modifierType) + { + if (!typeof(BaseModifier).IsAssignableFrom(modifierType)) + { + return; + } + + IdToTypeModifiers.Add(GetNextId(), modifierType); + TypeToIdModifiers.Add(modifierType, _nextId); + } + + public static void AssignModifiers(List plrs) + { + Random rand = new Random(); + + List filteredModifiers = []; + + foreach (var modifier in IdToTypeModifiers) + { + var mod = (GameModifier)Activator.CreateInstance(modifier.Value); + var num = mod.GetAmountPerGame(); + var chance = mod.GetAssignmentChance(); + + for (int i = 0; i < num; i++) + { + int randomNum = rand.Next(100); + + if (randomNum < Math.Clamp(chance, 0, 100)) + { + filteredModifiers.Add(TypeToIdModifiers[modifier.Value]); + } + } + } + + var shuffledModifiers = filteredModifiers.Randomize(); + if (shuffledModifiers.Count > plrs.Count) + { + shuffledModifiers = shuffledModifiers.GetRange(0, plrs.Count); + } + + while (shuffledModifiers.Count > 0) + { + var id = shuffledModifiers[0]; + + var mod = (GameModifier)Activator.CreateInstance(IdToTypeModifiers[id]); + var plr = plrs.Random(); + + if (plr.Data.Role is ICustomRole modRole) + { + if (!modRole.IsModifierApplicable(mod)) continue; + } + + if (plr.HasModifier(id) || mod.IsModifierValidOn(plr.Data.Role)) continue; + + shuffledModifiers.RemoveAt(0); + ModifierComponent.RpcAddModifier(plr, id); + } + } + + public static void SyncAllModifiers(int targetId = -1) + { + var data = new List(); + + if (targetId == -1) + { + foreach (var player in GameData.Instance.AllPlayers) + { + data.Add(GetPlayerModifiers(player.Object)); + } + } + else + { + var player = GameData.Instance.GetPlayerById((byte)targetId).Object; + + data.Add(GetPlayerModifiers(player)); + } + + + Rpc.Instance.Send(PlayerControl.LocalPlayer, data.ToArray(), true); + } + + public static void HandleSyncModifiers(NetData[] data) + { + foreach (var netData in data) + { + var ids = new uint[netData.Data.Length / 4]; + Buffer.BlockCopy(netData.Data, 0, ids, 0, netData.Data.Length); + + var plr = GameData.Instance.GetPlayerById((byte)netData.Id).Object; + var modifierComponent = plr.GetComponent(); + + if (!modifierComponent) + { + continue; + } + + foreach (var modifier in modifierComponent.ActiveModifiers) + { + modifier.OnDeactivate(); + } + modifierComponent.ActiveModifiers.Clear(); + + foreach (var id in ids) + { + ModifierComponent.AddModifier(plr, id); + } + } + } + + private static NetData GetPlayerModifiers(PlayerControl player) + { + var bytes = new List(); + var modifierComponent = player.GetComponent(); + if (!modifierComponent) + { + return new NetData(player.PlayerId, []); + } + + foreach (var modifier in modifierComponent.ActiveModifiers) + { + bytes.AddRange(BitConverter.GetBytes(TypeToIdModifiers[modifier.GetType()])); + } + + return new NetData(player.PlayerId, bytes.ToArray()); + } +} \ No newline at end of file diff --git a/MiraAPI/Modifiers/RegisterModifierAttribute.cs b/MiraAPI/Modifiers/RegisterModifierAttribute.cs new file mode 100644 index 0000000..4c2fb56 --- /dev/null +++ b/MiraAPI/Modifiers/RegisterModifierAttribute.cs @@ -0,0 +1,6 @@ +using System; + +namespace MiraAPI.Modifiers; + +[AttributeUsage(AttributeTargets.Class)] +public class RegisterModifierAttribute : Attribute; \ No newline at end of file diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs new file mode 100644 index 0000000..cda0844 --- /dev/null +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -0,0 +1,7 @@ +namespace MiraAPI.Modifiers.Types; +public abstract class GameModifier : BaseModifier +{ + public abstract int GetAssignmentChance(); + public abstract int GetAmountPerGame(); + public virtual bool IsModifierValidOn(RoleBehaviour role) => true; +} \ No newline at end of file diff --git a/MiraAPI/Modifiers/Types/TimedModifier.cs b/MiraAPI/Modifiers/Types/TimedModifier.cs new file mode 100644 index 0000000..8e8fda3 --- /dev/null +++ b/MiraAPI/Modifiers/Types/TimedModifier.cs @@ -0,0 +1,41 @@ +using Reactor.Utilities; +using UnityEngine; + +namespace MiraAPI.Modifiers.Types; +public abstract class TimedModifier : BaseModifier +{ + public abstract float Duration { get; } + public virtual bool AutoStart => false; + public abstract void OnTimerComplete(); + + public bool TimerActive = false; + public float TimeRemaining; + + public override void FixedUpdate() + { + if (!Player.AmOwner) return; + + if (TimeRemaining > 0 && TimerActive) + { + TimeRemaining -= Time.fixedDeltaTime; + } + else + { + TimerActive = false; + TimeRemaining = Duration; + OnTimerComplete(); + } + } + + public void StartTimer() + { + if (TimerActive) + { + Logger.Error("Can't start a timer that has already been started."); + return; + } + + TimerActive = true; + TimeRemaining = Duration; + } +} \ No newline at end of file diff --git a/MiraAPI/Networking/MiraRpc.cs b/MiraAPI/Networking/MiraRpc.cs index 4b73cf8..50625bc 100644 --- a/MiraAPI/Networking/MiraRpc.cs +++ b/MiraAPI/Networking/MiraRpc.cs @@ -3,5 +3,8 @@ public enum MiraRpc : uint { SyncGameOptions, - SyncRoleOptions + SyncRoleOptions, + AddModifier, + RemoveModifier, + SyncModifiers } \ No newline at end of file diff --git a/MiraAPI/Networking/SyncModifiersRpc.cs b/MiraAPI/Networking/SyncModifiersRpc.cs new file mode 100644 index 0000000..e760522 --- /dev/null +++ b/MiraAPI/Networking/SyncModifiersRpc.cs @@ -0,0 +1,46 @@ +using Hazel; +using MiraAPI.Modifiers; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; + +namespace MiraAPI.Networking; + +[RegisterCustomRpc((uint)MiraRpc.SyncModifiers)] +public class SyncModifiersRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc(plugin, id) +{ + public override RpcLocalHandling LocalHandling => RpcLocalHandling.None; + + public override void Write(MessageWriter writer, NetData[] data) + { + writer.WritePacked((uint)data.Length); + foreach (var netData in data) + { + writer.WritePacked(netData.Id); + writer.WriteBytesAndSize(netData.Data); + } + } + + public override NetData[] Read(MessageReader reader) + { + var length = reader.ReadPackedUInt32(); + var data = new NetData[length]; + for (var i = 0; i < length; i++) + { + var id = reader.ReadPackedUInt32(); + var bytes = reader.ReadBytesAndSize(); + data[i] = new NetData(id, bytes); + } + + return data; + } + + public override void Handle(PlayerControl playerControl, NetData[] data) + { + if (AmongUsClient.Instance.HostId != playerControl.OwnerId) + { + return; + } + + ModifierManager.HandleSyncModifiers(data); + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/AmongUsClientSyncPatch.cs b/MiraAPI/Patches/AmongUsClientSyncPatch.cs index 121eca5..3e71ae5 100644 --- a/MiraAPI/Patches/AmongUsClientSyncPatch.cs +++ b/MiraAPI/Patches/AmongUsClientSyncPatch.cs @@ -14,7 +14,7 @@ public static class AmongUsClientSyncPatch { return; } - + if (clientData.Id == AmongUsClient.Instance.HostId) { return; @@ -22,5 +22,6 @@ public static class AmongUsClientSyncPatch ModdedOptionsManager.SyncAllOptions(clientData.Id); CustomRoleManager.SyncAllRoleSettings(clientData.Id); + // ModifierManager.SyncAllModifiers(clientData.Id); } } \ No newline at end of file diff --git a/MiraAPI/Patches/Modifiers/VentPatches.cs b/MiraAPI/Patches/Modifiers/VentPatches.cs new file mode 100644 index 0000000..e2c6e61 --- /dev/null +++ b/MiraAPI/Patches/Modifiers/VentPatches.cs @@ -0,0 +1,46 @@ +using HarmonyLib; +using MiraAPI.Modifiers; +using MiraAPI.Utilities; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace MiraAPI.Patches.Modifiers; + +[HarmonyPatch(typeof(Vent))] +public static class VentPatches +{ + [HarmonyPostfix] + [HarmonyPatch(nameof(Vent.CanUse))] + public static void CanUseVentPatch(Vent __instance, ref float __result, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] ref bool canUse, [HarmonyArgument(2)] ref bool couldUse) + { + var @object = pc.Object; + var role = @object.Data.Role; + + List modifiers = @object.GetModifierComponent().ActiveModifiers; + if (modifiers.Count > 0) + { + if (role.CanVent && modifiers.Any(x => !x.CanVent())) + { + couldUse = canUse = false; + return; + } + else if (!role.CanVent && modifiers.Any(x => x.CanVent())) + { + couldUse = true; + } + + var num = float.MaxValue; + + canUse = couldUse; + if (canUse) + { + var center = @object.Collider.bounds.center; + var position = __instance.transform.position; + num = Vector2.Distance(center, position); + canUse &= num <= __instance.UsableDistance && !PhysicsHelpers.AnythingBetween(@object.Collider, center, position, Constants.ShipOnlyMask, false); + } + __result = num; + } + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index b7b46cd..fbcdb37 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -223,7 +223,7 @@ public static class RoleSettingMenuPatches { ICustomRole customRole = role as ICustomRole; __instance.roleDescriptionText.text = customRole.RoleLongDescription; - __instance.roleTitleText.text = DestroyableSingleton.Instance.GetString(role.StringName, Array.Empty()); + __instance.roleTitleText.text = DestroyableSingleton.Instance.GetString(role.StringName, []); __instance.roleScreenshot.sprite = Sprite.Create(customRole.OptionsScreenshot.LoadAsset().texture, new Rect(0, 0, 370, 230), Vector2.one / 2, 100); __instance.roleScreenshot.drawMode = SpriteDrawMode.Sliced; __instance.roleHeaderSprite.color = customRole.RoleColor; diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index 89060e0..1e0145e 100644 --- a/MiraAPI/Patches/PlayerControlPatches.cs +++ b/MiraAPI/Patches/PlayerControlPatches.cs @@ -1,13 +1,23 @@ using HarmonyLib; using MiraAPI.Hud; using MiraAPI.Roles; +using MiraAPI.Modifiers; +using Reactor.Utilities.Extensions; namespace MiraAPI.Patches; [HarmonyPatch(typeof(PlayerControl))] public static class PlayerControlPatches { - + [HarmonyPrefix] + [HarmonyPatch(nameof(PlayerControl.Start))] + public static void PlayerControlStartPostfix(PlayerControl __instance) + { + if (__instance.gameObject.TryGetComponent(out var comp)) comp.DestroyImmediate(); + + __instance.gameObject.AddComponent(); + } + [HarmonyPostfix] [HarmonyPatch(nameof(PlayerControl.FixedUpdate))] public static void PlayerControlFixedUpdatePostfix(PlayerControl __instance) diff --git a/MiraAPI/Patches/Roles/RoleManagerPatches.cs b/MiraAPI/Patches/Roles/RoleManagerPatches.cs index e1139dd..8c48f72 100644 --- a/MiraAPI/Patches/Roles/RoleManagerPatches.cs +++ b/MiraAPI/Patches/Roles/RoleManagerPatches.cs @@ -1,6 +1,8 @@ using AmongUs.GameOptions; using HarmonyLib; +using MiraAPI.Modifiers; using MiraAPI.Roles; +using System.Linq; using UnityEngine; namespace MiraAPI.Patches.Roles; @@ -8,6 +10,15 @@ namespace MiraAPI.Patches.Roles; [HarmonyPatch(typeof(RoleManager))] public static class RoleManagerPatches { + [HarmonyPostfix] + [HarmonyPatch(nameof(RoleManager.SelectRoles))] + public static void ModifierSelectionPatches(RoleManager __instance) + { + if (!AmongUsClient.Instance.AmHost) return; + + ModifierManager.AssignModifiers(PlayerControl.AllPlayerControls.ToArray().Where(plr => !plr.Data.IsDead && !plr.Data.Disconnected).ToList()); + } + [HarmonyPrefix] [HarmonyPatch(nameof(RoleManager.AssignRoleOnDeath))] public static bool AssignRoleOnDeath(RoleManager __instance, [HarmonyArgument(0)] PlayerControl plr) diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 13c9c37..6071aeb 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -1,8 +1,8 @@ using BepInEx.Unity.IL2CPP; -using Il2CppInterop.Runtime.Injection; using MiraAPI.GameOptions; using MiraAPI.GameOptions.Attributes; using MiraAPI.Hud; +using MiraAPI.Modifiers; using MiraAPI.Roles; using MiraAPI.Utilities.Colors; using Reactor.Utilities; @@ -39,6 +39,7 @@ internal class MiraPluginManager var info = new MiraPluginInfo(plugin as IMiraPlugin, pluginInfo); + RegisterModifierAttribute(assembly); RegisterAllOptions(assembly, info); RegisterRoleAttribute(assembly, info); @@ -150,6 +151,19 @@ internal class MiraPluginManager } } + private static void RegisterModifierAttribute(Assembly assembly) + { + foreach (var type in assembly.GetTypes()) + { + var attribute = type.GetCustomAttribute(); + if (attribute != null) + { + ModifierManager.RegisterModifier(type); + } + } + } + + private static void RegisterButtonAttribute(Assembly assembly) { foreach (var type in assembly.GetTypes()) diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 42cc91c..f6c74e3 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -1,11 +1,12 @@ -using System; -using AmongUs.GameOptions; +using AmongUs.GameOptions; using BepInEx.Configuration; +using HarmonyLib; +using MiraAPI.Modifiers; +using MiraAPI.Networking; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; +using System; using System.Text; -using HarmonyLib; -using MiraAPI.Networking; using MiraAPI.PluginLoading; using UnityEngine; @@ -76,6 +77,11 @@ public interface ICustomRole return taskStringBuilder; } + bool IsModifierApplicable(BaseModifier modifier) + { + return true; + } + NetData GetNetData() { ParentMod.PluginConfig.TryGetEntry(NumConfigDefinition, out var numEntry); diff --git a/MiraAPI/Roles/RoleId.cs b/MiraAPI/Roles/RoleId.cs index d1174f6..a3926db 100644 --- a/MiraAPI/Roles/RoleId.cs +++ b/MiraAPI/Roles/RoleId.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; namespace MiraAPI.Roles; diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 47f516d..0dd4231 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -1,10 +1,11 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; using MiraAPI.GameOptions; +using MiraAPI.Modifiers; +using Reactor.Utilities; using System.Linq; using MiraAPI.Networking; -using Reactor.Utilities; using UnityEngine; namespace MiraAPI.Utilities; @@ -70,6 +71,55 @@ public static class Extensions return ModdedOptionsManager.ModdedOptions.Values.Any(opt => opt.OptionBehaviour && opt.OptionBehaviour.Equals(optionBehaviour)); } + public static ModifierComponent? GetModifierComponent(this PlayerControl player) + { + return player.GetComponent(); + } + + public static List Randomize(this List list) + { + List randomizedList = []; + System.Random rnd = new(); + while (list.Count > 0) + { + int index = rnd.Next(0, list.Count); + randomizedList.Add(list[index]); + list.RemoveAt(index); + } + return randomizedList; + } + + public static bool HasModifier(this PlayerControl? player) where T : BaseModifier + { + return player?.GetModifierComponent() != null && player.GetModifierComponent().ActiveModifiers.Exists(x => x is T); + } + public static bool HasModifier(this PlayerControl? player, uint id) + { + return player?.GetModifierComponent() != null && player.GetModifierComponent().ActiveModifiers.Exists(x => x.ModifierId == id); + } + + public static void AddModifier(this PlayerControl player) where T : BaseModifier + { + if (!ModifierManager.TypeToIdModifiers.TryGetValue(typeof(T), out var id)) + { + Logger.Error($"Cannot add modifier {typeof(T).Name} because it is not registered."); + return; + } + + ModifierComponent.RpcAddModifier(player, id); + } + + public static void RemoveModifier(this PlayerControl player) where T : BaseModifier + { + if (!ModifierManager.TypeToIdModifiers.TryGetValue(typeof(T), out var id)) + { + Logger.Error($"Cannot add modifier {typeof(T).Name} because it is not registered."); + return; + } + + ModifierComponent.RpcRemoveModifier(player, id); + } + public static Color DarkenColor(this Color color) { return new Color(color.r - 0.45f, color.g - 0.45f, color.b - 0.45f); diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 765aaec..4444b12 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -1,6 +1,7 @@ using MiraAPI.Roles; using System.Linq; using System.Text; +using TMPro; using UnityEngine; namespace MiraAPI.Utilities; @@ -8,17 +9,43 @@ namespace MiraAPI.Utilities; public static class Helpers { public static readonly ContactFilter2D Filter = ContactFilter2D.CreateLegacyFilter(Constants.NotShipMask, float.MinValue, float.MaxValue); - public static PlainShipRoom GetRoom(Vector3 pos) { return ShipStatus.Instance.AllRooms.ToList().Find(room => room.roomArea.OverlapPoint(pos)); } + public static TextMeshPro CreateTextLabel(string name, Transform parent, +AspectPosition.EdgeAlignments alignment, Vector3 distance, float fontSize = 2f, +TextAlignmentOptions textAlignment = TextAlignmentOptions.Center) + { + var textObj = new GameObject(name) + { + transform = + { + parent = parent + }, + layer = LayerMask.NameToLayer("UI") + }; + + var textMeshPro = textObj.AddComponent(); + textMeshPro.fontSize = fontSize; + textMeshPro.alignment = textAlignment; + textMeshPro.font = HudManager.Instance.TaskPanel.taskText.font; + textMeshPro.fontMaterial = HudManager.Instance.TaskPanel.taskText.fontMaterial; + + var aspectPosition = textObj.AddComponent(); + aspectPosition.Alignment = alignment; + aspectPosition.DistanceFromEdge = distance; + aspectPosition.AdjustPosition(); + + return textMeshPro; + } + public static DeadBody GetBodyById(byte id) { return Object.FindObjectsOfType().FirstOrDefault(body => body.ParentId == id); } - + public static string GetSuffix(NumberSuffixes suffix) { return suffix switch diff --git a/README.md b/README.md index d57449c..c6eaba9 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ A thorough, but simple, Among Us modding API and utility library that covers: - Roles - Options +- Modifiers - Buttons - Assets - Compatibility -- ~~Modifiers~~ (coming soon) - ~~Game Modes~~ (coming soon) Mira API strives to be simple and easy to use, while also using as many base game elements as possible. The result is a less intrusive, better modding API that covers general use cases. @@ -62,6 +62,21 @@ Mira API handles everything else, from adding the proper options to the settings See [this file](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Roles/CustomRole.cs) for a code example. +## Modifiers +Mira API uses a different definition of 'modifiers' than other Among Us mods. For example, in Town Of Us, a modifier is an extra "ability" that is applied on top of the base role. However, in Mira API, modifiers are very flexible. A modifier is anything that "modifiers" a player's abilities or interactions. + +Mira provides 3 classes for working with modifiers: +- `BaseModifier`: The base for every modifier. You MUST add and remove this modifier from a player manually! +- `TimedModifier`: A modifier that has a time limit. This modifier has to be added manually, but will automatically remove itself after the time limit +- `GameModifier`: This works like the typical TOU modifier, where it is automatically applied at the beginning of the game, then removed at the end. + +Modifiers provide various overridable functions and properties for custom behaviour. They can also be used for "tagging" a player. You can check if a player has a modifier through the extension method `HasModifier` on a `PlayerControl` object. + +To start using a modifier, pick one of the base classes above and create a class that inherits from it. Implement the properties and methods you would like, then add the `[RegisterModifier]` attribute to the class. + +An example Game modifier can be found [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Modifiers/ExampleGameModifier.cs). +An example Timer modifier can be found [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Modifiers/ModifierTimerExample.cs). + ## Options Options are also very simple in Mira API. Mira API handles all the hard work behind the scenes, so developers only have to follow a few steps to create their custom options. The Options API is split up into Groups and Options. Every Option needs to be in a Group.