diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 278c205..ad0f754 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -6,11 +6,11 @@ jobs: build: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: true - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: | ~/.nuget/packages @@ -20,7 +20,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x @@ -29,17 +29,17 @@ jobs: with: verbosity: Diagnostic - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: MiraAPI.dll path: MiraAPI/bin/Release/net6.0/MiraAPI.dll - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: AllOfUs.MiraAPI.nupkg path: MiraAPI/bin/Release/AllOfUs.MiraAPI.*.nupkg - - uses: softprops/action-gh-release@v1 + - uses: softprops/action-gh-release@v2 if: github.ref_type == 'tag' with: draft: true diff --git a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs index 86feece..3b7363a 100644 --- a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs +++ b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs @@ -10,7 +10,6 @@ using UnityEngine; namespace MiraAPI.Example.Buttons.Freezer; -[RegisterButton] public class FreezeButton : CustomActionButton { public override string Name => "Freeze"; diff --git a/MiraAPI.Example/Buttons/MeetingButton.cs b/MiraAPI.Example/Buttons/MeetingButton.cs index 742c7e0..c4d59e3 100644 --- a/MiraAPI.Example/Buttons/MeetingButton.cs +++ b/MiraAPI.Example/Buttons/MeetingButton.cs @@ -6,7 +6,6 @@ using UnityEngine; namespace MiraAPI.Example.Buttons; -[RegisterButton] public class MeetingButton : CustomActionButton { public override string Name => "Call Meeting"; diff --git a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs index 564b738..ce20ddb 100644 --- a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs +++ b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs @@ -9,7 +9,6 @@ using UnityEngine; namespace MiraAPI.Example.Buttons.Teleporter; -[RegisterButton] public class TeleportButton : CustomActionButton { public override string Name => "Teleport"; @@ -17,6 +16,7 @@ public class TeleportButton : CustomActionButton public override float Cooldown => OptionGroupSingleton.Instance.TeleportCooldown.Value; public override float EffectDuration => OptionGroupSingleton.Instance.TeleportDuration; + public override int MaxUses => 2; public override LoadableAsset Sprite => ExampleAssets.TeleportButton; diff --git a/MiraAPI.Example/ExampleEventHandlers.cs b/MiraAPI.Example/ExampleEventHandlers.cs index 21904e4..e196b28 100644 --- a/MiraAPI.Example/ExampleEventHandlers.cs +++ b/MiraAPI.Example/ExampleEventHandlers.cs @@ -1,6 +1,8 @@ using MiraAPI.Events; using MiraAPI.Events.Mira; -using MiraAPI.Events.Vanilla; +using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.Events.Vanilla.Map; +using MiraAPI.Events.Vanilla.Player; using MiraAPI.Example.Buttons.Freezer; using Reactor.Utilities; @@ -14,6 +16,21 @@ public static class ExampleEventHandlers MiraEventManager.RegisterEventHandler>(FreezeButtonClickHandler, 1); MiraEventManager.RegisterEventHandler>(FreezeButtonCancelledHandler); MiraEventManager.RegisterEventHandler(UpdateSystemEventHandler); + + MiraEventManager.RegisterEventHandler(@event => + { + Logger.Info($"{@event.Source.Data.PlayerName} is about to kill {@event.Target.Data.PlayerName}"); + }); + + MiraEventManager.RegisterEventHandler(@event => + { + Logger.Info($"{@event.Source.Data.PlayerName} has killed {@event.Target.Data.PlayerName}"); + }); + + MiraEventManager.RegisterEventHandler(@event => + { + Logger.Info($"{@event.Player.Data.PlayerName} completed {@event.Task.TaskType.ToString()}"); + }); } public static void UpdateSystemEventHandler(UpdateSystemEvent @event) diff --git a/MiraAPI.Example/Modifiers/CaptainModifier.cs b/MiraAPI.Example/Modifiers/CaptainModifier.cs index 09c733c..63b7c41 100644 --- a/MiraAPI.Example/Modifiers/CaptainModifier.cs +++ b/MiraAPI.Example/Modifiers/CaptainModifier.cs @@ -1,9 +1,7 @@ -using MiraAPI.Modifiers; -using MiraAPI.Modifiers.Types; +using MiraAPI.Modifiers.Types; namespace MiraAPI.Example.Modifiers; -[RegisterModifier] public class CaptainModifier : GameModifier { public override string ModifierName => "Captain"; diff --git a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs index 12a87ec..12a9587 100644 --- a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs +++ b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs @@ -1,12 +1,10 @@ using Il2CppSystem; using MiraAPI.Example.Roles; -using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; using UnityEngine; namespace MiraAPI.Example.Modifiers.Freezer; -[RegisterModifier] public class FreezeModifier : TimedModifier { public override string ModifierName => "Freezed"; diff --git a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs index c96507a..c640ed8 100644 --- a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs +++ b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs @@ -1,9 +1,7 @@ -using MiraAPI.Modifiers; -using MiraAPI.Modifiers.Types; +using MiraAPI.Modifiers.Types; namespace MiraAPI.Example.Modifiers; -[RegisterModifier] public class HighPriorityModifier : GameModifier { public override string ModifierName => "High Priority"; diff --git a/MiraAPI.Example/Options/ExampleOptions.cs b/MiraAPI.Example/Options/ExampleOptions.cs index 9e9fa59..53bc9eb 100644 --- a/MiraAPI.Example/Options/ExampleOptions.cs +++ b/MiraAPI.Example/Options/ExampleOptions.cs @@ -16,7 +16,7 @@ public class ExampleOptions : AbstractOptionGroup [ModdedToggleOption("Toggle Opt 2")] public bool ToggleOpt2 { get; set; } = true; - [ModdedNumberOption("Number Opt", min: 0, max: 10, suffixType: MiraNumberSuffixes.Percent)] + [ModdedNumberOption("Number Opt", min: 0, max: 10, increment: .25f, formatString: "0.00", suffixType: MiraNumberSuffixes.Percent)] public float NumberOpt { get; set; } = 4f; [ModdedEnumOption("Best API", typeof(BestApi), ["Mira API", "Mitochondria", "Reactor"])] diff --git a/MiraAPI.Example/Roles/ChameleonRole.cs b/MiraAPI.Example/Roles/ChameleonRole.cs index e3eb6c4..1f2f61f 100644 --- a/MiraAPI.Example/Roles/ChameleonRole.cs +++ b/MiraAPI.Example/Roles/ChameleonRole.cs @@ -4,7 +4,6 @@ using UnityEngine; namespace MiraAPI.Example.Roles; -[RegisterCustomRole] public class ChameloenRole : CrewmateRole, ICustomRole { public string RoleName => "Chamelon"; diff --git a/MiraAPI.Example/Roles/FreezerRole.cs b/MiraAPI.Example/Roles/FreezerRole.cs index 4f07124..bef24d2 100644 --- a/MiraAPI.Example/Roles/FreezerRole.cs +++ b/MiraAPI.Example/Roles/FreezerRole.cs @@ -3,7 +3,6 @@ using UnityEngine; namespace MiraAPI.Example.Roles; -[RegisterCustomRole] public class FreezerRole : ImpostorRole, ICustomRole { public string RoleName => "Freezer"; diff --git a/MiraAPI.Example/Roles/NeutralKillerRole.cs b/MiraAPI.Example/Roles/NeutralKillerRole.cs index 0eb60af..e9b4344 100644 --- a/MiraAPI.Example/Roles/NeutralKillerRole.cs +++ b/MiraAPI.Example/Roles/NeutralKillerRole.cs @@ -3,22 +3,28 @@ using UnityEngine; namespace MiraAPI.Example.Roles; -[RegisterCustomRole] public class NeutralKillerRole : ImpostorRole, ICustomRole { - public string RoleName => "Neutral Killer"; - public string RoleDescription => "Neutral who can kill."; + public string RoleName => "Outcast Killer"; + public string RoleDescription => "Outcast who can kill."; public string RoleLongDescription => RoleDescription; public Color RoleColor => Color.magenta; - public ModdedRoleTeams Team => ModdedRoleTeams.Neutral; + public ModdedRoleTeams Team => ModdedRoleTeams.Custom; - public CustomRoleConfiguration Configuration => new CustomRoleConfiguration(this) + public CustomRoleConfiguration Configuration => new(this) { UseVanillaKillButton = true, CanGetKilled = true, CanUseVent = true, }; + public RoleOptionsGroup RoleOptionsGroup { get; } = new("Outcast", Color.gray); + + public TeamIntroConfiguration? IntroConfiguration { get; } = new( + Color.gray, + "NEUTRAL", + "You are an Outcast. You do not have a team."); + public override void SpawnTaskHeader(PlayerControl playerControl) { // remove existing task header. diff --git a/MiraAPI.Example/Roles/TeleporterRole.cs b/MiraAPI.Example/Roles/TeleporterRole.cs index 447e185..5b00f63 100644 --- a/MiraAPI.Example/Roles/TeleporterRole.cs +++ b/MiraAPI.Example/Roles/TeleporterRole.cs @@ -3,7 +3,6 @@ using UnityEngine; namespace MiraAPI.Example.Roles; -[RegisterCustomRole] public class TeleporterRole : CrewmateRole, ICustomRole { public string RoleName => "Teleporter"; @@ -19,4 +18,9 @@ public class TeleporterRole : CrewmateRole, ICustomRole DefaultChance = 73, DefaultRoleCount = 4, }; + + public bool CanLocalPlayerSeeRole(PlayerControl player) + { + return true; + } } diff --git a/MiraAPI/Events/MiraEventManager.cs b/MiraAPI/Events/MiraEventManager.cs index 719d69c..eff933d 100644 --- a/MiraAPI/Events/MiraEventManager.cs +++ b/MiraAPI/Events/MiraEventManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Reactor.Utilities; namespace MiraAPI.Events; @@ -22,7 +21,6 @@ public static class MiraEventManager EventWrappers.TryGetValue(typeof(T), out var handlers); if (handlers == null) { - Logger.Warning("No handlers for event " + typeof(T).Name); return false; } @@ -45,7 +43,6 @@ public static class MiraEventManager EventWrappers.TryGetValue(type, out var handlers); if (handlers == null) { - Logger.Warning("No handlers for event " + type.Name); return false; } diff --git a/MiraAPI/Events/Vanilla/AdminButtonClickEvent.cs b/MiraAPI/Events/Vanilla/AdminButtonClickEvent.cs deleted file mode 100644 index c37b933..0000000 --- a/MiraAPI/Events/Vanilla/AdminButtonClickEvent.cs +++ /dev/null @@ -1,21 +0,0 @@ -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/Gameplay/AfterMurderEvent.cs similarity index 94% rename from MiraAPI/Events/Vanilla/AfterMurderEvent.cs rename to MiraAPI/Events/Vanilla/Gameplay/AfterMurderEvent.cs index a425cd2..fcf9df4 100644 --- a/MiraAPI/Events/Vanilla/AfterMurderEvent.cs +++ b/MiraAPI/Events/Vanilla/Gameplay/AfterMurderEvent.cs @@ -1,4 +1,4 @@ -namespace MiraAPI.Events.Vanilla; +namespace MiraAPI.Events.Vanilla.Gameplay; /// /// Event that is invoked after a player is murdered. Only called after a successful murder. This event is not cancelable. diff --git a/MiraAPI/Events/Vanilla/BeforeMurderEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/BeforeMurderEvent.cs similarity index 94% rename from MiraAPI/Events/Vanilla/BeforeMurderEvent.cs rename to MiraAPI/Events/Vanilla/Gameplay/BeforeMurderEvent.cs index f4f2be3..82694f7 100644 --- a/MiraAPI/Events/Vanilla/BeforeMurderEvent.cs +++ b/MiraAPI/Events/Vanilla/Gameplay/BeforeMurderEvent.cs @@ -1,4 +1,4 @@ -namespace MiraAPI.Events.Vanilla; +namespace MiraAPI.Events.Vanilla.Gameplay; /// /// Event that is invoked before a player is murdered. This event is cancelable. diff --git a/MiraAPI/Events/Vanilla/Gameplay/GameEndEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/GameEndEvent.cs new file mode 100644 index 0000000..1f06533 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Gameplay/GameEndEvent.cs @@ -0,0 +1,21 @@ +namespace MiraAPI.Events.Vanilla.Gameplay; + +/// +/// The event that is invoked when the end game screen is shown. Non cancelable. +/// +public class GameEndEvent : MiraEvent +{ + /// + /// Gets the instance of the EndGameManager. + /// + public EndGameManager EndGameManager { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The instance of the EndGameManager. + public GameEndEvent(EndGameManager manager) + { + EndGameManager = manager; + } +} diff --git a/MiraAPI/Events/Vanilla/Gameplay/IntroBeginEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/IntroBeginEvent.cs new file mode 100644 index 0000000..5d5e8d8 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Gameplay/IntroBeginEvent.cs @@ -0,0 +1,21 @@ +namespace MiraAPI.Events.Vanilla.Gameplay; + +/// +/// The event that is invoked when the intro cutscene is shown. Non cancelable. +/// +public class IntroBeginEvent : MiraEvent +{ + /// + /// Gets the instance of the IntroCutscene. + /// + public IntroCutscene IntroCutscene { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The intro cutscene. + public IntroBeginEvent(IntroCutscene introCutscene) + { + IntroCutscene = introCutscene; + } +} diff --git a/MiraAPI/Events/Vanilla/Gameplay/SetRoleEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/SetRoleEvent.cs new file mode 100644 index 0000000..4c00cf2 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Gameplay/SetRoleEvent.cs @@ -0,0 +1,30 @@ +using AmongUs.GameOptions; + +namespace MiraAPI.Events.Vanilla.Gameplay; + +/// +/// Event that is invoked after a player's role is set. This event is not cancelable. +/// +public class SetRoleEvent : MiraEvent +{ + /// + /// Gets the player whos role was changed. + /// + public PlayerControl Player { get; } + + /// + /// Gets the role that the player was set to. + /// + public RoleTypes Role { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The player. + /// The new role. + public SetRoleEvent(PlayerControl player, RoleTypes role) + { + Player = player; + Role = role; + } +} diff --git a/MiraAPI/Events/Vanilla/CloseDoorsEvent.cs b/MiraAPI/Events/Vanilla/Map/CloseDoorsEvent.cs similarity index 92% rename from MiraAPI/Events/Vanilla/CloseDoorsEvent.cs rename to MiraAPI/Events/Vanilla/Map/CloseDoorsEvent.cs index 8d0b9bf..86f2bfd 100644 --- a/MiraAPI/Events/Vanilla/CloseDoorsEvent.cs +++ b/MiraAPI/Events/Vanilla/Map/CloseDoorsEvent.cs @@ -1,4 +1,4 @@ -namespace MiraAPI.Events.Vanilla; +namespace MiraAPI.Events.Vanilla.Map; /// /// Event fired when a player closes the doors in a room. diff --git a/MiraAPI/Events/Vanilla/Map/PlayerOpenSabotageEvent.cs b/MiraAPI/Events/Vanilla/Map/PlayerOpenSabotageEvent.cs new file mode 100644 index 0000000..8b2b6cb --- /dev/null +++ b/MiraAPI/Events/Vanilla/Map/PlayerOpenSabotageEvent.cs @@ -0,0 +1,21 @@ +namespace MiraAPI.Events.Vanilla.Map; + +/// +/// Sabotage from Vanilla Among Us. +/// +public class PlayerOpenSabotageEvent : MiraCancelableEvent +{ + /// + /// Gets the MapBehaviour. + /// + public MapBehaviour MapBehaviour { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The MapBehaviour. + public PlayerOpenSabotageEvent(MapBehaviour mapBehaviour) + { + MapBehaviour = mapBehaviour; + } +} diff --git a/MiraAPI/Events/Vanilla/UpdateSystemEvent.cs b/MiraAPI/Events/Vanilla/Map/UpdateSystemEvent.cs similarity index 96% rename from MiraAPI/Events/Vanilla/UpdateSystemEvent.cs rename to MiraAPI/Events/Vanilla/Map/UpdateSystemEvent.cs index 1ac8bda..6b6d907 100644 --- a/MiraAPI/Events/Vanilla/UpdateSystemEvent.cs +++ b/MiraAPI/Events/Vanilla/Map/UpdateSystemEvent.cs @@ -1,4 +1,4 @@ -namespace MiraAPI.Events.Vanilla; +namespace MiraAPI.Events.Vanilla.Map; /// /// Cancelable event that is invoked before a system is updated. Usually used for sabotage events. diff --git a/MiraAPI/Events/Vanilla/Meeting/EjectionEvent.cs b/MiraAPI/Events/Vanilla/Meeting/EjectionEvent.cs new file mode 100644 index 0000000..1d25098 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Meeting/EjectionEvent.cs @@ -0,0 +1,21 @@ +namespace MiraAPI.Events.Vanilla.Meeting; + +/// +/// The event that is invoked when a player is ejected. Non-cancelable. +/// +public class EjectionEvent : MiraEvent +{ + /// + /// Gets the instance of the ExileController. + /// + public ExileController ExileController { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The exile controller. + public EjectionEvent(ExileController controller) + { + ExileController = controller; + } +} diff --git a/MiraAPI/Events/Vanilla/StartMeetingEvent.cs b/MiraAPI/Events/Vanilla/Meeting/StartMeetingEvent.cs similarity index 91% rename from MiraAPI/Events/Vanilla/StartMeetingEvent.cs rename to MiraAPI/Events/Vanilla/Meeting/StartMeetingEvent.cs index 2e3d8c2..b988b2f 100644 --- a/MiraAPI/Events/Vanilla/StartMeetingEvent.cs +++ b/MiraAPI/Events/Vanilla/Meeting/StartMeetingEvent.cs @@ -1,7 +1,7 @@ -namespace MiraAPI.Events.Vanilla; +namespace MiraAPI.Events.Vanilla.Meeting; /// -/// The event that is invoked when a meeting is called +/// The event that is invoked when a meeting is called. /// public class StartMeetingEvent : MiraCancelableEvent { diff --git a/MiraAPI/Events/Vanilla/Player/CompleteTaskEvent.cs b/MiraAPI/Events/Vanilla/Player/CompleteTaskEvent.cs new file mode 100644 index 0000000..31fd9d4 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Player/CompleteTaskEvent.cs @@ -0,0 +1,28 @@ +namespace MiraAPI.Events.Vanilla.Player; + +/// +/// The event that is invoked when a player completes a task. Non cancelable. +/// +public class CompleteTaskEvent : MiraEvent +{ + /// + /// Gets the instance of the . + /// + public PlayerControl Player { get; } + + /// + /// Gets the instance of the that the player completed. + /// + public PlayerTask Task { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The player who completed the task. + /// The task that the player completed. + public CompleteTaskEvent(PlayerControl player, PlayerTask task) + { + Player = player; + Task = task; + } +} diff --git a/MiraAPI/Events/Vanilla/Player/PlayerDeathEvent.cs b/MiraAPI/Events/Vanilla/Player/PlayerDeathEvent.cs new file mode 100644 index 0000000..e787b13 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Player/PlayerDeathEvent.cs @@ -0,0 +1,28 @@ +namespace MiraAPI.Events.Vanilla.Player; + +/// +/// Event that is invoked when a player dies. Non cancelable. +/// +public class PlayerDeathEvent : MiraEvent +{ + /// + /// Gets the player who died. + /// + public PlayerControl Player { get; } + + /// + /// Gets the reason the player died. + /// + public DeathReason DeathReason { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The player who died. + /// The reason the player died. + public PlayerDeathEvent(PlayerControl player, DeathReason reason) + { + Player = player; + DeathReason = reason; + } +} diff --git a/MiraAPI/Events/Vanilla/Player/PlayerJoinEvent.cs b/MiraAPI/Events/Vanilla/Player/PlayerJoinEvent.cs new file mode 100644 index 0000000..ba9bfb5 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Player/PlayerJoinEvent.cs @@ -0,0 +1,23 @@ +using InnerNet; + +namespace MiraAPI.Events.Vanilla.Player; + +/// +/// Event that is invoked when a player joins the lobby. Non cancelable. +/// +public class PlayerJoinEvent : MiraEvent +{ + /// + /// Gets the player who joined. + /// + public ClientData ClientData { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The data of the player who joined. + public PlayerJoinEvent(ClientData data) + { + ClientData = data; + } +} diff --git a/MiraAPI/Events/Vanilla/Player/PlayerLeaveEvent.cs b/MiraAPI/Events/Vanilla/Player/PlayerLeaveEvent.cs new file mode 100644 index 0000000..4607305 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Player/PlayerLeaveEvent.cs @@ -0,0 +1,30 @@ +using InnerNet; + +namespace MiraAPI.Events.Vanilla.Player; + +/// +/// Event that is invoked when a player leaves the game. Non cancelable. +/// +public class PlayerLeaveEvent : MiraEvent +{ + /// + /// Gets the player who left. + /// + public ClientData ClientData { get; } + + /// + /// Gets the reason why the player left. + /// + public DisconnectReasons Reason { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The data of the player who left. + /// The reason why the player left. + public PlayerLeaveEvent(ClientData data, DisconnectReasons reason) + { + ClientData = data; + Reason = reason; + } +} diff --git a/MiraAPI/Events/Vanilla/SabotageButtonClickEvent.cs b/MiraAPI/Events/Vanilla/SabotageButtonClickEvent.cs deleted file mode 100644 index b657486..0000000 --- a/MiraAPI/Events/Vanilla/SabotageButtonClickEvent.cs +++ /dev/null @@ -1,21 +0,0 @@ -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/EnterVentEvent.cs b/MiraAPI/Events/Vanilla/Usables/EnterVentEvent.cs similarity index 94% rename from MiraAPI/Events/Vanilla/EnterVentEvent.cs rename to MiraAPI/Events/Vanilla/Usables/EnterVentEvent.cs index 63e872c..ce3b3ca 100644 --- a/MiraAPI/Events/Vanilla/EnterVentEvent.cs +++ b/MiraAPI/Events/Vanilla/Usables/EnterVentEvent.cs @@ -1,4 +1,4 @@ -namespace MiraAPI.Events.Vanilla; +namespace MiraAPI.Events.Vanilla.Usables; /// /// Event that is invoked when a player enters a vent. This event is cancelable. diff --git a/MiraAPI/Events/Vanilla/ExitVentEvent.cs b/MiraAPI/Events/Vanilla/Usables/ExitVentEvent.cs similarity index 94% rename from MiraAPI/Events/Vanilla/ExitVentEvent.cs rename to MiraAPI/Events/Vanilla/Usables/ExitVentEvent.cs index 1269b8a..351411f 100644 --- a/MiraAPI/Events/Vanilla/ExitVentEvent.cs +++ b/MiraAPI/Events/Vanilla/Usables/ExitVentEvent.cs @@ -1,4 +1,4 @@ -namespace MiraAPI.Events.Vanilla; +namespace MiraAPI.Events.Vanilla.Usables; /// /// Event that is invoked when a player exits a vent. This event is cancelable. diff --git a/MiraAPI/Events/Vanilla/Usables/PlayerCanUseEvent.cs b/MiraAPI/Events/Vanilla/Usables/PlayerCanUseEvent.cs new file mode 100644 index 0000000..f9ba115 --- /dev/null +++ b/MiraAPI/Events/Vanilla/Usables/PlayerCanUseEvent.cs @@ -0,0 +1,15 @@ +namespace MiraAPI.Events.Vanilla.Usables; + +/// +/// Event for if a can use an from Vanilla Among Us. Will always be ran locally. +/// +public class PlayerCanUseEvent : PlayerUseEvent +{ + /// + /// Initializes a new instance of the class. + /// + /// The IUsable. + public PlayerCanUseEvent(IUsable usable) : base(usable) + { + } +} diff --git a/MiraAPI/Events/Vanilla/Usables/PlayerUseEvent.cs b/MiraAPI/Events/Vanilla/Usables/PlayerUseEvent.cs new file mode 100644 index 0000000..655a46c --- /dev/null +++ b/MiraAPI/Events/Vanilla/Usables/PlayerUseEvent.cs @@ -0,0 +1,34 @@ +namespace MiraAPI.Events.Vanilla; + +/// +/// Event for using a from Vanilla Among Us. Will always be ran locally. +/// +public class PlayerUseEvent : MiraCancelableEvent +{ + /// + /// Gets the instance of that was used. + /// + public IUsable Usable { get; } + + /// + /// Gets a value indicating whether the IUsable is a , , or . + /// + public bool IsPrimaryConsole { get; } + + /// + /// Gets a value indicating whether the IUsable is a . + /// + public bool IsVent { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The IUsable. + public PlayerUseEvent(IUsable usable) + { + Usable = usable; + + IsPrimaryConsole = usable.TryCast() || usable.TryCast() || usable.TryCast(); + IsVent = usable.TryCast(); + } +} diff --git a/MiraAPI/Events/Vanilla/UseButtonClickEvent.cs b/MiraAPI/Events/Vanilla/UseButtonClickEvent.cs deleted file mode 100644 index 043b166..0000000 --- a/MiraAPI/Events/Vanilla/UseButtonClickEvent.cs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index a46601f..0000000 --- a/MiraAPI/Events/Vanilla/VentButtonClickEvent.cs +++ /dev/null @@ -1,21 +0,0 @@ -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/GameModes/DefaultMode.cs b/MiraAPI/GameModes/DefaultMode.cs index 73c4ca8..83c11ba 100644 --- a/MiraAPI/GameModes/DefaultMode.cs +++ b/MiraAPI/GameModes/DefaultMode.cs @@ -1,6 +1,5 @@ namespace MiraAPI.GameModes; -[RegisterGameMode] public class DefaultMode : CustomGameMode { public override string Name => "Default"; diff --git a/MiraAPI/GameModes/RegisterGameModeAttribute.cs b/MiraAPI/GameModes/RegisterGameModeAttribute.cs deleted file mode 100644 index 4c66c35..0000000 --- a/MiraAPI/GameModes/RegisterGameModeAttribute.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; - -namespace MiraAPI.GameModes; - -[AttributeUsage(AttributeTargets.Class)] -public class RegisterGameModeAttribute : Attribute -{ - private static readonly HashSet RegisteredAssemblies = []; - - internal static void Register(Assembly assembly) - { - if (!RegisteredAssemblies.Add(assembly)) - { - return; - } - - foreach (var type in assembly.GetTypes()) - { - var attribute = type.GetCustomAttribute(); - if (attribute != null) - { - CustomGameModeManager.RegisterGameMode(type); - } - } - } -} diff --git a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs index 2bd4438..b7b85c2 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs @@ -15,13 +15,14 @@ public class ModdedNumberOptionAttribute( float max, float increment = 1, MiraNumberSuffixes suffixType = MiraNumberSuffixes.None, + string? formatString = null, bool zeroInfinity = false, Type? roleType = null) : ModdedOptionAttribute(title, roleType) { internal override IModdedOption CreateOption(object? value, PropertyInfo property) { - return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, zeroInfinity, RoleType); + return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, formatString, zeroInfinity, RoleType); } /// diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index b6c42bb..a6dd1c9 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -31,7 +31,6 @@ public static class ModdedOptionsManager { if (Activator.CreateInstance(type) is not AbstractOptionGroup group) { - Logger.Error($"Skipping option group {type.Name}. Does not inherit AbstractOptionGroup."); return false; } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs index 24cd346..deb8052 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs @@ -46,6 +46,7 @@ public class ModdedNumberOption : ModdedOption /// The maximum value. /// The increment. /// The suffix type. + /// Optional format string for the option screen. /// Whether zero is infinity or not. /// An optional role type. public ModdedNumberOption( @@ -55,6 +56,7 @@ public class ModdedNumberOption : ModdedOption float max, float increment, MiraNumberSuffixes suffixType, + string? formatString = null, bool zeroInfinity = false, Type? roleType = null) : base(title, defaultValue, roleType) { @@ -74,7 +76,13 @@ public class ModdedNumberOption : ModdedOption data.Value = Value; data.Increment = Increment; data.ValidRange = new FloatRange(Min, Max); - data.FormatString = Increment % 1 == 0 && Value % 1 == 0 && Min % 1 == 0 && Max % 1 == 0 ? "0" : "0.0"; // not sure if the == checks will work since this is floats but we'll find out + data.FormatString = formatString ?? + (!Mathf.Approximately(defaultValue, 0) || + !Mathf.Approximately(Increment % 1, 0) || + !Mathf.Approximately(Value % 1, 0) || + !Mathf.Approximately(Min % 1, 0) || + !Mathf.Approximately(Max % 1, 0) ? "0.0" : "0"); + data.ZeroIsInfinity = ZeroInfinity; data.SuffixType = (NumberSuffixes)SuffixType; data.OptionName = FloatOptionNames.Invalid; diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs index 3e47d59..7049a8e 100644 --- a/MiraAPI/Hud/CustomActionButton.cs +++ b/MiraAPI/Hud/CustomActionButton.cs @@ -1,7 +1,8 @@ -using System; -using MiraAPI.Events; +using MiraAPI.Events; using MiraAPI.Events.Mira; +using MiraAPI.Patches; using MiraAPI.Utilities.Assets; +using System; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; @@ -47,7 +48,7 @@ public abstract class CustomActionButton /// /// Gets the location of the button on the screen. /// - public virtual ButtonLocation Location => ButtonLocation.BottomLeft; + public virtual ButtonLocation Location { get; private set; } = ButtonLocation.BottomLeft; /// /// Gets a value indicating whether the button has an effect ability. @@ -64,6 +65,11 @@ public abstract class CustomActionButton /// public bool EffectActive { get; protected set; } + /// + /// Gets or sets a value indicating whether the timer is currently active. + /// + public bool TimerPaused { get; protected set; } + /// /// Gets or sets the amount of uses left. /// @@ -79,7 +85,11 @@ public abstract class CustomActionButton /// protected ActionButton? Button { get; private set; } - internal void CreateButton(Transform parent) + /// + /// The method used to create the button. + /// + /// The parent of the button. + public virtual void CreateButton(Transform parent) { if (Button) { @@ -89,6 +99,7 @@ public abstract class CustomActionButton UsesLeft = MaxUses; Timer = AmongUsClient.Instance?.NetworkMode == NetworkModes.FreePlay ? 0 : InitialCooldown; EffectActive = false; + TimerPaused = false; Button = Object.Instantiate(HudManager.Instance.AbilityButton, parent); Button.name = Name + "Button"; @@ -142,10 +153,49 @@ public abstract class CustomActionButton })); } + /// + /// Allows you to change the button's location. + /// + /// The new location. + /// Whether the button's position should change ingame. + public virtual void SetButtonLocation(ButtonLocation location, bool moveButton = true) + { + if (!HudManager.InstanceExists || Button == null) + { + return; + } + + this.Location = location; + + if (!moveButton) return; + + if (HudManagerPatches._bottomLeft == null || HudManagerPatches._bottomRight == null) + { + return; + } + + switch (location) + { + case ButtonLocation.BottomLeft: + var gridArrange = HudManagerPatches._bottomLeft.GetComponent(); + var aspectPosition = HudManagerPatches._bottomLeft.GetComponent(); + + Button.transform.SetParent(HudManagerPatches._bottomLeft.transform); + + gridArrange.Start(); + gridArrange.ArrangeChilds(); + aspectPosition.AdjustPosition(); + break; + case ButtonLocation.BottomRight: + Button.transform.SetParent(HudManagerPatches._bottomRight); + break; + } + } + /// /// A utility function to reset the cooldown and/or effect of the button. /// - public void ResetCooldownAndOrEffect() + public virtual void ResetCooldownAndOrEffect() { Timer = Cooldown; if (EffectActive) @@ -160,7 +210,7 @@ public abstract class CustomActionButton /// A utility function to override the sprite of the button. /// /// The new sprite to override with. - public void OverrideSprite(Sprite sprite) + public virtual void OverrideSprite(Sprite sprite) { if (Button != null) { @@ -172,7 +222,7 @@ public abstract class CustomActionButton /// A utility function to override the name of the button. /// /// The new name to override with. - public void OverrideName(string name) + public virtual void OverrideName(string name) { Button?.OverrideText(name); } @@ -181,7 +231,7 @@ public abstract class CustomActionButton /// Set the button's timer. /// /// The time you want to set to. - public void SetTimer(float time) + public virtual void SetTimer(float time) { Timer = Mathf.Clamp(time, -1, float.MaxValue); } @@ -190,7 +240,7 @@ public abstract class CustomActionButton /// Increase the button's timer. /// /// The amount you want to increase by. - public void IncreaseTimer(float amount) + public virtual void IncreaseTimer(float amount) { SetTimer(Timer + amount); } @@ -199,26 +249,40 @@ public abstract class CustomActionButton /// Decrease the button's timer. /// /// The amount you want to decrease by. - public void DecreaseTimer(float amount) + 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 void SetUses(int amount) + 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 void IncreaseUses(int amount = 1) + public virtual void IncreaseUses(int amount = 1) { SetUses(UsesLeft + amount); } @@ -227,7 +291,7 @@ public abstract class CustomActionButton /// Decrease the amount of uses this button has left. /// /// The amount you want to decrease by. Default: 1. - public void DecreaseUses(int amount = 1) + public virtual void DecreaseUses(int amount = 1) { SetUses(UsesLeft - amount); } @@ -262,14 +326,24 @@ public abstract class CustomActionButton } /// - /// When the button is enabled, this method is called to determine if the button can be used. + /// 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 used or not. + /// A value that represents whether the button can be clicked or not. + public virtual bool CanClick() + { + return Timer <= 0 && !EffectActive && 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 Timer <= 0 && !EffectActive && (!LimitedUses || UsesLeft > 0); + return !LimitedUses || UsesLeft > 0; } /// @@ -291,7 +365,7 @@ public abstract class CustomActionButton /// public virtual void ClickHandler() { - if (!CanUse()) + if (!CanClick()) { return; } @@ -303,7 +377,7 @@ public abstract class CustomActionButton } OnClick(); - Button?.SetDisabled(); + if (HasEffect) { EffectActive = true; @@ -323,7 +397,7 @@ public abstract class CustomActionButton /// The local PlayerControl. public virtual void FixedUpdateHandler(PlayerControl playerControl) { - if (Timer >= 0) + if (Timer >= 0 && !TimerPaused) { Timer -= Time.deltaTime; } @@ -343,7 +417,17 @@ public abstract class CustomActionButton Button?.SetDisabled(); } - Button?.SetCoolDown(Timer, EffectActive ? EffectDuration : Cooldown); + if (EffectActive) + { + Button?.SetFillUp(Timer, EffectDuration); + + Button!.cooldownTimerText.text = Mathf.CeilToInt(Timer).ToString(); + Button!.cooldownTimerText.gameObject.SetActive(true); + } + else + { + Button?.SetCoolDown(Timer, Cooldown); + } FixedUpdate(playerControl); } @@ -401,4 +485,19 @@ public abstract class CustomActionButton : CustomActionButton where T : MonoB return base.CanUse() && Target != null; } + + /// + public override bool CanClick() + { + return base.CanClick() && Target != null; + } + + /// + /// Use this to reset the button's target after used. + /// + public virtual void ResetTarget() + { + Target = null; + SetOutline(false); + } } diff --git a/MiraAPI/Hud/CustomButtonManager.cs b/MiraAPI/Hud/CustomButtonManager.cs index 398aa58..2bdb9bf 100644 --- a/MiraAPI/Hud/CustomButtonManager.cs +++ b/MiraAPI/Hud/CustomButtonManager.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using MiraAPI.Events.Mira; +using MiraAPI.Events.Mira; using MiraAPI.PluginLoading; -using Reactor.Utilities; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Reflection; namespace MiraAPI.Hud; @@ -12,22 +12,20 @@ namespace MiraAPI.Hud; /// public static class CustomButtonManager { + /// + /// Gets a list of all registered custom buttons. + /// + public static ReadOnlyCollection Buttons { get; internal set; } = new([]); + internal static readonly List CustomButtons = []; internal static readonly Dictionary ButtonEventTypes = []; internal static readonly Dictionary ButtonCancelledEventTypes = []; - internal static void RegisterButton(Type buttonType, MiraPluginInfo pluginInfo) + internal static bool RegisterButton(Type buttonType, MiraPluginInfo pluginInfo) { - if (!typeof(CustomActionButton).IsAssignableFrom(buttonType)) + if (!buttonType.IsAssignableTo(typeof(CustomActionButton)) || Activator.CreateInstance(buttonType) is not CustomActionButton button) { - Logger.Error($"Skipping button {buttonType.Name}. Does not inherit CustomActionButton!"); - return; - } - - if (Activator.CreateInstance(buttonType) is not CustomActionButton button) - { - Logger.Error($"Failed to create button from {buttonType.Name}"); - return; + return false; } CustomButtons.Add(button); @@ -38,5 +36,7 @@ public static class CustomButtonManager ButtonEventTypes.Add(buttonType, typeof(MiraButtonClickEvent<>).MakeGenericType(buttonType)); ButtonCancelledEventTypes.Add(buttonType, typeof(MiraButtonCancelledEvent<>).MakeGenericType(buttonType)); + + return true; } } diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs index 7c861ec..a188596 100644 --- a/MiraAPI/Hud/CustomPlayerMenu.cs +++ b/MiraAPI/Hud/CustomPlayerMenu.cs @@ -1,11 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using AmongUs.GameOptions; +using AmongUs.GameOptions; using Il2CppInterop.Runtime.Attributes; using MiraAPI.Patches.Stubs; using Reactor.Utilities.Attributes; using Reactor.Utilities.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; using UnityEngine; using UnityEngine.Events; @@ -86,10 +86,16 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr) /// Function to determine if player should show in the custom menu. /// Onclick action for player. [HideFromIl2Cpp] - public void Begin(Func playerMatch, Action onClick) + public void Begin(Func playerMatch, Action onClick) { MinigameStubs.Begin(this, null); + var back = BackButton.GetComponent(); + back.OnClick.AddListener((UnityAction)(() => + { + onClick(null); + })); + DestroyableSingleton.Instance.Analytics.MinigameOpened(PlayerControl.LocalPlayer.Data, TaskType); var list = PlayerControl.AllPlayerControls.ToArray().Where(playerMatch).ToList(); potentialVictims = []; diff --git a/MiraAPI/Hud/RegisterButtonAttribute.cs b/MiraAPI/Hud/RegisterButtonAttribute.cs deleted file mode 100644 index 82e0ca5..0000000 --- a/MiraAPI/Hud/RegisterButtonAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace MiraAPI.Hud; - -/// -/// Attribute to register a button in the HUD. Necessary for the button to be recognized by Mira. -/// -[AttributeUsage(AttributeTargets.Class)] -public class RegisterButtonAttribute : Attribute; diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 47cd491..9f6aa93 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -30,6 +30,16 @@ public abstract class BaseModifier /// public virtual bool HideOnUi => false; + /// + /// Gets a value indicating whether the modifier is shown in the freeplay menu. + /// + public virtual bool ShowInFreeplay => false; + + /// + /// Gets a value indicating whether the modifier is unique. If true, the player can only have one instance of this modifier. + /// + public virtual bool Unique => true; + /// /// Gets the HUD information for this modifier. Defaults to the modifier name. Does nothing if is true. /// diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs index 8b91d21..2b00652 100644 --- a/MiraAPI/Modifiers/ModifierComponent.cs +++ b/MiraAPI/Modifiers/ModifierComponent.cs @@ -1,14 +1,14 @@ -using System; +using Il2CppInterop.Runtime.Attributes; +using MiraAPI.Modifiers.Types; +using MiraAPI.Utilities; +using Reactor.Utilities; +using Reactor.Utilities.Attributes; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Text; -using Il2CppInterop.Runtime.Attributes; -using MiraAPI.Modifiers.Types; -using MiraAPI.Utilities; -using Reactor.Utilities; -using Reactor.Utilities.Attributes; using TMPro; using UnityEngine; @@ -107,7 +107,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) var baseModifiers = filteredModifiers as BaseModifier[] ?? filteredModifiers.ToArray(); - if (baseModifiers.Length != 0) + if (baseModifiers.Length != 0 && !MeetingHud.Instance) { var stringBuild = new StringBuilder(); foreach (var mod in baseModifiers) @@ -122,6 +122,38 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) } } + /// + /// Gets a collection of modifiers by their type, or null if the player doesn't have one. + /// + /// The Type of the Modifier. + /// The Modifier if it is found, null otherwise. + public IEnumerable GetModifiersByType() where T : BaseModifier + { + return ActiveModifiers.OfType(); + } + + /// + /// Gets a modifier by its type, or null if the player doesn't have it. + /// + /// The Type of the Modifier. + /// The Modifier if it is found, null otherwise. + public T? GetModifier() where T : BaseModifier + { + return GetModifiersByType().FirstOrDefault(); + } + + /// + /// Tries to get a modifier by its type. + /// + /// The modifier or null. + /// The Type of the Modifier. + /// True if the modifier was found, false otherwise. + public bool TryGetModifier(out T? modifier) where T : BaseModifier + { + modifier = GetModifier(); + return modifier != null; + } + /// /// Removes a modifier from the player. /// @@ -228,7 +260,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) return null; } - if (Modifiers.Find(x => x.ModifierId == modifierId) != null) + if (Modifiers.Find(x => x.ModifierId == modifierId && x.Unique) != null) { Logger.Error($"Player already has modifier with id {modifierId}!"); return null; @@ -260,19 +292,23 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// Checks if a player has an active or queued modifier by its ID. /// /// The Modifier ID. + /// The predicate to check the modifier. /// True if the Modifier is present, false otherwise. - public bool HasModifier(uint id) + public bool HasModifier(uint id, Func? predicate=null) { - return ActiveModifiers.Exists(x => x.ModifierId == id) || _toAdd.Exists(x => x.ModifierId == id); + return ActiveModifiers.Exists(x => x.ModifierId == id && (predicate == null || predicate(x))) || + _toAdd.Exists(x => x.ModifierId == id && (predicate == null || predicate(x))); } /// /// Checks if a player has an active or queued modifier by its type. /// + /// The predicate to check the modifier. /// The Type of the Modifier. /// True if the Modifier is present, false otherwise. - public bool HasModifier() + public bool HasModifier(Func? predicate=null) where T : BaseModifier { - return ActiveModifiers.Exists(x => x is T) || _toAdd.Exists(x => x is T); + return ModifierManager.GetModifierId(typeof(T)) is { } id && + HasModifier(id, (Func?)predicate); } } diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index fab840c..d4d4004 100644 --- a/MiraAPI/Modifiers/ModifierManager.cs +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Serialization; using MiraAPI.Modifiers.Types; using MiraAPI.Networking; +using MiraAPI.PluginLoading; using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Networking.Rpc; @@ -49,19 +51,22 @@ public static class ModifierManager return TypeToIdModifierMap.GetValueOrDefault(type); } - internal static void RegisterModifier(Type modifierType) + internal static bool RegisterModifier(Type modifierType, MiraPluginInfo info) { if (!typeof(BaseModifier).IsAssignableFrom(modifierType)) { - return; + return false; } IdToTypeModifierMap.Add(GetNextId(), modifierType); TypeToIdModifierMap.Add(modifierType, _nextId); + var bm = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType); // this probably isn't a great idea + bm.ModifierId = _nextId; + info.Modifiers.Add(bm); if (!typeof(GameModifier).IsAssignableFrom(modifierType)) { - return; + return true; } var mod = Activator.CreateInstance(modifierType) as GameModifier; @@ -73,6 +78,7 @@ public static class ModifierManager } list.Add(_nextId); + return true; } internal static void AssignModifiers(List plrs) diff --git a/MiraAPI/Modifiers/RegisterModifierAttribute.cs b/MiraAPI/Modifiers/RegisterModifierAttribute.cs deleted file mode 100644 index 4858b96..0000000 --- a/MiraAPI/Modifiers/RegisterModifierAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace MiraAPI.Modifiers; - -/// -/// Marks a class as a modifier that can be registered with the . -/// -[AttributeUsage(AttributeTargets.Class)] -public class RegisterModifierAttribute : Attribute; diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index a7b1fe1..abcd1f7 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -5,6 +5,9 @@ /// public abstract class GameModifier : BaseModifier { + /// + public override bool ShowInFreeplay => true; + /// /// Gets the chance of the modifier being assigned to a player. /// diff --git a/MiraAPI/Networking/CustomMurderRpc.cs b/MiraAPI/Networking/CustomMurderRpc.cs index e28ddf2..1a46865 100644 --- a/MiraAPI/Networking/CustomMurderRpc.cs +++ b/MiraAPI/Networking/CustomMurderRpc.cs @@ -1,14 +1,13 @@ -using System.Collections; -using System.Linq; -using AmongUs.GameOptions; +using AmongUs.GameOptions; using Assets.CoreScripts; using BepInEx.Unity.IL2CPP.Utils; using MiraAPI.Events; -using MiraAPI.Events.Vanilla; +using MiraAPI.Events.Vanilla.Gameplay; using Reactor.Networking.Attributes; using Reactor.Networking.Rpc; -using Reactor.Utilities; using Reactor.Utilities.Extensions; +using System.Collections; +using System.Linq; using UnityEngine; namespace MiraAPI.Networking; @@ -84,7 +83,6 @@ public static class CustomMurderRpc bool playKillSound = true) { source.isKilling = false; - Logger.Error($"{source.PlayerId} trying to murder {target.PlayerId}"); var data = target.Data; if (resultFlags.HasFlag(MurderResultFlags.FailedError)) { @@ -117,7 +115,6 @@ public static class CustomMurderRpc target.RemoveProtection(); } - Logger.Error($"{source.PlayerId} failed to murder {target.PlayerId} due to guardian angel protection"); return; } @@ -185,7 +182,6 @@ public static class CustomMurderRpc source.shapeshiftTargetPlayerId, target.PlayerId); source.MyPhysics.StartCoroutine(source.KillAnimations.Random()?.CoPerformCustomKill(source, target, createDeadBody, teleportMurderer)); - Logger.Error($"{source.PlayerId} succeeded in murdering {target.PlayerId}"); } /// @@ -251,11 +247,11 @@ public static class CustomMurderRpc } target.Die(DeathReason.Kill, true); - yield return source.MyPhysics.Animations.CoPlayCustomAnimation(anim.BlurAnim); - sourcePhys.Animations.PlayIdleAnimation(); if (teleportMurderer) { + yield return source.MyPhysics.Animations.CoPlayCustomAnimation(anim.BlurAnim); + sourcePhys.Animations.PlayIdleAnimation(); source.NetTransform.SnapTo(target.transform.position); KillAnimation.SetMovement(source, true); } @@ -267,6 +263,9 @@ public static class CustomMurderRpc deadBody.enabled = true; } + var afterMurderEvent = new AfterMurderEvent(source, target); + MiraEventManager.InvokeEvent(afterMurderEvent); + if (!isParticipant) { yield break; @@ -279,8 +278,5 @@ 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/AmongUsClientSyncPatch.cs b/MiraAPI/Patches/AmongUsClientSyncPatch.cs index 3034b42..4a76283 100644 --- a/MiraAPI/Patches/AmongUsClientSyncPatch.cs +++ b/MiraAPI/Patches/AmongUsClientSyncPatch.cs @@ -1,5 +1,7 @@ using HarmonyLib; using InnerNet; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Player; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.Roles; @@ -9,11 +11,16 @@ namespace MiraAPI.Patches; /// /// Sync all options, role settings, and modifiers to the player when they join the game. /// -[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.CreatePlayer))] +[HarmonyPatch(typeof(AmongUsClient))] public static class AmongUsClientSyncPatch { - public static void Postfix(ClientData clientData) + [HarmonyPostfix] + [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.CreatePlayer))] + public static void CreatePlayerPatch(ClientData clientData) { + var joinEvent = new PlayerJoinEvent(clientData); + MiraEventManager.InvokeEvent(joinEvent); + if (!AmongUsClient.Instance.AmHost) { return; @@ -28,4 +35,12 @@ public static class AmongUsClientSyncPatch CustomRoleManager.SyncAllRoleSettings(clientData.Id); ModifierManager.SyncAllModifiers(clientData.Id); } + + [HarmonyPostfix] + [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnPlayerLeft))] + public static void PlayerLeftPatch(ClientData data, DisconnectReasons reason) + { + var leftEvent = new PlayerLeaveEvent(data, reason); + MiraEventManager.InvokeEvent(leftEvent); + } } diff --git a/MiraAPI/Patches/Colors/ScrollingColorsPatch.cs b/MiraAPI/Patches/Colors/ScrollingColorsPatch.cs index e89de23..f5ef949 100644 --- a/MiraAPI/Patches/Colors/ScrollingColorsPatch.cs +++ b/MiraAPI/Patches/Colors/ScrollingColorsPatch.cs @@ -4,17 +4,19 @@ using UnityEngine; namespace MiraAPI.Patches.Colors; -[HarmonyPatch(typeof(PlayerTab))] +/// +/// This patch adds scrolling to the colors tab in the player customization menu. +/// +[HarmonyPatch(typeof(PlayerTab), nameof(PlayerTab.OnEnable))] public static class ScrollingColorsPatch { // Collider private static BoxCollider2D? _collider; /// - /// Add scrolling to the colors tab + /// Add scrolling to the colors tab. /// - [HarmonyPostfix, HarmonyPatch("OnEnable")] - public static void AddScrollingToColorsTabPatch(PlayerTab __instance) + public static void Postfix(PlayerTab __instance) { if (!PlayerCustomizationMenu.Instance) { diff --git a/MiraAPI/Patches/Events/CanUsePatches.cs b/MiraAPI/Patches/Events/CanUsePatches.cs new file mode 100644 index 0000000..aa80fd7 --- /dev/null +++ b/MiraAPI/Patches/Events/CanUsePatches.cs @@ -0,0 +1,42 @@ +using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Usables; +using System.Collections.Generic; +using System.Reflection; + +namespace MiraAPI.Patches.Events; + +[HarmonyPatch] +public static class CanUsePatches +{ + public static IEnumerable TargetMethods() + { + yield return AccessTools.Method(typeof(Console), nameof(Console.CanUse)); + yield return AccessTools.Method(typeof(MapConsole), nameof(MapConsole.CanUse)); + yield return AccessTools.Method(typeof(SystemConsole), nameof(SystemConsole.CanUse)); + yield return AccessTools.Method(typeof(Ladder), nameof(Ladder.CanUse)); + yield return AccessTools.Method(typeof(PlatformConsole), nameof(PlatformConsole.CanUse)); + yield return AccessTools.Method(typeof(OpenDoorConsole), nameof(OpenDoorConsole.CanUse)); + yield return AccessTools.Method(typeof(DoorConsole), nameof(DoorConsole.CanUse)); + yield return AccessTools.Method(typeof(OptionsConsole), nameof(OptionsConsole.CanUse)); + yield return AccessTools.Method(typeof(ZiplineConsole), nameof(ZiplineConsole.CanUse)); + } + + [HarmonyPriority(Priority.Last)] + [HarmonyPrefix] + public static bool CanUsePatch(Il2CppSystem.Object __instance, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) + { + canUse = couldUse = false; + + IUsable usable = __instance.Cast(); + if (usable != null) + { + var @event = new PlayerCanUseEvent(__instance.Cast()); + MiraEventManager.InvokeEvent(@event); + + return !@event.IsCancelled; + } + + return true; + } +} diff --git a/MiraAPI/Patches/Events/EndGamePatches.cs b/MiraAPI/Patches/Events/EndGamePatches.cs new file mode 100644 index 0000000..4f7118b --- /dev/null +++ b/MiraAPI/Patches/Events/EndGamePatches.cs @@ -0,0 +1,20 @@ +using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Gameplay; + +namespace MiraAPI.Patches.Events; + +/// +/// Patch for trigging the GameEndEvent. +/// +[HarmonyPatch] +public static class EndGamePatches +{ + [HarmonyPostfix] + [HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.SetEverythingUp))] + public static void SetEverythingUpPatch(EndGameManager __instance) + { + var @event = new GameEndEvent(__instance); + MiraEventManager.InvokeEvent(@event); + } +} diff --git a/MiraAPI/Patches/Events/HudButtonEventPatches.cs b/MiraAPI/Patches/Events/HudButtonEventPatches.cs deleted file mode 100644 index 42dde11..0000000 --- a/MiraAPI/Patches/Events/HudButtonEventPatches.cs +++ /dev/null @@ -1,48 +0,0 @@ -using HarmonyLib; -using MiraAPI.Events; -using MiraAPI.Events.Vanilla; - -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/KillAnimationPatches.cs b/MiraAPI/Patches/Events/KillAnimationPatches.cs new file mode 100644 index 0000000..13b4cff --- /dev/null +++ b/MiraAPI/Patches/Events/KillAnimationPatches.cs @@ -0,0 +1,25 @@ +using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Gameplay; + +namespace MiraAPI.Patches; + +/// +/// General patches for the KillAnimation class. +/// +[HarmonyPatch] +public static class KillAnimationPatches +{ + /// + /// Used to trigger the . + /// + /// The killer. + /// The target. + [HarmonyPostfix] + [HarmonyPatch(typeof(KillAnimation), nameof(KillAnimation.CoPerformKill))] + public static void KillAnimPostfix(PlayerControl source, PlayerControl target) + { + var afterMurderEvent = new AfterMurderEvent(source, target); + MiraEventManager.InvokeEvent(afterMurderEvent); + } +} diff --git a/MiraAPI/Patches/Events/MapBehaviourPatches.cs b/MiraAPI/Patches/Events/MapBehaviourPatches.cs new file mode 100644 index 0000000..a409226 --- /dev/null +++ b/MiraAPI/Patches/Events/MapBehaviourPatches.cs @@ -0,0 +1,21 @@ +using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Map; + +namespace MiraAPI.Patches.Events; + +/// +/// Patch for map related MiraEvents. +/// +[HarmonyPatch] +public static class MapBehaviourPatches +{ + [HarmonyPrefix] + [HarmonyPatch(typeof(MapBehaviour), nameof(MapBehaviour.ShowSabotageMap))] + public static bool MapShowSabotagePatch(MapBehaviour __instance) + { + var @event = new PlayerOpenSabotageEvent(__instance); + MiraEventManager.InvokeEvent(@event); + return !@event.IsCancelled; + } +} diff --git a/MiraAPI/Patches/Events/SabotageEventPatches.cs b/MiraAPI/Patches/Events/SabotageEventPatches.cs index b8a1fd2..4f04d7e 100644 --- a/MiraAPI/Patches/Events/SabotageEventPatches.cs +++ b/MiraAPI/Patches/Events/SabotageEventPatches.cs @@ -1,6 +1,6 @@ using HarmonyLib; using MiraAPI.Events; -using MiraAPI.Events.Vanilla; +using MiraAPI.Events.Vanilla.Map; namespace MiraAPI.Patches.Events; diff --git a/MiraAPI/Patches/Events/StartMeetingEventPatch.cs b/MiraAPI/Patches/Events/StartMeetingEventPatch.cs index 00f8ba4..2012b5d 100644 --- a/MiraAPI/Patches/Events/StartMeetingEventPatch.cs +++ b/MiraAPI/Patches/Events/StartMeetingEventPatch.cs @@ -1,6 +1,6 @@ using HarmonyLib; using MiraAPI.Events; -using MiraAPI.Events.Vanilla; +using MiraAPI.Events.Vanilla.Meeting; using MiraAPI.Utilities; namespace MiraAPI.Patches.Events; diff --git a/MiraAPI/Patches/Events/UsePatches.cs b/MiraAPI/Patches/Events/UsePatches.cs new file mode 100644 index 0000000..beb1f69 --- /dev/null +++ b/MiraAPI/Patches/Events/UsePatches.cs @@ -0,0 +1,35 @@ +using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla; +using System.Collections.Generic; +using System.Reflection; + +namespace MiraAPI.Patches.Events; + +[HarmonyPatch] +public static class UsePatches +{ + public static IEnumerable TargetMethods() + { + yield return AccessTools.Method(typeof(Console), nameof(Console.Use)); + yield return AccessTools.Method(typeof(MapConsole), nameof(MapConsole.Use)); + yield return AccessTools.Method(typeof(SystemConsole), nameof(SystemConsole.Use)); + yield return AccessTools.Method(typeof(Vent), nameof(Vent.Use)); + yield return AccessTools.Method(typeof(Ladder), nameof(Ladder.Use)); + yield return AccessTools.Method(typeof(PlatformConsole), nameof(PlatformConsole.Use)); + yield return AccessTools.Method(typeof(OpenDoorConsole), nameof(OpenDoorConsole.Use)); + yield return AccessTools.Method(typeof(DoorConsole), nameof(DoorConsole.Use)); + yield return AccessTools.Method(typeof(OptionsConsole), nameof(OptionsConsole.Use)); + yield return AccessTools.Method(typeof(ZiplineConsole), nameof(ZiplineConsole.Use)); + } + + [HarmonyPriority(Priority.Last)] + [HarmonyPrefix] + public static bool UsePatch(Il2CppSystem.Object __instance) + { + var @event = new PlayerUseEvent(__instance.Cast()); + MiraEventManager.InvokeEvent(@event); + + return !@event.IsCancelled; + } +} diff --git a/MiraAPI/Patches/Events/VentEventPatches.cs b/MiraAPI/Patches/Events/VentEventPatches.cs index 2617ff8..0b1b1cd 100644 --- a/MiraAPI/Patches/Events/VentEventPatches.cs +++ b/MiraAPI/Patches/Events/VentEventPatches.cs @@ -1,7 +1,7 @@ -using System.Linq; -using HarmonyLib; +using HarmonyLib; using MiraAPI.Events; -using MiraAPI.Events.Vanilla; +using MiraAPI.Events.Vanilla.Usables; +using System.Linq; namespace MiraAPI.Patches.Events; diff --git a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs new file mode 100644 index 0000000..fb57d70 --- /dev/null +++ b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs @@ -0,0 +1,36 @@ +using HarmonyLib; +using MiraAPI.Roles; +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI.Patches.Freeplay; + +[HarmonyPatch(typeof(TaskAddButton))] +internal static class TaskAddButtonPatches +{ + [HarmonyPrefix] + [HarmonyPatch(nameof(TaskAddButton.Start))] + public static bool StartPrefix(TaskAddButton __instance) + { + // if this becomes problematic in the future, find a new method. + if (uint.TryParse(__instance.name, out var result)) + { + __instance.Overlay.sprite = __instance.CheckImage; + __instance.Overlay.enabled = PlayerControl.LocalPlayer.HasModifier(result); + return false; + } + + return true; + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(TaskAddButton.Role), MethodType.Setter)] + public static void RoleGetterPatch(TaskAddButton __instance) + { + if (__instance.role is ICustomRole { Team: ModdedRoleTeams.Custom } customRole) + { + __instance.FileImage.color = customRole.IntroConfiguration?.IntroTeamColor ?? Color.gray; + } + __instance.RolloverHandler.OutColor = __instance.FileImage.color; + } +} diff --git a/MiraAPI/Patches/Roles/TaskAdderPatch.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs similarity index 56% rename from MiraAPI/Patches/Roles/TaskAdderPatch.cs rename to MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index ee3c34d..d1c6cb9 100644 --- a/MiraAPI/Patches/Roles/TaskAdderPatch.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -1,28 +1,27 @@ -using AmongUs.GameOptions; +using System; +using System.Globalization; +using System.Linq; +using AmongUs.GameOptions; using HarmonyLib; -using System; -using System.Collections.Generic; +using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Roles; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; -using System.Linq; using TMPro; using UnityEngine; +using UnityEngine.UI; using Object = UnityEngine.Object; -namespace MiraAPI.Patches.Roles; +namespace MiraAPI.Patches.Freeplay; -/// -/// Patch TaskAdder for organizing Roles and adding scroller. -/// -[HarmonyPatch] -public static class TaskAdderPatch +[HarmonyPatch(typeof(TaskAdderGame))] +internal static class TaskAdderPatches { - private static readonly Dictionary ModsFolders = []; private static Scroller? _scroller; [HarmonyPostfix] - [HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.Begin))] + [HarmonyPatch(nameof(TaskAdderGame.Begin))] public static void AddRolesFolder(TaskAdderGame __instance) { GameObject inner = new("Inner"); @@ -63,41 +62,38 @@ public static class TaskAdderPatch rolesFolder.FolderName = "Roles"; rolesFolder.name = "RolesFolder"; + var modifiersFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); + modifiersFolder.gameObject.SetActive(false); + modifiersFolder.FolderName = "Modifiers"; + modifiersFolder.name = "ModifiersFolder"; + foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins()) { - if (plugin.CustomRoles.Count == 0) + if (plugin.CustomRoles.Any(x => x.Value is ICustomRole { Configuration.ShowInFreeplay: true })) { - continue; + var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); + newFolder.name = plugin.PluginId+":Roles"; + newFolder.FolderName = plugin.PluginInfo.Metadata.Name; + newFolder.gameObject.SetActive(false); + rolesFolder.SubFolders.Add(newFolder); } - var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); - newFolder.FolderName = newFolder.name = plugin.PluginInfo.Metadata.Name; - newFolder.gameObject.SetActive(false); - rolesFolder.SubFolders.Add(newFolder); - - if (!ModsFolders.ContainsKey(plugin.PluginInfo.Metadata.Name)) + if (plugin.Modifiers.Exists(x => x.ShowInFreeplay)) { - ModsFolders.Add(plugin.PluginInfo.Metadata.Name, plugin.PluginId); + var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner); + newFolder.name = plugin.PluginId+":Modifiers"; + newFolder.FolderName = plugin.PluginInfo.Metadata.Name; + newFolder.gameObject.SetActive(false); + modifiersFolder.SubFolders.Add(newFolder); } } __instance.Root.SubFolders.Add(rolesFolder); + __instance.Root.SubFolders.Add(modifiersFolder); __instance.GoToRoot(); } - [HarmonyPostfix] - [HarmonyPatch(typeof(TaskAddButton), "Role", MethodType.Setter)] - public static void RoleGetterPatch(TaskAddButton __instance) - { - if (__instance.role is ICustomRole { Team: ModdedRoleTeams.Neutral }) - { - __instance.FileImage.color = Color.gray; - } - - __instance.RolloverHandler.OutColor = __instance.FileImage.color; - } - private static void AddFileAsChildCustom( this TaskAdderGame instance, TaskAddButton item, @@ -122,8 +118,8 @@ public static class TaskAdderPatch // yes it might be crazy patching the entire method, but i tried so many other methods and only this works :cry: [HarmonyPrefix] - [HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.ShowFolder))] - public static bool ShowPatch(TaskAdderGame __instance, [HarmonyArgument(0)] TaskFolder taskFolder) + [HarmonyPatch(nameof(TaskAdderGame.ShowFolder))] + public static bool ShowPatch(TaskAdderGame __instance, TaskFolder taskFolder) { var stringBuilder = new Il2CppSystem.Text.StringBuilder(64); __instance.Hierarchy.Add(taskFolder); @@ -186,23 +182,23 @@ public static class TaskAdderPatch switch (taskAddButton.MyTask.TaskType) { case TaskTypes.DivertPower: - { - var targetSystem = taskAddButton.MyTask.Cast().TargetSystem; - taskAddButton.Text.text = TranslationController.Instance.GetString( - StringNames.DivertPowerTo, - TranslationController.Instance.GetString(targetSystem)); - break; - } + { + var targetSystem = taskAddButton.MyTask.Cast().TargetSystem; + taskAddButton.Text.text = TranslationController.Instance.GetString( + StringNames.DivertPowerTo, + TranslationController.Instance.GetString(targetSystem)); + break; + } case TaskTypes.FixWeatherNode: - { - var nodeId = taskAddButton.MyTask.Cast().NodeId; - taskAddButton.Text.text = - TranslationController.Instance.GetString( - StringNames.FixWeatherNode) + " " + - TranslationController.Instance.GetString( - WeatherSwitchGame.ControlNames[nodeId]); - break; - } + { + var nodeId = taskAddButton.MyTask.Cast().NodeId; + taskAddButton.Text.text = + TranslationController.Instance.GetString( + StringNames.FixWeatherNode) + " " + + TranslationController.Instance.GetString( + WeatherSwitchGame.ControlNames[nodeId]); + break; + } default: taskAddButton.Text.text = TranslationController.Instance.GetString(taskAddButton.MyTask.TaskType); @@ -269,46 +265,31 @@ public static class TaskAdderPatch } } - if (ModsFolders.TryGetValue(taskFolder.FolderName, out var guid)) + if (taskFolder) { - var plugin = MiraPluginManager.GetPluginByGuid(guid); - for (var m = 0; m < plugin.CustomRoles.Count; m++) + var split = taskFolder.name.Split(':'); + if (split.Length == 2) { - var roleBehaviour = plugin.CustomRoles.ElementAt(m).Value; - if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost || - roleBehaviour.IsDead) - { - continue; - } + var pluginId = split[0]; + var folderName = split[1]; - var taskAddButton2 = Object.Instantiate(__instance.RoleButton); - taskAddButton2.SafePositionWorld = __instance.SafePositionWorld; - taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; - __instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); - taskAddButton2.Role = roleBehaviour; - if (taskAddButton2.Button == null) + if (MiraPluginManager.GetPluginByGuid(pluginId) is { } plugin) { - continue; - } - - ControllerManager.Instance.AddSelectableUiElement(taskAddButton2.Button); - switch (m) - { - case 0 when __instance.restorePreviousSelectionFound != null: - ControllerManager.Instance.SetDefaultSelection(__instance.restorePreviousSelectionFound); - __instance.restorePreviousSelectionByFolderName = string.Empty; - __instance.restorePreviousSelectionFound = null; - break; - case 0: - ControllerManager.Instance.SetDefaultSelection(taskAddButton2.Button); - break; + if (folderName == "Roles(Clone)") + { + PluginRoles(__instance, plugin, ref num, ref num2, ref num3); + } + else if (folderName == "Modifiers(Clone)") + { + PluginModifiers(__instance, plugin, ref num, ref num2, ref num3); + } } } } foreach (var chip in __instance.ActiveItems) { - chip.GetComponentInChildren().EnableMasking(); + chip.GetComponentInChildren().EnableStencilMasking(); chip.GetComponent().maskInteraction = SpriteMaskInteraction.VisibleInsideMask; } @@ -328,4 +309,97 @@ public static class TaskAdderPatch ControllerManager.Instance.SetBackButton(__instance.FolderBackButton); return false; } + + private static void PluginModifiers( + TaskAdderGame instance, + MiraPluginInfo plugin, + ref float num, + ref float num2, + ref float num3) + { + for (var m = 0; m < plugin.Modifiers.Count; m++) + { + var modifier = plugin.Modifiers[m]; + if (!modifier.ShowInFreeplay) + { + continue; + } + + var taskAddButton = Object.Instantiate(instance.RoleButton); + taskAddButton.name = modifier.ModifierId.ToString(CultureInfo.InvariantCulture); + taskAddButton.role = null; + taskAddButton.MyTask = null; + taskAddButton.SafePositionWorld = instance.SafePositionWorld; + taskAddButton.Text.text = modifier.ModifierName; + taskAddButton.Text.EnableMasking(); + + taskAddButton.Button.OnClick = new Button.ButtonClickedEvent(); + taskAddButton.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => + { + if (PlayerControl.LocalPlayer.HasModifier(modifier.ModifierId)) + { + PlayerControl.LocalPlayer.RemoveModifier(modifier.ModifierId); + taskAddButton.Overlay.enabled = false; + } + else + { + PlayerControl.LocalPlayer.AddModifier(ModifierManager.GetModifierType(modifier.ModifierId)!); + taskAddButton.Overlay.enabled = true; + } + })); + + instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3); + + ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button); + switch (m) + { + case 0 when instance.restorePreviousSelectionFound != null: + ControllerManager.Instance.SetDefaultSelection(instance.restorePreviousSelectionFound); + instance.restorePreviousSelectionByFolderName = string.Empty; + instance.restorePreviousSelectionFound = null; + break; + case 0: + ControllerManager.Instance.SetDefaultSelection(taskAddButton.Button); + break; + } + } + } + + private static void PluginRoles(TaskAdderGame instance, MiraPluginInfo plugin, ref float num, ref float num2, ref float num3) + { + for (var m = 0; m < plugin.CustomRoles.Count; m++) + { + var roleBehaviour = plugin.CustomRoles.ElementAt(m).Value; + if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost || + roleBehaviour is ICustomRole { Configuration.ShowInFreeplay: false }) + { + continue; + } + + var taskAddButton2 = Object.Instantiate(instance.RoleButton); + taskAddButton2.SafePositionWorld = instance.SafePositionWorld; + taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe"; + taskAddButton2.Text.EnableMasking(); + + instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3); + taskAddButton2.Role = roleBehaviour; + if (taskAddButton2.Button == null) + { + continue; + } + + ControllerManager.Instance.AddSelectableUiElement(taskAddButton2.Button); + switch (m) + { + case 0 when instance.restorePreviousSelectionFound != null: + ControllerManager.Instance.SetDefaultSelection(instance.restorePreviousSelectionFound); + instance.restorePreviousSelectionByFolderName = string.Empty; + instance.restorePreviousSelectionFound = null; + break; + case 0: + ControllerManager.Instance.SetDefaultSelection(taskAddButton2.Button); + break; + } + } + } } diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index 85d7750..fa057d4 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -13,7 +13,9 @@ namespace MiraAPI.Patches; public static class HudManagerPatches { // Custom buttons parent. - private static GameObject? _bottomLeft; + internal static GameObject? _bottomLeft; + internal static Transform? _bottomRight; + internal static Transform? _buttons; /* /// @@ -34,12 +36,19 @@ public static class HudManagerPatches [HarmonyPatch(nameof(HudManager.Start))] public static void StartPostfix(HudManager __instance) { - var buttons = __instance.transform.Find("Buttons"); - var bottomRight = buttons.Find("BottomRight"); + if (_buttons == null) + { + _buttons = __instance.transform.Find("Buttons"); + } + + if (_bottomRight == null) + { + _bottomRight = _buttons.Find("BottomRight"); + } if (_bottomLeft == null) { - _bottomLeft = Object.Instantiate(bottomRight.gameObject, buttons); + _bottomLeft = Object.Instantiate(_bottomRight.gameObject, _buttons); } foreach (var t in _bottomLeft.GetComponentsInChildren(true)) @@ -59,7 +68,7 @@ public static class HudManagerPatches var location = button.Location switch { ButtonLocation.BottomLeft => _bottomLeft.transform, - ButtonLocation.BottomRight => bottomRight, + ButtonLocation.BottomRight => _bottomRight, _ => null, }; diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs index 2250d3d..b98c58e 100644 --- a/MiraAPI/Patches/IntroCutscenePatches.cs +++ b/MiraAPI/Patches/IntroCutscenePatches.cs @@ -1,4 +1,6 @@ using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Gameplay; using MiraAPI.Roles; using MiraAPI.Utilities; using UnityEngine; @@ -20,6 +22,32 @@ public static class IntroCutscenePatches } }*/ + [HarmonyPostfix] + [HarmonyPatch(nameof(IntroCutscene.CoBegin))] + public static void IntroBeginPatch(IntroCutscene __instance) + { + var @event = new IntroBeginEvent(__instance); + MiraEventManager.InvokeEvent(@event); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(IntroCutscene.BeginImpostor))] + public static void BeginImpostorPatch(IntroCutscene __instance) + { + if (PlayerControl.LocalPlayer.Data.Role is not ICustomRole customRole) + { + return; + } + + if (customRole.IntroConfiguration is { } introConfig) + { + __instance.BackgroundBar.material.SetColor(ShaderID.Color, introConfig.IntroTeamColor); + __instance.TeamTitle.color = introConfig.IntroTeamColor; + __instance.TeamTitle.text = introConfig.IntroTeamTitle; + __instance.ImpostorText.text = introConfig.IntroTeamDescription; + } + } + [HarmonyPrefix] [HarmonyPatch(nameof(IntroCutscene.BeginCrewmate))] public static bool BeginCrewmatePatch(IntroCutscene __instance) @@ -29,7 +57,15 @@ public static class IntroCutscenePatches return true; } - if (customRole.Team is not ModdedRoleTeams.Neutral) + if (customRole.IntroConfiguration is { } introConfig) + { + __instance.BackgroundBar.material.SetColor(ShaderID.Color, introConfig.IntroTeamColor); + __instance.TeamTitle.color = introConfig.IntroTeamColor; + __instance.TeamTitle.text = introConfig.IntroTeamTitle; + __instance.ImpostorText.text = introConfig.IntroTeamDescription; + } + + if (customRole.Team is not ModdedRoleTeams.Custom) { return true; } @@ -39,11 +75,7 @@ public static class IntroCutscenePatches position.y -= 0.25f; barTransform.position = position; - __instance.BackgroundBar.material.SetColor(ShaderID.Color, Color.gray); - __instance.TeamTitle.text = "NEUTRAL"; __instance.impostorScale = 1f; - __instance.ImpostorText.text = "You are Neutral. You do not have a team."; - __instance.TeamTitle.color = Color.gray; __instance.ourCrewmate = __instance.CreatePlayer( 0, diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index a44b35c..0cdca29 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -157,8 +157,6 @@ public static class GameSettingMenuPatches roles.AdvancedRolesSettings.gameObject.SetActive(false); roles.RoleChancesSettings.gameObject.SetActive(true); roles.SetQuotaTab(); - - roles.scrollBar.CalculateAndSetYBounds(roles.roleChances.Count + 5, 1f, 6f, 0.43f); roles.scrollBar.ScrollToTop(); } diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index ee26822..e9d3c42 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -6,6 +6,7 @@ using MiraAPI.Utilities.Assets; using System; using System.Collections.Generic; using System.Linq; +using Reactor.Localization.Utilities; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; @@ -211,34 +212,43 @@ public static class LobbyViewPanePatches var list = new List(); - foreach (var team in Enum.GetValues()) - { - var filteredRoles = SelectedMod.CustomRoles.Values - .OfType() - .Where(x => !x.Configuration.HideSettings && x.Team == team).ToList(); + var roleGroups = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values.OfType() + .ToLookup(x => x.RoleOptionsGroup); - if (filteredRoles.Count == 0) + if (roleGroups is null) + { + return; + } + + // sort the groups by priority + var sortedRoleGroups = roleGroups + .OrderBy(x => x.Key.Priority) + .ThenBy(x => x.Key.Name); + + foreach (var grouping in sortedRoleGroups) + { + if (!grouping.Any()) { continue; } - var categoryHeaderRoleVariant = - Object.Instantiate(instance.categoryHeaderRoleOrigin, instance.settingsContainer, true); + var group = grouping.Key; - switch (team) + var name = group.Name switch { - case ModdedRoleTeams.Crewmate: - categoryHeaderRoleVariant.SetHeader(StringNames.CrewmateRolesHeader, 61); - break; - case ModdedRoleTeams.Impostor: - categoryHeaderRoleVariant.SetHeader(StringNames.ImpostorRolesHeader, 61); - break; - default: - categoryHeaderRoleVariant.SetHeader(StringNames.CrewmateRolesHeader, 61); - categoryHeaderRoleVariant.Title.text = team + " Roles"; - categoryHeaderRoleVariant.Background.color = Color.gray; - categoryHeaderRoleVariant.Title.color = Color.white; - break; + "Crewmate" => StringNames.CrewmateRolesHeader, + "Impostor" => StringNames.ImpostorRolesHeader, + _ => CustomStringName.CreateAndRegister(group.Name), + }; + + var categoryHeaderRoleVariant = Object.Instantiate(instance.categoryHeaderRoleOrigin, instance.settingsContainer, true); + categoryHeaderRoleVariant.SetHeader(name, 61); + + if (name is not (StringNames.CrewmateRolesHeader or StringNames.ImpostorRolesHeader)) + { + var veryDarkColor = group.Color.DarkenColor(.35f); + categoryHeaderRoleVariant.Title.color = veryDarkColor; + categoryHeaderRoleVariant.Background.color = group.Color; } categoryHeaderRoleVariant.transform.localScale = Vector3.one; @@ -246,7 +256,7 @@ public static class LobbyViewPanePatches instance.settingsInfo.Add(categoryHeaderRoleVariant.gameObject); num -= 0.696f; - foreach (var customRole in filteredRoles) + foreach (var customRole in grouping) { var roleBehaviour = customRole as RoleBehaviour; if (roleBehaviour == null) @@ -259,8 +269,6 @@ public static class LobbyViewPanePatches var numPerGame = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleBehaviour.Role); - var flag = numPerGame == 0; - var viewSettingsInfoPanelRoleVariant = Object.Instantiate( instance.infoPanelRoleOrigin, @@ -273,7 +281,7 @@ public static class LobbyViewPanePatches .Where(x => x.AdvancedRole == customRole.GetType()) .ToList(); - if (!flag && advancedRoleOptions.Count > 0) + if (numPerGame > 0 && advancedRoleOptions.Count > 0) { list.Add(customRole.GetType()); } @@ -286,6 +294,8 @@ public static class LobbyViewPanePatches customRole.RoleColor, customRole.Configuration.Icon.LoadAsset(), true); + viewSettingsInfoPanelRoleVariant.iconSprite.transform.localScale = new Vector3(0.365f, 0.365f, 1f); + viewSettingsInfoPanelRoleVariant.iconSprite.transform.localPosition = new Vector3(0.7144f, -0.028f, -2); viewSettingsInfoPanelRoleVariant.titleText.color = viewSettingsInfoPanelRoleVariant.chanceTitle.color = @@ -370,6 +380,7 @@ public static class LobbyViewPanePatches maskLayer, role.TeamType == RoleTeamTypes.Crewmate, customRole.Configuration.Icon.LoadAsset()); + viewPanel.header.icon.transform.localScale = new Vector3(0.465f, 0.465f, 1f); viewPanel.divider.material.SetInt(PlayerMaterial.MaskLayer, maskLayer); var num = viewPanel.yPosStart; diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 4461423..f8dd48d 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -4,10 +4,12 @@ using MiraAPI.Networking; using MiraAPI.Roles; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; +using Reactor.Localization.Utilities; using Reactor.Networking.Rpc; using Reactor.Utilities; using Reactor.Utilities.Extensions; using System; +using System.Collections.Generic; using System.Linq; using TMPro; using UnityEngine; @@ -20,10 +22,21 @@ namespace MiraAPI.Patches.Options; [HarmonyPatch(typeof(RolesSettingsMenu))] public static class RoleSettingMenuPatches { + private static Dictionary RoleGroupHidden { get; set; } = new(); + private static List Headers { get; set; } = new(); + private static List RoleOptionSettings { get; set; } = new(); + + private static float ScrollerNum { get; set; } = 0.522f; + [HarmonyPrefix] [HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))] public static bool PatchStart(RolesSettingsMenu __instance) { + Headers.ForEach(Object.Destroy); + RoleOptionSettings.ForEach(Object.Destroy); + Headers.Clear(); + RoleOptionSettings.Clear(); + __instance.roleChances = new Il2CppSystem.Collections.Generic.List(); __instance.advancedSettingChildren = new Il2CppSystem.Collections.Generic.List(); @@ -42,7 +55,7 @@ public static class RoleSettingMenuPatches return true; } - var num = 0.522f; + ScrollerNum = 0.522f; __instance.AllButton.transform.parent.gameObject.SetActive(false); __instance.AllButton.gameObject.SetActive(false); @@ -54,92 +67,169 @@ public static class RoleSettingMenuPatches var num3 = 0; - var crewRoles = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values - .OfType() - .Where(role => role is { Team: ModdedRoleTeams.Crewmate, Configuration.HideSettings: false }) - .ToList() ?? []; + var roleGroups = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values.OfType() + .ToLookup(x => x.RoleOptionsGroup); - if (crewRoles is { Count: > 0 }) + if (roleGroups is null) { - var categoryHeaderEditRole = Object.Instantiate( - __instance.categoryHeaderEditRoleOrigin, + return true; + } + + // sort the groups by priority + var sortedRoleGroups = roleGroups + .OrderBy(x => x.Key.Priority) + .ThenBy(x => x.Key.Name); + + var quotaThing = __instance.categoryHeaderEditRoleOrigin.transform.FindChild("QuotaHeader"); + var template = __instance.transform.parent.parent.GetComponent().GameSettingsTab.categoryHeaderOrigin; + + + foreach (var grouping in sortedRoleGroups) + { + if (!grouping.Any()) + { + continue; + } + + var group = grouping.Key; + + RoleGroupHidden.TryAdd(group, false); + + var name = group.Name switch + { + "Crewmate" => StringNames.CrewmateRolesHeader, + "Impostor" => StringNames.ImpostorRolesHeader, + _ => CustomStringName.CreateAndRegister(group.Name), + }; + + var categoryHeaderMasked = Object.Instantiate( + template, Vector3.zero, Quaternion.identity, __instance.RoleChancesSettings.transform); - categoryHeaderEditRole.SetHeader(StringNames.CrewmateRolesHeader, 20); - categoryHeaderEditRole.transform.localPosition = new Vector3(4.986f, num, -2f); - num -= 0.522f; - foreach (var role in crewRoles) + categoryHeaderMasked.SetHeader(name, 20); + + var quotaInst = Object.Instantiate(quotaThing, categoryHeaderMasked.transform); + quotaInst.transform.localScale = new Vector3(1.3f, 1.3f, 1.3f); + quotaInst.transform.localPosition = new Vector3(0.7f, -0.82f, 0); + + var chanceText = quotaInst.transform.FindChild("Chance Text"); + chanceText.transform.localPosition = new Vector3(4.3f, 0.0993f, 0); + + var countText = quotaInst.transform.FindChild("# Text"); + countText.transform.localPosition = new Vector3(1.9f, 0.0993f, 0f); + + var blankLabel = quotaInst.transform.FindChild("BlankLabel").gameObject; + var chanceLabel = quotaInst.transform.FindChild("Chance Label").gameObject; + var countLabel = quotaInst.transform.FindChild("# Label").gameObject; + blankLabel.Destroy(); + chanceLabel.Destroy(); + countLabel.Destroy(); + + if (name is StringNames.CrewmateRolesHeader) { - if (role is RoleBehaviour roleBehaviour) + categoryHeaderMasked.Title.color = Palette.CrewmateRoleHeaderTextBlue; + categoryHeaderMasked.Background.color = Palette.CrewmateRoleHeaderBlue; + } + else if (name is StringNames.ImpostorRolesHeader) + { + categoryHeaderMasked.Title.color = Palette.ImpostorRoleHeaderTextRed; + categoryHeaderMasked.Background.color = Palette.ImpostorRoleHeaderRed; + } + else + { + categoryHeaderMasked.Title.color = group.Color; + categoryHeaderMasked.Divider.color = group.Color; + categoryHeaderMasked.Background.color = group.Color; + } + + categoryHeaderMasked.Title.color = categoryHeaderMasked.Title.color.IsColorDark() ? + categoryHeaderMasked.Title.color.LightenColor(0.3f) : categoryHeaderMasked.Title.color.LightenColor(0.1f); + + categoryHeaderMasked.Title.fontStyle = __instance.categoryHeaderEditRoleOrigin.Title.fontStyle; + categoryHeaderMasked.Title.font = __instance.categoryHeaderEditRoleOrigin.Title.font; + categoryHeaderMasked.Title.fontMaterial = __instance.categoryHeaderEditRoleOrigin.Title.fontMaterial; + + categoryHeaderMasked.Divider.color = categoryHeaderMasked.Background.color; + categoryHeaderMasked.Background.size = new Vector2(categoryHeaderMasked.Background.size.x + 1.5f, categoryHeaderMasked.Background.size.y); + categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; + categoryHeaderMasked.transform.localPosition = new Vector3(-0.44f, ScrollerNum, -2f); + categoryHeaderMasked.gameObject.SetActive(true); + quotaInst.gameObject.SetActive(!RoleGroupHidden[group]); + + var label = RoleGroupHidden[group] + ? "(Click to open)" + : "(Click to close)"; + var newText = Object.Instantiate(categoryHeaderMasked.Title, categoryHeaderMasked.transform); + newText.text = $"{label}"; + newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f); + newText.gameObject.GetComponent().Destroy(); + + Headers.Add(categoryHeaderMasked.gameObject); + ScrollerNum -= 0.422f; + + if (!RoleGroupHidden[group]) + { + foreach (var role in grouping) { - CreateQuotaOption(__instance, roleBehaviour, ref num, num3); + if (role is not RoleBehaviour roleBehaviour) + { + continue; + } + + var option = CreateQuotaOption(__instance, roleBehaviour, num3); + if (option is not null) + { + RoleOptionSettings.Add(option); + num3++; + } } - num3++; + } + + var boxCol = categoryHeaderMasked.gameObject.AddComponent(); + boxCol.size = new Vector2(7, 0.7f); + boxCol.offset = new Vector2(1.5f, -0.3f); + + var headerBtn = categoryHeaderMasked.gameObject.AddComponent(); + headerBtn.ClickSound = __instance.BackButton.GetComponent().ClickSound; + headerBtn.OnMouseOver = new UnityEvent(); + headerBtn.OnMouseOut = new UnityEvent(); + headerBtn.OnClick.AddListener( + (UnityAction)(() => + { + if (RoleGroupHidden.TryGetValue(group, out var value)) + { + RoleGroupHidden[group] = !value; + } + foreach (var header in Headers) + { + header.Destroy(); + } + Headers.Clear(); + foreach (var option in RoleOptionSettings) + { + option.gameObject.Destroy(); + } + RoleOptionSettings.Clear(); + __instance.SetQuotaTab(); + })); + headerBtn.SetButtonEnableState(true); + + if (RoleGroupHidden.TryGetValue(group, out var value) && !value) + { + ScrollerNum -= 0.4f; } } - - var impRoles = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values - .OfType() - .Where(role => role is { Team: ModdedRoleTeams.Impostor, Configuration.HideSettings: false }) - .ToList() ?? []; - - if (impRoles is { Count: > 0 }) - { - num -= 0.4f; - var categoryHeaderEditRole2 = Object.Instantiate( - __instance.categoryHeaderEditRoleOrigin, - Vector3.zero, - Quaternion.identity, - __instance.RoleChancesSettings.transform); - categoryHeaderEditRole2.SetHeader(StringNames.ImpostorRolesHeader, 20); - categoryHeaderEditRole2.transform.localPosition = new Vector3(4.986f, num, -2f); - num -= 0.522f; - - foreach (var role in impRoles) - { - if (role is RoleBehaviour roleBehaviour) - { - CreateQuotaOption(__instance, roleBehaviour, ref num, num3); - } - num3++; - } - } - - var neutRoles = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values - .OfType() - .Where(role => role is { Team: ModdedRoleTeams.Neutral, Configuration.HideSettings: false }) - .ToList() ?? []; - - if (neutRoles is { Count: > 0 }) - { - num -= 0.4f; - var categoryHeaderEditRole3 = Object.Instantiate( - __instance.categoryHeaderEditRoleOrigin, - Vector3.zero, - Quaternion.identity, - __instance.RoleChancesSettings.transform); - categoryHeaderEditRole3.SetHeader(StringNames.None, 20); - categoryHeaderEditRole3.Title.text = "Neutral Roles"; - categoryHeaderEditRole3.Background.color = Color.gray; - categoryHeaderEditRole3.Title.color = Color.white; - categoryHeaderEditRole3.transform.localPosition = new Vector3(4.986f, num, -2f); - num -= 0.522f; - - foreach (var role in neutRoles) - { - if (role is RoleBehaviour roleBehaviour) - { - CreateQuotaOption(__instance, roleBehaviour, ref num, num3); - } - num3++; - } - } - + __instance.scrollBar.SetScrollBounds(); return false; } + private static void SetScrollBounds(this Scroller scroller) + { + scroller.CalculateAndSetYBounds(1 + 1.5f * Headers.Count + RoleOptionSettings.Count, 1f, 6f, 0.43f); + } + [HarmonyPrefix] [HarmonyPatch(nameof(RolesSettingsMenu.Update))] public static bool UpdatePatch() @@ -151,7 +241,12 @@ public static class RoleSettingMenuPatches [HarmonyPatch(nameof(RolesSettingsMenu.OpenChancesTab))] public static void OpenChancesTabPostfix(RolesSettingsMenu __instance) { - __instance.scrollBar.CalculateAndSetYBounds(__instance.roleChances.Count + 5, 1f, 6f, 0.43f); + if (GameSettingMenuPatches.SelectedModIdx == 0) + { + return; + } + + __instance.scrollBar.SetScrollBounds(); __instance.scrollBar.ScrollToTop(); } @@ -199,14 +294,15 @@ public static class RoleSettingMenuPatches private static void CreateAdvancedSettings(RolesSettingsMenu __instance, RoleBehaviour role) { - foreach (var option in __instance.advancedSettingChildren) + foreach (var optBehaviour in __instance.AdvancedRolesSettings.GetComponentsInChildren()) { - Object.Destroy(option.gameObject); + optBehaviour.gameObject.DestroyImmediate(); } __instance.advancedSettingChildren.Clear(); - var num = -0.872f; + var hasImage = role.RoleScreenshot != null; + var num = hasImage ? -0.872f : -1; var filteredOptions = GameSettingMenuPatches.SelectedMod?.Options.Where(x => x.AdvancedRole == role.GetType()) ?? []; @@ -223,7 +319,7 @@ public static class RoleSettingMenuPatches __instance.stringOptionOrigin, __instance.AdvancedRolesSettings.transform); - newOpt.transform.localPosition = new Vector3(2.17f, num, -2f); + newOpt.transform.localPosition = new Vector3(hasImage ? 2.17f : 1.1f, num, -2f); newOpt.SetClickMask(__instance.ButtonClickMask); SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true); @@ -261,15 +357,63 @@ public static class RoleSettingMenuPatches __instance.roleTitleText.text = TranslationController.Instance.GetString( role.StringName, new Il2CppReferenceArray(0)); - __instance.roleScreenshot.sprite = Sprite.Create( - customRole.Configuration.OptionsScreenshot.LoadAsset().texture, - new Rect(0, 0, 370, 230), - Vector2.one / 2, - 100); - __instance.roleScreenshot.drawMode = SpriteDrawMode.Sliced; + + var imgBg = __instance.AdvancedRolesSettings.transform.FindChild("Imagebackground"); + var labelBg = __instance.AdvancedRolesSettings.transform.FindChild("InfoLabelBackground"); + if (role.RoleScreenshot == null) + { + imgBg.gameObject.SetActive(false); + __instance.roleScreenshot.gameObject.SetActive(false); + __instance.roleDescriptionText.transform.parent.localPosition = new Vector3(1.5f, -0.2731f, -1); + __instance.roleDescriptionText.transform.parent.localScale = new Vector3(0.09f, 0.2f, 0.5687f); + labelBg.localPosition = new Vector3(-0.7f, 0.1054f, -2.5f); + } + else + { + imgBg.gameObject.SetActive(true); + __instance.roleScreenshot.gameObject.SetActive(true); + __instance.roleDescriptionText.transform.parent.localPosition = new Vector3(2.5176f, -0.2731f, -1f); + __instance.roleDescriptionText.transform.parent.localScale = new Vector3(0.0675f, 0.1494f, 0.5687f); + labelBg.transform.localPosition = new Vector3(1.082f, 0.1054f, -2.5f); + + __instance.roleScreenshot.sprite = Sprite.Create( + role.RoleScreenshot.texture, + new Rect(0, 0, 370, 230), + Vector2.one / 2, + 100); + __instance.roleScreenshot.drawMode = SpriteDrawMode.Sliced; + } + __instance.roleHeaderSprite.color = customRole.RoleColor; __instance.roleHeaderText.color = customRole.RoleColor.GetAlternateColor(); + var categoryHeaderMasked = __instance.AdvancedRolesSettings.transform.Find("CategoryHeaderMasked").GetComponent(); + + if (categoryHeaderMasked.Title.gameObject.TryGetComponent(out var comp)) + { + comp.Destroy(); + } + + categoryHeaderMasked.Title.text = "RETURN TO ROLE SETTINGS"; + + if (!categoryHeaderMasked.gameObject.TryGetComponent(out _)) + { + var boxCol = categoryHeaderMasked.gameObject.AddComponent(); + boxCol.size = new Vector2(7, 0.7f); + boxCol.offset = new Vector2(1.5f, -0.3f); + + var headerBtn = categoryHeaderMasked.gameObject.AddComponent(); + headerBtn.ClickSound = __instance.BackButton.GetComponent().ClickSound; + headerBtn.OnMouseOver = new UnityEvent(); + headerBtn.OnMouseOut = new UnityEvent(); + headerBtn.OnClick.AddListener( + (UnityAction)(() => + { + __instance.OpenChancesTab(); + })); + headerBtn.SetButtonEnableState(true); + } + var bg = __instance.AdvancedRolesSettings.transform.Find("Background"); bg.localPosition = new Vector3(1.4041f, -7.08f, 0); bg.GetComponent().size = new Vector2(89.4628f, 100); @@ -295,12 +439,17 @@ public static class RoleSettingMenuPatches __instance.RefreshChildren(); } - private static void CreateQuotaOption(RolesSettingsMenu __instance, RoleBehaviour role, ref float yPos, int index) + private static RoleOptionSetting? CreateQuotaOption(RolesSettingsMenu __instance, RoleBehaviour role, int index) { if (role is not ICustomRole customRole) { Logger.Error($"Role {role.NiceName} is not a custom role."); - return; + return null; + } + + if (customRole.Configuration.HideSettings) + { + return null; } var roleOptionSetting = Object.Instantiate( @@ -308,7 +457,7 @@ public static class RoleSettingMenuPatches Vector3.zero, Quaternion.identity, __instance.RoleChancesSettings.transform); - roleOptionSetting.transform.localPosition = new Vector3(-0.15f, yPos, -2f); + roleOptionSetting.transform.localPosition = new Vector3(-0.1f, ScrollerNum, -2f); roleOptionSetting.SetRole(GameOptionsManager.Instance.CurrentGameOptions.RoleOptions, role, 20); roleOptionSetting.labelSprite.color = customRole.RoleColor; @@ -355,7 +504,9 @@ public static class RoleSettingMenuPatches if (index < GameSettingMenuPatches.SelectedMod?.CustomRoles.Count - 1) { - yPos += -0.43f; + ScrollerNum += -0.43f; } + + return roleOptionSetting; } } diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index e0d647d..fbabe1a 100644 --- a/MiraAPI/Patches/PlayerControlPatches.cs +++ b/MiraAPI/Patches/PlayerControlPatches.cs @@ -1,8 +1,12 @@ using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.Events.Vanilla.Player; using MiraAPI.Hud; using MiraAPI.Modifiers; using MiraAPI.Roles; using Reactor.Utilities.Extensions; +using System.Linq; namespace MiraAPI.Patches; @@ -36,14 +40,50 @@ public static class PlayerControlPatches [HarmonyPatch(nameof(PlayerControl.Die))] public static void PlayerControlDiePostfix(PlayerControl __instance, DeathReason reason) { + var deathEvent = new PlayerDeathEvent(__instance, reason); + MiraEventManager.InvokeEvent(deathEvent); + var modifiersComponent = __instance.GetComponent(); if (modifiersComponent) { - modifiersComponent.ActiveModifiers.ForEach(x=>x.OnDeath(reason)); + modifiersComponent.ActiveModifiers.ForEach(x => x.OnDeath(reason)); } } + /// + /// Used to trigger the . + /// + /// PlayerControl instance. + /// The task id. + [HarmonyPostfix] + [HarmonyPatch(nameof(PlayerControl.CompleteTask))] + public static void PlayerCompleteTaskPostfix(PlayerControl __instance, uint idx) + { + var playerTask = __instance.myTasks.ToArray().First(playerTask => playerTask.Id == idx); + if (playerTask != null) + { + var completeTaskEvent = new CompleteTaskEvent(__instance, playerTask); + MiraEventManager.InvokeEvent(completeTaskEvent); + } + } + + /// + /// Used to trigger the . + /// + /// The source player. + /// The target. + /// Whether the kill succeeded. + [HarmonyPrefix] + [HarmonyPatch(nameof(PlayerControl.RpcMurderPlayer))] + public static void PlayerControlMurderPrefix(PlayerControl __instance, PlayerControl target, ref bool didSucceed) + { + var beforeMurderEvent = new BeforeMurderEvent(__instance, target); + MiraEventManager.InvokeEvent(beforeMurderEvent); + + didSucceed = beforeMurderEvent.IsCancelled; + } + /// /// FixedUpdate handler for custom roles and custom buttons. /// @@ -86,6 +126,6 @@ public static class PlayerControlPatches [HarmonyPatch(nameof(PlayerControl.OnDestroy))] public static void PlayerControlOnDestroyPrefix(PlayerControl __instance) { - ModifierExtensions.ModifierComponents.Remove(__instance); + Utilities.ModifierExtensions.ModifierComponents.Remove(__instance); } } diff --git a/MiraAPI/Patches/Roles/EjectionPatches.cs b/MiraAPI/Patches/Roles/EjectionPatches.cs index 9f85872..48d6abb 100644 --- a/MiraAPI/Patches/Roles/EjectionPatches.cs +++ b/MiraAPI/Patches/Roles/EjectionPatches.cs @@ -1,4 +1,6 @@ using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Meeting; using MiraAPI.Roles; namespace MiraAPI.Patches.Roles; @@ -13,6 +15,9 @@ public static class EjectionPatches [HarmonyPatch(nameof(ExileController.Begin))] public static void BeginPostfix(ExileController __instance) { + var @event = new EjectionEvent(__instance); + MiraEventManager.InvokeEvent(@event); + if (!__instance.initData.networkedPlayer || !__instance.initData.networkedPlayer.Role || __instance.initData.networkedPlayer.Role is not ICustomRole role) { diff --git a/MiraAPI/Patches/Roles/HauntMenuPatch.cs b/MiraAPI/Patches/Roles/HauntMenuPatch.cs new file mode 100644 index 0000000..b174165 --- /dev/null +++ b/MiraAPI/Patches/Roles/HauntMenuPatch.cs @@ -0,0 +1,27 @@ +using HarmonyLib; +using MiraAPI.Roles; +using UnityEngine; + +namespace MiraAPI.Patches.Roles; + +/// +/// Patches the haunt menu to show the actual role name rather then team. +/// +[HarmonyPatch(typeof(HauntMenuMinigame), nameof(HauntMenuMinigame.SetFilterText))] +public static class HauntMenuMinigamePatch +{ + public static void Postfix(HauntMenuMinigame __instance) + { + if (__instance.HauntTarget.Data.IsDead) + { + __instance.FilterText.color = Color.white; + return; + } + + var role = __instance.HauntTarget.Data.Role; + var color = role is ICustomRole custom ? custom.RoleColor : role.TeamColor; + + __instance.FilterText.text = role.NiceName; + __instance.FilterText.color = color; + } +} \ No newline at end of file diff --git a/MiraAPI/Patches/Roles/NameTagPatch.cs b/MiraAPI/Patches/Roles/NameTagPatch.cs index 821db1c..7974351 100644 --- a/MiraAPI/Patches/Roles/NameTagPatch.cs +++ b/MiraAPI/Patches/Roles/NameTagPatch.cs @@ -1,10 +1,11 @@ using HarmonyLib; +using MiraAPI.Roles; using UnityEngine; namespace MiraAPI.Patches.Roles; /// -/// Set nametag color depending on option +/// Set nametag color depending on visibility. /// [HarmonyPatch(typeof(PlayerNameColor))] public static class NameTagPatch @@ -13,6 +14,12 @@ public static class NameTagPatch [HarmonyPatch(nameof(PlayerNameColor.Get), typeof(RoleBehaviour))] public static bool GetPatch([HarmonyArgument(0)] RoleBehaviour otherPlayerRole, ref Color __result) { + if (otherPlayerRole is ICustomRole customRole && customRole.CanLocalPlayerSeeRole(otherPlayerRole.Player)) + { + __result = customRole.RoleColor; + return false; + } + if (PlayerControl.LocalPlayer.Data.Role.IsImpostor && otherPlayerRole.IsImpostor) { return true; diff --git a/MiraAPI/Patches/Roles/RoleBehaviourPatches.cs b/MiraAPI/Patches/Roles/RoleBehaviourPatch.cs similarity index 85% rename from MiraAPI/Patches/Roles/RoleBehaviourPatches.cs rename to MiraAPI/Patches/Roles/RoleBehaviourPatch.cs index 98cb5e6..55cc5a5 100644 --- a/MiraAPI/Patches/Roles/RoleBehaviourPatches.cs +++ b/MiraAPI/Patches/Roles/RoleBehaviourPatch.cs @@ -5,10 +5,10 @@ using UnityEngine; namespace MiraAPI.Patches.Roles; [HarmonyPatch(typeof(RoleBehaviour))] -public static class RoleBehaviourPatches +public static class RoleBehaviourPatch { /// - /// Update TeamColor text for Launchpad roles + /// Update TeamColor text for Mira roles. /// [HarmonyPrefix] [HarmonyPatch(nameof(RoleBehaviour.TeamColor), MethodType.Getter)] @@ -22,4 +22,4 @@ public static class RoleBehaviourPatches __result = behaviour.RoleColor; return false; } -} +} \ No newline at end of file diff --git a/MiraAPI/Patches/Roles/RoleManagerPatches.cs b/MiraAPI/Patches/Roles/RoleManagerPatches.cs index 357b2eb..ed1c5e2 100644 --- a/MiraAPI/Patches/Roles/RoleManagerPatches.cs +++ b/MiraAPI/Patches/Roles/RoleManagerPatches.cs @@ -1,5 +1,7 @@ using AmongUs.GameOptions; using HarmonyLib; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Gameplay; using MiraAPI.Modifiers; using MiraAPI.Roles; using System.Linq; @@ -9,6 +11,14 @@ namespace MiraAPI.Patches.Roles; [HarmonyPatch(typeof(RoleManager))] public static class RoleManagerPatches { + [HarmonyPostfix] + [HarmonyPatch(nameof(RoleManager.SetRole))] + public static void SetRolePatch(RoleManager __instance, PlayerControl targetPlayer, RoleTypes roleType) + { + var @event = new SetRoleEvent(targetPlayer, roleType); + MiraEventManager.InvokeEvent(@event); + } + [HarmonyPostfix] [HarmonyPatch(nameof(RoleManager.SelectRoles))] public static void ModifierSelectionPatches(RoleManager __instance) diff --git a/MiraAPI/Patches/Stubs/MinigameStubs.cs b/MiraAPI/Patches/Stubs/MinigameStubs.cs index aa8d05f..3818d09 100644 --- a/MiraAPI/Patches/Stubs/MinigameStubs.cs +++ b/MiraAPI/Patches/Stubs/MinigameStubs.cs @@ -21,4 +21,16 @@ public static class MinigameStubs { // nothing needed } + + /// + /// Reverse patch for Minigame.Close. + /// + /// The Minigame instance. + [HarmonyReversePatch] + [HarmonyPatch(typeof(Minigame), nameof(Minigame.Close), [])] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void Close(Minigame instance) + { + // nothing needed + } } diff --git a/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs b/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs new file mode 100644 index 0000000..0248aaa --- /dev/null +++ b/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs @@ -0,0 +1,229 @@ +using System.Runtime.CompilerServices; +using HarmonyLib; +using Il2CppSystem.Text; + +namespace MiraAPI.Patches.Stubs; + +[HarmonyPatch] +public static class RoleBehaviourStubs +{ + /// + /// Stub method for RoleBehaviour.Initialize. + /// + /// The RoleBehaviour object. + /// The PlayerControl to initialize. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.Initialize))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void Initialize(RoleBehaviour instance, PlayerControl player) + { + // nothing needed + } + + /// + /// Stub method for RoleBehaviour.AdjustTasks. + /// + /// The RoleBehaviour object. + /// The PlayerControl to adjust tasks for. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.AdjustTasks))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void AdjustTasks(RoleBehaviour instance, PlayerControl player) + { + // nothing needed + } + + /// + /// Stub method for RoleBehaviour.AppendTaskHint. + /// + /// The RoleBehaviour object. + /// The StringBuilder to append the task hint to. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.AppendTaskHint), typeof(StringBuilder))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void AppendTaskHint(RoleBehaviour instance, StringBuilder taskStringBuilder) + { + // nothing needed + } + + /// + /// Stub method for RoleBehaviour.CanUse. + /// + /// The RoleBehaviour object. + /// The IUsable console to check. + /// Whether the console can be used. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.CanUse))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool CanUse(RoleBehaviour instance, IUsable console) + { + return default; + } + + /// + /// Stub method for RoleBehaviour.Deinitialize. + /// + /// The RoleBehaviour object. + /// The PlayerControl to deinitialize for. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.Deinitialize))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void Deinitialize(RoleBehaviour instance, PlayerControl targetPlayer) + { + // nothing needed + } + + /// + /// Stub method for RoleBehaviour.DidWin. + /// + /// The RoleBehaviour object. + /// The reason for game over. + /// Whether the role won. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.DidWin))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool DidWin(RoleBehaviour instance, GameOverReason gameOverReason) + { + return default; + } + + /// + /// Stub method for RoleBehaviour.FindClosestTarget. + /// + /// The RoleBehaviour object. + /// The closest target PlayerControl. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.FindClosestTarget))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static PlayerControl FindClosestTarget(RoleBehaviour instance) + { + return null; + } + + /// + /// Stub method for RoleBehaviour.GetAbilityDistance. + /// + /// The RoleBehaviour object. + /// The ability distance. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.GetAbilityDistance))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static float GetAbilityDistance(RoleBehaviour instance) + { + return default; + } + + /// + /// Stub method for RoleBehaviour.IsValidTarget. + /// + /// The RoleBehaviour object. + /// The NetworkedPlayerInfo to validate. + /// Whether the target is valid. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.IsValidTarget))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool IsValidTarget(RoleBehaviour instance, NetworkedPlayerInfo target) + { + return default; + } + + /// + /// Stub method for RoleBehaviour.OnDeath. + /// + /// The RoleBehaviour object. + /// The reason for death. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.OnDeath))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void OnDeath(RoleBehaviour instance, DeathReason reason) + { + // nothing needed + } + + /// + /// Stub method for RoleBehaviour.OnMeetingStart. + /// + /// The RoleBehaviour object. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.OnMeetingStart))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void OnMeetingStart(RoleBehaviour instance) + { + // nothing needed + } + + /// + /// Stub method for RoleBehaviour.OnVotingComplete. + /// + /// The RoleBehaviour object. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.OnVotingComplete))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void OnVotingComplete(RoleBehaviour instance) + { + // nothing needed + } + + /// + /// Stub method for RoleBehaviour.SetCooldown. + /// + /// The RoleBehaviour object. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.SetCooldown))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void SetCooldown(RoleBehaviour instance) + { + // nothing needed + } + + /// + /// Stub method for RoleBehaviour.SetPlayerTarget. + /// + /// The RoleBehaviour object. + /// The PlayerControl target to set. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.SetPlayerTarget))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void SetPlayerTarget(RoleBehaviour instance, PlayerControl target) + { + // nothing needed + } + + /// + /// Stub method for RoleBehaviour.SetUsableTarget. + /// + /// The RoleBehaviour object. + /// The IUsable target to set. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.SetUsableTarget))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void SetUsableTarget(RoleBehaviour instance, IUsable target) + { + // nothing needed + } + + /// + /// Stub method for RoleBehaviour.SpawnTaskHeader. + /// + /// The RoleBehaviour object. + /// The PlayerControl to spawn the task header for. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.SpawnTaskHeader))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void SpawnTaskHeader(RoleBehaviour instance, PlayerControl playerControl) + { + // nothing needed + } + + /// + /// Stub method for RoleBehaviour.UseAbility. + /// + /// The RoleBehaviour object. + [HarmonyReversePatch] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.UseAbility))] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void UseAbility(RoleBehaviour instance) + { + // nothing needed + } +} diff --git a/MiraAPI/Patches/VentPatches.cs b/MiraAPI/Patches/VentPatches.cs index a41c690..621abbc 100644 --- a/MiraAPI/Patches/VentPatches.cs +++ b/MiraAPI/Patches/VentPatches.cs @@ -1,6 +1,8 @@ using HarmonyLib; -using MiraAPI.Modifiers; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Usables; using MiraAPI.Roles; +using MiraAPI.Utilities; using UnityEngine; namespace MiraAPI.Patches; @@ -18,6 +20,15 @@ public static class VentPatches [HarmonyPatch(nameof(Vent.CanUse))] public static void VentCanUsePostfix(Vent __instance, ref float __result, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] ref bool canUse, [HarmonyArgument(2)] ref bool couldUse) { + var @event = new PlayerCanUseEvent(__instance.Cast()); + MiraEventManager.InvokeEvent(@event); + + if (@event.IsCancelled) + { + canUse = couldUse = false; + return; + } + var @object = pc.Object; var role = @object.Data.Role; @@ -29,10 +40,10 @@ public static class VentPatches { switch (canVent) { - case true when modifiers.Exists(x => x.CanVent().HasValue && x.CanVent()==false): + case true when modifiers.Exists(x => x.CanVent().HasValue && x.CanVent() == false): couldUse = canUse = false; return; - case false when modifiers.Exists(x => x.CanVent().HasValue && x.CanVent()==true): + case false when modifiers.Exists(x => x.CanVent().HasValue && x.CanVent() == true): couldUse = true; break; } @@ -48,6 +59,7 @@ public static class VentPatches num = Vector2.Distance(center, position); canUse &= num <= __instance.UsableDistance && !PhysicsHelpers.AnythingBetween(@object.Collider, center, position, Constants.ShipOnlyMask, false); } + __result = num; } } diff --git a/MiraAPI/PluginLoading/MiraIgnoreAttribute.cs b/MiraAPI/PluginLoading/MiraIgnoreAttribute.cs new file mode 100644 index 0000000..b8ee939 --- /dev/null +++ b/MiraAPI/PluginLoading/MiraIgnoreAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace MiraAPI.PluginLoading; + +/// +/// Skip an element during plugin loading. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, Inherited = false)] +public class MiraIgnoreAttribute : Attribute; diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index 25a4dca..81412e8 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -5,6 +5,7 @@ using MiraAPI.GameOptions; using System.Collections.Generic; using System.Collections.ObjectModel; using MiraAPI.Hud; +using MiraAPI.Modifiers; namespace MiraAPI.PluginLoading; @@ -21,6 +22,15 @@ public class MiraPluginInfo PluginId = info.Metadata.GUID; } + /// + /// Get a read only collection of this plugin's modifiers. This is probably not safe because they are uninitialized. + /// + /// Readonly collection of modifiers. + public IReadOnlyCollection GetModifiers() + { + return Modifiers.AsReadOnly(); + } + /// /// Get a read only collection of this plugin's Option Groups. /// @@ -61,6 +71,8 @@ public class MiraPluginInfo internal List Options { get; } = []; + internal List Modifiers { get; } = []; + internal Dictionary CustomRoles { get; } = []; internal Dictionary GameModes { get; } = []; diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index acb6ebe..a7ed480 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using BepInEx.Unity.IL2CPP; +using BepInEx.Unity.IL2CPP; using MiraAPI.Colors; using MiraAPI.Events; using MiraAPI.GameOptions; @@ -13,6 +9,11 @@ using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Networking; using Reactor.Utilities; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; namespace MiraAPI.PluginLoading; @@ -38,42 +39,75 @@ public sealed class MiraPluginManager } var info = new MiraPluginInfo(miraPlugin, pluginInfo); + var roles = new List(); - RegisterModifierAttribute(assembly); - RegisterAllOptions(assembly, info); + foreach (var type in assembly.GetTypes()) + { + if (type.GetCustomAttribute() != null) + { + continue; + } - RegisterRoleAttribute(assembly, info); - RegisterButtonAttribute(assembly, info); + if (RegisterModifier(type, info)) + { + continue; + } - RegisterColorClasses(assembly); + if (RegisterOptions(type, info)) + { + continue; + } + + if (RegisterRoleAttribute(type, info, out var role)) + { + roles.Add(role!); + continue; + } + + if (RegisterButtonAttribute(type, info)) + { + continue; + } + + RegisterColorClasses(type); + } + + info.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority)); + CustomRoleManager.RegisterRoleTypes(roles, info); _registeredPlugins.Add(assembly, info); - Logger.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API."); }; IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors; IL2CPPChainloader.Instance.Finished += MiraEventManager.SortAllHandlers; + IL2CPPChainloader.Instance.Finished += () => + { + CustomButtonManager.Buttons = new ReadOnlyCollection(CustomButtonManager.CustomButtons); + }; } /// /// Get a mira plugin by its GUID. /// - /// The plugin GUID. + /// The plugin GUID. /// A MiraPluginInfo. - public static MiraPluginInfo GetPluginByGuid(string guid) + public static MiraPluginInfo? GetPluginByGuid(string pluginId) { - return Instance._registeredPlugins.Values.First(plugin => plugin.PluginId == guid); + return Instance._registeredPlugins.Values.FirstOrDefault(plugin => plugin.PluginId == pluginId); } - private static void RegisterAllOptions(Assembly assembly, MiraPluginInfo pluginInfo) + private static bool RegisterOptions(Type type, MiraPluginInfo pluginInfo) { - var filteredTypes = assembly.GetTypes().Where(type => type.IsAssignableTo(typeof(AbstractOptionGroup))); - - foreach (var type in filteredTypes) + try { + if (!type.IsAssignableTo(typeof(AbstractOptionGroup))) + { + return false; + } + if (!ModdedOptionsManager.RegisterGroup(type, pluginInfo)) { - continue; + return false; } foreach (var property in type.GetProperties()) @@ -92,53 +126,55 @@ public sealed class MiraPluginManager ModdedOptionsManager.RegisterAttributeOption(type, attribute, property, pluginInfo); } - } - pluginInfo.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority)); + return true; + } + catch (Exception e) + { + Logger.Error($"Failed to register options for {type.Name}: {e}"); + } + return false; } - private static void RegisterRoleAttribute(Assembly assembly, MiraPluginInfo pluginInfo) + private static bool RegisterRoleAttribute(Type type, MiraPluginInfo pluginInfo, out Type? role) { - List roles = []; - foreach (var type in assembly.GetTypes()) + role = null; + try { - var attribute = type.GetCustomAttribute(); - if (attribute == null) - { - continue; - } - if (!(typeof(RoleBehaviour).IsAssignableFrom(type) && typeof(ICustomRole).IsAssignableFrom(type))) { - Logger.Error($"{type.Name} does not inherit from RoleBehaviour or ICustomRole."); - continue; + return false; } if (!ModList.GetById(pluginInfo.PluginId).IsRequiredOnAllClients) { Logger.Error("Custom roles are only supported on all clients."); - return; + return false; } - roles.Add(type); + role = type; + return true; } - - CustomRoleManager.RegisterRoleTypes(roles, pluginInfo); + catch (Exception e) + { + Logger.Error($"Failed to register role for {type.Name}: {e}"); + } + return false; } - private static void RegisterColorClasses(Assembly assembly) + private static void RegisterColorClasses(Type type) { - foreach (var type in assembly.GetTypes()) + try { if (type.GetCustomAttribute() == null) { - continue; + return; } if (!type.IsStatic()) { Logger.Error($"Color class {type.Name} must be static."); - continue; + return; } foreach (var property in type.GetProperties()) @@ -157,29 +193,36 @@ public sealed class MiraPluginManager PaletteManager.CustomColors.Add(color); } } - } - - private static void RegisterModifierAttribute(Assembly assembly) - { - foreach (var type in assembly.GetTypes()) + catch (Exception e) { - var attribute = type.GetCustomAttribute(); - if (attribute != null) - { - ModifierManager.RegisterModifier(type); - } + Logger.Error($"Failed to register color class {type.Name}: {e}"); } } - private static void RegisterButtonAttribute(Assembly assembly, MiraPluginInfo pluginInfo) + private static bool RegisterModifier(Type type, MiraPluginInfo info) { - foreach (var type in assembly.GetTypes()) + try { - var attribute = type.GetCustomAttribute(); - if (attribute != null) - { - CustomButtonManager.RegisterButton(type, pluginInfo); - } + return ModifierManager.RegisterModifier(type, info); } + catch (Exception e) + { + Logger.Error($"Failed to register modifier {type.Name}: {e}"); + return false; + } + } + + private static bool RegisterButtonAttribute(Type type, MiraPluginInfo pluginInfo) + { + try + { + return CustomButtonManager.RegisterButton(type, pluginInfo); + } + catch (Exception e) + { + Logger.Error($"Failed to register button {type.Name}: {e}"); + } + + return false; } } diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs index 5bd29bb..efaf444 100644 --- a/MiraAPI/Roles/CustomRoleConfiguration.cs +++ b/MiraAPI/Roles/CustomRoleConfiguration.cs @@ -7,7 +7,7 @@ namespace MiraAPI.Roles; /// /// Used to configure the specific settings of a role. /// -public struct CustomRoleConfiguration +public record struct CustomRoleConfiguration { /// /// Initializes a new instance of the struct. @@ -17,11 +17,6 @@ public struct CustomRoleConfiguration { var roleBehaviour = role as RoleBehaviour; - MaxRoleCount = 15; - DefaultRoleCount = 0; - DefaultChance = 0; - - OptionsScreenshot = Icon = MiraAssets.Empty; AffectedByLightOnAirship = role.Team == ModdedRoleTeams.Crewmate; KillButtonOutlineColor = role.Team switch { @@ -29,7 +24,6 @@ public struct CustomRoleConfiguration ModdedRoleTeams.Crewmate => Palette.CrewmateBlue, _ => role.RoleColor, }; - RoleHintType = RoleHintType.RoleTab; GhostRole = role.Team is ModdedRoleTeams.Impostor ? RoleTypes.ImpostorGhost : RoleTypes.CrewmateGhost; CanGetKilled = roleBehaviour?.IsDead == false && role.Team is not ModdedRoleTeams.Impostor; UseVanillaKillButton = role.Team is ModdedRoleTeams.Impostor; @@ -37,86 +31,92 @@ public struct CustomRoleConfiguration CanUseSabotage = role.Team is ModdedRoleTeams.Impostor; TasksCountForProgress = role.Team is ModdedRoleTeams.Crewmate; HideSettings = roleBehaviour?.IsDead == true; - CanModifyChance = true; + ShowInFreeplay = roleBehaviour?.IsDead == false; + OptionsScreenshot = null; } /// - /// Gets the hard limit of players that can have this role. This property is used to set a limit in the Role Options menu. If set to 0, the role will not be assigned at start. + /// Gets or sets the hard limit of players that can have this role. This property is used to set a limit in the Role Options menu. If set to 0, the role will not be assigned at start. /// - public int MaxRoleCount; + public int MaxRoleCount { get; set; } = 15; /// - /// Gets the default role count. + /// Gets or sets the default role count. /// - public int DefaultRoleCount; + public int DefaultRoleCount { get; set; } = 0; /// - /// Gets the default role chance. + /// Gets or sets the default role chance. /// - public int DefaultChance; + public int DefaultChance { get; set; } = 0; /// - /// Whether the chance option can be changed or not. + /// Gets or sets a value indicating whether the chance option can be changed or not. /// - public bool CanModifyChance; + public bool CanModifyChance { get; set; } = true; /// - /// Gets the Sprite used for the Role Options menu screenshot. + /// Gets or sets the Sprite used for the Role Options menu screenshot. /// - public LoadableAsset OptionsScreenshot; + public LoadableAsset? OptionsScreenshot { get; set; } /// - /// Gets the Sprite used for the Role Icon. + /// Gets or sets the Sprite used for the Role Icon. /// - public LoadableAsset Icon; + public LoadableAsset Icon { get; set; } = MiraAssets.Empty; /// - /// Gets a value indicating whether the role is affected by light affectors on Airship. + /// Gets or sets a value indicating whether the role is affected by light affectors on Airship. /// - public bool AffectedByLightOnAirship; + public bool AffectedByLightOnAirship { get; set; } /// - /// Gets a value indicating whether the role can be killed by vanilla murder system. + /// Gets or sets a value indicating whether the role can be killed by vanilla murder system. /// - public bool CanGetKilled; + public bool CanGetKilled { get; set; } /// - /// Gets a value indicating whether the role should use the vanilla kill button. + /// Gets or sets a value indicating whether the role should use the vanilla kill button. /// - public bool UseVanillaKillButton; + public bool UseVanillaKillButton { get; set; } /// - /// Gets a value indicating whether the role can use vents. + /// Gets or sets a value indicating whether the role can use vents. /// - public bool CanUseVent; + public bool CanUseVent { get; set; } /// - /// Gets a value indicating whether the role can use the sabotage button. + /// Gets or sets a value indicating whether the role can use the sabotage button. /// - public bool CanUseSabotage; + public bool CanUseSabotage { get; set; } /// - /// Gets a value indicating whether the role's tasks count towards task progress. + /// Gets or sets a value indicating whether the role's tasks count towards task progress. /// - public bool TasksCountForProgress; + public bool TasksCountForProgress { get; set; } /// - /// Gets a value indicating whether the role should show up in the Role Options menu. + /// Gets or sets a value indicating whether the role should show up in the Role Options menu. /// - public bool HideSettings; + public bool HideSettings { get; set; } /// - /// Gets the outline color for the KillButton if is true. + /// Gets or sets a value indicating whether the role should show up in the Freeplay Role Selection menu. /// - public Color KillButtonOutlineColor; + public bool ShowInFreeplay { get; set; } /// - /// Gets the role hint style. See enum for all options. + /// Gets or sets the outline color for the KillButton if is true. /// - public RoleHintType RoleHintType; + public Color KillButtonOutlineColor { get; set; } /// - /// Gets the Ghost role that is applied when the player is killed. + /// Gets or sets the role hint style. See enum for all options. /// - public RoleTypes GhostRole; -} \ No newline at end of file + public RoleHintType RoleHintType { get; set; } = RoleHintType.RoleTab; + + /// + /// Gets or sets the Ghost role that is applied when the player is killed. + /// + public RoleTypes GhostRole { get; set; } +} diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index eb291d8..c31e922 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -79,7 +79,7 @@ public static class CustomRoleManager var roleId = RoleIds[roleType]; roleBehaviour.Role = (RoleTypes)roleId; - roleBehaviour.TeamType = customRole.Team == ModdedRoleTeams.Neutral ? RoleTeamTypes.Crewmate : (RoleTeamTypes)customRole.Team; + roleBehaviour.TeamType = customRole.Team == ModdedRoleTeams.Custom ? RoleTeamTypes.Crewmate : (RoleTeamTypes)customRole.Team; roleBehaviour.NameColor = customRole.RoleColor; roleBehaviour.StringName = CustomStringName.CreateAndRegister(customRole.RoleName); roleBehaviour.BlurbName = CustomStringName.CreateAndRegister(customRole.RoleDescription); @@ -91,7 +91,7 @@ public static class CustomRoleManager roleBehaviour.CanVent = customRole.Configuration.CanUseVent; roleBehaviour.DefaultGhostRole = customRole.Configuration.GhostRole; roleBehaviour.MaxCount = customRole.Configuration.MaxRoleCount; - roleBehaviour.RoleScreenshot = customRole.Configuration.OptionsScreenshot.LoadAsset(); + roleBehaviour.RoleScreenshot = customRole.Configuration.OptionsScreenshot?.LoadAsset(); if (roleBehaviour.IsDead) { diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index ba318b2..a452d1e 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -2,8 +2,8 @@ using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Utilities; -using System.Text; using Reactor.Utilities; +using System.Text; using UnityEngine; namespace MiraAPI.Roles; @@ -43,6 +43,26 @@ public interface ICustomRole /// CustomRoleConfiguration Configuration { get; } + /// + /// Gets the role options group. + /// + public RoleOptionsGroup RoleOptionsGroup => Team switch + { + ModdedRoleTeams.Crewmate => RoleOptionsGroup.Crewmate, + ModdedRoleTeams.Impostor => RoleOptionsGroup.Impostor, + ModdedRoleTeams.Custom => RoleOptionsGroup.Neutral, + _ => new RoleOptionsGroup(RoleName, RoleColor), + }; + + /// + /// Gets the role's TeamIntroCutscene configuration. + /// + public TeamIntroConfiguration? IntroConfiguration => Team switch + { + ModdedRoleTeams.Custom => TeamIntroConfiguration.Neutral, + _ => null, + }; + /// /// Gets the parent mod of this role. /// @@ -63,7 +83,7 @@ public interface ICustomRole /// Gets the role chance option. /// /// The role chance option. - public int? GetChance() + public virtual int? GetChance() { if (!Configuration.CanModifyChance) { @@ -82,7 +102,7 @@ public interface ICustomRole /// Gets the role count option. /// /// The role count option. - public int? GetCount() + public virtual int? GetCount() { if (ParentMod.PluginConfig.TryGetEntry(NumConfigDefinition, out ConfigEntry entry)) { @@ -96,7 +116,7 @@ public interface ICustomRole /// Sets the role chance option. /// /// The chance between 0 and 100. - public void SetChance(int chance) + public virtual void SetChance(int chance) { if (!Configuration.CanModifyChance) { @@ -117,7 +137,7 @@ public interface ICustomRole /// Sets the role count option. /// /// The amount of this role between zero and its MaxRoleCount in the Configuration. - public void SetCount(int count) + public virtual void SetCount(int count) { if (ParentMod.PluginConfig.TryGetEntry(NumConfigDefinition, out ConfigEntry entry)) { @@ -128,6 +148,16 @@ public interface ICustomRole Logger.Error($"Error getting count configuration for role: {RoleName}"); } + /// + /// Whether the local player can see this role. + /// + /// The player with the role. + /// Whether they can see the role (name color) or not. + public virtual bool CanLocalPlayerSeeRole(PlayerControl player) + { + return (PlayerControl.LocalPlayer.Data.Role.IsImpostor && player.Data.Role.IsImpostor) || PlayerControl.LocalPlayer.Data.IsDead; + } + /// /// This method runs on the HudManager.Update method ONLY when the LOCAL player has this role. /// diff --git a/MiraAPI/Roles/ModdedRoleTeams.cs b/MiraAPI/Roles/ModdedRoleTeams.cs index e599cd7..0d8c6a1 100644 --- a/MiraAPI/Roles/ModdedRoleTeams.cs +++ b/MiraAPI/Roles/ModdedRoleTeams.cs @@ -4,5 +4,5 @@ public enum ModdedRoleTeams { Crewmate, Impostor, - Neutral, + Custom, } diff --git a/MiraAPI/Roles/RegisterCustomRoleAttribute.cs b/MiraAPI/Roles/RegisterCustomRoleAttribute.cs deleted file mode 100644 index a6352f1..0000000 --- a/MiraAPI/Roles/RegisterCustomRoleAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System; - -namespace MiraAPI.Roles; - -[AttributeUsage(AttributeTargets.Class)] -public class RegisterCustomRoleAttribute : Attribute; diff --git a/MiraAPI/Roles/RoleOptionsGroup.cs b/MiraAPI/Roles/RoleOptionsGroup.cs new file mode 100644 index 0000000..a1785b0 --- /dev/null +++ b/MiraAPI/Roles/RoleOptionsGroup.cs @@ -0,0 +1,27 @@ +using UnityEngine; + +namespace MiraAPI.Roles; + +/// +/// Represents a group of roles. +/// +/// The group's name. +/// The group's color. +/// The priority to sort by on role setting screen. Negative means first. +public record struct RoleOptionsGroup(string Name, Color Color, int Priority = 0) +{ + /// + /// Gets the default group for crewmates. + /// + public static RoleOptionsGroup Crewmate { get; } = new("Crewmate", Palette.CrewmateBlue, -2); + + /// + /// Gets the default group for impostors. + /// + public static RoleOptionsGroup Impostor { get; } = new("Impostor", Palette.ImpostorRed, -1); + + /// + /// Gets the default group for neutral roles. + /// + public static RoleOptionsGroup Neutral { get; } = new("Neutral Roles", Color.gray); +} diff --git a/MiraAPI/Roles/TeamIntroConfiguration.cs b/MiraAPI/Roles/TeamIntroConfiguration.cs new file mode 100644 index 0000000..9177722 --- /dev/null +++ b/MiraAPI/Roles/TeamIntroConfiguration.cs @@ -0,0 +1,20 @@ +using UnityEngine; + +namespace MiraAPI.Roles; + +/// +/// Used to configure the TEAM part of the intro cutscene. +/// +/// The color to show during the "Team" part of the intro cutscene. +/// The title text to show during the "Team" part of the intro cutscene. +/// The description text to show during the "Team" part of the intro cutscene. +public record struct TeamIntroConfiguration(Color IntroTeamColor, string IntroTeamTitle, string IntroTeamDescription) +{ + /// + /// Gets the default configuration for the "Neutral" team. + /// + public static TeamIntroConfiguration Neutral { get; } = new( + Color.gray, + "NEUTRAL", + "You are Neutral. You do not have a team."); +} diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index d177d60..8d95294 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -1,11 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; using HarmonyLib; using MiraAPI.GameOptions; using MiraAPI.Networking; using MiraAPI.Roles; using Reactor.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using TMPro; using UnityEngine; namespace MiraAPI.Utilities; @@ -37,6 +38,16 @@ public static class Extensions BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value))); } + /// + /// Enables stencil masking on a TMP text object. + /// + /// The TMP text. + public static void EnableStencilMasking(this TMP_Text text) + { + text.fontMaterial.SetFloat(ShaderID.Get("_Stencil"), 1); + text.fontMaterial.SetFloat(ShaderID.Get("_StencilComp"), 4); + } + /// /// Checks if a type is static. /// @@ -251,4 +262,16 @@ public static class Extensions return predicate != null ? filteredPlayers.Find(predicate) : filteredPlayers.FirstOrDefault(); } + + /// + /// Fixed version of Reactor's SetOutline. + /// + /// The renderer you want to update the outline for. + /// The outline color. + public static void UpdateOutline(this Renderer renderer, Color? color) + { + renderer.material.SetFloat(ShaderID.Outline, color.HasValue ? 1 : 0); + renderer.material.SetColor(ShaderID.OutlineColor, color ?? Color.clear); + renderer.material.SetColor(ShaderID.AddColor, color ?? Color.clear); + } } diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index cea3bef..29ae10e 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -1,5 +1,5 @@ -using System.Collections; -using MiraAPI.Roles; +using MiraAPI.Roles; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -15,6 +15,15 @@ namespace MiraAPI.Utilities; /// public static class Helpers { + /// + /// Get all living players. + /// + /// A list of alive players. + public static List GetAlivePlayers() + { + return PlayerControl.AllPlayerControls.ToArray().Where(x => !x.Data.IsDead).ToList(); + } + /// /// Returns an empty coroutine. /// @@ -99,14 +108,14 @@ public static class Helpers } return (from player in newList - let vector = player.GetTruePosition() - source - let magnitude = vector.magnitude - where !PhysicsHelpers.AnyNonTriggersBetween( - source, - vector.normalized, - magnitude, - Constants.ShipAndObjectsMask) - select player).ToList(); + let vector = player.GetTruePosition() - source + let magnitude = vector.magnitude + where !PhysicsHelpers.AnyNonTriggersBetween( + source, + vector.normalized, + magnitude, + Constants.ShipAndObjectsMask) + select player).ToList(); } /// @@ -168,7 +177,7 @@ public static class Helpers select playerControl); outputList.Sort( - delegate(PlayerControl a, PlayerControl b) + delegate (PlayerControl a, PlayerControl b) { var magnitude2 = (a.GetTruePosition() - source).magnitude; var magnitude3 = (b.GetTruePosition() - source).magnitude; diff --git a/MiraAPI/Utilities/ModifierExtensions.cs b/MiraAPI/Utilities/ModifierExtensions.cs new file mode 100644 index 0000000..dadb62c --- /dev/null +++ b/MiraAPI/Utilities/ModifierExtensions.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using MiraAPI.Modifiers; +using MiraAPI.Networking; +using Reactor.Networking.Attributes; +using Reactor.Utilities; + +namespace MiraAPI.Utilities; + +/// +/// Extensions for modifiers. +/// +public static class ModifierExtensions +{ + /// + /// Gets the dictionary to cache ModifierComponents. + /// + public static Dictionary ModifierComponents { get; } = []; + + /// + /// Remote Procedure Call to add a modifier to a player. + /// + /// The player to add the modifier to. + /// The modifier ID. + [MethodRpc((uint)MiraRpc.AddModifier)] + public static void RpcAddModifier(this PlayerControl target, uint modifierId) + { + var type = ModifierManager.GetModifierType(modifierId); + if (type == null) + { + Logger.Error($"Cannot add modifier with id {modifierId} because it is not registered."); + return; + } + + target.AddModifier(type); + } + + /// + /// Remote Procedure Call to add a modifier to a player. + /// + /// The player to add the modifier to. + /// The modifier Type. + public static void RpcAddModifier(this PlayerControl player) where T : BaseModifier + { + var id = ModifierManager.GetModifierId(typeof(T)); + if (id == null) + { + Logger.Error($"Cannot add modifier {typeof(T).Name} because it is not registered."); + return; + } + + player.RpcAddModifier(id.Value); + } + + /// + /// Remote Procedure Call to remove a modifier from a player. + /// + /// The player to remove the modifier from. + /// The ID of the modifier. + [MethodRpc((uint)MiraRpc.RemoveModifier)] + public static void RpcRemoveModifier(this PlayerControl target, uint modifierId) + { + target.RemoveModifier(modifierId); + } + + /// + /// Remote Procedure Call to remove a modifier from a player. + /// + /// The player to remove the modifier from. + /// The Type of the Modifier. + public static void RpcRemoveModifier(this PlayerControl player) where T : BaseModifier + { + var id = ModifierManager.GetModifierId(typeof(T)); + if (id == null) + { + Logger.Error($"Cannot add modifier {typeof(T).Name} because it is not registered."); + return; + } + + player.RpcRemoveModifier(id.Value); + } + + /// + /// Gets the ModifierComponent for a player. + /// + /// The PlayerControl object. + /// A ModifierComponent if there is one, null otherwise. + public static ModifierComponent? GetModifierComponent(this PlayerControl player) + { + if (ModifierComponents.TryGetValue(player, out var component)) + { + return component; + } + + component = player.GetComponent(); + if (!component) + { + return null; + } + + ModifierComponents[player] = component; + return component; + } + + /// + /// Gets the ModifierComponent for a player. + /// + /// The PlayerControl object. + /// The ModifierComponent if it is found. + /// A ModifierComponent if there is one, null otherwise. + public static bool TryGetModifierComponent(this PlayerControl player, out ModifierComponent? component) + { + if (ModifierComponents.TryGetValue(player, out component)) + { + return component; + } + + component = player.GetComponent(); + return component; + } + + /// + /// Gets a collection of modifiers by their type, or null if the player doesn't have one. + /// + /// The PlayerControl object. + /// The Type of the Modifier. + /// The Modifier if it is found, null otherwise. + public static IEnumerable GetModifiersByType(this PlayerControl player) where T : BaseModifier + { + return player.GetModifierComponent()!.GetModifiersByType(); + } + + /// + /// Gets a modifier by its type, or null if the player doesn't have it. + /// + /// The PlayerControl object. + /// The Type of the Modifier. + /// The Modifier if it is found, null otherwise. + public static T? GetModifier(this PlayerControl player) where T : BaseModifier + { + return player.GetModifierComponent()!.GetModifier(); + } + + /// + /// Tries to get a modifier by its type. + /// + /// The PlayerControl object. + /// The modifier or null. + /// The Type of the Modifier. + /// True if the modifier was found, false otherwise. + public static bool TryGetModifier(this PlayerControl player, out T? modifier) where T : BaseModifier + { + return player.GetModifierComponent()!.TryGetModifier(out modifier); + } + + /// + /// Removes a modifier from the player. + /// + /// The PlayerControl object. + /// The modifier type. + public static void RemoveModifier(this PlayerControl player, Type type) + { + player.GetModifierComponent()!.RemoveModifier(type); + } + + /// + /// Removes a modifier from the player. + /// + /// The PlayerControl object. + /// The modifier type. + public static void RemoveModifier(this PlayerControl player) where T : BaseModifier + { + player.GetModifierComponent()!.RemoveModifier(); + } + + /// + /// Removes a modifier from the player. + /// + /// The PlayerControl object. + /// The modifier ID. + public static void RemoveModifier(this PlayerControl player, uint modifierId) + { + player.GetModifierComponent()!.RemoveModifier(modifierId); + } + + /// + /// Removes a modifier from the player. + /// + /// The PlayerControl object. + /// The modifier object. + public static void RemoveModifier(this PlayerControl player, BaseModifier modifier) + { + player.GetModifierComponent()!.RemoveModifier(modifier); + } + + /// + /// Adds a modifier to the player. + /// + /// The PlayerControl object. + /// The modifier to add. + /// The modifier that was added. + public static BaseModifier? AddModifier(this PlayerControl player, BaseModifier modifier) + { + return player.GetModifierComponent()!.AddModifier(modifier); + } + + /// + /// Adds a modifier to the player. + /// + /// The PlayerControl object. + /// The modifier type. + /// The modifier that was added. + public static BaseModifier? AddModifier(this PlayerControl player, Type type) + { + return player.GetModifierComponent()!.AddModifier(type); + } + + /// + /// Adds a modifier to the player. + /// + /// The PlayerControl object. + /// The Type of the modifier. + /// The new modifier. + public static T? AddModifier(this PlayerControl player) where T : BaseModifier + { + return player.GetModifierComponent()!.AddModifier(); + } + + /// + /// Checks if a player has an active or queued modifier by its ID. + /// + /// The PlayerControl object. + /// The Modifier ID. + /// The predicate to check the modifier. + /// True if the Modifier is present, false otherwise. + public static bool HasModifier(this PlayerControl player, uint id, Func? predicate=null) + { + return player.GetModifierComponent()!.HasModifier(id, predicate); + } + + /// + /// Checks if a player has an active or queued modifier by its type. + /// + /// The PlayerControl object. + /// The predicate to check the modifier. + /// The Type of the Modifier. + /// True if the Modifier is present, false otherwise. + public static bool HasModifier(this PlayerControl player, Func? predicate=null) where T : BaseModifier + { + return player.GetModifierComponent()!.HasModifier(predicate); + } +}