Compare commits

..
51 Commits
Author SHA1 Message Date
XtraCube 5628b79814 Merge remote-tracking branch 'origin/presets' into dev
# Conflicts:
#	MiraAPI/GameOptions/IModdedOption.cs
#	MiraAPI/GameOptions/ModdedOptionsManager.cs
#	MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
#	MiraAPI/Patches/Options/GameSettingMenuPatches.cs
#	MiraAPI/Patches/Options/LobbyViewPanePatches.cs
#	MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
#	MiraAPI/Utilities/Assets/MiraAssets.cs
#	MiraAPI/Utilities/Assets/SpriteTools.cs
#	MiraAPI/Utilities/Extensions.cs
2025-06-23 11:44:30 -04:00
XtraCube 72a44f65f1 fix role menu scrolling bug 2025-06-23 11:31:49 -04:00
XtraCube db6a336a63 Add modifier tab scroll positions and fix scroll positions not saving in some cases. 2025-06-23 11:07:28 -04:00
XtraCube 90b23ec2ca fix role scroll position issue 2025-06-23 10:31:47 -04:00
XtraCube 20e927208c make options wrap around in values 2025-06-21 20:17:57 -04:00
XtraCube 13ef3bdd49 add format string for cooldown timers 2025-06-21 19:32:28 -04:00
XtraCube b3ff6755bb suppress naming warnings 2025-06-21 18:52:18 -04:00
XtraCube 67604600cd remove obsolete code 2025-06-21 18:52:09 -04:00
XtraCube 6e35de6185 add BeforeGameEndEvent.cs 2025-06-21 18:43:56 -04:00
XtraCube a26ec51c7d send murder RPC immediately 2025-06-21 18:24:32 -04:00
XtraCube 3f6ae4cc41 send gameover immediately 2025-06-21 18:21:37 -04:00
XtraCube 15a2cdcd5b Merge pull request #36 from All-Of-Us-Mods/2025.5.20
Update to 2025.5.20
2025-06-21 17:27:23 -04:00
XtraCube 372a9e3aa9 suppress RPC warnings 2025-06-16 21:31:58 -04:00
XtraCube 10a9dcfa34 suppress RPC warnings 2025-06-11 21:26:08 -04:00
XtraCube d46fdb0185 comments about rpcmurder inlining 2025-05-30 17:15:52 -04:00
XtraCube a361afbcbf add comment about meetinghud inline issue 2025-05-30 17:12:03 -04:00
XtraCube 24cdac0243 Fix role options collection inlining 2025-05-30 17:07:11 -04:00
XtraCube 5fe75843ad fix haunt menu inlining 2025-05-30 17:01:43 -04:00
XtraCube a0436dcbba Fix ToggleOption inlining 2025-05-30 16:58:32 -04:00
XtraCube 35be381a77 Fix LobbyViewPane inlining 2025-05-30 16:53:15 -04:00
XtraCube 1298beb517 fix kill anim inlining 2025-05-30 16:06:21 -04:00
XtraCube d41c19a176 Adjust kill anim patch for inlining 2025-05-30 15:45:59 -04:00
XtraCube 476f4400ad Fix ReportBodyEventPatch.cs 2025-05-29 22:04:34 -04:00
XtraCube 2b46d90570 Merge branch 'dev' into 2025.5.20 2025-05-27 16:09:29 -04:00
XtraCube ae4aaf7af9 adjust mirabundle name to work on epic 2025-05-27 16:09:14 -04:00
XtraCube 757b9436ca adjust role message 2025-05-27 16:09:05 -04:00
XtraCube 7ea10afffb dont need reactor dev nuget anymore 2025-05-21 18:18:10 -04:00
XtraCube 0ab80c42f9 Update discord badge 2025-05-20 22:25:21 -04:00
XtraCube bc144568f4 fix harmony error due to renamed parameter 2025-05-20 18:51:55 -04:00
XtraCube 07f402e659 update reactor 2025-05-20 18:42:59 -04:00
XtraCube 1a6aed539a fix lobbyview modifier options showing in game options 2025-05-18 14:49:40 -04:00
XtraCube 023cdaa470 fix role setting menu bug 2025-05-18 14:46:24 -04:00
XtraCube 98dd5ca056 fix more warnings 2025-05-18 12:40:40 -04:00
XtraCube 1f3e21ce19 cleanup more warnings 2025-05-18 10:01:48 -04:00
XtraCube 15e441dd71 add documentation in mira assets 2025-05-18 09:37:27 -04:00
XtraCube 8c67bbbd44 cleanup warnings 2025-05-18 09:32:33 -04:00
XtraCube aa584a8f44 update role notifications to match custom role colors 2025-05-18 09:25:26 -04:00
XtraCube a8a584b61b Fix bug with CustomRoleSingleton.cs 2025-05-18 09:09:14 -04:00
XtraCube acc4b1c8dd Save scroll positions on options and roles pages 2025-05-17 23:27:25 -04:00
XtraCube f9600bc906 fix chameleon role again 2025-05-17 23:27:14 -04:00
XtraCube f3da06870f fix chameleon role errors 2025-05-17 23:20:26 -04:00
XtraCube 9180dec9e1 update example mod to fit new role update 2025-05-17 23:16:01 -04:00
XtraCube ef6dedbd55 add errors for incorrect usage of registrations 2025-05-17 22:40:02 -04:00
XtraCube 5d3c3ed319 Update example events and catch exceptions in events 2025-05-17 22:32:29 -04:00
XtraCube 5b22d667fd remove redundant ICustomRole methods 2025-05-17 22:31:34 -04:00
XtraCube fb894c9f2c add wrappers around modifiers to catch exceptions 2025-05-17 17:42:19 -04:00
XtraCube d0449f57b5 adjust naming in plugin manager 2025-05-17 17:40:05 -04:00
XtraCube 5ffd591258 add dead body to death and murder events 2025-05-17 17:34:04 -04:00
XtraCube cc0ac1b988 Merge remote-tracking branch 'origin/dev' into dev 2025-05-17 17:28:08 -04:00
XtraCube 00b96f6e61 Merge pull request #34 from All-Of-Us-Mods/2025.3.25
2025.3.25 (16.0.0) support
2025-05-17 17:28:06 -04:00
XtraCube 6cb7f9f145 add unregistration of events 2025-05-17 17:26:29 -04:00
65 changed files with 783 additions and 313 deletions

No files matched your search

+1 -2
View File
@@ -1,2 +1 @@
[*.{cs,vb}]
dotnet_diagnostic.CA1707.severity = none
[*.{cs,vb}]
+2 -2
View File
@@ -1,8 +1,8 @@
<Project>
<ItemGroup>
<PackageReference Include="Reactor" Version="2.3.1" />
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.697" Private="false" ExcludeAssets="runtime;native" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2025.3.25" PrivateAssets="all" />
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.735" Private="false" ExcludeAssets="runtime;native" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2025.4.15" 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" />
+4 -4
View File
@@ -9,10 +9,7 @@ using MiraAPI.Events.Vanilla.Player;
using MiraAPI.Example.Buttons.Freezer;
using MiraAPI.Example.Roles;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using TMPro;
using UnityEngine;
namespace MiraAPI.Example;
@@ -21,7 +18,7 @@ public static class ExampleEventHandlers
public static void Initialize()
{
// You can register event handlers with the MiraEventManager class.
MiraEventManager.RegisterEventHandler<BeforeMurderEvent>(@event =>
var handle = MiraEventManager.RegisterEventHandler<BeforeMurderEvent>(@event =>
{
Logger<ExamplePlugin>.Info($"{@event.Source.Data.PlayerName} is about to kill {@event.Target.Data.PlayerName}");
});
@@ -35,6 +32,9 @@ public static class ExampleEventHandlers
{
Logger<ExamplePlugin>.Info($"{@event.Player.Data.PlayerName} completed {@event.Task.TaskType.ToString()}");
});
// You can also unregister event handlers using the handle returned from RegisterEventHandler.
MiraEventManager.UnregisterEventHandler(handle);
}
// If you want to add extra votes to a player, do something like this.
+1 -4
View File
@@ -1,12 +1,9 @@
using Il2CppSystem.Collections.Generic;
using MiraAPI.Example.Options.Modifiers;
using MiraAPI.Example.Options.Modifiers;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.Modifiers.Types;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
using UnityEngine;
using Random = System.Random;
namespace MiraAPI.Example.Modifiers;
@@ -1,6 +1,4 @@
using MiraAPI.Modifiers.Types;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.Modifiers;
+35 -13
View File
@@ -1,4 +1,5 @@
using AmongUs.GameOptions;
using MiraAPI.Patches.Stubs;
using MiraAPI.Roles;
using Reactor.Utilities;
using TMPro;
@@ -14,6 +15,8 @@ public class ChameloenRole : CrewmateRole, ICustomRole
public Color RoleColor => Palette.AcceptedGreen;
public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate;
private bool _shouldHide;
public CustomRoleConfiguration Configuration => new(this)
{
OptionsScreenshot = ExampleAssets.Banner,
@@ -22,35 +25,54 @@ public class ChameloenRole : CrewmateRole, ICustomRole
public override void Initialize(PlayerControl player)
{
Player = player;
Logger<ExamplePlugin>.Error("Initializing ChamelonRole for player: " + player.PlayerId);
Logger<ExamplePlugin>.Info("Initializing ChamelonRole for player: " + player.PlayerId);
RoleBehaviourStubs.Initialize(this, player);
_shouldHide = true;
}
public void PlayerControlFixedUpdate(PlayerControl playerControl)
public void FixedUpdate()
{
if (playerControl.MyPhysics.Velocity.magnitude > 0)
if (!Player || !_shouldHide)
{
SpriteRenderer rend = playerControl.cosmetics.currentBodySprite.BodySprite;
TextMeshPro tmp = playerControl.cosmetics.nameText;
return;
}
if (Player.MyPhysics.Velocity.magnitude > 0)
{
var rend = Player.cosmetics.currentBodySprite.BodySprite;
var tmp = Player.cosmetics.nameText;
tmp.color = Color.Lerp(tmp.color, new Color(tmp.color.r, tmp.color.g, tmp.color.b, 1), Time.deltaTime * 4f);
rend.color = Color.Lerp(rend.color, new Color(1, 1, 1, 1), Time.deltaTime * 4f);
foreach (var cosmetic in playerControl.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>())
foreach (var cosmetic in Player.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>())
{
cosmetic.color = Color.Lerp(cosmetic.color, new Color(1, 1, 1, 1), Time.deltaTime * 4f);
}
}
else
{
SpriteRenderer rend = playerControl.cosmetics.currentBodySprite.BodySprite;
TextMeshPro tmp = playerControl.cosmetics.nameText;
tmp.color = Color.Lerp(tmp.color, new Color(tmp.color.r, tmp.color.g, tmp.color.b, playerControl.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
rend.color = Color.Lerp(rend.color, new Color(1, 1, 1, playerControl.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
SpriteRenderer rend = Player.cosmetics.currentBodySprite.BodySprite;
TextMeshPro tmp = Player.cosmetics.nameText;
tmp.color = Color.Lerp(tmp.color, new Color(tmp.color.r, tmp.color.g, tmp.color.b, Player.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
rend.color = Color.Lerp(rend.color, new Color(1, 1, 1, Player.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
foreach (var cosmetic in playerControl.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>())
foreach (var cosmetic in Player.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>())
{
cosmetic.color = Color.Lerp(cosmetic.color, new Color(1, 1, 1, playerControl.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
cosmetic.color = Color.Lerp(cosmetic.color, new Color(1, 1, 1, Player.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
}
}
}
public override void Deinitialize(PlayerControl targetPlayer)
{
Logger<ExamplePlugin>.Info("Deinitializing ChamelonRole for player: " + targetPlayer.PlayerId);
RoleBehaviourStubs.Deinitialize(this, targetPlayer);
_shouldHide = false;
foreach (var cosmetic in Player.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>(true))
{
cosmetic.color = Color.white;
}
Player.cosmetics.currentBodySprite.BodySprite.color = Color.white;
Player.cosmetics.nameText.color = Color.white;
}
}
+22
View File
@@ -0,0 +1,22 @@
using System;
namespace MiraAPI.Events;
/// <summary>
/// Event handle to use when unregistering an event.
/// </summary>
public class MiraEventHandle
{
/// <summary>
/// Gets the type of event.
/// </summary>
public Type EventType { get; }
internal MiraEventWrapper EventWrapper { get; }
internal MiraEventHandle(Type type, MiraEventWrapper eventWrapper)
{
EventType = type;
EventWrapper = eventWrapper;
}
}
+47 -6
View File
@@ -28,7 +28,14 @@ public static class MiraEventManager
foreach (var handler in handlers)
{
((Action<T>)handler.EventHandler).Invoke(eventInstance);
try
{
((Action<T>)handler.EventHandler).Invoke(eventInstance);
}
catch (Exception ex)
{
Logger<MiraApiPlugin>.Error($"Error invoking event handler for {typeof(T).Name}: {ex.ToString()}");
}
}
return true;
@@ -50,7 +57,14 @@ public static class MiraEventManager
foreach (var handler in handlers)
{
handler.EventHandler.DynamicInvoke(eventInstance);
try
{
handler.EventHandler.DynamicInvoke(eventInstance);
}
catch (Exception ex)
{
Logger<MiraApiPlugin>.Error($"Error invoking event handler for {type.Name}: {ex.ToString()}");
}
}
return true;
@@ -62,12 +76,12 @@ public static class MiraEventManager
/// <param name="type">The type of event.</param>
/// <param name="methodInfo">The MethodInfo of the event handler.</param>
/// <param name="priority">The priority of the event handler. Lower values are called first.</param>
public static void RegisterEventHandler(Type type, MethodInfo methodInfo, int priority = 0)
/// <returns>An event handle to use when unregistering the event.</returns>
public static MiraEventHandle RegisterEventHandler(Type type, MethodInfo methodInfo, int priority = 0)
{
if (!type.IsSubclassOf(typeof(MiraEvent)))
{
Logger<MiraApiPlugin>.Error($"Type must be a subclass of MiraEvent: {type.FullName}");
return;
throw new InvalidOperationException($"Type must be a subclass of MiraEvent: {type.FullName}");
}
EventWrappers.TryAdd(type, []);
@@ -84,6 +98,7 @@ public static class MiraEventManager
}
handlers.Insert(index, eventWrapper);
return new MiraEventHandle(type, eventWrapper);
}
/// <summary>
@@ -92,7 +107,8 @@ public static class MiraEventManager
/// <param name="handler">The callback method/handler for the event.</param>
/// <param name="priority">The priority of the event handler. Lower values are called first.</param>
/// <typeparam name="T">Type of event.</typeparam>
public static void RegisterEventHandler<T>(Action<T> handler, int priority = 0) where T : MiraEvent
/// <returns>An event handle to use when unregistering the event.</returns>
public static MiraEventHandle RegisterEventHandler<T>(Action<T> handler, int priority = 0) where T : MiraEvent
{
EventWrappers.TryAdd(typeof(T), []);
@@ -107,5 +123,30 @@ public static class MiraEventManager
}
handlers.Insert(index, eventWrapper);
return new MiraEventHandle(typeof(T), eventWrapper);
}
/// <summary>
/// Unregister an event using the event handle.
/// </summary>
/// <param name="eventHandle">A handle to the event.</param>
/// <returns>True if the event was unregistered successfully, false otherwise.</returns>
public static bool UnregisterEventHandler(MiraEventHandle eventHandle)
{
if (!EventWrappers.TryGetValue(eventHandle.EventType, out var handlers))
{
return false;
}
if (!handlers.Remove(eventHandle.EventWrapper))
{
return false;
}
if (handlers.Count == 0)
{
EventWrappers.Remove(eventHandle.EventType);
}
return true;
}
}
@@ -15,14 +15,21 @@ public class AfterMurderEvent : MiraEvent
/// </summary>
public PlayerControl Target { get; }
/// <summary>
/// Gets the player's body, if it exists.
/// </summary>
public DeadBody? DeadBody { get; }
/// <summary>
/// Initializes a new instance of the <see cref="AfterMurderEvent"/> class.
/// </summary>
/// <param name="source">The killer.</param>
/// <param name="target">The killed player.</param>
public AfterMurderEvent(PlayerControl source, PlayerControl target)
/// <param name="deadBody">The player's vody, if it exists.</param>
public AfterMurderEvent(PlayerControl source, PlayerControl target, DeadBody? deadBody)
{
Source = source;
Target = target;
DeadBody = deadBody;
}
}
@@ -0,0 +1,21 @@
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// Invoked before RpcEndGame is called, allowing cancellation of the game end entirely.
/// </summary>
public class BeforeGameEndEvent : MiraCancelableEvent
{
/// <summary>
/// Gets the reason for the game end.
/// </summary>
public GameOverReason Reason { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BeforeGameEndEvent"/> class.
/// </summary>
/// <param name="reason">The reason for the game end.</param>
public BeforeGameEndEvent(GameOverReason reason)
{
Reason = reason;
}
}
@@ -24,6 +24,7 @@ public class UpdateSystemEvent : MiraCancelableEvent
/// Initializes a new instance of the <see cref="UpdateSystemEvent"/> class.
/// </summary>
/// <param name="systemType">The SystemType being updated.</param>
/// <param name="player">The player that is updating the system.</param>
/// <param name="amount">Amount to update System to.</param>
public UpdateSystemEvent(SystemTypes systemType, PlayerControl player, byte amount)
{
@@ -15,14 +15,21 @@ public class PlayerDeathEvent : MiraEvent
/// </summary>
public DeathReason DeathReason { get; }
/// <summary>
/// Gets the dead body associated with the player, if any.
/// </summary>
public DeadBody? DeadBody { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PlayerDeathEvent"/> class.
/// </summary>
/// <param name="player">The player who died.</param>
/// <param name="reason">The reason the player died.</param>
public PlayerDeathEvent(PlayerControl player, DeathReason reason)
/// <param name="deadBody">The player's body, if it exists.</param>
public PlayerDeathEvent(PlayerControl player, DeathReason reason, DeadBody? deadBody)
{
Player = player;
DeathReason = reason;
DeadBody = deadBody;
}
}
+2
View File
@@ -0,0 +1,2 @@
[*.cs]
dotnet_diagnostic.CS1591.severity = none
+1 -1
View File
@@ -21,7 +21,7 @@ public static class CustomGameModeManager
}
/// <summary>
/// Current gamemode.
/// Gets the current gamemode.
/// </summary>
public static CustomGameMode? ActiveMode { get; internal set; } = new DefaultMode();
+9 -2
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using BepInEx.Configuration;
@@ -45,7 +46,9 @@ public static class ModdedOptionsManager
pluginInfo.InternalOptionGroups.Add(group);
typeof(OptionGroupSingleton<>).MakeGenericType(type)
#pragma warning disable S3011
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
#pragma warning restore S3011
.SetValue(null, group);
return true;
@@ -173,8 +176,10 @@ public static class ModdedOptionsManager
/// </summary>
/// <param name="__originalMethod">The original setter method.</param>
/// <param name="value">The new object value.</param>
// ReSharper disable once InconsistentNaming
#pragma warning disable CA1707
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
public static void PropertySetterPatch(MethodBase __originalMethod, object value)
#pragma warning restore CA1707
{
var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value;
attribute.SetValue(value);
@@ -186,8 +191,10 @@ public static class ModdedOptionsManager
/// <param name="__originalMethod">The original getter method.</param>
/// <param name="__result">The result of the property getter.</param>
/// <returns>False so the original getter gets skipped.</returns>
// ReSharper disable InconsistentNaming
#pragma warning disable CA1707
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result)
#pragma warning restore CA1707
{
var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value;
__result = attribute.GetValue();
@@ -2,7 +2,9 @@
namespace MiraAPI.GameOptions;
#pragma warning disable CA1852
internal class ModifierOptionGroup : AbstractOptionGroup
#pragma warning restore CA1852
{
public override string GroupName { get; }
+15 -9
View File
@@ -3,6 +3,7 @@ using System.Globalization;
using MiraAPI.Events;
using MiraAPI.Events.Mira;
using MiraAPI.Patches;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using UnityEngine;
using UnityEngine.Events;
@@ -36,6 +37,11 @@ public abstract class CustomActionButton
/// </summary>
public abstract LoadableAsset<Sprite> Sprite { get; }
/// <summary>
/// Gets the format string for the cooldown timer.
/// </summary>
public virtual string CooldownTimerFormatString => "0";
/// <summary>
/// Gets the button's effect duration in seconds. If the button has no effect, set to 0.
/// </summary>
@@ -180,7 +186,7 @@ public abstract class CustomActionButton
if (!moveButton) return;
if (HudManagerPatches._bottomLeft == null || HudManagerPatches._bottomRight == null)
if (HudManagerPatches.BottomLeft == null || HudManagerPatches.BottomRight == null)
{
return;
}
@@ -188,17 +194,17 @@ public abstract class CustomActionButton
switch (location)
{
case ButtonLocation.BottomLeft:
var gridArrange = HudManagerPatches._bottomLeft.GetComponent<GridArrange>();
var aspectPosition = HudManagerPatches._bottomLeft.GetComponent<AspectPosition>();
var gridArrange = HudManagerPatches.BottomLeft.GetComponent<GridArrange>();
var aspectPosition = HudManagerPatches.BottomLeft.GetComponent<AspectPosition>();
Button.transform.SetParent(HudManagerPatches._bottomLeft.transform);
Button.transform.SetParent(HudManagerPatches.BottomLeft.transform);
gridArrange.Start();
gridArrange.ArrangeChilds();
aspectPosition.AdjustPosition();
break;
case ButtonLocation.BottomRight:
Button.transform.SetParent(HudManagerPatches._bottomRight);
Button.transform.SetParent(HudManagerPatches.BottomRight);
break;
}
}
@@ -435,23 +441,23 @@ public abstract class CustomActionButton
{
if (CanUse())
{
Button.SetEnabled();
Button!.SetEnabled();
}
else
{
Button.SetDisabled();
Button!.SetDisabled();
}
if (EffectActive)
{
Button.SetFillUp(Timer, EffectDuration);
Button.cooldownTimerText.text = Mathf.CeilToInt(Timer).ToString(NumberFormatInfo.InvariantInfo);
Button.cooldownTimerText.text = Timer.ToString(CooldownTimerFormatString, NumberFormatInfo.InvariantInfo);
Button.cooldownTimerText.gameObject.SetActive(true);
}
else
{
Button.SetCoolDown(Timer, Cooldown);
Button.SetCooldownFormat(Timer, Cooldown, CooldownTimerFormatString);
}
}
+2
View File
@@ -31,7 +31,9 @@ public static class CustomButtonManager
CustomButtons.Add(button);
pluginInfo.InternalButtons.Add(button);
typeof(CustomButtonSingleton<>).MakeGenericType(buttonType)
#pragma warning disable S3011
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
#pragma warning restore S3011
.SetValue(null, button);
ButtonEventTypes.Add(buttonType, typeof(MiraButtonClickEvent<>).MakeGenericType(buttonType));
+3 -3
View File
@@ -1,4 +1,5 @@
using System.Linq;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace MiraAPI.Hud;
@@ -13,7 +14,6 @@ public static class CustomButtonSingleton<T> where T : CustomActionButton
/// <summary>
/// Gets the instance of the button.
/// </summary>
#pragma warning disable CA1000
[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a utility class to get the instance of a custom action button.")]
public static T Instance => _instance ??= CustomButtonManager.CustomButtons.OfType<T>().Single();
#pragma warning restore CA1000
}
+33 -7
View File
@@ -3,14 +3,11 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Modifiers.ModifierDisplay;
using MiraAPI.Modifiers.Types;
using MiraAPI.Utilities;
using Reactor.Utilities;
using Reactor.Utilities.Attributes;
using TMPro;
using UnityEngine;
namespace MiraAPI.Modifiers;
@@ -72,7 +69,15 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
foreach (var modifier in _toRemove.ToArray())
{
_toRemove.Remove(modifier);
modifier.OnDeactivate();
try
{
modifier.OnDeactivate();
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while deactivating modifier {modifier.ModifierName}: {e.ToString()}");
}
Modifiers.Remove(modifier);
}
@@ -82,7 +87,14 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
_toAdd.Remove(modifier);
Modifiers.Add(modifier);
modifier.Initialized = true;
modifier.OnActivate();
try
{
modifier.OnActivate();
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while activating modifier {modifier.ModifierName}: {e.ToString()}");
}
if (modifier is TimedModifier { AutoStart: true } timer)
{
@@ -102,7 +114,14 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
foreach (var modifier in ActiveModifiers)
{
modifier.FixedUpdate();
try
{
modifier.FixedUpdate();
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while (fixed) updating modifier {modifier.ModifierName}: {e.ToString()}");
}
}
if (_player.AmOwner && ModifierDisplay && HudManager.InstanceExists)
@@ -115,7 +134,14 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
foreach (var modifier in ActiveModifiers)
{
modifier.Update();
try
{
modifier.Update();
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while updating modifier {modifier.ModifierName}: {e.ToString()}");
}
}
}
@@ -17,7 +17,7 @@ namespace MiraAPI.Modifiers.ModifierDisplay;
/// The code used to display Mira modifiers.
/// </summary>
[RegisterInIl2Cpp]
public class ModifierDisplayComponent(nint ptr) : MonoBehaviour(ptr)
public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
{
/// <summary>
/// Gets the instance of the Modifier Display.
@@ -34,8 +34,8 @@ public class ModifierDisplayComponent(nint ptr) : MonoBehaviour(ptr)
private PassiveButton _nextButton = null!;
private PassiveButton _backButton = null!;
private int _currentPage = 0;
private const int _itemsPerPage = 3;
private int _currentPage;
private const int ItemsPerPage = 3;
private readonly Dictionary<BaseModifier, ModifierUiComponent> _modifiers = [];
@@ -142,10 +142,10 @@ public class ModifierDisplayComponent(nint ptr) : MonoBehaviour(ptr)
private void UpdatePage()
{
var totalPages = Mathf.CeilToInt((float)_modifiers.Count / _itemsPerPage);
var totalPages = Mathf.CeilToInt((float)_modifiers.Count / ItemsPerPage);
_currentPage = Mathf.Clamp(_currentPage, 0, Mathf.Max(0, totalPages - 1));
if (_modifiers.Count <= _itemsPerPage)
if (_modifiers.Count <= ItemsPerPage)
{
_modifiers.Do(x => x.Value.gameObject.SetActive(true));
_pagination.gameObject.SetActive(false);
@@ -162,8 +162,8 @@ public class ModifierDisplayComponent(nint ptr) : MonoBehaviour(ptr)
_backButton.gameObject.SetActive(true);
_modifiers.Do(x => x.Value.gameObject.SetActive(false));
var start = _currentPage * _itemsPerPage;
var end = Mathf.Min(start + _itemsPerPage, _modifiers.Count);
var start = _currentPage * ItemsPerPage;
var end = Mathf.Min(start + ItemsPerPage, _modifiers.Count);
var modifiers = _modifiers.ToArray();
for (var i = start; i < end; i++)
@@ -9,7 +9,7 @@ namespace MiraAPI.Modifiers.ModifierDisplay;
/// The code placed on every Modifier HUD object. Used to handle updating.
/// </summary>
[RegisterInIl2Cpp]
public class ModifierUiComponent(nint ptr) : MonoBehaviour(ptr)
public class ModifierUiComponent(nint cppPtr) : MonoBehaviour(cppPtr)
{
/// <summary>
/// Gets the modifier which this component is for.
@@ -17,12 +17,13 @@ public class ModifierUiComponent(nint ptr) : MonoBehaviour(ptr)
[HideFromIl2Cpp]
public BaseModifier? Modifier { get; internal set; }
// ReSharper disable InconsistentNaming
private SpriteRenderer modBg = null!;
private TextMeshPro desc = null!;
private SpriteRenderer icon = null!;
private TextMeshPro nameText = null!;
private RectTransform descRect = null!;
// ReSharper restore InconsistentNaming
private void Awake()
{
if (Modifier == null) return;
+2 -2
View File
@@ -30,7 +30,7 @@ public static class ModifierExtensions
_ = ModifierManager.GetModifierType(typeId) ?? throw new InvalidOperationException(
$"Modifier with ID {typeId} is not registered.");
Rpc<AddModifierRpc>.Instance.Send(target, new ModifierData(typeId, Guid.NewGuid(), args));
Rpc<AddModifierRpc>.Instance.Send(target, new ModifierData(typeId, Guid.NewGuid(), args), true);
}
/// <summary>
@@ -63,7 +63,7 @@ public static class ModifierExtensions
/// </summary>
/// <param name="target">The player to remove the modifier from.</param>
/// <param name="uniqueId">The unique ID of the modifier.</param>
[MethodRpc((uint)MiraRpc.RemoveModifier)]
[MethodRpc((uint)MiraRpc.RemoveModifier, SendImmediately = true)]
public static void RpcRemoveModifier(this PlayerControl target, Guid uniqueId)
{
target.RemoveModifier(uniqueId);
+6 -4
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using MiraAPI.Utilities;
@@ -11,7 +12,7 @@ namespace MiraAPI.Modifiers;
/// </summary>
public static class ModifierFactory
{
private static readonly Dictionary<(Type, Type[]), Func<object[], BaseModifier>> _constructorCache = [];
private static readonly Dictionary<(Type, Type[]), Func<object[], BaseModifier>> ConstructorCache = [];
private static Func<object[], BaseModifier> CreateConstructor(Type type, params object[] args)
{
@@ -45,13 +46,13 @@ public static class ModifierFactory
/// <returns>An instance of the modifier.</returns>
public static BaseModifier CreateInstance(Type type, params object[] args)
{
var argTypes = args.Select(arg => arg?.GetType() ?? typeof(object)).ToArray();
var argTypes = args.Select(arg => arg.GetType()).ToArray();
var key = (type, argTypes);
if (!_constructorCache.TryGetValue(key, out var constructor))
if (!ConstructorCache.TryGetValue(key, out var constructor))
{
constructor = CreateConstructor(type, args);
_constructorCache[key] = constructor;
ConstructorCache[key] = constructor;
}
return constructor(args);
@@ -95,6 +96,7 @@ public static class ModifierFactory<T> where T : BaseModifier
/// </summary>
/// <param name="args">Parameters for the constructor.</param>
/// <returns>An instance of the modifier.</returns>
[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a factory class.")]
public static T CreateInstance(params object[] args)
{
return Constructor(args);
-1
View File
@@ -5,7 +5,6 @@ using System.Runtime.Serialization;
using MiraAPI.Modifiers.Types;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using Random = System.Random;
+1 -1
View File
@@ -264,7 +264,7 @@ public static class CustomMurderRpc
deadBody.enabled = true;
}
var afterMurderEvent = new AfterMurderEvent(source, target);
var afterMurderEvent = new AfterMurderEvent(source, target, deadBody);
MiraEventManager.InvokeEvent(afterMurderEvent);
if (!isParticipant)
+1 -1
View File
@@ -46,7 +46,7 @@ public enum MiraRpc : uint
RemoveVote,
/// <summary>
/// Custom RPC to replace Among Us CastVote
/// Custom RPC to replace Among Us CastVote.
/// </summary>
CastVote,
}
+5
View File
@@ -0,0 +1,5 @@
[*.{cs,vb}]
dotnet_diagnostic.CA1707.severity = none
dotnet_diagnostic.CS1591.severity = none
dotnet_diagnostic.SA1611.severity = none
dotnet_diagnostic.SA1615.severity = none
+28 -13
View File
@@ -5,29 +5,44 @@ using MiraAPI.GameEnd;
namespace MiraAPI.Patches.Events;
[HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.SetEverythingUp))]
internal static class EndGamePatches
{
public static bool Prefix(EndGameManager __instance)
[HarmonyPatch(typeof(GameManager), nameof(GameManager.RpcEndGame))]
public static class GameManagerPatches
{
if (CustomGameOver.Instance is not { } gameOver)
public static bool Prefix(GameOverReason endReason)
{
return true;
}
var @event = new BeforeGameEndEvent(endReason);
MiraEventManager.InvokeEvent(@event);
var result = gameOver.BeforeEndGameSetup(__instance);
return result;
return !@event.IsCancelled;
}
}
public static void Postfix(EndGameManager __instance)
[HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.SetEverythingUp))]
public static class EndGameManagerPatches
{
if (CustomGameOver.Instance is { } gameOver)
public static bool Prefix(EndGameManager __instance)
{
gameOver.AfterEndGameSetup(__instance);
CustomGameOver.Instance = null;
if (CustomGameOver.Instance is not { } gameOver)
{
return true;
}
var result = gameOver.BeforeEndGameSetup(__instance);
return result;
}
var @event = new GameEndEvent(__instance);
MiraEventManager.InvokeEvent(@event);
public static void Postfix(EndGameManager __instance)
{
if (CustomGameOver.Instance is { } gameOver)
{
gameOver.AfterEndGameSetup(__instance);
CustomGameOver.Instance = null;
}
var @event = new GameEndEvent(__instance);
MiraEventManager.InvokeEvent(@event);
}
}
}
+10 -5
View File
@@ -1,6 +1,7 @@
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Utilities;
namespace MiraAPI.Patches.Events;
@@ -13,13 +14,17 @@ public static class KillAnimationPatches
/// <summary>
/// Used to trigger the <see cref="AfterMurderEvent"/>.
/// </summary>
/// <param name="source">The killer.</param>
/// <param name="target">The target.</param>
[HarmonyPostfix]
[HarmonyPatch(typeof(KillAnimation), nameof(KillAnimation.CoPerformKill))]
public static void KillAnimPostfix(PlayerControl source, PlayerControl target)
[HarmonyPatch(typeof(KillAnimation._CoPerformKill_d__2), nameof(KillAnimation._CoPerformKill_d__2.MoveNext))]
public static void KillAnimPostfix(KillAnimation._CoPerformKill_d__2 __instance)
{
var afterMurderEvent = new AfterMurderEvent(source, target);
// Checking if state is -1 to wait for the end of coroutine.
if (__instance.__1__state != -1)
{
return;
}
var afterMurderEvent = new AfterMurderEvent(__instance.source, __instance.target, Helpers.GetBodyById(__instance.target.PlayerId));
MiraEventManager.InvokeEvent(afterMurderEvent);
}
}
+36 -5
View File
@@ -1,21 +1,52 @@
using HarmonyLib;
using AmongUs.InnerNet.GameDataMessages;
using HarmonyLib;
using InnerNet;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Meeting;
using MiraAPI.Utilities;
using Reactor.Utilities;
namespace MiraAPI.Patches.Events;
[HarmonyPatch]
public static class ReportBodyEventPatch
{
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdReportDeadBody))]
public static bool PlayerControlCmdReportDeadBodyPrefix(PlayerControl __instance, NetworkedPlayerInfo? target)
private static bool ReportBodyEventHook(PlayerControl source, NetworkedPlayerInfo? target)
{
var body = target != null ? Helpers.GetBodyById(target.PlayerId) : null;
var @event = new ReportBodyEvent(__instance, target, body);
var @event = new ReportBodyEvent(source, target, body);
MiraEventManager.InvokeEvent(@event);
return !@event.IsCancelled;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.ReportDeadBody))]
public static bool PlayerControlCmdReportDeadBodyPrefix(PlayerControl __instance, NetworkedPlayerInfo? target)
{
return ReportBodyEventHook(__instance, target);
}
[HarmonyPrefix]
[HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.LateBroadcastReliableMessage))]
public static bool LateBroadcastReliableMessagePrefix(IGameDataMessage rpcMessage)
{
var reportBodyMessage = rpcMessage.TryCast<RpcReportDeadBodyMessage>();
if (reportBodyMessage == null)
{
return true;
}
var source = AmongUsClient.Instance.FindObjectByNetId<PlayerControl>(reportBodyMessage.rpcObjectNetId);
if (source == null)
{
Logger<MiraApiPlugin>.Error("Failed to find source player for RpcReportDeadBodyMessage with NetId: " + reportBodyMessage.rpcObjectNetId);
return true;
}
var target = GameData.Instance.GetPlayerById(reportBodyMessage.targetPlayerId);
return ReportBodyEventHook(source, target);
}
}
+4 -4
View File
@@ -23,10 +23,10 @@ public static class VentEventPatches
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.RpcEnterVent))]
public static bool PlayerPhysicsRpcEnterVentPrefix(PlayerPhysics __instance, int id)
public static bool PlayerPhysicsRpcEnterVentPrefix(PlayerPhysics __instance, int ventId)
{
var pc = __instance.myPlayer;
var vent = ShipStatus.Instance.AllVents.FirstOrDefault(v => v.Id == id);
var vent = ShipStatus.Instance.AllVents.FirstOrDefault(v => v.Id == ventId);
var @event = new EnterVentEvent(pc, vent);
MiraEventManager.InvokeEvent(@event);
@@ -40,10 +40,10 @@ public static class VentEventPatches
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.RpcExitVent))]
public static bool PlayerPhysicsRpcExitVentPrefix(PlayerPhysics __instance, int id)
public static bool PlayerPhysicsRpcExitVentPrefix(PlayerPhysics __instance, int ventId)
{
var pc = __instance.myPlayer;
var vent = ShipStatus.Instance.AllVents.First(v => v.Id == id);
var vent = ShipStatus.Instance.AllVents.First(v => v.Id == ventId);
var @event = new ExitVentEvent(pc, vent);
MiraEventManager.InvokeEvent(@event);
+2
View File
@@ -0,0 +1,2 @@
[*.cs]
dotnet_diagnostic.S125.severity = none
+1 -1
View File
@@ -4,7 +4,7 @@ using MiraAPI.Hud;
namespace MiraAPI.Patches.Hud;
/// <summary>
/// Reset button patches
/// Reset button patches.
/// </summary>
[HarmonyPatch]
public static class ButtonResetPatches
+15 -15
View File
@@ -13,9 +13,9 @@ namespace MiraAPI.Patches;
public static class HudManagerPatches
{
// Custom buttons parent.
internal static GameObject? _bottomLeft;
internal static Transform? _bottomRight;
internal static Transform? _buttons;
public static GameObject? BottomLeft { get; private set; }
public static Transform? BottomRight { get; private set; }
public static Transform? Buttons { get; private set; }
/*
/// <summary>
@@ -36,30 +36,30 @@ public static class HudManagerPatches
[HarmonyPatch(nameof(HudManager.Start))]
public static void StartPostfix(HudManager __instance)
{
if (_buttons == null)
if (Buttons == null)
{
_buttons = __instance.transform.Find("Buttons");
Buttons = __instance.transform.Find("Buttons");
}
if (_bottomRight == null)
if (BottomRight == null)
{
_bottomRight = _buttons.Find("BottomRight");
BottomRight = Buttons.Find("BottomRight");
}
if (_bottomLeft == null)
if (BottomLeft == null)
{
_bottomLeft = Object.Instantiate(_bottomRight.gameObject, _buttons);
BottomLeft = Object.Instantiate(BottomRight.gameObject, Buttons);
}
foreach (var t in _bottomLeft.GetComponentsInChildren<ActionButton>(true))
foreach (var t in BottomLeft.GetComponentsInChildren<ActionButton>(true))
{
t.gameObject.Destroy();
}
var gridArrange = _bottomLeft.GetComponent<GridArrange>();
var aspectPosition = _bottomLeft.GetComponent<AspectPosition>();
var gridArrange = BottomLeft.GetComponent<GridArrange>();
var aspectPosition = BottomLeft.GetComponent<AspectPosition>();
_bottomLeft.name = "BottomLeft";
BottomLeft.name = "BottomLeft";
gridArrange.Alignment = GridArrange.StartAlign.Right;
aspectPosition.Alignment = AspectPosition.EdgeAlignments.LeftBottom;
@@ -67,8 +67,8 @@ public static class HudManagerPatches
{
var location = button.Location switch
{
ButtonLocation.BottomLeft => _bottomLeft.transform,
ButtonLocation.BottomRight => _bottomRight,
ButtonLocation.BottomLeft => BottomLeft.transform,
ButtonLocation.BottomRight => BottomRight,
_ => null,
};
+1 -1
View File
@@ -108,7 +108,7 @@ public static class NameplatesTabPatches
var x = (NamePlateViewData viewdata) =>
{
colorChip.transform.GetChild(1).GetComponent<SpriteRenderer>().sprite = viewdata?.Image;
//(colorChip as NameplateChip).image.sprite = viewdata?.Image;
// (colorChip as NameplateChip).image.sprite = viewdata?.Image;
};
__instance.StartCoroutine(AddressableAssetExtensions.CoLoadAssetAsync<NamePlateViewData>(__instance, namePlate.GetAssetReference(), x));
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
+1 -1
View File
@@ -3,7 +3,7 @@
namespace MiraAPI.Patches;
/// <summary>
/// Used to fix the Minigame crash errors (credits Submerged/PH Gaming for the fix)
/// Used to fix the Minigame crash errors (credits Submerged/PH Gaming for the fix).
/// </summary>
[HarmonyPatch]
public static class MinigameBeginErrorPatch
@@ -32,11 +32,30 @@ internal static class GameSettingMenuPatches
private static PassiveButton? _modifiersButton;
private static PassiveButton? _smallRolesButton;
private static Dictionary<int, Vector3> OptionScrolls = [];
private static Dictionary<int, Vector3> OptionsPositions { get; } = [];
private static Dictionary<int, Vector3> ModifiersPositions { get; } = [];
private static void SaveScrollPositions(GameSettingMenu gameSettingMenu)
{
if (_modifiersTab)
{
ModifiersPositions[SelectedModIdx] = _modifiersTab!.scrollBar.Inner.localPosition;
}
RoleSettingMenuPatches.RolePositions[SelectedModIdx] = gameSettingMenu.RoleSettingsTab.scrollBar.Inner.localPosition;
OptionsPositions[SelectedModIdx] = gameSettingMenu.GameSettingsTab.scrollBar.Inner.localPosition;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(GameSettingMenu.ChangeTab))]
public static void ChangeTabPrefix(GameSettingMenu __instance)
{
SaveScrollPositions(__instance);
}
[HarmonyPostfix]
[HarmonyPatch(nameof(GameSettingMenu.ChangeTab))]
// ReSharper disable once InconsistentNaming
public static void ChangeTabPostfix(GameSettingMenu __instance, int tabNum, bool previewOnly)
{
if ((previewOnly && Controller.currentTouchType == Controller.TouchType.Joystick) || !previewOnly)
@@ -90,7 +109,6 @@ internal static class GameSettingMenuPatches
/// <param name="__instance">The GameSettingMenu instance.</param>
[HarmonyPrefix]
[HarmonyPatch(nameof(GameSettingMenu.Start))]
// ReSharper disable once InconsistentNaming
public static void StartPrefix(GameSettingMenu __instance)
{
_roleBtnOgPos = __instance.RoleSettingsButton.transform.localPosition;
@@ -123,11 +141,7 @@ internal static class GameSettingMenuPatches
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
RoleSettingMenuPatches.Scrolls[SelectedModIdx] =
__instance.RoleSettingsTab.scrollBar.Inner.localPosition;
OptionScrolls[SelectedModIdx] =
__instance.GameSettingsTab.scrollBar.Inner.localPosition;
SaveScrollPositions(__instance);
SelectedModIdx += 1;
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins.Length)
{
@@ -146,11 +160,7 @@ internal static class GameSettingMenuPatches
backButton.gameObject.GetComponent<PassiveButton>().OnClick.AddListener(
(UnityAction)(() =>
{
RoleSettingMenuPatches.Scrolls[SelectedModIdx] =
__instance.RoleSettingsTab.scrollBar.Inner.localPosition;
OptionScrolls[SelectedModIdx] =
__instance.GameSettingsTab.scrollBar.Inner.localPosition;
SaveScrollPositions(__instance);
SelectedModIdx -= 1;
if (SelectedModIdx < 0)
{
@@ -436,29 +446,7 @@ internal static class GameSettingMenuPatches
if (_modifiersTab?.Children?.Count > 0)
{
CleanupSettings(_modifiersTab);
}
void CleanupSettings(GameOptionsMenu gameOptMenu)
{
ClearOptions(gameOptMenu.Children);
gameOptMenu.Children = null;
gameOptMenu.settingsContainer
.GetComponentsInChildren<CategoryHeaderMasked>()
.ToList()
.ForEach(header => header.gameObject.DestroyImmediate());
gameOptMenu.Initialize();
if (OptionScrolls.TryGetValue(SelectedModIdx, out var pos))
{
gameOptMenu.scrollBar.Inner.localPosition = pos;
gameOptMenu.scrollBar.UpdateScrollBars();
}
else
{
gameOptMenu.scrollBar.ScrollToTop();
}
CleanupSettings(_modifiersTab, true);
}
void CleanupRoleSettings(RolesSettingsMenu rolesMenu)
@@ -485,17 +473,38 @@ internal static class GameSettingMenuPatches
rolesMenu.AdvancedRolesSettings.gameObject.SetActive(false);
rolesMenu.RoleChancesSettings.gameObject.SetActive(true);
rolesMenu.SetQuotaTab();
if (SelectedModIdx == 0)
if (RoleSettingMenuPatches.RolePositions.TryGetValue(SelectedModIdx, out var pos))
{
if (RoleSettingMenuPatches.Scrolls.TryGetValue(0, out var pos))
{
rolesMenu.scrollBar.Inner.localPosition = pos;
rolesMenu.scrollBar.UpdateScrollBars();
}
else
{
rolesMenu.scrollBar.ScrollToTop();
}
rolesMenu.scrollBar.Inner.localPosition = pos;
rolesMenu.scrollBar.UpdateScrollBars();
}
else
{
rolesMenu.scrollBar.ScrollToTop();
}
}
void CleanupSettings(GameOptionsMenu gameOptMenu, bool modifiers = false)
{
ClearOptions(gameOptMenu.Children);
gameOptMenu.Children = null;
gameOptMenu.settingsContainer
.GetComponentsInChildren<CategoryHeaderMasked>()
.ToList()
.ForEach(header => header.gameObject.DestroyImmediate());
gameOptMenu.Initialize();
var positions = modifiers ? ModifiersPositions : OptionsPositions;
if (positions.TryGetValue(SelectedModIdx, out var pos))
{
gameOptMenu.scrollBar.Inner.localPosition = pos;
gameOptMenu.scrollBar.UpdateScrollBars();
}
else
{
gameOptMenu.scrollBar.ScrollToTop();
}
}
}
@@ -113,14 +113,16 @@ public static class LobbyViewPanePatches
menu.scrollBar.ScrollToTop();
}
[HarmonyPrefix]
[HarmonyPatch(nameof(LobbyViewSettingsPane.SetTab))]
public static bool SetTabPatch(LobbyViewSettingsPane __instance)
[HarmonyPostfix]
[HarmonyPatch(nameof(LobbyViewSettingsPane.ChangeTab))]
[HarmonyPatch(nameof(LobbyViewSettingsPane.RefreshTab))]
// CHANGED BECAUSE OF INLINING
public static void SetTabPatch(LobbyViewSettingsPane __instance)
{
if (__instance.currentTab != ModifiersTabName || SelectedMod == null)
{
ModifiersTabButton?.SelectButton(false);
return true;
return;
}
__instance.taskTabButton.SelectButton(false);
@@ -130,7 +132,6 @@ public static class LobbyViewPanePatches
var filteredGroups = SelectedMod.InternalOptionGroups
.Where(x => x.GroupVisible() && (x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true));
DrawOptions(__instance, filteredGroups);
return false;
}
[HarmonyPrefix]
@@ -142,6 +143,11 @@ public static class LobbyViewPanePatches
return true;
}
if (__instance.currentTab == ModifiersTabName)
{
return false;
}
var filteredGroups = SelectedMod.InternalOptionGroups
.Where(x => x.OptionableType == null && x.GroupVisible.Invoke());
DrawOptions(__instance, filteredGroups);
@@ -157,6 +163,11 @@ public static class LobbyViewPanePatches
return true;
}
if (__instance.currentTab == ModifiersTabName)
{
return false;
}
DrawRolesTab(__instance);
return false;
}
@@ -2,6 +2,8 @@
using HarmonyLib;
using Il2CppSystem;
using MiraAPI.Roles;
using UnityEngine;
using Object = Il2CppSystem.Object;
namespace MiraAPI.Patches.Options;
@@ -23,15 +25,13 @@ public static class NotificationPopperPatch
return true;
}
var text = teamType == RoleTeamTypes.Crewmate
? Palette.CrewmateSettingChangeText.ToTextColor()
: Palette.ImpostorRed.ToTextColor();
var textColor = customRole.OptionsMenuColor.ToTextColor();
var item = TranslationController.Instance.GetString(
StringNames.LobbyChangeSettingNotificationRole,
string.Concat(
"<font=\"Barlow-Black SDF\" material=\"Barlow-Black Outline\">",
text,
textColor,
TranslationController.Instance.GetString(key, Array.Empty<Object>()),
"</color></font>"
),
+100 -19
View File
@@ -23,8 +23,32 @@ public static class OptionsPatches
}
[HarmonyPrefix]
[HarmonyPatch(typeof(ToggleOption), nameof(ToggleOption.UpdateValue))]
[HarmonyPatch(typeof(ToggleOption), nameof(ToggleOption.Toggle))]
// UpdateValue was inlined.
public static bool ToggleUpdate(ToggleOption __instance)
{
if (!__instance.IsCustom())
{
return true;
}
__instance.CheckMark.enabled = !__instance.CheckMark.enabled;
__instance.OnValueChanged.Invoke(__instance);
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(StringOption), nameof(StringOption.UpdateValue))]
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.UpdateValue))]
public static bool UpdateValuePrefix(OptionBehaviour __instance)
{
return !__instance.IsCustom();
}
[HarmonyPrefix]
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.AdjustButtonsActiveState))]
[HarmonyPatch(typeof(StringOption), nameof(StringOption.AdjustButtonsActiveState))]
public static bool AdjustButtonsPrefix(OptionBehaviour __instance)
{
return !__instance.IsCustom();
}
@@ -38,17 +62,84 @@ public static class OptionsPatches
return true;
}
__instance.TitleText.text =
TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>());
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>());
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.UpdateValue))]
public static bool NumberUpdate(NumberOption __instance)
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Increase))]
public static bool NumberIncrease(NumberOption __instance)
{
return !__instance.IsCustom();
if (!__instance.IsCustom())
{
return true;
}
__instance.Value += __instance.Increment;
if (__instance.Value > __instance.ValidRange.max)
{
__instance.Value = __instance.ValidRange.min;
}
__instance.OnValueChanged.Invoke(__instance);
__instance.AdjustButtonsActiveState();
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Decrease))]
public static bool NumberDecrease(NumberOption __instance)
{
if (!__instance.IsCustom())
{
return true;
}
__instance.Value -= __instance.Increment;
if (__instance.Value < __instance.ValidRange.min)
{
__instance.Value = __instance.ValidRange.max;
}
__instance.OnValueChanged.Invoke(__instance);
__instance.AdjustButtonsActiveState();
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(StringOption), nameof(StringOption.Increase))]
public static bool StringOptionIncrease(StringOption __instance)
{
if (!__instance.IsCustom())
{
return true;
}
__instance.Value += 1;
if (__instance.Value >= __instance.Values.Length)
{
__instance.Value = 0;
}
__instance.OnValueChanged.Invoke(__instance);
__instance.AdjustButtonsActiveState();
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(StringOption), nameof(StringOption.Decrease))]
public static bool StringOptionDecrease(StringOption __instance)
{
if (!__instance.IsCustom())
{
return true;
}
__instance.Value -= 1;
if (__instance.Value < 0)
{
__instance.Value = __instance.Values.Length - 1;
}
__instance.OnValueChanged.Invoke(__instance);
__instance.AdjustButtonsActiveState();
return false;
}
[HarmonyPrefix]
@@ -60,19 +151,9 @@ public static class OptionsPatches
return true;
}
__instance.TitleText.text =
TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>());
__instance.ValueText.text = TranslationController.Instance.GetString(
__instance.Values[__instance.Value],
Array.Empty<Object>());
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>());
__instance.ValueText.text = TranslationController.Instance.GetString(__instance.Values[__instance.Value], Array.Empty<Object>());
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(StringOption), nameof(StringOption.UpdateValue))]
public static bool StringUpdate(StringOption __instance)
{
return !__instance.IsCustom();
}
}
@@ -22,13 +22,21 @@ namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(RolesSettingsMenu))]
public static class RoleSettingMenuPatches
{
public static Dictionary<int, Vector3> Scrolls { get; } = [];
private static Dictionary<RoleOptionsGroup, bool> RoleGroupHidden { get; } = [];
private static List<GameObject> Headers { get; } = [];
private static List<RoleOptionSetting> RoleOptionSettings { get; } = [];
public static Dictionary<int, Vector3> RolePositions { get; set; } = [];
private static Dictionary<RoleOptionsGroup, bool> RoleGroupHidden { get; set; } = [];
private static List<GameObject> Headers { get; set; } = [];
private static List<RoleOptionSetting> RoleOptionSettings { get; set; } = [];
private static float ScrollerNum { get; set; } = 0.522f;
[HarmonyPostfix]
[HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))]
public static void SetQuotaTabPostfix(RolesSettingsMenu __instance)
{
__instance.scrollBar.SetScrollBounds(__instance);
}
[HarmonyPrefix]
[HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))]
public static bool SetQuotaTabPatch(RolesSettingsMenu __instance)
@@ -226,14 +234,20 @@ public static class RoleSettingMenuPatches
ScrollerNum -= 0.4f;
}
}
__instance.scrollBar.SetScrollBounds();
__instance.scrollBar.SetScrollBounds(__instance);
return false;
}
private static void SetScrollBounds(this Scroller scroller)
private static void SetScrollBounds(this Scroller scroller, RolesSettingsMenu roleMenu)
{
scroller.CalculateAndSetYBounds(1 + 1.5f * Headers.Count + RoleOptionSettings.Count, 1f, 6f, 0.43f);
if (Scrolls.TryGetValue(GameSettingMenuPatches.SelectedModIdx, out var scroll))
if (GameSettingMenuPatches.SelectedModIdx == 0)
{
scroller.CalculateAndSetYBounds(roleMenu.roleChances.Count + 3, 1f, 6f, 0.43f);
return;
}
scroller.CalculateAndSetYBounds(1.5f * Headers.Count + RoleOptionSettings.Count, 1f, 6f, 0.43f);
if (RolePositions.TryGetValue(GameSettingMenuPatches.SelectedModIdx, out var scroll))
{
scroller.Inner.localPosition = scroll;
scroller.UpdateScrollBars();
@@ -255,12 +269,12 @@ public static class RoleSettingMenuPatches
[HarmonyPatch(nameof(RolesSettingsMenu.OpenChancesTab))]
public static void OpenChancesTabPostfix(RolesSettingsMenu __instance)
{
if (GameSettingMenuPatches.SelectedModIdx == 0)
if (!__instance.scrollBar)
{
return;
}
__instance.scrollBar.SetScrollBounds();
__instance.scrollBar.SetScrollBounds(__instance);
}
private static void ValueChanged(OptionBehaviour obj)
@@ -353,7 +367,6 @@ public static class RoleSettingMenuPatches
newOpt.Initialize();
}
Scrolls[GameSettingMenuPatches.SelectedModIdx] = __instance.scrollBar.Inner.localPosition;
__instance.scrollBar.CalculateAndSetYBounds(__instance.advancedSettingChildren.Count + 3, 1f, 6f, 0.45f);
__instance.scrollBar.ScrollToTop();
}
@@ -366,7 +379,7 @@ public static class RoleSettingMenuPatches
return;
}
Scrolls[GameSettingMenuPatches.SelectedModIdx] = __instance.scrollBar.Inner.localPosition;
RolePositions[GameSettingMenuPatches.SelectedModIdx] = __instance.scrollBar.Inner.localPosition;
__instance.roleDescriptionText.text = customRole.RoleLongDescription;
__instance.roleTitleText.text = TranslationController.Instance.GetString(
+3 -7
View File
@@ -5,7 +5,7 @@ using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Events.Vanilla.Player;
using MiraAPI.Hud;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using MiraAPI.Voting;
using Reactor.Utilities.Extensions;
@@ -41,7 +41,7 @@ internal static class PlayerControlPatches
// ReSharper disable once InconsistentNaming
public static void PlayerControlDiePostfix(PlayerControl __instance, DeathReason reason)
{
var deathEvent = new PlayerDeathEvent(__instance, reason);
var deathEvent = new PlayerDeathEvent(__instance, reason, Helpers.GetBodyById(__instance.PlayerId));
MiraEventManager.InvokeEvent(deathEvent);
var modifiersComponent = __instance.GetComponent<ModifierComponent>();
@@ -68,6 +68,7 @@ internal static class PlayerControlPatches
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerControl.RpcMurderPlayer))]
// ReSharper disable once InconsistentNaming
// Note: This method is partially inlined in 2025.5.20 but we already use custom murder RPCs.
public static void PlayerControlMurderPrefix(PlayerControl __instance, PlayerControl target, ref bool didSucceed)
{
var beforeMurderEvent = new BeforeMurderEvent(__instance, target);
@@ -81,11 +82,6 @@ internal static class PlayerControlPatches
// ReSharper disable once InconsistentNaming
public static void PlayerControlFixedUpdatePostfix(PlayerControl __instance)
{
if (__instance.Data?.Role is ICustomRole customRole)
{
customRole.PlayerControlFixedUpdate(__instance);
}
if (!__instance.AmOwner)
{
return;
+1 -3
View File
@@ -1,6 +1,4 @@
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Events.Vanilla.Meeting;
+1 -1
View File
@@ -10,7 +10,7 @@ public static class GameStartupPatch
private static bool _runOnce;
/// <summary>
/// This is used for registering roles when the game opens, might be a janky solution, but it works
/// This is used for registering roles when the game opens, might be a janky solution, but it works.
/// </summary>
[HarmonyPostfix]
[HarmonyPatch(nameof(MainMenuManager.Start))]
+3 -2
View File
@@ -7,7 +7,8 @@ namespace MiraAPI.Patches.Roles;
/// <summary>
/// Patches the haunt menu to show the actual role name rather then team.
/// </summary>
[HarmonyPatch(typeof(HauntMenuMinigame), nameof(HauntMenuMinigame.SetFilterText))]
[HarmonyPatch(typeof(HauntMenuMinigame), nameof(HauntMenuMinigame.SetHauntTarget))]
// SetFilterText was INLINED
public static class HauntMenuMinigamePatch
{
public static void Postfix(HauntMenuMinigame __instance)
@@ -24,4 +25,4 @@ public static class HauntMenuMinigamePatch
__instance.FilterText.text = role.NiceName;
__instance.FilterText.color = color;
}
}
}
@@ -62,8 +62,6 @@ public static class HudManagerPatches
if (role is ICustomRole { Configuration.RoleHintType: RoleHintType.RoleTab } customRole)
{
customRole.HudUpdate(__instance);
if (_roleTab == null)
{
_roleTab = CustomRoleManager.CreateRoleTab(customRole);
@@ -14,6 +14,8 @@ public static class RoleManagerPatches
{
[HarmonyPrefix]
[HarmonyPatch(nameof(RoleManager.SetRole))]
// NOTE: As of 2025.5.20 update, this is INLINED in ONE place: GameManager::ReviveEveryoneFreeplay(void).
// Should not affect normal gameplay, but may cause issues in Freeplay.
public static bool SetRolePatch(RoleManager __instance, PlayerControl targetPlayer, RoleTypes roleType)
{
if (!targetPlayer)
@@ -1,5 +1,6 @@
using AmongUs.GameOptions;
using HarmonyLib;
using Il2CppSystem.Collections.Generic;
using MiraAPI.Roles;
using Reactor.Utilities;
@@ -12,7 +13,24 @@ namespace MiraAPI.Patches.Roles;
public static class RoleOptionsCollectionPatch
{
/// <summary>
/// Set the role chance for custom Launchpad roles based on config
/// This patch fixes GetNumPerGame being inlined (2025.5.20) in the original code.
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(nameof(RoleOptionsCollectionV09.AnyRolesEnabled))]
public static bool AnyRolesEnabledPrefix(RoleOptionsCollectionV09 __instance, ref bool __result)
{
foreach (KeyValuePair<RoleTypes, RoleDataV09> keyValuePair in __instance.roles)
{
if (__instance.GetNumPerGame(keyValuePair.Key) > 0)
{
return true;
}
}
return false;
}
/// <summary>
/// Set the role chance for custom Launchpad roles based on config.
/// </summary>
/// <returns>Return false to skip original method, true to not.</returns>
[HarmonyPrefix]
@@ -42,7 +60,7 @@ public static class RoleOptionsCollectionPatch
}
/// <summary>
/// Set the amount for custom Launchpad roles based on config
/// Set the amount for custom Launchpad roles based on config.
/// </summary>
/// <returns>Return false to skip original method, true to not.</returns>
[HarmonyPrefix]
+1 -1
View File
@@ -4,7 +4,7 @@ using MiraAPI.Roles;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(SabotageButton))]
public class SabotageButtonPatch
public static class SabotageButtonPatch
{
/// <summary>
/// Patches the Sabotage button to check if the player's custom role can use sabotage.
+1 -1
View File
@@ -8,7 +8,6 @@ using MiraAPI.Events.Vanilla.Meeting.Voting;
using MiraAPI.Modifiers;
using MiraAPI.Utilities;
using MiraAPI.Voting;
using Reactor.Utilities;
namespace MiraAPI.Patches.Voting;
@@ -206,6 +205,7 @@ internal static class MeetingHudPatches
// TODO: figure out a way to do host-authorization since right now any player can send RpcCastVote
[HarmonyPrefix]
[HarmonyPatch(nameof(MeetingHud.CmdCastVote))]
// Although this method is inlined in MeetingHud.Confirm, the next patch fixes that.
public static bool CmdCastVoteOverridePatch(MeetingHud __instance, byte playerId, byte suspectIdx)
{
VotingUtils.RpcCastVote(PlayerControl.LocalPlayer, playerId, suspectIdx);
+11
View File
@@ -2,8 +2,19 @@
namespace MiraAPI.PluginLoading;
/// <summary>
/// The interface that all Mira plugins must implement.
/// </summary>
public interface IMiraPlugin
{
/// <summary>
/// Gets the name to display on the options menu.
/// </summary>
string OptionsTitleText { get; }
/// <summary>
/// Gets the BepInEx configuration file for the plugin.
/// </summary>
/// <returns>The BepInEx configuration file for the plugin.</returns>
public ConfigFile GetConfigFile();
}
+40 -16
View File
@@ -55,21 +55,29 @@ public sealed class MiraPluginManager
continue;
}
foreach (var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public))
foreach (var method in type.GetMethods())
{
var eventAttribute = method.GetCustomAttribute<RegisterEventAttribute>();
if (eventAttribute != null)
if (eventAttribute == null)
{
var parameters = method.GetParameters();
if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent)))
{
Logger<MiraApiPlugin>.Error($"Invalid event registration method {method.Name} in {type.Name}");
continue;
}
var paramType = parameters[0].ParameterType;
MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority);
continue;
}
if (!method.IsStatic)
{
Logger<MiraApiPlugin>.Error($"Event method {method.Name} in {type.Name} must be static.");
continue;
}
var parameters = method.GetParameters();
if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent)))
{
Logger<MiraApiPlugin>.Error($"Invalid event registration method {method.Name} in {type.Name}");
continue;
}
var paramType = parameters[0].ParameterType;
MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority);
}
if (RegisterModifier(type, info))
@@ -82,13 +90,13 @@ public sealed class MiraPluginManager
continue;
}
if (RegisterRoleAttribute(type, info, out var role))
if (RegisterRole(type, info, out var role))
{
roles.Add(role);
continue;
}
if (RegisterButtonAttribute(type, info))
if (RegisterButton(type, info))
{
continue;
}
@@ -164,6 +172,12 @@ public sealed class MiraPluginManager
foreach (var property in type.GetProperties())
{
if (property.GetMethod?.IsStatic == true)
{
Logger<MiraApiPlugin>.Error($"Option property {property.Name} in {type.Name} must not be static.");
continue;
}
if (property.PropertyType.IsAssignableTo(typeof(IModdedOption)))
{
ModdedOptionsManager.RegisterPropertyOption(type, property, pluginInfo);
@@ -179,16 +193,21 @@ public sealed class MiraPluginManager
ModdedOptionsManager.RegisterAttributeOption(type, attribute, property, pluginInfo);
}
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(IModdedOption))))
{
Logger<MiraApiPlugin>.Error($"{field.Name} is a field, not a property. Use properties for options.");
}
return true;
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register options for {type.Name}: {e}");
Logger<MiraApiPlugin>.Error($"Failed to register options for {type.Name}: {e.ToString()}");
}
return false;
}
private static bool RegisterRoleAttribute(Type type, MiraPluginInfo pluginInfo, [NotNullWhen(true)] out Type? role)
private static bool RegisterRole(Type type, MiraPluginInfo pluginInfo, [NotNullWhen(true)] out Type? role)
{
role = null;
try
@@ -244,6 +263,11 @@ public sealed class MiraPluginManager
PaletteManager.CustomColors.Add(color);
}
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(CustomColor))))
{
Logger<MiraApiPlugin>.Error($"{field.Name} is a field, not a property. Use properties for colors.");
}
}
catch (Exception e)
{
@@ -264,7 +288,7 @@ public sealed class MiraPluginManager
}
}
private static bool RegisterButtonAttribute(Type type, MiraPluginInfo pluginInfo)
private static bool RegisterButton(Type type, MiraPluginInfo pluginInfo)
{
try
{
+5 -5
View File
@@ -1,4 +1,5 @@
using System.Linq;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace MiraAPI.Roles;
@@ -8,12 +9,11 @@ namespace MiraAPI.Roles;
/// <typeparam name="T">The role you are trying to access.</typeparam>
public static class CustomRoleSingleton<T> where T : ICustomRole
{
private static ICustomRole? _instance;
private static T? _instance;
/// <summary>
/// Gets the instance of the role.
/// </summary>
#pragma warning disable CA1000
public static ICustomRole Instance => _instance ??= CustomRoleManager.CustomRoles.Values.OfType<T>().Single();
#pragma warning restore CA1000
[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a utility class to get the instance of a custom role.")]
public static T Instance => _instance ??= CustomRoleManager.CustomRoles.Values.OfType<T>().Single();
}
+1 -1
View File
@@ -42,7 +42,7 @@ public static class CustomRoleUtils
public static StringBuilder CreateForRole(ICustomRole role)
{
var taskStringBuilder = new StringBuilder();
taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleColor.ToTextColor()}You are a <b>{role.RoleName}.</b></color>");
taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleColor.ToTextColor()}Your role is <b>{role.RoleName}.</b></color>");
taskStringBuilder.Append("<size=70%>");
taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleLongDescription}");
return taskStringBuilder;
-16
View File
@@ -73,14 +73,6 @@ public interface ICustomRole : IOptionable
/// </summary>
MiraPluginInfo ParentMod => CustomRoleManager.FindParentMod(this);
/// <summary>
/// This method runs on the PlayerControl.FixedUpdate method for ALL players with this role.
/// </summary>
/// <param name="playerControl">The PlayerControl that has this role.</param>
void PlayerControlFixedUpdate(PlayerControl playerControl)
{
}
internal ConfigDefinition NumConfigDefinition => new("Roles", $"Num {GetType().FullName}");
internal ConfigDefinition ChanceConfigDefinition => new("Roles", $"Chance {GetType().FullName}");
@@ -222,14 +214,6 @@ public interface ICustomRole : IOptionable
return true;
}
/// <summary>
/// This method runs on the HudManager.Update method ONLY when the LOCAL player has this role.
/// </summary>
/// <param name="hudManager">Reference to HudManager instance.</param>
void HudUpdate(HudManager hudManager)
{
}
/// <summary>
/// Gets a custom ejection message for the role. Return null to use the default message.
/// </summary>
+14
View File
@@ -1,8 +1,22 @@
namespace MiraAPI.Roles;
/// <summary>
/// Enum for modded role teams.
/// </summary>
public enum ModdedRoleTeams
{
/// <summary>
/// The crewmate team.
/// </summary>
Crewmate,
/// <summary>
/// The impostor team.
/// </summary>
Impostor,
/// <summary>
/// Custom team, catchall for any other teams.
/// </summary>
Custom,
}
+16 -1
View File
@@ -2,9 +2,18 @@
namespace MiraAPI.Roles;
/// <summary>
/// Utility class to get the ID of a role.
/// </summary>
public static class RoleId
{
public static ushort Get<T>() where T : RoleBehaviour
/// <summary>
/// Gets the ID of a role.
/// </summary>
/// <typeparam name="T">The type of the role.</typeparam>
/// <returns>The role ID as a ushort.</returns>
/// <exception cref="InvalidOperationException">Thrown when the role is not registered.</exception>
public static ushort Get<T>() where T : ICustomRole
{
if (!CustomRoleManager.RoleIds.TryGetValue(typeof(T), out var roleId))
{
@@ -14,6 +23,12 @@ public static class RoleId
return roleId;
}
/// <summary>
/// Gets the ID of a role.
/// </summary>
/// <param name="type">The type of the role.</param>
/// <returns>The ID as a ushort.</returns>
/// <exception cref="InvalidOperationException">Thrown when the role is not registered.</exception>
public static ushort Get(Type type)
{
if (!CustomRoleManager.RoleIds.TryGetValue(type, out var roleId))
@@ -7,6 +7,7 @@ namespace MiraAPI.Utilities.Assets;
/// A utility class for loading assets from embedded resources.
/// </summary>
/// <param name="path">The path to the embedded resource.</param>
/// <param name="pixelsPerUnit">The pixels per unit for the loaded sprite.</param>
public class LoadableResourceAsset(string path, float pixelsPerUnit=100f) : LoadableAsset<Sprite>
{
private readonly Assembly _assembly = Assembly.GetCallingAssembly();
@@ -14,9 +15,9 @@ public class LoadableResourceAsset(string path, float pixelsPerUnit=100f) : Load
/// <inheritdoc />
public override Sprite LoadAsset()
{
if (LoadedAsset != null)
if (LoadedAsset)
{
return LoadedAsset;
return LoadedAsset!;
}
return LoadedAsset = SpriteTools.LoadSpriteFromPath(path, _assembly, pixelsPerUnit);
+41 -22
View File
@@ -4,7 +4,7 @@ using UnityEngine;
namespace MiraAPI.Utilities.Assets;
/// <summary>
/// Various assets used throughout Mira API.
/// A static class that contains various assets used in the Mira API.
/// </summary>
public static class MiraAssets
{
@@ -25,21 +25,40 @@ public static class MiraAssets
RoundedBox = new LoadableAssetWrapper<Sprite>(boxSprite);
}
internal static readonly AssetBundle MiraAssetBundle = AssetBundleManager.Load("mirabundle");
internal static readonly LoadableAsset<GameObject> ModifierDisplay = new LoadableBundleAsset<GameObject>("Modifiers", MiraAssetBundle);
internal static readonly LoadableAsset<GameObject> PresetSavePopup = new LoadableBundleAsset<GameObject>("PresetSavePopup", MiraAssetBundle);
internal static readonly LoadableAsset<Sprite> RefreshIcon = new LoadableBundleAsset<Sprite>("refresh", MiraAssetBundle);
internal static readonly LoadableAsset<Sprite> FolderIcon = new LoadableBundleAsset<Sprite>("freePlay_folderTaskRoom", MiraAssetBundle);
/// <summary>
/// Gets the accepted teal color used for various selections in the UI.
/// Gets the color used for teal highlighting in UI.
/// </summary>
public static Color32 AcceptedTeal { get; } = new(43, 233, 198, 255);
/// <summary>
/// Gets the empty sprite.
/// Gets the Mira API asset bundle.
/// </summary>
public static LoadableAsset<Sprite> Empty { get; } = new LoadableResourceAsset("MiraAPI.Resources.Empty.png");
public static AssetBundle MiraAssetBundle { get; } = AssetBundleManager.Load("mirabundle");
/// <summary>
/// Gets the ModifierDisplay prefab.
/// </summary>
public static LoadableAsset<GameObject> ModifierDisplay { get; } = new LoadableBundleAsset<GameObject>("Modifiers", MiraAssetBundle);
/// <summary>
/// Gets the Popup prefab for saving presets.
/// </summary>
public static LoadableAsset<GameObject> PresetSavePopup { get; } = new LoadableBundleAsset<GameObject>("PresetSavePopup", MiraAssetBundle);
/// <summary>
/// Gets the Refresh Icon sprite.
/// </summary>
public static LoadableAsset<Sprite> RefreshIcon { get; } = new LoadableBundleAsset<Sprite>("refresh", MiraAssetBundle);
/// <summary>
/// Gets the Folder Icon sprite.
/// </summary>
public static LoadableAsset<Sprite> FolderIcon { get; } = new LoadableBundleAsset<Sprite>("freePlay_folderTaskRoom", MiraAssetBundle);
/// <summary>
/// Gets the empty sprite asset.
/// </summary>
public static LoadableResourceAsset Empty { get; } = new("MiraAPI.Resources.Empty.png");
/// <summary>
/// Gets the RoundedBox sprite, which is a rounded rectangle used for UI elements.
@@ -47,32 +66,32 @@ public static class MiraAssets
public static LoadableAsset<Sprite> RoundedBox { get; }
/// <summary>
/// Gets the NextButton sprite.
/// Gets the Next Button sprite.
/// </summary>
public static LoadableAsset<Sprite> NextButton { get; } = new LoadableResourceAsset("MiraAPI.Resources.NextButton.png");
public static LoadableResourceAsset NextButton { get; } = new("MiraAPI.Resources.NextButton.png");
/// <summary>
/// Gets the active variant of the NextButton sprite.
/// Gets the highlighted Next Button sprite.
/// </summary>
public static LoadableAsset<Sprite> NextButtonActive { get; } = new LoadableResourceAsset("MiraAPI.Resources.NextButtonActive.png");
public static LoadableResourceAsset NextButtonActive { get; } = new("MiraAPI.Resources.NextButtonActive.png");
/// <summary>
/// Gets the Cog icon sprite.
/// Gets the Cog icon used in Role Settings Menu.
/// </summary>
public static LoadableAsset<Sprite> Cog { get; } = new LoadableResourceAsset("MiraAPI.Resources.Cog.png");
public static LoadableResourceAsset Cog { get; } = new("MiraAPI.Resources.Cog.png");
/// <summary>
/// Gets the Checkmark icon sprite.
/// Gets the Checkmark Box used in the Settings Menu.
/// </summary>
public static LoadableAsset<Sprite> Checkmark { get; } = new LoadableResourceAsset("MiraAPI.Resources.Checkmark.png");
public static LoadableResourceAsset CheckmarkBox { get; } = new("MiraAPI.Resources.CheckMarkBox.png");
/// <summary>
/// Gets the Checkmark Box sprite.
/// Gets the Checkmark used in the Settings Menu.
/// </summary>
public static LoadableAsset<Sprite> CheckmarkBox { get; } = new LoadableResourceAsset("MiraAPI.Resources.CheckMarkBox.png");
public static LoadableResourceAsset Checkmark { get; } = new("MiraAPI.Resources.Checkmark.png");
/// <summary>
/// Gets the custom category header sprite used in the options menu.
/// Gets the white CategoryHeader used in the Settings Menu.
/// </summary>
public static LoadableAsset<Sprite> CategoryHeader { get; } = new LoadableResourceAsset("MiraAPI.Resources.CategoryHeader.png");
public static LoadableResourceAsset CategoryHeader { get; } = new("MiraAPI.Resources.CategoryHeader.png");
}
+58 -22
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
@@ -84,6 +85,51 @@ public static class Extensions
self.StartCoroutine(self.CoDestroySelf());
}
/// <summary>
/// Sets the cooldown of a button with a formatted string.
/// </summary>
/// <param name="button">The ActionButton to set the cooldown for.</param>
/// <param name="timer">The current timer value.</param>
/// <param name="maxTimer">The maximum timer value.</param>
/// <param name="format">The format string to use for the timer text.</param>
public static void SetCooldownFormat(this ActionButton button, float timer, float maxTimer, string format="0")
{
var num = Mathf.Clamp(timer / maxTimer, 0f, 1f);
button.isCoolingDown = num > 0f;
button.SetCooldownFill(num);
if (button.isCoolingDown)
{
button.cooldownTimerText.text = timer.ToString(format, NumberFormatInfo.InvariantInfo);
button.cooldownTimerText.gameObject.SetActive(true);
return;
}
button.cooldownTimerText.gameObject.SetActive(false);
}
/// <summary>
/// Sets the fill-up variant of a cooldown button with a formatted string.
/// </summary>
/// <param name="button">The ActionButton to set the cooldown for.</param>
/// <param name="timer">The current timer value.</param>
/// <param name="maxTimer">The maximum timer value.</param>
/// <param name="format">The format string to use for the timer text.</param>
public static void SetFillUpFormat(this ActionButton button, float timer, float maxTimer, string format="0")
{
var num = Mathf.Clamp(timer / maxTimer, 0f, 1f);
button.isCoolingDown = num > 0f;
if (button.isCoolingDown && timer < 3f)
{
button.graphic.transform.localPosition = button.position + (Vector3)UnityEngine.Random.insideUnitCircle * 0.05f;
button.cooldownTimerText.text = timer.ToString(format, NumberFormatInfo.InvariantInfo);
button.cooldownTimerText.gameObject.SetActive(true);
}
else
{
button.graphic.transform.localPosition = button.position;
}
button.SetCooldownFill(num);
}
/// <summary>
/// Gets a PlayerControl from their PlayerVoteArea in a meeting.
/// </summary>
@@ -145,6 +191,12 @@ public static class Extensions
return component;
}
/// <summary>
/// Gets the maximum value from a dictionary of integers, returning the key and value.
/// </summary>
/// <param name="self">The dictionary to search.</param>
/// <param name="tie">Whether there is a tie for the maximum value.</param>
/// <returns>The key-value pair with the maximum value.</returns>
public static KeyValuePair<byte, int> MaxPair(this Dictionary<byte, int> self, out bool tie)
{
tie = true;
@@ -164,6 +216,12 @@ public static class Extensions
return result;
}
/// <summary>
/// Gets the maximum value from a dictionary of floats, returning the key and value.
/// </summary>
/// <param name="self">The dictionary to search.</param>
/// <param name="tie">Whether there is a tie for the maximum value.</param>
/// <returns>The key-value pair with the maximum value.</returns>
public static KeyValuePair<byte, float> MaxPair(this Dictionary<byte, float> self, out bool tie)
{
tie = true;
@@ -385,18 +443,6 @@ public static class Extensions
return new Color(color.r - amount, color.g - amount, color.b - amount);
}
/// <summary>
/// Gets an alternate color based on the original color.
/// </summary>
/// <param name="color">The original color.</param>
/// <param name="amount">The amount to darken or lighten the original color by between 0.0 and 1.0.</param>
/// <returns>An alternate color that has been darkened or lightened.</returns>
[Obsolete("Use FindAlternateColor for WACG compliance.")]
public static Color GetAlternateColor(this Color color, float amount = 0.45f)
{
return color.IsColorDark() ? LightenColor(color, amount) : DarkenColor(color, amount);
}
/// <summary>
/// Lightens a color by a specified amount.
/// </summary>
@@ -408,16 +454,6 @@ public static class Extensions
return new Color(color.r + amount, color.g + amount, color.b + amount);
}
/// <summary>
/// Checks if a color is dark.
/// </summary>
/// <param name="color">The color to check.</param>
/// <returns>True if the color is dark, false otherwise.</returns>
public static bool IsColorDark(this Color color)
{
return color.r < 0.5f && color is { g: < 0.5f, b: < 0.5f };
}
/// <summary>
/// Gets the nearest dead body to a player.
/// </summary>
+19
View File
@@ -1,8 +1,27 @@
namespace MiraAPI.Utilities;
/// <summary>
/// Suffixes used for options numbers.
/// </summary>
public enum MiraNumberSuffixes
{
/// <summary>
/// No suffix.
/// </summary>
None,
/// <summary>
/// Multiplier suffix (x).
/// </summary>
Multiplier,
/// <summary>
/// Seconds suffix (s).
/// </summary>
Seconds,
/// <summary>
/// Percentage suffix (%).
/// </summary>
Percent,
}
+2 -2
View File
@@ -71,7 +71,7 @@ public static class VotingUtils
/// <param name="source">The player who is sending the RPC. Should be the host.</param>
/// <param name="voterId">The player who voted.</param>
/// <param name="votedFor">The player who the voter voted for.</param>
[MethodRpc((uint)MiraRpc.RemoveVote)]
[MethodRpc((uint)MiraRpc.RemoveVote, SendImmediately = true)]
public static void RpcRemoveVote(PlayerControl source, byte voterId, byte votedFor)
{
if (!source.IsHost()) return;
@@ -107,7 +107,7 @@ public static class VotingUtils
/// <param name="source">The player who sent this RPC.</param>
/// <param name="srcPlayerId">The id of the player who casted the vote.</param>
/// <param name="suspectPlayerId">The voted player's id.</param>
[MethodRpc((uint)MiraRpc.CastVote)]
[MethodRpc((uint)MiraRpc.CastVote, SendImmediately = true)]
public static void RpcCastVote(PlayerControl source, byte srcPlayerId, byte suspectPlayerId)
{
CustomCastVote(srcPlayerId, suspectPlayerId);
+1 -1
View File
@@ -1,4 +1,4 @@
[![](https://dcbadge.limes.pink/api/server/all-of-us-launchpad-794950428756410429)](https://discord.gg/all-of-us-launchpad-794950428756410429)
[![](https://dcbadge.limes.pink/api/server/AEfHJGwggC)](https://discord.gg/AEfHJGwggC)
# Mira API