Compare commits

..
Author SHA1 Message Date
Atony 83be29d6a2 Add wip caching 2026-06-13 12:10:21 -05:00
Atony e3b0eb68c0 Temporary measures for clearing up memory leaks 2026-06-12 18:03:13 -05:00
amber 90921ff786 Remove UseVanillaVentButton as it's unnecessary 2026-06-11 22:37:10 -04:00
amber c1d0bb5d26 Seperate vanilla vent data receiving from the actual button 2026-06-11 22:34:27 -04:00
Atony c59ed8eaa6 Fix screenshake within options menu 2026-05-31 09:17:26 -05:00
Atony d2fafc3650 Fix errors in How to Play menu 2026-05-04 09:55:37 -05:00
Atony 0c0157fed2 Add soft-support for Corsac Cosmetics 2026-05-04 07:43:53 -05:00
Atony 90bf402cbc Fix UI resizing with the top right buttons 2026-05-03 11:57:04 -05:00
Atony fed3abafb2 Bump version 2026-04-18 08:31:59 -05:00
Atony 925d47a275 Fix framerate slider 2026-04-18 08:29:11 -05:00
Atony 45a3597ab2 Update dependencies 2026-04-04 20:50:27 -05:00
Chipseq faeb39d3de use AccessTools to allow any method to be an event handler 2026-04-04 22:05:41 +02:00
Atony 6b46cffe5e Add isKilling check for basic kills 2026-04-03 20:31:27 -05:00
Atony 00a60bb7ad Add methods for StateMachineWrapper 2026-04-03 09:46:44 -05:00
Atony d89bce6956 Merge branch 'master' into dev 2026-04-01 21:33:21 -05:00
Atony 7f92e94293 Fix options menu error on mobile 2026-04-01 21:30:28 -05:00
Atony 6199e55110 Add Guardian Angel protection checks 2026-04-01 21:30:19 -05:00
Pietro420 eac442443b Random Snapshot for kill anim 2026-03-29 22:34:43 -03:00
Pietro420 6321753ec0 Don't use nullable for kill anim 2026-03-29 22:26:07 -03:00
XtraCube e66d45bd21 revert ID change for now 2026-03-23 19:33:16 -04:00
XtraCube 1e00181220 Change plugin identifier for Mira API 2026-03-23 12:07:08 -04:00
Atony fcd7856571 Merge pull request #82 from All-Of-Us-Mods/dev
MiraAPI 0.3.9
2026-03-21 16:25:04 -05:00
Atony 5680d27db1 Merge branch 'master' into dev 2026-03-21 16:22:27 -05:00
Atony 49d77fcf40 Fix issues with PlayerControl.Die race condition 2026-03-11 15:40:50 -05:00
Atony dcb4a3876f Fix neutral color in view pane 2026-03-11 00:23:32 -05:00
Atony 9270de408d Verify that target isn't already dead or dc'd 2026-03-10 18:04:22 -05:00
Atony 8940de76bb Add one last murder check 2026-03-10 00:18:55 -05:00
Atony 1813fe5a84 Adjust functions 2026-03-09 09:43:00 -05:00
Atony 272b14f0e0 Change murders to check with the host beforehand 2026-03-09 09:35:36 -05:00
XtraCube 6cc1d4b762 updated readme 2026-02-27 12:11:41 -05:00
Atony 9dfad78df6 Merge pull request #81 from All-Of-Us-Mods/dev
MiraAPI 0.3.8
2026-02-11 11:50:15 -06:00
Atony 7f506000a4 Check for vanilla kills 2026-02-11 11:40:55 -06:00
Atony 795b6a81d7 Merge branch 'dev' of https://github.com/All-Of-Us-Mods/MiraAPI into dev 2026-02-10 14:00:51 -06:00
Atony bd8d7a3208 Add lobby checks for kills 2026-02-10 14:00:47 -06:00
Atony e8536e5d4b Merge pull request #80 from All-Of-Us-Mods/dev
## What's New
- Individual game options can be reset if they are a part of a preset.
- A new meeting-specific murder rpc is now available for use, checking for kills happening within and outside of a meeting.
- ## What's Changed
- Framerate Slider has been renamed to be more accurate
- Memory usage per frame has been improved
2026-02-08 16:30:05 -06:00
Atony 090db766b2 Bump version from 0.3.6 to 0.3.7 2026-02-08 16:25:37 -06:00
Atony 1f0027108e Automatically change reactor dev build indicator 2026-02-01 13:22:57 -06:00
Atony 665724010d Fix how to play errors without custom roles 2026-01-26 13:02:39 -06:00
Atony 2d58c91289 Replace alt murder rpc 2026-01-21 04:47:27 -06:00
XtraCube b3ed6d67f9 Reduce memory allocations in HudManagerPatches.cs 2026-01-20 16:35:44 -05:00
Atony 64ef78e3a2 Fix wordwrap on local setting 2026-01-20 10:21:37 -06:00
Atony 5e9e4012ef Update CustomMurderRpc.cs 2026-01-19 21:38:45 -06:00
Atony fe04a7033a Change kill button to use alt murder rpc 2026-01-19 21:15:52 -06:00
Atony ba0ee49255 Add reset button for game options 2026-01-19 20:10:21 -06:00
Atony 97528b4380 Add alternative Murder RPC for meeting checks 2026-01-19 19:31:31 -06:00
Atony 50030c113b Merge pull request #78 from All-Of-Us-Mods/dev
## What's New
- Screen shake no longer interferes with scrolling in the host options menu
## What's Changed
- Sabotage flashes no longer break on Starlight
- Using the button scale setting no longer breaks the UI.
- If a role fails to register to a marshalling error (nullables) the game will no longer crash.
2026-01-04 12:41:46 -06:00
Atony 20e51cc993 Bump version 2026-01-04 12:38:43 -06:00
Atony 64b5e01ba1 Improve scrolling in roles menu itself 2026-01-03 19:06:51 -06:00
Atony d4469153ab Improve scrolling in roles menu 2026-01-03 19:00:46 -06:00
Atony ab67c233d8 Add FPS slider (default to 120 fps) 2026-01-03 18:55:12 -06:00
Atony c72eacc7a2 Fix options scroll with screen shake 2026-01-03 18:54:57 -06:00
Atony 13072d6834 Fix dualshock light errors on Starlight 2026-01-02 22:57:58 -06:00
Atony c6342750ef Add checks to prevent errors on Starlight 2026-01-02 15:17:31 -06:00
XtraCube 5c2e95376d add exception handling around role registration 2025-12-31 22:38:01 -05:00
Atony 067859b9fd Fix scale factor not setting properly when changed 2025-12-31 18:36:19 -06:00
Atony fe3bfcdbc4 Merge pull request #77 from All-Of-Us-Mods/dev
- Added Vanilla Button Click Event (and respective Cancel Event)
- Added accessibility settings for sabotage flashes and blares
- Added an in-game slider for resizing ability buttons
2025-12-28 18:00:57 -06:00
Atony 5d01462a32 Adjust local setting groups 2025-12-28 17:55:33 -06:00
Atony a9b6f40ed0 Add Button Scale Slider 2025-12-28 17:51:39 -06:00
Atony eae0024ec3 Add Accessibility settings for sabotages 2025-12-22 21:41:36 -06:00
Atony 2907564d6a Add Vanilla Button Click Event (& Cancel Event) 2025-12-16 22:14:50 -06:00
50 changed files with 1419 additions and 393 deletions

No files matched your search

+2 -2
View File
@@ -1,8 +1,8 @@
<Project>
<ItemGroup>
<PackageReference Include="Reactor" Version="2.3.1" />
<PackageReference Include="Reactor" Version="2.5.0-ci.371" />
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.735" Private="false" ExcludeAssets="runtime;native" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2025.10.14" PrivateAssets="all" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2026.3.31" PrivateAssets="all" />
<PackageReference Include="BepInEx.AutoPlugin" Version="1.1.0" PrivateAssets="all" />
<PackageReference Include="BepInEx.IL2CPP.MSBuild" Version="2.1.0-rc.1" PrivateAssets="all" ExcludeAssets="runtime" />
+2 -2
View File
@@ -8,7 +8,7 @@
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<VersionPrefix>0.3.4</VersionPrefix>
<VersionPrefix>0.4.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<Authors>All Of Us, XtraCube, Angxl</Authors>
@@ -42,4 +42,4 @@
<PropertyGroup>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)\stylecop.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>
</Project>
+18 -2
View File
@@ -1,4 +1,5 @@
using System.Linq;
using System.Globalization;
using System.Linq;
using MiraAPI.Events;
using MiraAPI.Events.Mira;
using MiraAPI.Events.Vanilla.Gameplay;
@@ -17,7 +18,7 @@ public static class ExampleEventHandlers
{
public static void Initialize()
{
// You can register event handlers with the MiraEventManager class.
// You can register event handlers with the MiraEventManager class
var handle = MiraEventManager.RegisterEventHandler<BeforeMurderEvent>(@event =>
{
Logger<ExamplePlugin>.Info($"{@event.Source.Data.PlayerName} is about to kill {@event.Target.Data.PlayerName}");
@@ -37,6 +38,21 @@ public static class ExampleEventHandlers
MiraEventManager.UnregisterEventHandler(handle);
}
// If you want to check vanilla buttons, use something like this!
[RegisterEvent]
public static void VanillaButtonClickHandler(VanillaButtonClickEvent @event)
{
Logger<ExamplePlugin>.Info($"{@event.Button.buttonLabelText.text} is being used!");
if (@event.PlayerTarget != null)
{
Logger<ExamplePlugin>.Info($"{@event.PlayerTarget.Data.PlayerName} is being targeted!");
}
else if (@event.VentTarget != null)
{
Logger<ExamplePlugin>.Info($"Vent with id {@event.VentTarget.Id.ToString(CultureInfo.InvariantCulture)} is being targeted!");
}
}
// If you want to add extra votes to a player, do something like this.
[RegisterEvent]
public static void StartMeetingEvent(StartMeetingEvent _)
@@ -1,4 +1,6 @@
namespace MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Networking;
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// Event that is invoked before a player is murdered. This event is cancelable.
@@ -15,6 +17,24 @@ public sealed class BeforeMurderEvent : MiraCancelableEvent
/// </summary>
public PlayerControl Target { get; }
/// <summary>
/// Gets whether the murder was meant to be done in a meeting via an enum.
/// </summary>
public MeetingCheck InMeeting { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BeforeMurderEvent"/> class.
/// </summary>
/// <param name="source">The player that is killing the target.</param>
/// <param name="target">The player that is being killed.</param>
/// <param name="inMeeting">Whether the murder is intended to be triggered in a meeting.</param>
public BeforeMurderEvent(PlayerControl source, PlayerControl target, MeetingCheck inMeeting)
{
Source = source;
Target = target;
InMeeting = inMeeting;
}
/// <summary>
/// Initializes a new instance of the <see cref="BeforeMurderEvent"/> class.
/// </summary>
@@ -24,5 +44,6 @@ public sealed class BeforeMurderEvent : MiraCancelableEvent
{
Source = source;
Target = target;
InMeeting = MeetingCheck.Ignore;
}
}
@@ -0,0 +1,44 @@
using MiraAPI.Hud;
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// Invoked when a Vanilla Button click is cancelled. Do not use for custom buttons.
/// </summary>
/// <typeparam name="T">The Vanilla Button type.</typeparam>
public sealed class VanillaButtonCancelledEvent<T> : MiraEvent where T : AbilityButton
{
/// <summary>
/// Gets Vanilla Button whose click was cancelled.
/// </summary>
public T Button { get; }
/// <summary>
/// Initializes a new instance of the <see cref="VanillaButtonCancelledEvent{T}"/> class.
/// </summary>
/// <param name="button">The Mira Button whose click was cancelled.</param>
public VanillaButtonCancelledEvent(T button)
{
Button = button;
}
}
/// <summary>
/// Invoked when a Vanilla Button click is cancelled. Do not use for custom buttons.
/// </summary>
public sealed class VanillaButtonCancelledEvent : MiraEvent
{
/// <summary>
/// Gets Vanilla Button whose click was cancelled.
/// </summary>
public AbilityButton Button { get; }
/// <summary>
/// Initializes a new instance of the <see cref="VanillaButtonCancelledEvent"/> class.
/// </summary>
/// <param name="button">The Vanilla Button whose click was cancelled.</param>
public VanillaButtonCancelledEvent(AbilityButton button)
{
Button = button;
}
}
@@ -0,0 +1,65 @@
using MiraAPI.Hud;
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// Button click event for Vanilla Buttons only. Do not use for custom buttons.
/// </summary>
/// <typeparam name="T">The Vanilla Button type.</typeparam>
public sealed class VanillaButtonClickEvent<T> : MiraCancelableEvent where T : AbilityButton
{
/// <summary>
/// Gets Vanilla Button that was clicked.
/// </summary>
public T Button { get; }
/// <summary>
/// Gets the generic click event that is invoked before this button-specific event.
/// </summary>
public VanillaButtonClickEvent GenericClickEvent { get; }
/// <summary>
/// Initializes a new instance of the <see cref="VanillaButtonClickEvent{T}"/> class.
/// </summary>
/// <param name="button">The Mira Button that was clicked.</param>
/// <param name="genericClickEvent">The generic click event invoked before button-specific events.</param>
public VanillaButtonClickEvent(T button, VanillaButtonClickEvent genericClickEvent)
{
GenericClickEvent = genericClickEvent;
Button = button;
}
}
/// <summary>
/// Button click event for Vanilla Buttons only. Do not use for custom buttons.
/// </summary>
public sealed class VanillaButtonClickEvent : MiraCancelableEvent
{
/// <summary>
/// Gets Vanilla Button that was clicked.
/// </summary>
public AbilityButton Button { get; }
/// <summary>
/// Gets the target player, if any, of the Vanilla Button that was clicked.
/// </summary>
public PlayerControl? PlayerTarget { get; }
/// <summary>
/// Gets the target vent, if any, of the Vanilla Button that was clicked.
/// </summary>
public Vent? VentTarget { get; }
/// <summary>
/// Initializes a new instance of the <see cref="VanillaButtonClickEvent"/> class.
/// </summary>
/// <param name="button">The Vanilla Button that was clicked.</param>
/// <param name="playerTarget">The Player target, if any.</param>
/// <param name="ventTarget">The Vent target, if any.</param>
public VanillaButtonClickEvent(AbilityButton button, PlayerControl? playerTarget = null, Vent? ventTarget = null)
{
Button = button;
PlayerTarget = playerTarget;
VentTarget = ventTarget;
}
}
+1 -1
View File
@@ -69,4 +69,4 @@ public class BaseKeybind
{
Handler -= action;
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ namespace MiraAPI.Keybinds;
public class MiraKeybind : BaseKeybind
{
/// <summary>
/// Gets name of the keybind
/// Gets name of the keybind.
/// </summary>
public string Name { get; }
+12 -1
View File
@@ -1,4 +1,5 @@
global using static Reactor.Utilities.Logger<MiraAPI.MiraApiPlugin>;
using System;
using BepInEx;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
@@ -21,6 +22,11 @@ namespace MiraAPI;
[ReactorModFlags(ModFlags.RequireOnAllClients)]
public partial class MiraApiPlugin : BasePlugin
{
/// <summary>
/// Gets a value indicating whether the current device is running Starlight (on mobile).
/// </summary>
public static bool IsMobile => Constants.GetPlatformType() is Platforms.Android or Platforms.IPhone;
/// <summary>
/// Gets the branding Mira API color.
/// </summary>
@@ -31,6 +37,11 @@ public partial class MiraApiPlugin : BasePlugin
/// </summary>
public static Color DefaultHeaderColor { get; } = new Color32(77, 77, 77, 255);
/// <summary>
/// Gets a value indicating whether the current build is a development build or not. This mostly avoids confusion for users asking why the mod appears red.
/// </summary>
public static bool IsDevBuild => Version.Contains("ci", StringComparison.OrdinalIgnoreCase) || Version.Contains("dev", StringComparison.OrdinalIgnoreCase);
private static MiraPluginManager? PluginManager { get; set; }
internal Harmony Harmony { get; } = new(Id);
@@ -39,7 +50,7 @@ public partial class MiraApiPlugin : BasePlugin
{
Harmony.PatchAll();
ReactorCredits.Register("Mira API", Version, true, ReactorCredits.AlwaysShow);
ReactorCredits.Register("Mira API", Version, IsDevBuild, ReactorCredits.AlwaysShow);
PluginManager = new MiraPluginManager();
PluginManager.Initialize();
+63 -4
View File
@@ -1,12 +1,15 @@
using BepInEx.Configuration;
using MiraAPI.LocalSettings;
using MiraAPI.LocalSettings.Attributes;
using MiraAPI.Patches;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI;
/// <summary>
/// Mira API Config File Handler
/// Mira API Config File Handler.
/// </summary>
public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
{
@@ -21,22 +24,78 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
HideIconOnHover = false,
};
/// <summary>
/// Gets or sets the value stored for scaling buttons properly.
/// </summary>
public static float OldButtonScaleFactor { get; set; }
/// <inheritdoc />
public override void Open()
{
base.Open();
OldButtonScaleFactor = ButtonUIFactorSlider.Value;
}
/// <inheritdoc />
public override void OnOptionChanged(ConfigEntryBase configEntry)
{
base.OnOptionChanged(configEntry);
if (configEntry == ButtonUIFactorSlider)
{
if (HudManager.InstanceExists)
{
HudManagerPatches.ResizeUI(1f / OldButtonScaleFactor);
HudManagerPatches.ResizeUI(ButtonUIFactorSlider.Value);
}
OldButtonScaleFactor = ButtonUIFactorSlider.Value;
}
else if (configEntry == SetFpsSlider)
{
Application.targetFrameRate = (int)SetFpsSlider.Value;
}
}
/// <summary>
/// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers.
/// </summary>
[LocalToggleSetting]
public ConfigEntry<bool> ModifiersHudLeftSide { get; private set; } = config.Bind("Displays", "Show Modifiers HUD on Left Side", false);
public ConfigEntry<bool> ModifiersHudLeftSide { get; private set; } = config.Bind("Visuals/UI", "Show Modifiers HUD on Left Side", false);
/// <summary>
/// Gets whether to show keybinds on buttons.
/// </summary>
[LocalToggleSetting]
public ConfigEntry<bool> ShowKeybinds { get; private set; } = config.Bind("Keybinds", "Show Keybinds on Buttons", true);
// This would be placed in the keybinds menu, but it crashes for Epic Games users. - Atony
public ConfigEntry<bool> ShowKeybinds { get; private set; } = config.Bind("Visuals/UI", "Show Keybinds on Buttons", true);
/// <summary>
/// Gets the scale of the buttons.
/// </summary>
[LocalSliderSetting(min: 0.5f, max: 1.5f, suffixType: MiraNumberSuffixes.Multiplier, formatString: "0.00", displayValue: true)]
public ConfigEntry<float> ButtonUIFactorSlider { get; private set; } =
config.Bind("Visuals/UI", "Button Scale Factor", 0.75f);
/// <summary>
/// Gets the fps specified by the player.
/// </summary>
[LocalSliderSetting(min: 60f, max: 240f, suffixType: MiraNumberSuffixes.None, formatString: "0", displayValue: true, roundValue: true)]
public ConfigEntry<float> SetFpsSlider { get; private set; } =
config.Bind("Visuals/UI", "Max FPS", 120f);
/// <summary>
/// Gets whether to apply cosmetic changes to the TaskAdder.
/// </summary>
[LocalToggleSetting]
public ConfigEntry<bool> PrettyTaskAdder { get; private set; } = config.Bind("Freeplay", "Pretty Task Laptop", true);
/// <summary>
/// Gets whether to show the red flash from sabotages.
/// </summary>
[LocalToggleSetting]
public ConfigEntry<bool> EnableSabotageFlashes { get; private set; } = config.Bind("Accessibility", "Enable Sabotage Flashes", true);
/// <summary>
/// Gets whether to enable the sabotage sound effects or not.
/// </summary>
[LocalToggleSetting]
public ConfigEntry<bool> EnableSabotageBlares { get; private set; } = config.Bind("Accessibility", "Enable Sabotage Blare", true);
}
@@ -4,6 +4,7 @@ using System.Linq;
using HarmonyLib;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.LocalSettings;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Attributes;
using Reactor.Utilities.Extensions;
@@ -137,7 +138,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
internal void DestroyComponent(ModifierUiComponent component)
{
_modifiers.Remove(component.Modifier!);
component.gameObject.DestroyImmediate();
component.gameObject.DeepDestroy();
RefreshModifiers();
}
@@ -207,7 +208,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
continue;
}
mod.Value.gameObject.Destroy();
mod.Value.gameObject.DeepDestroy();
_modifiers.Remove(mod.Key);
}
+134 -8
View File
@@ -7,10 +7,12 @@ using Assets.CoreScripts;
using BepInEx.Unity.IL2CPP.Utils;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Utilities;
using Reactor.Networking.Attributes;
using Reactor.Networking.Rpc;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using Rewired;
using UnityEngine;
using Object = UnityEngine.Object;
@@ -22,7 +24,7 @@ namespace MiraAPI.Networking;
public static class CustomMurderRpc
{
/// <summary>
/// Networked Custom Murder method.
/// Networked Custom Murder method, ran by clients for the host to confirm kills.
/// </summary>
/// <param name="source">The killer.</param>
/// <param name="target">The player to murder.</param>
@@ -32,7 +34,6 @@ public static class CustomMurderRpc
/// <param name="teleportMurderer">Should the killer be snapped to the dead player.</param>
/// <param name="showKillAnim">Should the kill animation be shown.</param>
/// <param name="playKillSound">Should the kill sound be played.</param>
[MethodRpc((uint)MiraRpc.CustomMurder, LocalHandling = RpcLocalHandling.Before)]
public static void RpcCustomMurder(
this PlayerControl source,
PlayerControl target,
@@ -43,16 +44,117 @@ public static class CustomMurderRpc
bool showKillAnim = true,
bool playKillSound = true)
{
source.RpcCustomMurder(
target,
MeetingCheck.Ignore,
didSucceed,
resetKillTimer,
createDeadBody,
teleportMurderer,
showKillAnim,
playKillSound);
}
/// <summary>
/// Networked Custom Murder method, which checks for meetings as well, ran by clients for the host to confirm kills.
/// </summary>
/// <param name="source">The killer.</param>
/// <param name="target">The player to murder.</param>
/// <param name="inMeeting">Whether the murder is intended to be triggered in a meeting.</param>
/// <param name="didSucceed">Whether the murder was successful or not.</param>
/// <param name="resetKillTimer">Should the kill timer be reset.</param>
/// <param name="createDeadBody">Should a dead body be created.</param>
/// <param name="teleportMurderer">Should the killer be snapped to the dead player.</param>
/// <param name="showKillAnim">Should the kill animation be shown.</param>
/// <param name="playKillSound">Should the kill sound be played.</param>
[MethodRpc((uint)MiraRpc.CustomMurder, LocalHandling = RpcLocalHandling.Before)]
public static void RpcCustomMurder(
this PlayerControl source,
PlayerControl target,
MeetingCheck inMeeting,
bool didSucceed = true,
bool resetKillTimer = true,
bool createDeadBody = true,
bool teleportMurderer = true,
bool showKillAnim = true,
bool playKillSound = true)
{
if (LobbyBehaviour.Instance)
{
return;
}
var murderResultFlags = didSucceed ? MurderResultFlags.Succeeded : MurderResultFlags.FailedError;
var beforeMurderEvent = new BeforeMurderEvent(source, target);
var beforeMurderEvent = new BeforeMurderEvent(source, target, inMeeting);
MiraEventManager.InvokeEvent(beforeMurderEvent);
var isMeetingActive = MeetingHud.Instance != null || ExileController.Instance != null;
if ((inMeeting is MeetingCheck.ForMeeting && !isMeetingActive) || (inMeeting is MeetingCheck.OutsideMeeting && isMeetingActive))
{
beforeMurderEvent.Cancel();
}
if (beforeMurderEvent.IsCancelled)
if (target.ProtectedByGa())
{
beforeMurderEvent.Cancel();
murderResultFlags = MurderResultFlags.FailedProtected;
}
else if (beforeMurderEvent.IsCancelled)
{
murderResultFlags = MurderResultFlags.FailedError;
}
if (beforeMurderEvent.IsCancelled && source.AmOwner)
{
source.isKilling = true;
}
if (!PlayerControl.LocalPlayer.IsHost())
{
return;
}
RpcConfirmCustomMurder(
PlayerControl.LocalPlayer,
source,
target,
murderResultFlags,
resetKillTimer,
createDeadBody,
teleportMurderer,
showKillAnim,
playKillSound);
}
/// <summary>
/// Networked Custom Murder method, which checks for meetings as well, ran by clients for the host to confirm kills.
/// </summary>
/// <param name="host">The game host.</param>
/// <param name="source">The killer.</param>
/// <param name="target">The player to murder.</param>
/// <param name="murderResultFlags">End result for the murder.</param>
/// <param name="resetKillTimer">Should the kill timer be reset.</param>
/// <param name="createDeadBody">Should a dead body be created.</param>
/// <param name="teleportMurderer">Should the killer be snapped to the dead player.</param>
/// <param name="showKillAnim">Should the kill animation be shown.</param>
/// <param name="playKillSound">Should the kill sound be played.</param>
[MethodRpc((uint)MiraRpc.ConfirmCustomMurder, LocalHandling = RpcLocalHandling.After)]
public static void RpcConfirmCustomMurder(
this PlayerControl host,
PlayerControl source,
PlayerControl target,
MurderResultFlags murderResultFlags,
bool resetKillTimer = true,
bool createDeadBody = true,
bool teleportMurderer = true,
bool showKillAnim = true,
bool playKillSound = true)
{
if (LobbyBehaviour.Instance || !host.IsHost() || target.Data.IsDead || target.Data.Disconnected)
{
source.isKilling = false;
return;
}
var murderResultFlags2 = MurderResultFlags.DecisionByHost | murderResultFlags;
source.CustomMurder(
@@ -87,11 +189,10 @@ public static class CustomMurderRpc
bool playKillSound = true)
{
source.isKilling = false;
if (resultFlags.HasFlag(MurderResultFlags.FailedError))
if (resultFlags.HasFlag(MurderResultFlags.FailedError) || LobbyBehaviour.Instance)
{
return;
}
var data = target.Data;
if (resultFlags.HasFlag(MurderResultFlags.FailedProtected) ||
(resultFlags.HasFlag(MurderResultFlags.DecisionByHost) && target.protectedByGuardianId > -1))
@@ -174,7 +275,7 @@ public static class CustomMurderRpc
{
try
{
HudManager.Instance.KillOverlay.ShowKillAnimation(source.Data, data);
HudManager.Instance.KillOverlay.ShowKillAnimation(source.Data, target.Data);
}
catch (Exception e)
{
@@ -192,7 +293,7 @@ public static class CustomMurderRpc
source.CurrentOutfitType == PlayerOutfitType.Shapeshifted,
source.shapeshiftTargetPlayerId,
target.PlayerId);
source.MyPhysics.StartCoroutine(source.KillAnimations.Random()?.CoPerformCustomKill(source, target, createDeadBody, teleportMurderer));
source.MyPhysics.StartCoroutine(source.KillAnimations.RandomSnapshot().CoPerformCustomKill(source, target, createDeadBody, teleportMurderer));
}
/// <summary>
@@ -211,6 +312,10 @@ public static class CustomMurderRpc
bool createDeadBody = true,
bool teleportMurderer = true)
{
if (LobbyBehaviour.Instance)
{
yield break;
}
var cam = Camera.main?.GetComponent<FollowerCamera>();
var isParticipant = source.AmOwner || target.AmOwner;
var sourcePhys = source.MyPhysics;
@@ -298,3 +403,24 @@ public static class CustomMurderRpc
source.isKilling = false;
}
}
/// <summary>
/// Checks for custom murders.
/// </summary>
public enum MeetingCheck
{
/// <summary>
/// Checks for meetings.
/// </summary>
ForMeeting,
/// <summary>
/// Checks for a meeting to not be active.
/// </summary>
OutsideMeeting,
/// <summary>
/// Doesn't check for a meeting at all.
/// </summary>
Ignore,
}
+5
View File
@@ -35,6 +35,11 @@ public enum MiraRpc : uint
/// </summary>
CustomMurder,
/// <summary>
/// Custom Murder RPC Confirmation.
/// </summary>
ConfirmCustomMurder,
/// <summary>
/// Custom Game Over RPC.
/// </summary>
@@ -0,0 +1,67 @@
using System.Collections;
using BepInEx.Unity.IL2CPP.Utils.Collections;
using HarmonyLib;
using MiraAPI.LocalSettings;
using UnityEngine;
namespace MiraAPI.Patches.Accessibility;
[HarmonyPatch]
public static class HudManagerFlashPatches
{
[HarmonyPatch(typeof(HudManager), nameof(HudManager.StartReactorFlash))]
[HarmonyPrefix]
public static bool ReactorFlashPrefix(HudManager __instance)
{
if (__instance.ReactorFlash == null)
{
__instance.ReactorFlash = __instance.StartCoroutine(CoReactorFlash().WrapToIl2Cpp());
}
return false;
}
[HarmonyPatch(typeof(HudManager), nameof(HudManager.StartOxyFlash))]
[HarmonyPrefix]
public static bool OxygenFlashPrefix(HudManager __instance)
{
if (__instance.OxyFlash == null)
{
__instance.OxyFlash = __instance.StartCoroutine(CoReactorFlash().WrapToIl2Cpp());
}
return false;
}
public static IEnumerator CoReactorFlash()
{
if (!HudManager.InstanceExists)
{
yield break;
}
var hudManager = HudManager.Instance;
WaitForSeconds wait = new WaitForSeconds(1f);
bool light = false;
hudManager.FullScreen.color = new Color(1f, 0f, 0f, 0.37254903f);
while (true)
{
var settins = LocalSettingsTabSingleton<MiraApiSettings>.Instance;
hudManager.FullScreen.gameObject.SetActive(settins.EnableSabotageFlashes.Value && !hudManager.FullScreen.gameObject.activeSelf);
if (settins.EnableSabotageBlares.Value)
{
SoundManager.Instance.PlaySound(ShipStatus.Instance.SabotageSound, false, 1f, null);
}
light = !light;
/*if (!MiraApiPlugin.IsMobile)
{
if (hudManager.lightFlashHandle == null)
{
hudManager.lightFlashHandle = DualshockLightManager.Instance.AllocateLight();
hudManager.lightFlashHandle.color = new Color(1f, 0f, 0f, 1f);
hudManager.lightFlashHandle.intensity = 1f;
}
hudManager.lightFlashHandle.color.SetAlpha(light ? 1f : 0f);
}*/
yield return wait;
}
}
}
@@ -0,0 +1,75 @@
using System.Linq;
using AmongUs.GameOptions;
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Events.Vanilla.Usables;
namespace MiraAPI.Patches.Events;
/// <summary>
/// Patches to invoke vent related MiraEvents.
/// </summary>
[HarmonyPatch]
public static class VanillaButtonPatches
{
[HarmonyPrefix]
[HarmonyPatch(typeof(AbilityButton), nameof(AbilityButton.DoClick))]
public static bool DoClickPrefix(AbilityButton __instance)
{
// Invoke the generic button click event.
PlayerControl? playerTarget = null;
Vent? ventTarget = null;
var role = PlayerControl.LocalPlayer.Data.Role;
if (role.Role is RoleTypes.Tracker)
{
playerTarget = role.Cast<TrackerRole>().currentTarget;
}
else if (role.Role is RoleTypes.GuardianAngel)
{
playerTarget = role.Cast<GuardianAngelRole>().currentTarget;
}
else if (role.Role is RoleTypes.Engineer)
{
ventTarget = role.Cast<EngineerRole>().currentTarget;
}
var genericEvent = new VanillaButtonClickEvent(__instance, playerTarget, ventTarget);
MiraEventManager.InvokeEvent(genericEvent);
if (genericEvent.IsCancelled)
{
MiraEventManager.InvokeEvent(new VanillaButtonCancelledEvent(__instance));
}
if (!genericEvent.IsCancelled)
{
return true;
}
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(SecondaryAbilityButton), nameof(SecondaryAbilityButton.DoClick))]
public static bool SecondaryDoClickPrefix(AbilityButton __instance)
{
// Invoke the generic button click event.
PlayerControl? playerTarget = null;
var role = PlayerControl.LocalPlayer.Data.Role;
if (role.Role is RoleTypes.Detective)
{
playerTarget = role.Cast<DetectiveRole>().currentTarget;
}
var genericEvent = new VanillaButtonClickEvent(__instance, playerTarget);
MiraEventManager.InvokeEvent(genericEvent);
if (genericEvent.IsCancelled)
{
MiraEventManager.InvokeEvent(new VanillaButtonCancelledEvent(__instance));
}
if (!genericEvent.IsCancelled)
{
return true;
}
return false;
}
}
+2 -1
View File
@@ -276,8 +276,9 @@ public static class TaskAdderPatches
__instance.transform.FindChild("TitleText_TMP")?.gameObject.DestroyImmediate();
}
__instance.ActiveItems.ToArray().Do(x => x.gameObject.Destroy());
__instance.ActiveItems.ToArray().Do(x => x.gameObject.DeepDestroy(false));
__instance.ActiveItems.Clear();
Utilities.Extensions.ClearGarbageCollector();
float num = 0f;
float num2 = 0f;
+120 -12
View File
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using HarmonyLib;
using MiraAPI.Hud;
using MiraAPI.Keybinds;
@@ -25,9 +25,87 @@ public static class HudManagerPatches
public static Transform? BottomRight { get; private set; }
public static Transform? Buttons { get; private set; }
private static Dictionary<TextMeshPro, int> vanillaKeybindIcons = new();
private static Dictionary<TextMeshPro, int> vanillaKeybindIcons = [];
internal static List<TextMeshPro> ModdedKeybindIcons = new();
internal static List<TextMeshPro> ModdedKeybindIcons = [];
public static IEnumerator CoResizeUI()
{
while (!HudManager.Instance)
{
yield return null;
}
yield return new WaitForSeconds(0.01f);
ResizeUI(LocalSettingsTabSingleton<MiraApiSettings>.Instance.ButtonUIFactorSlider.Value);
}
public static void ResizeUI(float scaleFactor)
{
var baseButtons = HudManager.Instance.transform.FindChild("Buttons");
if (baseButtons != null)
{
foreach (var aspect in baseButtons.GetComponentsInChildren<AspectPosition>(true))
{
if (aspect.gameObject == null)
{
continue;
}
if (aspect.gameObject.name.Contains("TopRight"))
{
continue;
}
aspect.gameObject.SetActive(!aspect.isActiveAndEnabled);
aspect.DistanceFromEdge *= new Vector2(scaleFactor, scaleFactor);
aspect.gameObject.SetActive(!aspect.isActiveAndEnabled);
}
}
foreach (var button in HudManager.Instance.GetComponentsInChildren<ActionButton>(true))
{
if (button.gameObject == null)
{
continue;
}
button.gameObject.SetActive(!button.isActiveAndEnabled);
button.gameObject.transform.localScale *= scaleFactor;
button.gameObject.SetActive(!button.isActiveAndEnabled);
}
if (baseButtons != null)
{
foreach (var arrange in baseButtons.GetComponentsInChildren<GridArrange>(true))
{
if (!arrange.gameObject || !arrange.transform)
{
continue;
}
if (arrange.gameObject.name.Contains("TopRight"))
{
continue;
}
arrange.gameObject.SetActive(!arrange.isActiveAndEnabled);
arrange.CellSize = new Vector2(scaleFactor, scaleFactor);
arrange.gameObject.SetActive(!arrange.isActiveAndEnabled);
if (arrange.isActiveAndEnabled && arrange.gameObject.transform.childCount != 0)
{
try
{
arrange.ArrangeChilds();
}
catch
{
// Error($"Error arranging child objects in GridArrange: {e}");
}
}
}
}
}
/*
/// <summary>
@@ -65,8 +143,9 @@ public static class HudManagerPatches
foreach (var t in BottomLeft.GetComponentsInChildren<ActionButton>(true))
{
t.gameObject.Destroy();
t.gameObject.DeepDestroy(false);
}
Utilities.Extensions.ClearGarbageCollector();
var gridArrange = BottomLeft.GetComponent<GridArrange>();
var aspectPosition = BottomLeft.GetComponent<AspectPosition>();
@@ -146,9 +225,15 @@ public static class HudManagerPatches
}
var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos);
vanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent<TextMeshPro>(), actionId);
var comp = buttonObj.GetComponent<ActionButton>();
KeybindManager.VanillaKeybinds[comp.GetType()].Button = comp;
if (!MiraApiPlugin.IsMobile)
{
var comp = buttonObj.GetComponent<ActionButton>();
KeybindManager.VanillaKeybinds[comp.GetType()].Button = comp;
}
}
MiraApiSettings.OldButtonScaleFactor =
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ButtonUIFactorSlider.Value;
Coroutines.Start(CoResizeUI());
}
/// <summary>
@@ -187,31 +272,54 @@ public static class HudManagerPatches
{
var canSeeBinds = ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard &&
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value;
foreach (var btnIcon in vanillaKeybindIcons)
{
btnIcon.Key.text = KeybindUtils.GetKeycodeByActionId(btnIcon.Value).ToString();
btnIcon.Key.transform.parent.gameObject.SetActive(canSeeBinds);
}
foreach (var btnIcon in ModdedKeybindIcons)
{
btnIcon.transform.parent.gameObject.SetActive(canSeeBinds);
}
var player = ReInput.players.GetPlayer(0);
var keyboard = player.controllers.Keyboard;
foreach (var entry in KeybindManager.Keybinds)
{
var keyboard = player.controllers.Keyboard;
bool modifierKeysPressed = entry.ModifierKeys.All(k => keyboard.GetModifierKey(k)) ||
entry.ModifierKeys.Length <= 0;
var modKeys = entry.ModifierKeys;
bool modifierKeysPressed;
if (modKeys.Length == 0)
{
modifierKeysPressed = true;
}
else
{
modifierKeysPressed = true;
foreach (var key in modKeys)
{
if (keyboard.GetModifierKey(key)) continue;
modifierKeysPressed = false;
break;
}
}
if (player.GetButtonDown(entry.Id) && modifierKeysPressed)
{
entry.Invoke();
}
}
foreach (var entry in KeybindManager.VanillaKeybinds.Values.Where(x => player.GetButtonDown(x.Id)))
// suppressed warning because we want minimum allocations here
#pragma warning disable S3267
foreach (var entry in KeybindManager.VanillaKeybinds.Values)
#pragma warning restore S3267
{
entry.Invoke();
if (player.GetButtonDown(entry.Id))
{
entry.Invoke();
}
}
}
}
@@ -16,8 +16,15 @@ public static class OptionsMenuBehaviourPatch
private static void OpenPostfix(OptionsMenuBehaviour __instance)
{
_remap_rollover = GameObject.Find("Remap Controls")?.GetComponent<ButtonRolloverHandler>()!;
_remap_background = _remap_rollover.transform.FindChild("Background").GetComponent<SpriteRenderer>();
_remap_background.color = _conflicts ? Color.red : Color.white;
try
{
_remap_background = _remap_rollover.transform.FindChild("Background").GetComponent<SpriteRenderer>();
_remap_background.color = _conflicts ? Color.red : Color.white;
}
catch
{
// ignored, this normally breaks when on mobile since there's no remap button screen.
}
}
[HarmonyPostfix]
+11 -4
View File
@@ -11,8 +11,6 @@ using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
@@ -41,6 +39,10 @@ public static class HatsTabPatches
[HarmonyPrefix]
public static bool OnEnablePrefix(HatsTab __instance)
{
if (!AddressablesLoader.AddressableHatsExist)
{
return true;
}
__instance.currentHat = HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat);
var allHats = HatManager.Instance.GetUnlockedHats().ToImmutableList();
@@ -66,6 +68,10 @@ public static class HatsTabPatches
[HarmonyPrefix]
public static void UpdatePrefix(HatsTab __instance)
{
if (!AddressablesLoader.AddressableHatsExist)
{
return;
}
if (sortedHats.Count == 0) return;
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
@@ -86,9 +92,10 @@ public static class HatsTabPatches
if (loadRoutine != null) Coroutines.Stop(loadRoutine);
hatIndex = 0;
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.DeepDestroy(false);
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
Utilities.Extensions.ClearGarbageCollector();
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
+14 -1
View File
@@ -1,5 +1,9 @@
using HarmonyLib;
using System.Collections;
using HarmonyLib;
using MiraAPI.LocalSettings;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using UnityEngine;
namespace MiraAPI.Patches.Menu;
@@ -17,5 +21,14 @@ public static class MainMenuManagerPatches
public static void AwakePostfix()
{
AddressablesLoader.LoadAll();
Coroutines.Start(SetFps());
}
private static IEnumerator SetFps()
{
Application.targetFrameRate = (int)LocalSettingsTabSingleton<MiraApiSettings>.Instance.SetFpsSlider.Value;
yield return new WaitForSeconds(1f);
Application.targetFrameRate = (int)LocalSettingsTabSingleton<MiraApiSettings>.Instance.SetFpsSlider.Value;
}
}
+12 -2
View File
@@ -6,6 +6,7 @@ using Reactor.Utilities.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using MiraAPI.Utilities.Assets;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
@@ -44,6 +45,10 @@ public static class NameplatesTabPatches
[HarmonyPrefix]
public static bool OnEnablePrefix(NameplatesTab __instance)
{
if (!AddressablesLoader.AddressableNameplatesExist)
{
return true;
}
__instance.plateId = HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.namePlate).ProdId;
if (!SortedNameplates.ContainsKey("Vanilla")) AddRange(DestroyableSingleton<HatManager>.Instance.GetUnlockedNamePlates().Select(x => ("Vanilla", x)));
@@ -60,6 +65,10 @@ public static class NameplatesTabPatches
public static void UpdatePrefix(NameplatesTab __instance)
{
if (!AddressablesLoader.AddressableNameplatesExist)
{
return;
}
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
PreviousPage(__instance);
@@ -72,9 +81,10 @@ public static class NameplatesTabPatches
private static void GenerateHats(NameplatesTab __instance, int page)
{
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.DeepDestroy(false);
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
Utilities.Extensions.ClearGarbageCollector();
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
+12 -2
View File
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using MiraAPI.Utilities.Assets;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
@@ -36,6 +37,10 @@ public static class SkinsTabPatches
[HarmonyPrefix]
public static bool OnEnablePrefix(SkinsTab __instance)
{
if (!AddressablesLoader.AddressableSkinsExist)
{
return true;
}
__instance.skinId = HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin).ProdId;
var allSkins = HatManager.Instance.GetUnlockedSkins().ToImmutableList();
@@ -62,6 +67,10 @@ public static class SkinsTabPatches
public static void UpdatePrefix(SkinsTab __instance)
{
if (!AddressablesLoader.AddressableSkinsExist)
{
return;
}
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
PreviousPage(__instance);
@@ -74,9 +83,10 @@ public static class SkinsTabPatches
private static void GenerateHats(SkinsTab __instance, int page)
{
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.DeepDestroy(false);
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
Utilities.Extensions.ClearGarbageCollector();
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
+12 -2
View File
@@ -5,6 +5,7 @@ using Reactor.Utilities.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using MiraAPI.Utilities.Assets;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
@@ -44,6 +45,10 @@ public static class VisorsTabPatches
[HarmonyPrefix]
public static bool OnEnablePrefix(VisorsTab __instance)
{
if (!AddressablesLoader.AddressableVisorsExist)
{
return true;
}
__instance.visorId = HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor).ProdId;
if (!SortedVisors.ContainsKey("Vanilla")) AddRange(DestroyableSingleton<HatManager>.Instance.GetUnlockedVisors().Select(x => ("Vanilla", x)));
@@ -60,6 +65,10 @@ public static class VisorsTabPatches
public static void UpdatePrefix(VisorsTab __instance)
{
if (!AddressablesLoader.AddressableVisorsExist)
{
return;
}
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
PreviousPage(__instance);
@@ -72,9 +81,10 @@ public static class VisorsTabPatches
private static void GenerateHats(VisorsTab __instance, int page)
{
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.DeepDestroy(false);
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
Utilities.Extensions.ClearGarbageCollector();
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
+170 -28
View File
@@ -1,8 +1,11 @@
using System;
using System.Linq;
using AmongUs.GameOptions;
using HarmonyLib;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.Presets;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Localization.Utilities;
@@ -10,6 +13,7 @@ using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Options;
@@ -17,6 +21,20 @@ namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(GameOptionsMenu))]
internal static class GameOptionsMenuPatch
{
[HarmonyPostfix]
[HarmonyPatch(nameof(GameOptionsMenu.OnEnable))]
public static void OpenPatch(GameOptionsMenu __instance)
{
HudManager.Instance.PlayerCam.OverrideScreenShakeEnabled = false;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(GameOptionsMenu.OnDisable))]
public static void ClosePatch(GameOptionsMenu __instance)
{
HudManager.Instance.PlayerCam.OverrideScreenShakeEnabled = true;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(GameOptionsMenu.Update))]
// ReSharper disable once InconsistentNaming
@@ -121,11 +139,25 @@ internal static class GameOptionsMenuPatch
private static void CustomMenuOneCreate(GameOptionsMenu menu)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ParentMenu == MenuCategory.CustomOne) ?? [];
foreach (var group in groups)
var existing = GameSettingMenuPatches.CachedGameSettings.FirstOrDefault(x => x.ModId == GameSettingMenuPatches.SelectedModIdx && x.Category == MenuCategory.CustomOne);
if (existing != null)
{
CreateGroup(menu, group);
GameSettingMenuPatches.ShowGameOptions(existing, menu);
}
else
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ParentMenu == MenuCategory.CustomOne) ?? [];
foreach (var group in groups)
{
CreateGroup(menu, group);
}
GameSettingMenuPatches.AddGameSettings(
MenuCategory.CustomOne,
menu.Children,
menu.settingsContainer
.GetComponentsInChildren<CategoryHeaderMasked>()
.ToList());
}
}
@@ -142,18 +174,32 @@ internal static class GameOptionsMenuPatch
private static void CustomMenuTwoCreate(GameOptionsMenu menu)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ParentMenu == MenuCategory.CustomTwo) ?? [];
foreach (var group in groups)
var existing = GameSettingMenuPatches.CachedGameSettings.FirstOrDefault(x => x.ModId == GameSettingMenuPatches.SelectedModIdx && x.Category == MenuCategory.CustomTwo);
if (existing != null)
{
CreateGroup(menu, group);
GameSettingMenuPatches.ShowGameOptions(existing, menu);
}
else
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ParentMenu == MenuCategory.CustomTwo) ?? [];
foreach (var group in groups)
{
CreateGroup(menu, group);
}
GameSettingMenuPatches.AddGameSettings(
MenuCategory.CustomTwo,
menu.Children,
menu.settingsContainer
.GetComponentsInChildren<CategoryHeaderMasked>()
.ToList());
}
}
private static void ModifiersUpdate(ref float num)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? [];
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) ?? [];
foreach (var modGroup in groups)
{
@@ -163,11 +209,25 @@ internal static class GameOptionsMenuPatch
private static void ModifiersCreate(GameOptionsMenu menu)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? [];
foreach (var group in groups)
var existing = GameSettingMenuPatches.CachedGameSettings.FirstOrDefault(x => x.ModId == GameSettingMenuPatches.SelectedModIdx && x.Category == MenuCategory.Modifiers);
if (existing != null)
{
CreateGroup(menu, group);
GameSettingMenuPatches.ShowGameOptions(existing, menu);
}
else
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) ?? [];
foreach (var group in groups)
{
CreateGroup(menu, group);
}
GameSettingMenuPatches.AddGameSettings(
MenuCategory.Modifiers,
menu.Children,
menu.settingsContainer
.GetComponentsInChildren<CategoryHeaderMasked>()
.ToList());
}
}
@@ -199,12 +259,20 @@ internal static class GameOptionsMenuPatch
return false;
}
var filteredGroups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu && x.ParentMenu == MenuCategory.Roles) ?? [];
foreach (var group in filteredGroups)
var existing = GameSettingMenuPatches.CachedGameSettings.FirstOrDefault(x => x.ModId == GameSettingMenuPatches.SelectedModIdx && x.Category == MenuCategory.Game);
if (existing != null)
{
CreateGroup(__instance, group);
GameSettingMenuPatches.ShowGameOptions(existing, __instance);
}
else
{
var filteredGroups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu && x.ParentMenu == MenuCategory.Roles) ?? [];
foreach (var group in filteredGroups)
{
CreateGroup(__instance, group);
}
}
return false;
@@ -257,7 +325,9 @@ internal static class GameOptionsMenuPatch
categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister(group.GroupName), 20);
categoryHeaderMasked.Background.color = group.GroupColor;
categoryHeaderMasked.Divider.color = group.GroupColor;
categoryHeaderMasked.Title.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) ? Color.white : group.GroupColor.FindAlternateColor();
categoryHeaderMasked.Title.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset();
categoryHeaderMasked.Background.sprite.texture.filterMode = FilterMode.Bilinear;
@@ -276,13 +346,20 @@ internal static class GameOptionsMenuPatch
newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f);
newText.gameObject.GetComponent<TextTranslatorTMP>().Destroy();
var options = group.Options.Select(
opt => opt.CreateOption(
menu.checkboxOrigin,
menu.numberOptionOrigin,
menu.stringOptionOrigin,
menu.playerOptionOrigin,
menu.settingsContainer));
var options = group.Options.Select(opt => opt.CreateOption(
menu.checkboxOrigin,
menu.numberOptionOrigin,
menu.stringOptionOrigin,
menu.playerOptionOrigin,
menu.settingsContainer));
OptionPreset? defaultPreset = null;
if (GameSettingMenuPatches.SelectedMod != null && PresetManager.DefaultPresets.TryGetValue(
GameSettingMenuPatches.SelectedMod,
out var preset))
{
defaultPreset = preset;
}
foreach (var newOpt in options)
{
@@ -318,14 +395,79 @@ internal static class GameOptionsMenuPatch
if (newOpt is ToggleOption toggle)
{
toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset();
toggle.CheckMark.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor ? group.GroupColor : MiraAssets.AcceptedTeal;
toggle.CheckMark.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor
? group.GroupColor
: MiraAssets.AcceptedTeal;
var rend = toggle.CheckMark.transform.parent.FindChild("ActiveSprite")
.GetComponent<SpriteRenderer>();
rend.sprite = MiraAssets.CheckmarkBox.LoadAsset();
rend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor ? group.GroupColor : MiraAssets.AcceptedTeal;
rend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor
? group.GroupColor
: MiraAssets.AcceptedTeal;
}
menu.Children.Add(newOpt);
var resetBtn = new GameObject("ResetOption");
resetBtn.transform.parent = newOpt.transform;
resetBtn.transform.localScale = new(.5f, .5f, 1);
resetBtn.layer = LayerMask.NameToLayer("UI");
resetBtn.transform.localPosition = new Vector3(-3.1f, 0f, -2f);
var resetRend = resetBtn.AddComponent<SpriteRenderer>();
resetRend.sprite = MiraAssets.ResetButton.LoadAsset();
resetRend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
var resetBoxCol = resetBtn.gameObject.AddComponent<BoxCollider2D>();
resetBoxCol.size = new Vector2(1f, 1f);
resetBoxCol.offset = new Vector2(0, 0);
var passiveButton = resetBtn.AddComponent<PassiveButton>();
passiveButton.OnClick = new Button.ButtonClickedEvent();
passiveButton.ClickSound = menu.BackButton.GetComponent<PassiveButton>().ClickSound;
passiveButton.OnMouseOver = new UnityEvent();
passiveButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
resetRend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor
? group.GroupColor
: MiraAssets.AcceptedTeal;
}));
passiveButton.OnMouseOut = new UnityEvent();
passiveButton.OnMouseOut.AddListener(
(UnityAction)(() =>
{
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
}));
if (newOpt is ToggleOption toggleOpt)
{
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
defaultPreset!.ResetOption(toggleOpt);
}));
}
else if (newOpt is NumberOption numOpt)
{
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
defaultPreset!.ResetOption(numOpt);
}));
}
else if (newOpt is StringOption strOpt)
{
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
defaultPreset!.ResetOption(strOpt);
}));
}
if (!defaultPreset!.IsOptionInPreset(newOpt))
{
resetBtn.Destroy();
}
newOpt.Initialize();
newOpt.gameObject.SetActive(false);
@@ -262,7 +262,7 @@ internal static class GamePresetsTabPatches
iconSpriteRend.sprite = MiraAssets.RefreshIcon.LoadAsset();
Helpers.DivideSize(refreshButton.gameObject, 2f);
refreshButton.buttonText.gameObject.Destroy();
refreshButton.buttonText.gameObject.DeepDestroy();
refreshButton.OnClick = new Button.ButtonClickedEvent();
refreshButton.OnClick.AddListener((UnityAction)Refresh);
+146 -38
View File
@@ -6,6 +6,7 @@ using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
using MiraAPI.Presets;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
using TMPro;
@@ -40,6 +41,140 @@ internal static class GameSettingMenuPatches
private static PassiveButton? _customTwoButton;
private static GameObject? _nextModButton;
private static GameObject? _previousModButton;
public static List<GameSettingsCache> CachedGameSettings { get; } = [];
public static List<RoleSettingsCache> CachedRoleSettings { get; } = [];
public sealed class GameSettingsCache
{
public int ModId { get; set; }
public MenuCategory Category { get; set; } = MenuCategory.Game;
public Il2CppSystem.Collections.Generic.List<OptionBehaviour> Options { get; set; }
public List<CategoryHeaderMasked> CategoryHeaders { get; set; }
public GameObject CacheHolder { get; set; }
}
public static GameSettingsCache GetCurrentSettings(MenuCategory category)
{
var existing = CachedGameSettings.FirstOrDefault(x => x.ModId == SelectedModIdx && x.Category == category);
if (existing != null)
{
return existing;
}
var menu = category switch
{
MenuCategory.Modifiers => _modifiersTab!,
MenuCategory.CustomOne => _customOneTab!,
MenuCategory.CustomTwo => _customTwoTab!,
_ => GameSettingMenu.Instance.GameSettingsTab,
};
return AddGameSettings(
category,
menu.Children,
menu.settingsContainer
.GetComponentsInChildren<CategoryHeaderMasked>()
.ToList());
}
public static GameSettingsCache AddGameSettings(MenuCategory category, Il2CppSystem.Collections.Generic.List<OptionBehaviour> options, List<CategoryHeaderMasked> categoryHeaders)
{
var modName = SelectedMod?.MiraPlugin.OptionsTitleText ?? "Vanilla";
var newHolder = new GameObject($"{modName} {category}");
newHolder.DontUnload().DontDestroy();
newHolder.SetActive(false);
var newCache = new GameSettingsCache
{
ModId = SelectedModIdx,
Category = category,
Options = options,
CategoryHeaders = categoryHeaders,
CacheHolder = newHolder,
};
CachedGameSettings.Add(newCache);
return newCache;
}
public static void HideGameOptions(GameSettingsCache cached, GameOptionsMenu gameOptMenu)
{
foreach (var obj in gameOptMenu.scrollBar.Inner.GetComponentsInChildren<Transform>())
{
if (obj.gameObject.name == "MapPicker" || !obj.gameObject)
{
continue;
}
obj.SetParent(cached.CacheHolder.transform);
}
}
public static void ShowGameOptions(GameSettingsCache cached, GameOptionsMenu gameOptMenu)
{
foreach (var obj in cached.CacheHolder.GetComponentsInChildren<Transform>())
{
if (!obj.gameObject)
{
continue;
}
obj.SetParent(gameOptMenu.scrollBar.Inner);
}
}
public sealed class RoleSettingsCache
{
public int ModId { get; set; }
public Il2CppSystem.Collections.Generic.List<OptionBehaviour> Options { get; set; }
public List<CategoryHeaderEditRole> RoleHeaders { get; set; }
public List<RoleOptionSetting> RoleOptions { get; set; }
public List<CategoryHeaderEditRole> CategoryHeaders { get; set; }
public GameObject CacheHolder { get; set; }
}
public static RoleSettingsCache GetCurrentRoleSettings()
{
var existing = CachedRoleSettings.FirstOrDefault(x => x.ModId == SelectedModIdx);
if (existing != null)
{
return existing;
}
var menu = GameSettingMenu.Instance.RoleSettingsTab;
return AddRoleSettings(
menu.advancedSettingChildren,
menu.RoleChancesSettings
.GetComponentsInChildren<CategoryHeaderEditRole>()
.ToList());
}
public static RoleSettingsCache AddRoleSettings(Il2CppSystem.Collections.Generic.List<OptionBehaviour> options, List<CategoryHeaderEditRole> categoryHeaders)
{
var modName = SelectedMod?.MiraPlugin.OptionsTitleText ?? "Vanilla";
var newHolder = new GameObject($"{modName} Roles");
newHolder.DontUnload().DontDestroy();
newHolder.SetActive(false);
var newCache = new RoleSettingsCache
{
ModId = SelectedModIdx,
Options = options,
CategoryHeaders = categoryHeaders,
CacheHolder = newHolder,
};
CachedRoleSettings.Add(newCache);
return newCache;
}
public static void HideRoleOptions(RoleSettingsCache cached, RolesSettingsMenu gameOptMenu)
{
foreach (var obj in gameOptMenu.scrollBar.Inner.GetComponentsInChildren<Transform>())
{
if (obj.gameObject.name == "MapPicker" || !obj.gameObject)
{
continue;
}
obj.SetParent(cached.CacheHolder.transform);
}
}
public static void ShowRoleOptions(RoleSettingsCache cached, RolesSettingsMenu gameOptMenu)
{
foreach (var obj in cached.CacheHolder.GetComponentsInChildren<Transform>())
{
if (!obj.gameObject)
{
continue;
}
obj.SetParent(gameOptMenu.scrollBar.Inner);
}
}
private static Dictionary<int, Vector3> OptionsPositions { get; } = [];
private static Dictionary<int, Vector3> ModifiersPositions { get; } = [];
@@ -564,21 +699,6 @@ internal static class GameSettingMenuPatches
CleanupTab(settings, roles);
}
private static void ClearOptions(Il2CppSystem.Collections.Generic.List<OptionBehaviour> options)
{
foreach (var child in options)
{
if (child.TryCast<GameOptionsMapPicker>() || !child.gameObject)
{
continue;
}
child.gameObject.DestroyImmediate();
}
options.Clear();
}
private static void CleanupTab(GameOptionsMenu settings, RolesSettingsMenu roles)
{
if (roles.roleChances != null)
@@ -608,24 +728,10 @@ internal static class GameSettingMenuPatches
void CleanupRoleSettings(RolesSettingsMenu rolesMenu)
{
if (rolesMenu.advancedSettingChildren != null)
{
ClearOptions(rolesMenu.advancedSettingChildren);
rolesMenu.advancedSettingChildren = null;
}
HideRoleOptions(GetCurrentRoleSettings(), rolesMenu);
rolesMenu.advancedSettingChildren = null;
rolesMenu.RoleChancesSettings
.transform
.GetComponentsInChildren<CategoryHeaderEditRole>()
.ToList()
.ForEach(header => header.gameObject.DestroyImmediate());
foreach (var role in rolesMenu.roleChances)
{
role.gameObject.DestroyImmediate();
}
rolesMenu.roleChances?.Clear();
rolesMenu.roleChances.Clear();
rolesMenu.AdvancedRolesSettings.gameObject.SetActive(false);
rolesMenu.RoleChancesSettings.gameObject.SetActive(true);
@@ -643,14 +749,16 @@ internal static class GameSettingMenuPatches
void CleanupSettings(GameOptionsMenu gameOptMenu, MenuCategory menuCategory = MenuCategory.Roles)
{
ClearOptions(gameOptMenu.Children);
if (menuCategory is MenuCategory.Roles)
{
HideRoleOptions(GetCurrentRoleSettings(), gameOptMenu.RolesMenu);
}
else
{
HideGameOptions(GetCurrentSettings(menuCategory), gameOptMenu);
}
gameOptMenu.Children = null;
gameOptMenu.settingsContainer
.GetComponentsInChildren<CategoryHeaderMasked>()
.ToList()
.ForEach(header => header.gameObject.DestroyImmediate());
gameOptMenu.Initialize();
var positions = OptionsPositions;
switch (menuCategory)
@@ -210,12 +210,19 @@ public static class LobbyViewPanePatches
list.Add(roleBehaviour);
}
var color = (i == 0) ? Palette.CrewmateRoleBlue : Palette.ImpostorRoleRed;
if (roleBehaviour is ICustomRole custom && (custom.Team is not ModdedRoleTeams.Crewmate &&
custom.Team is not ModdedRoleTeams.Impostor))
{
color = Color.grey;
}
viewSettingsInfoPanelRoleVariant.SetInfo(
roleBehaviour.NiceName,
numPerGame,
chancePerGame,
61,
(i == 0) ? Palette.CrewmateRoleBlue : Palette.ImpostorRoleRed,
color,
roleBehaviour.RoleIconSolid,
i == 0,
flag);
@@ -513,6 +520,34 @@ public static class LobbyViewPanePatches
instance.scrollBar.SetYBoundsMax(-num);
}
public static void SetModdedHeader(this CategoryHeaderRoleVariant header, StringNames roleName, int maskLayer, ModdedRoleTeams team, Sprite? roleIcon = null)
{
header.SetHeader(roleName, maskLayer);
if (team is ModdedRoleTeams.Crewmate)
{
header.Background.color = Palette.CrewmateRoleHeaderBlue;
header.Divider.color = Palette.CrewmateRoleHeaderBlue;
header.Title.color = Palette.CrewmateRoleHeaderTextBlue;
}
else if (team is ModdedRoleTeams.Impostor)
{
header.Background.color = Palette.ImpostorRoleHeaderRed;
header.Divider.color = Palette.ImpostorRoleHeaderRed;
header.Title.color = Palette.ImpostorRoleHeaderTextRed;
}
else
{
header.Background.color = Color.grey;
header.Divider.color = Color.grey;
header.Title.color = new Color32(50, 50, 50, 255);
}
if (roleIcon != null && header.icon != null)
{
header.icon.material.SetInt(PlayerMaterial.MaskLayer, maskLayer);
header.icon.sprite = roleIcon;
}
}
private static float SetUpAdvancedRoleViewPanel(
AdvancedRoleViewPanel viewPanel,
Type roleType,
@@ -536,10 +571,10 @@ public static class LobbyViewPanePatches
return 0;
}
viewPanel.header.SetHeader(
viewPanel.header.SetModdedHeader(
role.StringName,
maskLayer,
role.TeamType == RoleTeamTypes.Crewmate,
customRole.Team,
customRole.Configuration.Icon != null ? customRole.Configuration.Icon.LoadAsset() : MiraAssets.Empty.LoadAsset());
viewPanel.header.icon.transform.localScale = new Vector3(0.465f, 0.465f, 1f);
viewPanel.divider.material.SetInt(PlayerMaterial.MaskLayer, maskLayer);
@@ -35,6 +35,13 @@ public static class RoleSettingMenuPatches
private static RoleBehaviour? CurrentRole { get; set; }
private static List<IModdedOption>? CurrentRoleOptions { get; set; }
[HarmonyPostfix]
[HarmonyPatch(nameof(RolesSettingsMenu.OnEnable))]
public static void OpenPatch(RolesSettingsMenu __instance)
{
HudManager.Instance.PlayerCam.OverrideScreenShakeEnabled = false;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))]
public static void SetQuotaTabPostfix(RolesSettingsMenu __instance)
@@ -46,9 +53,11 @@ public static class RoleSettingMenuPatches
[HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))]
public static bool SetQuotaTabPatch(RolesSettingsMenu __instance)
{
Headers.ForEach(Object.Destroy);
RoleOptionSettings.ForEach(Object.Destroy);
GameSettingMenuPatches.HideRoleOptions(GameSettingMenuPatches.GetCurrentRoleSettings(), __instance);
__instance.advancedSettingChildren = null;
Headers.Clear();
__instance.roleChances.Clear();
RoleOptionSettings.Clear();
CurrentRole = null;
CurrentRoleOptions = null;
@@ -126,6 +135,7 @@ public static class RoleSettingMenuPatches
num3++;
}
Utilities.Extensions.ClearGarbageCollector();
return false;
}
@@ -147,6 +157,7 @@ public static class RoleSettingMenuPatches
if (roleGroups is null)
{
Utilities.Extensions.ClearGarbageCollector();
return false;
}
@@ -197,9 +208,9 @@ public static class RoleSettingMenuPatches
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();
blankLabel.DeepDestroy(false);
chanceLabel.DeepDestroy(false);
countLabel.DeepDestroy(false);
categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset();
categoryHeaderMasked.Background.sprite.texture.filterMode = FilterMode.Bilinear;
@@ -282,15 +293,10 @@ public static class RoleSettingMenuPatches
{
RoleGroupHidden[group] = !groupHidden;
}
foreach (var header in Headers)
{
header.Destroy();
}
GameSettingMenuPatches.HideRoleOptions(GameSettingMenuPatches.GetCurrentRoleSettings(), __instance);
__instance.advancedSettingChildren = null;
Headers.Clear();
foreach (var option in RoleOptionSettings)
{
option.gameObject.Destroy();
}
__instance.roleChances.Clear();
RoleOptionSettings.Clear();
__instance.SetQuotaTab();
}));
@@ -301,6 +307,7 @@ public static class RoleSettingMenuPatches
ScrollerNum -= 0.4f;
}
}
Utilities.Extensions.ClearGarbageCollector();
__instance.scrollBar.SetScrollBounds(__instance);
return false;
@@ -415,8 +422,9 @@ public static class RoleSettingMenuPatches
{
foreach (var optBehaviour in __instance.AdvancedRolesSettings.GetComponentsInChildren<OptionBehaviour>())
{
optBehaviour.gameObject.DestroyImmediate();
optBehaviour.gameObject.DeepDestroy(false);
}
Utilities.Extensions.ClearGarbageCollector();
CurrentRole = role;
__instance.advancedSettingChildren.Clear();
@@ -611,7 +619,7 @@ public static class RoleSettingMenuPatches
var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform);
newButton.name = "ConfigButton";
newButton.transform.localPosition = new Vector3(0.4473f, -0.3f, -2f);
newButton.transform.FindChild("Text_TMP").gameObject.DestroyImmediate();
newButton.transform.FindChild("Text_TMP").gameObject.DeepDestroy();
newButton.activeSprites.Destroy();
var btnRend = newButton.transform.FindChild("ButtonSprite").GetComponent<SpriteRenderer>();
+14
View File
@@ -117,6 +117,20 @@ internal static class PlayerControlPatches
__instance.RpcMurderPlayer(target, true);
}
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerControl.RpcMurderPlayer))]
[HarmonyPatch(nameof(PlayerControl.MurderPlayer))]
// ReSharper disable once InconsistentNaming
public static bool MurderPlayerPrefix(PlayerControl __instance)
{
if (LobbyBehaviour.Instance)
{
return false;
}
return true;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.FixedUpdate))]
// ReSharper disable once InconsistentNaming
+32 -13
View File
@@ -13,27 +13,27 @@ internal static class HowToPlayPatch
// yes i patched the entire method
private static void Prefix(HowToPlayScene __instance)
{
if (RoleManager.Instance.AllRoles.ToArray().All(x => !x.IsCustomRole()))
{
return;
}
__instance.sceneIndex = 0;
__instance.category = HowToPlayScene.HowToPlayCategory.RolesSelection;
__instance.startPage.SetActive(false);
if (__instance.roleButtonsParent.childCount == 0)
{
using (IEnumerator<RoleBehaviour> enumerator = RoleManager.Instance.AllRoles.ToArray().Where(x => !x.IsCustomRole()).GetEnumerator())
foreach (var role in RoleManager.Instance.AllRoles.ToArray().Where(x => !x.IsCustomRole()))
{
while (enumerator.MoveNext())
if (!role.IsSimpleRole && role.Role != RoleTypes.CrewmateGhost && role.Role != RoleTypes.ImpostorGhost)
{
RoleBehaviour role = enumerator.Current;
if (!role.IsSimpleRole && role.Role != RoleTypes.CrewmateGhost && role.Role != RoleTypes.ImpostorGhost)
HowToPlayRoleButton component = Object.Instantiate(__instance.roleButtonPrefab, __instance.roleButtonsParent).GetComponent<HowToPlayRoleButton>();
Sprite roleIcon = __instance.rolesScenes.ToArray().First(r => r.role == role.Role).roleIcon;
component.SetRoleInfo(role, roleIcon);
component.SetButtonAction((Il2CppSystem.Action)(() =>
{
HowToPlayRoleButton component = Object.Instantiate(__instance.roleButtonPrefab, __instance.roleButtonsParent).GetComponent<HowToPlayRoleButton>();
Sprite roleIcon = __instance.rolesScenes.ToArray().First(r => r.role == role.Role).roleIcon;
component.SetRoleInfo(role, roleIcon);
component.SetButtonAction((Il2CppSystem.Action)(() =>
{
__instance.OpenRolePage(role.Role);
}));
__instance.controllerSelectables.Add(component.GetComponent<PassiveButton>());
}
OpenRolePage(__instance, role.Role);
}));
__instance.controllerSelectables.Add(component.GetComponent<PassiveButton>());
}
}
foreach (UiElement uiElement in __instance.controllerSelectables)
@@ -46,4 +46,23 @@ internal static class HowToPlayPatch
__instance.roleSelectionScene.SetActive(true);
ControllerManager.Instance.SetDefaultSelection(__instance.defaultButtonSelected, null);
}
public static void OpenRolePage(HowToPlayScene instance, RoleTypes roleType)
{
instance.category = HowToPlayScene.HowToPlayCategory.Roles;
var newList = instance.rolesScenes.ToArray().ToList();
var buttonList = instance.roleButtons;
instance.sceneIndex = newList.FindIndex(r => r.role == roleType);
if (roleType != RoleTypes.Crewmate)
{
foreach (var button in buttonList)
{
if (button.GetRole().Role == roleType)
{
instance.previouslySelectedRoleButton = button.GetComponent<PassiveButton>();
}
}
}
instance.SetupDots(instance.rolesScenes[instance.sceneIndex].rolePages.Count);
instance.ChangeScene(0);
}
}
+4 -2
View File
@@ -1,7 +1,9 @@
using HarmonyLib;
using InnerNet;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using UnityEngine;
namespace MiraAPI.Patches.Roles;
@@ -69,9 +71,9 @@ public static class HudManagerPatches
CustomRoleManager.UpdateRoleTab(RoleTab, customRole);
}
else if (RoleTab)
else if (RoleTab != null)
{
RoleTab?.gameObject.Destroy();
RoleTab.gameObject.DeepDestroy();
}
}
}
+1 -1
View File
@@ -58,7 +58,7 @@ public static class KillButtonPatches
return false;
}
PlayerControl.LocalPlayer.RpcCustomMurder(__instance.currentTarget);
PlayerControl.LocalPlayer.RpcCustomMurder(__instance.currentTarget, MeetingCheck.OutsideMeeting);
__instance.SetTarget(null);
return false;
@@ -39,7 +39,7 @@ public static class LogicRoleSelectionHnsPatch
foreach (RoleManager.RoleAssignmentData roleAssignmentData in guaranteedRoles.Select((x) =>
new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100)))
{
for (;;)
while (true)
{
RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData;
int count = roleAssignmentData2.Count;
@@ -122,7 +122,7 @@ public static class LogicRoleSelectionHnsPatch
foreach (RoleManager.RoleAssignmentData roleAssignmentData in guaranteedRoles.Select((x) =>
new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100)))
{
for (;;)
while (true)
{
RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData;
int count = roleAssignmentData2.Count;
@@ -36,7 +36,7 @@ public static class LogicRoleSelectionNormalPatch
foreach (RoleManager.RoleAssignmentData roleAssignmentData in guaranteedRoles.Select((x) =>
new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100)))
{
for (;;)
while (true)
{
RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData;
int count = roleAssignmentData2.Count;
+2 -1
View File
@@ -6,6 +6,7 @@ using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using UnityEngine;
namespace MiraAPI.Patches.Roles;
@@ -32,7 +33,7 @@ public static class RoleManagerPatches
if (data.Role)
{
data.Role.Deinitialize(targetPlayer);
Object.Destroy(data.Role.gameObject);
data.Role.gameObject.DeepDestroy();
}
var roleBehaviour = Object.Instantiate<RoleBehaviour>(__instance.AllRoles.ToArray().First(r => r.Role == roleType), data.gameObject.transform);
roleBehaviour.Initialize(targetPlayer);
+1 -1
View File
@@ -28,7 +28,7 @@ internal static class VentCanUsePatch
var @object = pc.Object;
var role = @object.Data.Role;
var canVent = role is ICustomRole customRole ? customRole.Configuration.CanUseVent : role.CanVent;
var canVent = role is ICustomRole customRole ? customRole.Configuration.CanUseVent || customRole.Configuration.GetsVentData : role.CanVent;
couldUse = canVent;
var modifiers = @object.GetModifierComponent().ActiveModifiers;
+2 -2
View File
@@ -6,6 +6,7 @@ using System.Linq;
using System.Reflection;
using BepInEx.Configuration;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using MiraAPI.Colors;
using MiraAPI.Events;
using MiraAPI.GameEnd;
@@ -21,7 +22,6 @@ using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Networking;
using Reactor.Utilities;
using Rewired;
namespace MiraAPI.PluginLoading;
@@ -60,7 +60,7 @@ public sealed class MiraPluginManager
continue;
}
foreach (var method in type.GetMethods())
foreach (var method in AccessTools.GetDeclaredMethods(type))
{
var eventAttribute = method.GetCustomAttribute<RegisterEventAttribute>();
if (eventAttribute == null)
+22
View File
@@ -71,4 +71,26 @@ public class OptionPreset
CustomRoleManager.SyncAllRoleSettings();
}
/// <summary>
/// Resets the specified option with the preset provided.
/// </summary>
public void ResetOption(OptionBehaviour baseOption)
{
var selectedOpt = Plugin.InternalOptions.First(x => x.OptionBehaviour == baseOption);
selectedOpt.LoadFromPreset(PresetConfig);
/*ModdedOptionsManager.SyncAllOptions();
CustomRoleManager.SyncAllRoleSettings();*/
}
/// <summary>
/// Gets whether the specified option is included in the preset.
/// </summary>
/// <returns>>The value of whether the option is in the preset.</returns>
public bool IsOptionInPreset(OptionBehaviour baseOption)
{
var selectedOpt = Plugin.InternalOptions.First(x => x.OptionBehaviour == baseOption);
return selectedOpt.ConfigDefinition != null && PresetConfig.ContainsKey(selectedOpt.ConfigDefinition);
}
}
+5 -1
View File
@@ -4,6 +4,7 @@ using System.Linq;
using BepInEx.Configuration;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Utilities;
using UnityEngine;
@@ -19,6 +20,7 @@ public static class PresetManager
/// </summary>
public static string PresetDirectory { get; } = Path.GetFullPath("mira_presets", Application.persistentDataPath);
internal static Dictionary<MiraPluginInfo, OptionPreset> DefaultPresets { get; } = [];
internal static List<OptionPreset> InternalMasterPresets { get; } = [];
/// <summary>
@@ -47,6 +49,7 @@ public static class PresetManager
}
presetConfig.Save();
DefaultPresets.Add(plugin, new OptionPreset("Default", plugin, presetConfig));
}
/// <summary>
@@ -59,9 +62,10 @@ public static class PresetManager
{
if (btn != null)
{
Object.DestroyImmediate(btn);
btn.DeepDestroy(false);
}
}
Utilities.Extensions.ClearGarbageCollector();
plugin.InternalPresets.Clear();
if (!Directory.Exists(PresetDirectory))
+1 -1
View File
@@ -26,7 +26,7 @@ public class SavePresetPopup(nint cppPtr) : Minigame(cppPtr)
// Cleanup holder object
public void OnDestroy()
{
transform.parent.gameObject.Destroy();
transform.parent.gameObject.DeepDestroy();
}
public override void Close()
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

+10 -1
View File
@@ -1,5 +1,6 @@
using AmongUs.GameOptions;
using System;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Patches.Freeplay;
using MiraAPI.Utilities.Assets;
using UnityEngine;
@@ -77,16 +78,19 @@ public record struct CustomRoleConfiguration
/// <summary>
/// Gets or sets the Sprite used for the Role Options menu screenshot.
/// </summary>
[HideFromIl2Cpp]
public LoadableAsset<Sprite>? OptionsScreenshot { get; set; } = null;
/// <summary>
/// Gets or sets the Sprite used for the Role Icon.
/// </summary>
[HideFromIl2Cpp]
public LoadableAsset<Sprite>? Icon { get; set; } = null;
/// <summary>
/// Gets or sets the Intro sound for the Role.
/// </summary>
[HideFromIl2Cpp]
public LoadableAsset<AudioClip>? IntroSound { get; set; } = null;
/// <summary>
@@ -105,10 +109,15 @@ public record struct CustomRoleConfiguration
public bool UseVanillaKillButton { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the role can use vents.
/// Gets or sets a value indicating whether the role can use vents. Also sends vanilla vent data.
/// </summary>
public bool CanUseVent { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the role can receive vanilla vent data.
/// </summary>
public bool GetsVentData { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the role can use the sabotage button.
/// </summary>
+16 -9
View File
@@ -89,15 +89,22 @@ public static class CustomRoleManager
foreach (var roleType in roles)
{
ClassInjector.RegisterTypeInIl2Cpp(roleType);
var role = RegisterRole(roleType, pluginInfo);
if (role is null)
try
{
continue;
}
ClassInjector.RegisterTypeInIl2Cpp(roleType);
pluginInfo.InternalRoles.Add((ushort)role.Role, role);
var role = RegisterRole(roleType, pluginInfo);
if (role is null)
{
continue;
}
pluginInfo.InternalRoles.Add((ushort)role.Role, role);
}
catch (Exception ex)
{
Error($"Failed to register role type {roleType.Name}: {ex}");
}
}
pluginInfo.PluginConfig.Save();
@@ -116,7 +123,7 @@ public static class CustomRoleManager
if (roleBehaviour is not ICustomRole customRole)
{
roleBehaviour.gameObject.Destroy();
roleBehaviour.gameObject.DeepDestroy();
return null;
}
@@ -132,7 +139,7 @@ public static class CustomRoleManager
roleBehaviour.CanBeKilled = customRole.Configuration.CanGetKilled;
roleBehaviour.CanUseKillButton = customRole.Configuration.UseVanillaKillButton;
roleBehaviour.TasksCountTowardProgress = customRole.Configuration.TasksCountForProgress;
roleBehaviour.CanVent = customRole.Configuration.CanUseVent;
roleBehaviour.CanVent = customRole.Configuration.CanUseVent || customRole.Configuration.GetsVentData;
roleBehaviour.DefaultGhostRole = customRole.Configuration.GhostRole;
roleBehaviour.MaxCount = customRole.Configuration.MaxRoleCount;
roleBehaviour.RoleScreenshot = customRole.Configuration.OptionsScreenshot?.LoadAsset();
@@ -24,6 +24,26 @@ public static class AddressablesLoader
private static readonly List<(string, string)> RegisteredNameplateKeys = [];
private static readonly List<string> RegisteredSkinKeys = [];
/// <summary>
/// Gets a value indicating whether hats have been loaded by the addressables system.
/// </summary>
public static bool AddressableHatsExist { get; private set; }
/// <summary>
/// Gets a value indicating whether visors have been loaded by the addressables system.
/// </summary>
public static bool AddressableVisorsExist { get; private set; }
/// <summary>
/// Gets a value indicating whether nameplates have been loaded by the addressables system.
/// </summary>
public static bool AddressableNameplatesExist { get; private set; }
/// <summary>
/// Gets a value indicating whether skins have been loaded by the addressables system.
/// </summary>
public static bool AddressableSkinsExist { get; private set; }
/// <summary>
/// Registers a specific addressables package to load asynchronously at the start of the game, when possible.
/// </summary>
@@ -52,6 +72,7 @@ public static class AddressablesLoader
return;
}
AddressableHatsExist = true;
RegisteredHatKeys.Add(addressablesKey);
}
@@ -67,6 +88,7 @@ public static class AddressablesLoader
return;
}
AddressableSkinsExist = true;
RegisteredSkinKeys.Add(addressablesKey);
}
@@ -83,6 +105,7 @@ public static class AddressablesLoader
return;
}
AddressableNameplatesExist = true;
RegisteredNameplateKeys.Add((addressablesKey, groupTitle));
}
@@ -99,6 +122,7 @@ public static class AddressablesLoader
return;
}
AddressableVisorsExist = true;
RegisteredVisorKeys.Add((addressablesKey, groupTitle));
}
+5
View File
@@ -90,6 +90,11 @@ public static class MiraAssets
/// </summary>
public static LoadableResourceAsset CheckmarkBox { get; } = new("MiraAPI.Resources.CheckMarkBox.png");
/// <summary>
/// Gets the Reset Box used in the Settings Menu.
/// </summary>
public static LoadableResourceAsset ResetButton { get; } = new("MiraAPI.Resources.ResetButton.png");
/// <summary>
/// Gets the Checkmark used in the Settings Menu.
/// </summary>
+152 -1
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -53,7 +54,7 @@ public static class Extensions
bool isComplete;
if (self.amClosing == Minigame.CloseState.Closing)
{
UnityEngine.Object.Destroy(self.gameObject);
self.gameObject.DeepDestroy();
return;
}
if (self.CloseSound && Constants.ShouldPlaySfx())
@@ -132,6 +133,31 @@ public static class Extensions
button.SetCooldownFill(num);
}
/// <summary>
/// Returns a random element from the specified sequence.
/// </summary>
/// <param name="input">
/// The sequence to select an element from.
/// </param>
/// <typeparam name="T">
/// The type of elements in the sequence.
/// </typeparam>
/// <returns>
/// A randomly selected element from <paramref name="input"/>.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Thrown when <paramref name="input"/> is empty.
/// </exception>
public static T RandomSnapshot<T>(this IEnumerable<T> input)
{
var list = input.ToList();
if (list.Count == 0)
throw new InvalidOperationException("Cannot get random element from an empty collection.");
return list[UnityEngine.Random.Range(0, list.Count)];
}
/// <summary>
/// Gets a PlayerControl from their PlayerVoteArea in a meeting.
/// </summary>
@@ -156,6 +182,16 @@ public static class Extensions
return TutorialManager.InstanceExists || AmongUsClient.Instance.HostId == playerControl.OwnerId;
}
/// <summary>
/// Checks if a PlayerControl is protected by a Guardian Angel.
/// </summary>
/// <param name="playerControl">The player you're checking for.</param>
/// <returns>If the player is protected, true, else false.</returns>
public static bool ProtectedByGa(this PlayerControl playerControl)
{
return playerControl.protectedByGuardianId > -1;
}
/// <summary>
/// Used to convert a System.Collections.Generic.List to Il2cppSystem.
/// </summary>
@@ -184,6 +220,121 @@ public static class Extensions
return Mathf.Approximately(number, Mathf.Round(number));
}
/// <summary>
/// Destroys the object properly.
/// </summary>
/// <param name="obj">The object to destroy.</param>
/// <param name="clearGc">Whether to run the garbage collector immediately.</param>
public static void DeepDestroy(this GameObject? obj, bool clearGc = true)
{
Coroutines.Start(Nuke(obj, clearGc));
}
private static IEnumerator Nuke(GameObject? go, bool clearGc)
{
if (go == null)
yield break;
try
{
go.transform.SetParent(null, false);
}
catch
{
// ignored
}
try
{
go.SetActive(false);
}
catch
{
// ignored
}
foreach (var mb in go.GetComponentsInChildren<MonoBehaviour>(true))
{
if (mb == null)
continue;
try
{
mb.StopAllCoroutines();
}
catch
{
// ignored
}
try
{
mb.enabled = false;
}
catch
{
// ignored
}
}
foreach (var renderer in go.GetComponentsInChildren<Renderer>(true))
{
if (renderer == null)
continue;
try
{
foreach (var mat in renderer.materials)
{
if (mat != null)
UnityEngine.Object.Destroy(mat);
}
}
catch
{
// ignored
}
}
foreach (var filter in go.GetComponentsInChildren<MeshFilter>(true))
{
if (filter == null)
continue;
try
{
var mesh = filter.mesh;
if (mesh != null)
UnityEngine.Object.Destroy(mesh);
}
catch
{
// ignored
}
}
UnityEngine.Object.Destroy(go);
yield return null;
if (clearGc)
{
yield return CoFreeResources();
}
}
public static void ClearGarbageCollector()
{
Coroutines.Start(CoFreeResources());
}
private static IEnumerator CoFreeResources()
{
yield return Resources.UnloadUnusedAssets();
GC.Collect(0, GCCollectionMode.Forced, blocking: true);
GC.WaitForPendingFinalizers();
GC.Collect(0, GCCollectionMode.Forced, blocking: true);
}
/// <summary>
/// Gets a cache of player's vote data components to improve performance.
/// </summary>
+1 -5
View File
@@ -539,17 +539,13 @@ public static class Helpers
}
/// <summary>
/// Returns whether the role is blacklisted from appearing and spawning. (Only applicable to vanilla roles)
/// Returns whether the role is blacklisted from appearing and spawning. Only applicable to vanilla roles.
/// </summary>
/// <param name="role">The role to check.</param>
/// <returns>The role's blacklist status.</returns>
public static bool IsRoleBlacklisted(this RoleBehaviour role)
{
// This should be patchable by mods when a vanilla role is meant to be replaced by a custom role.
if (role.Role is RoleTypes.Engineer)
{
return true;
}
return false;
}
}
+16 -1
View File
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes;
using Object = Il2CppSystem.Object;
namespace MiraAPI.Utilities;
@@ -17,6 +18,7 @@ public class StateMachineWrapper<T> where T : Il2CppObjectBase
// normally it is fields, but IL2CPP turns them into properties
private readonly PropertyInfo _thisProperty;
private readonly PropertyInfo _stateProperty;
private readonly PropertyInfo _currentProperty;
private readonly Dictionary<string, PropertyInfo> _propertyCache;
private T? _parentInstance;
@@ -37,8 +39,9 @@ public class StateMachineWrapper<T> where T : Il2CppObjectBase
var type = _stateMachine.GetType();
_thisProperty = AccessTools.Property(type, "__4__this");
_stateProperty = AccessTools.Property(type, "__1__state");
_currentProperty = AccessTools.Property(type, "__2__current");
if (_thisProperty == null || _stateProperty == null)
if (_thisProperty == null || _stateProperty == null || _currentProperty == null)
{
throw new MissingMemberException($"Could not find required properties in type '{type}'.");
}
@@ -52,6 +55,18 @@ public class StateMachineWrapper<T> where T : Il2CppObjectBase
/// <returns>The current state as an integer.</returns>
public int GetState() => (int)_stateProperty.GetValue(_stateMachine)!;
/// <summary>
/// Sets the current state of the state machine.
/// </summary>
/// <param name="newState">The new state to use.</param>
public void SetState(int newState) => _stateProperty.SetValue(_stateMachine, newState);
/// <summary>
/// Sets the newest yield return of the state machine.
/// </summary>
/// <param name="newReturn">The new return to use.</param>
public void SetRecentReturn(Object newReturn) => _currentProperty.SetValue(_stateMachine, newReturn);
/// <summary>
/// Gets a parameter from the state machine by its name.
/// </summary>
+24 -216
View File
@@ -5,6 +5,7 @@
# Mira API
A thorough, but simple, Among Us modding API and utility library that covers:
- Roles
- Options
- Modifiers
@@ -13,6 +14,9 @@ A thorough, but simple, Among Us modding API and utility library that covers:
- Events
- Voting
- Assets
- Keybinds
- Local Settings
- Custom Game Over Conditions
- Compatibility
- ~~Game Modes~~ (coming soon)
@@ -24,9 +28,10 @@ The result is a less intrusive, better modding API that covers general use cases
# Usage
To start using Mira API, you need to:
1. Add a reference to Mira API either through a [DLL](https://github.com/All-Of-Us-Mods/MiraAPI/releases), project reference, or [NuGet package](https://www.nuget.org/packages/AllOfUs.MiraAPI).
2. Add a BepInDependency on your plugin class like this: `[BepInDependency(MiraApiPlugin.Id)]`
3. Implement the IMiraPlugin interface in your plugin class.
3. Implement the `IMiraPlugin` interface on your plugin class.
Mira API also depends on [Reactor](https://github.com/NuclearPowered/Reactor) to function properly!
Remember to include it as a reference and `BepInDependency`!
@@ -34,8 +39,10 @@ Remember to include it as a reference and `BepInDependency`!
For a full example, see [this file](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/ExamplePlugin.cs).
## Recommended Project Structure
It is highly recommended to follow this project structure when using Mira API to keep your code clean and organized.
You can also view the Example Mod in this repository for some guidance.
```
MyMiraMod/
├── Buttons/
@@ -60,220 +67,21 @@ MyMiraMod/
└── MyModAssets.cs
```
## Roles
Roles are straightforward in Mira API. There are two things you need to do to create a custom role:
1. Create a class that inherits from a base game role (like `CrewmateRole`, `ImpostorRole`, etc.)
2. Implement the `ICustomRole` interface from Mira API.
# Documentation
**Disclaimer: Make sure your plugin class has the following attribute `[ReactorModFlags(ModFlags.RequireOnAllClients)]` or else your roles will not register correctly.**
Full documentation for every Mira API feature is available on the **[wiki](https://github.com/All-Of-Us-Mods/MiraAPI/wiki)**:
Note: For step 1, if you are making neutral roles,
you can choose either `CrewmateRole` or `ImpostorRole`,
or even `RoleBehaviour` if you are okay with starting from scratch.
Mira API handles everything else, from adding the proper options to the settings menu,
to managing the role assignment at the start of the game.
There are no extra steps on the developer's part.
See [this file](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Roles/CustomRole.cs) for a code example.
## Modifiers
Mira API uses a different definition of 'modifiers' than other Among Us mods.
For example, in Town Of Us, a modifier is an extra "ability" that is applied on top of the base role.
However, in Mira API, modifiers are very flexible.
A modifier is anything that "modifiers" a player's abilities or interactions.
Mira provides three classes for working with modifiers:
- `BaseModifier`: The base for every modifier. You MUST add and remove this modifier from a player manually!
- `TimedModifier`: A modifier that has a time limit. This modifier has to be added manually, but will automatically remove itself after the time limit.
- `GameModifier`: This works like the typical TOU modifier, where it is automatically applied at the beginning of the game, then removed at the end.
Modifiers provide various overridable functions and properties for custom behavior.
They can also be used for "tagging" a player.
You can check if a player has a modifier through the extension method `HasModifier` on a `PlayerControl` object.
To start using a modifier, pick one of the base classes above and create a class that inherits from it. Implement the properties and methods you would like, then add the `[RegisterModifier]` attribute to the class.
An example Game modifier can be found [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Modifiers/GameModifierExample.cs).
An example Timer modifier can be found [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Modifiers/ModifierTimerExample.cs).
## Options
Options are also simple in Mira API.
Mira API handles all the hard work behind the scenes,
so developers only have to follow a few steps to create their custom options.
The Options API is split up into Groups and Options.
Every Option needs to be in a Group.
To create a group, you need to create a class that inherits from the `AbstractOptionGroup` abstract class.
Groups contain some properties for developers to control,
including `GroupName`, `GroupColor`, `GroupVisible`, and `OptionableType`.
The full list of properties can be found in the `AbstractOptionGroup` class.
Only the `GroupName` is required.
Here is an example of a group class:
```csharp
public class MyOptionsGroup : AbstractOptionGroup
{
public override string GroupName => "My Options"; // this is required
[ModdedNumberOption("My Number Option", min: 0, max: 10)]
public float MyNumberOption { get; set; } = 5f;
}
```
You can access any group class using the `OptionGroupSingleton` class like this:
```csharp
// MyOptionsGroup is a class that inherits from AbstractOptionGroup
var myGroup = OptionGroupSingleton<MyOptionsGroup>.Instance; // gets the instance of the group
Logger<MyPlugin>.Info(myGroup.MyNumberOption); // prints the value of the option to the console
```
Once you have an options group, there are two ways to make the actual options:
- Use an Option Attribute with a property.
- Create a ModdedOption property.
### Option Attributes
This is an example of using an Option Attribute on a property:
```csharp
// The first parameter is always the name of the option. The rest are dependent on the type of option.
[ModdedNumberOption("Sussy level", min: 0, max: 10)]
public float SussyLevel { get; set; } = 4f; // You can set a default value here.
```
Here are the available Option Attributes and their signatures:
```csharp
ModdedEnumOption(string name, Type enumType, string[]? values = null, Type? roleType = null)
ModdedNumberOption(
string name,
float min,
float max,
float increment=1
NumberSuffixes suffixType = NumberSuffixes.None,
bool zeroInfinity = false,
Type? roleType = null)
ModdedToggleOption(string name, Type? roleType = null)
```
### ModdedOption Properties
And this is an example of a ModdedOption property:
```csharp
public ModdedToggleOption YeezusAbility { get; } = new ModdedToggleOption("Yeezus Ability", false);
```
There are currently three types of ModdedOptions you can create:
- `ModdedEnumOption`
- `ModdedNumberOption`
- `ModdedToggleOption`
To see a full example of an option class,
see [this file](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Options/ExampleOptions.cs).
### Role Options
You can also specify a role type for an option or option group.
To set the role type for an entire group, set the `OptionableType` property on that group like this:
```csharp
public class MyOptionsGroup : AbstractOptionGroup
{
public override string GroupName => "My Options";
public override Type? OptionableType => typeof(MyRole); // this is the role that will have these options
[ModdedNumberOption("Ability Uses", min: 0, max: 10)]
public float AbilityUses { get; set; } = 5f;
}
```
You can also use a modifier type for the `OptionableType` property.
This will associate that option group with the specified modifier type
and display it in the modifier options menu.
An example can be found [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Options/Roles/CustomRoleSettings.cs).
## Custom Murders
Mira API provides its own implementation for murders.
Our implementation allows for more customization on kills, and helps bypass server checks.
You can use `PlayerControl.RpcCustomMurder` to perform a networked custom murder, or `PlayerControl.CustomMurder` to normally perform a custom murder.
For example:
```cs
PlayerControl.LocalPlayer.RpcCustomMurder(Target, createDeadBody: false, teleportMurderer: false, playKillSound: false, resetKillTimer: false, showKillAnim: false);
```
This will kill a player without creating a dead body and without teleporting the murderer.
## Buttons
Mira API provides a simple interface for adding ability buttons to the game.
The only thing you need to do is create a class
that inherits from the `CustomActionButton` class and implement the properties and methods.
Mira API handles all other tasks and logic required to add the button to the game.
In case you need to access your `CustomActionButton` instance from another class,
you can use the `CustomButtonSingleton` class like this:
```csharp
var myButton = CustomButtonSingleton<MyCoolButton>.Instance;
```
The button API is straightforward, but provides a lot of flexibility.
There are various methods you can override to customize the behavior of your button.
See [this file](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI/Hud/CustomActionButton.cs)
for a full list of methods you can override.
An example button can be found [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Buttons/ExampleButton.cs).
## Custom Colors
Mira provides a simple Custom Color API that allows you to add custom player colors to the game.
Creating custom colors isn't difficult, but there are some requirements for Mira to register your colors.
1. Create a ***STATIC*** class to house all your `CustomColor` objects.
2. Inside this class, create a `CustomColor` property for each color you intend to add.
3. Add the `[RegisterCustomColors]` attribute to the class.
Here is an example of a custom color class:
```csharp
[RegisterCustomColors]
public static class MyCustomColors
{
public static CustomColor Cerulean { get; } = new CustomColor("Cerulean", new Color(0.0f, 0.48f, 0.65f));
public static CustomColor Rose { get; } = new CustomColor("Rose", new Color(0.98f, 0.26f, 0.62f));
public static CustomColor Gold { get; } = new CustomColor("Gold", new Color(1.0f, 0.84f, 0.0f));
}
```
## Assets
Mira API provides a simple, but expandable asset system.
The core of the system is the `LoadableAsset<T>` class.
This is a generic abstract class that provides a pattern for loading assets.
Mira API comes with a few asset loaders:
1. `LoadableBundleAsset<T>`: This is used for loading assets from AssetBundles.
2. `LoadableAddressableAsset<T>`: This is used for loading assets from Addressables.
3. `LoadableResourceAsset`: This is used for loading **only sprites** from the Embedded Resources within a mod.
4. `LoadableAudioResourceAsset`: This is used for loading **only audio clips** from the Embedded Resources within a mod.
The code below shows how to use an asset loaders:
```csharp
// Load a sprite from an AssetBundle
AssetBundle bundle = AssetBundleManager.Load("MyBundle"); // AssetBundleManager is a utility provided by Reactor
LoadableAsset<Sprite> mySpriteAsset = new LoadableBundleAsset<Sprite>("MySprite", bundle);
Sprite sprite = mySpriteAsset.LoadAsset();
// Load a sprite from an Embedded Resource
// Make sure to set the Build Action of your image to Embedded Resource!
LoadableAsset<Sprite> buttonAsset = new LoadableResourceAsset("ExampleMod.Resources.MyButton.png");
Sprite button = buttonSpriteAsset.LoadAsset();
```
You can view an example file [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/ExampleAssets.cs).
You can create your own asset loaders by inheriting from `LoadableAsset<T>` and implementing the `LoadAsset` method.
- [Get Started / IMiraPlugin](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Home)
- [Custom Roles](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Custom-Roles)
- [Options](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Options)
- [Modifiers](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Modifiers)
- [Custom Buttons](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Custom-Buttons)
- [Custom Colors](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Colors)
- [Events](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Events)
- [Meetings and Voting](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Meetings-and-Voting)
- [Assets](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Assets)
- [Keybinds](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Keybinds)
- [Local Settings](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Local-Settings)
- [Game Modes](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Game-Modes)
- [Custom Game Over](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Game-Over)
- [Utilities](https://github.com/All-Of-Us-Mods/MiraAPI/wiki/Utilities)
Binary file not shown.