diff --git a/MiraAPI.Example/ExampleAssets.cs b/MiraAPI.Example/ExampleAssets.cs
index 732d9f7..903afd8 100644
--- a/MiraAPI.Example/ExampleAssets.cs
+++ b/MiraAPI.Example/ExampleAssets.cs
@@ -5,6 +5,7 @@ namespace MiraAPI.Example;
public static class ExampleAssets
{
public static LoadableResourceAsset ExampleButton { get; } = new("MiraAPI.Example.Resources.ExampleButton.png");
+ public static LoadableResourceAsset CallMeetingButton { get; } = new("MiraAPI.Example.Resources.CallMeeting.png");
// Credit to EpicHorrors for the teleport button asset.
public static LoadableResourceAsset TeleportButton { get; } = new("MiraAPI.Example.Resources.TeleportButton.png");
diff --git a/MiraAPI.Example/ExampleEventHandlers.cs b/MiraAPI.Example/ExampleEventHandlers.cs
index 2e8fdf2..079fb9f 100644
--- a/MiraAPI.Example/ExampleEventHandlers.cs
+++ b/MiraAPI.Example/ExampleEventHandlers.cs
@@ -9,7 +9,10 @@ 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;
@@ -38,12 +41,9 @@ public static class ExampleEventHandlers
[RegisterEvent]
public static void StartMeetingEvent(StartMeetingEvent _)
{
- foreach (var plr in PlayerControl.AllPlayerControls)
+ foreach (var plr in PlayerControl.AllPlayerControls.ToArray().Where(player => player.Data.Role is MayorRole))
{
- if (plr.Data.Role is MayorRole)
- {
- plr.GetVoteData().IncreaseRemainingVotes(1);
- }
+ plr.GetVoteData().IncreaseRemainingVotes(1);
}
}
diff --git a/MiraAPI.Example/MiraAPI.Example.csproj b/MiraAPI.Example/MiraAPI.Example.csproj
index ff27bcc..d0b23ac 100644
--- a/MiraAPI.Example/MiraAPI.Example.csproj
+++ b/MiraAPI.Example/MiraAPI.Example.csproj
@@ -9,11 +9,9 @@
-
-
-
-
-
+
+
+
diff --git a/MiraAPI.Example/Modifiers/CaptainModifier.cs b/MiraAPI.Example/Modifiers/CaptainModifier.cs
index ca8a570..0a889d1 100644
--- a/MiraAPI.Example/Modifiers/CaptainModifier.cs
+++ b/MiraAPI.Example/Modifiers/CaptainModifier.cs
@@ -1,12 +1,29 @@
-using MiraAPI.Example.Options.Modifiers;
+using Il2CppSystem.Collections.Generic;
+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;
public class CaptainModifier : GameModifier
{
public override string ModifierName => "Captain";
+ public override LoadableAsset? ModifierIcon => ExampleAssets.CallMeetingButton;
+
+ public override void OnDeath(DeathReason reason)
+ {
+ Player.RemoveModifier(this);
+ }
+
+ public override string GetDescription()
+ {
+ return $"You can call a meeting from anywhere on the map.";
+ }
public override int GetAssignmentChance()
{
diff --git a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs
index 9f13b18..e1ad218 100644
--- a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs
+++ b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs
@@ -1,4 +1,6 @@
using MiraAPI.Modifiers.Types;
+using MiraAPI.Utilities.Assets;
+using UnityEngine;
namespace MiraAPI.Example.Modifiers;
@@ -6,6 +8,11 @@ public class HighPriorityModifier : GameModifier
{
public override string ModifierName => "High Priority";
+ public override string GetDescription()
+ {
+ return "You are high priority! Idrk what this means either tbh...";
+ }
+
public override int GetAssignmentChance()
{
return 100;
diff --git a/MiraAPI.Example/Resources/CallMeeting.png b/MiraAPI.Example/Resources/CallMeeting.png
new file mode 100644
index 0000000..e96524a
Binary files /dev/null and b/MiraAPI.Example/Resources/CallMeeting.png differ
diff --git a/MiraAPI.Example/Roles/ChameleonRole.cs b/MiraAPI.Example/Roles/ChameleonRole.cs
index 52550a3..6f485d7 100644
--- a/MiraAPI.Example/Roles/ChameleonRole.cs
+++ b/MiraAPI.Example/Roles/ChameleonRole.cs
@@ -1,5 +1,6 @@
using AmongUs.GameOptions;
using MiraAPI.Roles;
+using Reactor.Utilities;
using TMPro;
using UnityEngine;
@@ -19,6 +20,12 @@ public class ChameloenRole : CrewmateRole, ICustomRole
IntroSound = CustomRoleUtils.GetIntroSound(RoleTypes.Shapeshifter),
};
+ public override void Initialize(PlayerControl player)
+ {
+ Player = player;
+ Logger.Error("Initializing ChamelonRole for player: " + player.PlayerId);
+ }
+
public void PlayerControlFixedUpdate(PlayerControl playerControl)
{
if (playerControl.MyPhysics.Velocity.magnitude > 0)
diff --git a/MiraAPI/Events/MiraEventManager.cs b/MiraAPI/Events/MiraEventManager.cs
index b375504..52df8dd 100644
--- a/MiraAPI/Events/MiraEventManager.cs
+++ b/MiraAPI/Events/MiraEventManager.cs
@@ -21,7 +21,7 @@ public static class MiraEventManager
public static bool InvokeEvent(T eventInstance) where T : MiraEvent
{
EventWrappers.TryGetValue(typeof(T), out var handlers);
- if (handlers == null)
+ if (handlers == null || handlers.Count == 0)
{
return false;
}
@@ -43,7 +43,7 @@ public static class MiraEventManager
public static bool InvokeEvent(MiraEvent eventInstance, Type type)
{
EventWrappers.TryGetValue(type, out var handlers);
- if (handlers == null)
+ if (handlers == null || handlers.Count == 0)
{
return false;
}
@@ -61,47 +61,51 @@ public static class MiraEventManager
///
/// The type of event.
/// The MethodInfo of the event handler.
- /// The priority of the event handler. Higher values are called first.
+ /// The priority of the event handler. Lower values are called first.
public static void RegisterEventHandler(Type type, MethodInfo methodInfo, int priority = 0)
{
if (!type.IsSubclassOf(typeof(MiraEvent)))
{
- Logger.Error($"Type must be a subclass of MiraEvent: {nameof(type)}");
+ Logger.Error($"Type must be a subclass of MiraEvent: {type.FullName}");
return;
}
- if (!EventWrappers.TryGetValue(type, out var handlers))
- {
- handlers = [];
- EventWrappers.Add(type, handlers);
- }
+ EventWrappers.TryAdd(type, []);
+ var handlers = EventWrappers[type];
var @delegate = Delegate.CreateDelegate(typeof(Action<>).MakeGenericType(type), methodInfo);
- handlers.Add(new MiraEventWrapper(@delegate, priority));
+ var eventWrapper = new MiraEventWrapper(@delegate, priority);
+
+ var index = handlers.BinarySearch(eventWrapper, Comparer.Create((a, b) => a.Priority.CompareTo(b.Priority)));
+
+ if (index < 0)
+ {
+ index = ~index;
+ }
+
+ handlers.Insert(index, eventWrapper);
}
///
/// Register an event.
///
/// The callback method/handler for the event.
- /// The priority of the event handler. Higher values are called first.
+ /// The priority of the event handler. Lower values are called first.
/// Type of event.
public static void RegisterEventHandler(Action handler, int priority = 0) where T : MiraEvent
{
- if (!EventWrappers.ContainsKey(typeof(T)))
- {
- EventWrappers.Add(typeof(T), []);
- }
+ EventWrappers.TryAdd(typeof(T), []);
var handlers = EventWrappers[typeof(T)];
- handlers.Add(new MiraEventWrapper(handler, priority));
- }
+ var eventWrapper = new MiraEventWrapper(handler, priority);
- internal static void SortAllHandlers()
- {
- foreach (var handlers in EventWrappers.Values)
+ var index = handlers.BinarySearch(eventWrapper, Comparer.Create((a, b) => a.Priority.CompareTo(b.Priority)));
+
+ if (index < 0)
{
- handlers.Sort((a, b) => a.Priority.CompareTo(b.Priority));
+ index = ~index;
}
+
+ handlers.Insert(index, eventWrapper);
}
}
diff --git a/MiraAPI/Events/MiraEventWrapper.cs b/MiraAPI/Events/MiraEventWrapper.cs
index 0251ea9..7e754c3 100644
--- a/MiraAPI/Events/MiraEventWrapper.cs
+++ b/MiraAPI/Events/MiraEventWrapper.cs
@@ -13,7 +13,7 @@ public class MiraEventWrapper
public Delegate EventHandler { get; }
///
- /// Gets the priority of the handler. Higher values are called first.
+ /// Gets the priority of the handler. Lower values are called first.
///
public int Priority { get; }
diff --git a/MiraAPI/Events/RegisterEventAttribute.cs b/MiraAPI/Events/RegisterEventAttribute.cs
index 6988d79..169c125 100644
--- a/MiraAPI/Events/RegisterEventAttribute.cs
+++ b/MiraAPI/Events/RegisterEventAttribute.cs
@@ -9,14 +9,14 @@ namespace MiraAPI.Events;
public class RegisterEventAttribute : Attribute
{
///
- /// Gets the priority of the event.
+ /// Gets the priority of the event. Lower values are called first.
///
public int Priority { get; }
///
/// Initializes a new instance of the class.
///
- /// The priority of the event.
+ /// The priority of the event. Lower values are called first.
public RegisterEventAttribute(int priority = 0)
{
Priority = priority;
diff --git a/MiraAPI/Events/Vanilla/Gameplay/IntroEndEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/IntroEndEvent.cs
new file mode 100644
index 0000000..b27bcec
--- /dev/null
+++ b/MiraAPI/Events/Vanilla/Gameplay/IntroEndEvent.cs
@@ -0,0 +1,12 @@
+namespace MiraAPI.Events.Vanilla.Gameplay;
+
+///
+/// The event that is invoked when the intro cutscene has finished playing.
+///
+public class IntroEndEvent(IntroCutscene introCutscene) : MiraEvent
+{
+ ///
+ /// Gets the instance of the IntroCutscene.
+ ///
+ public IntroCutscene IntroCutscene { get; } = introCutscene;
+}
diff --git a/MiraAPI/Events/Vanilla/Gameplay/RoundStartEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/RoundStartEvent.cs
new file mode 100644
index 0000000..a50fde5
--- /dev/null
+++ b/MiraAPI/Events/Vanilla/Gameplay/RoundStartEvent.cs
@@ -0,0 +1,21 @@
+namespace MiraAPI.Events.Vanilla.Gameplay;
+
+///
+/// Start Round event, invoked on IntroCutscene.OnDestroy and ExileController.WrapUp.
+///
+public class RoundStartEvent : MiraEvent
+{
+ ///
+ /// Gets a value indicating whether the event was triggered by the IntroCutscene or ExileController.
+ ///
+ public bool TriggeredByIntro { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Whether the event was triggered by the intro or not.
+ public RoundStartEvent(bool triggeredByIntro)
+ {
+ TriggeredByIntro = triggeredByIntro;
+ }
+}
diff --git a/MiraAPI/Events/Vanilla/Meeting/EndMeetingEvent.cs b/MiraAPI/Events/Vanilla/Meeting/EndMeetingEvent.cs
new file mode 100644
index 0000000..55cdae9
--- /dev/null
+++ b/MiraAPI/Events/Vanilla/Meeting/EndMeetingEvent.cs
@@ -0,0 +1,12 @@
+namespace MiraAPI.Events.Vanilla.Meeting;
+
+///
+/// The event that is invoked when the meeting has ended.
+///
+public class EndMeetingEvent(MeetingHud meetingHud) : MiraEvent
+{
+ ///
+ /// Gets the MeetingHud instance.
+ ///
+ public MeetingHud MeetingHud { get; } = meetingHud;
+}
diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/VotingCompleteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/VotingCompleteEvent.cs
new file mode 100644
index 0000000..5f782bf
--- /dev/null
+++ b/MiraAPI/Events/Vanilla/Meeting/Voting/VotingCompleteEvent.cs
@@ -0,0 +1,13 @@
+namespace MiraAPI.Events.Vanilla.Meeting.Voting;
+
+///
+/// The event that is invoked when the voting phase of a meeting is complete.
+///
+/// The MeetingHud instance.
+public class VotingCompleteEvent(MeetingHud meetingHud) : MiraEvent
+{
+ ///
+ /// Gets the MeetingHud instance.
+ ///
+ public MeetingHud MeetingHud { get; } = meetingHud;
+}
diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs
index 6a721da..e215fda 100644
--- a/MiraAPI/Hud/CustomActionButton.cs
+++ b/MiraAPI/Hud/CustomActionButton.cs
@@ -417,9 +417,12 @@ public abstract class CustomActionButton
/// The local PlayerControl.
public virtual void FixedUpdateHandler(PlayerControl playerControl)
{
- if (Timer >= 0 && !TimerPaused)
+ if (Timer >= 0)
{
- Timer -= Time.deltaTime;
+ if (!TimerPaused)
+ {
+ Timer -= Time.deltaTime;
+ }
}
else if (HasEffect && EffectActive)
{
@@ -428,25 +431,28 @@ public abstract class CustomActionButton
OnEffectEnd();
}
- if (CanUse())
+ if (Button)
{
- Button?.SetEnabled();
- }
- else
- {
- Button?.SetDisabled();
- }
+ if (CanUse())
+ {
+ Button.SetEnabled();
+ }
+ else
+ {
+ Button.SetDisabled();
+ }
- if (EffectActive)
- {
- Button?.SetFillUp(Timer, EffectDuration);
+ if (EffectActive)
+ {
+ Button.SetFillUp(Timer, EffectDuration);
- Button!.cooldownTimerText.text = Mathf.CeilToInt(Timer).ToString(NumberFormatInfo.InvariantInfo);
- Button!.cooldownTimerText.gameObject.SetActive(true);
- }
- else
- {
- Button?.SetCoolDown(Timer, Cooldown);
+ Button.cooldownTimerText.text = Mathf.CeilToInt(Timer).ToString(NumberFormatInfo.InvariantInfo);
+ Button.cooldownTimerText.gameObject.SetActive(true);
+ }
+ else
+ {
+ Button.SetCoolDown(Timer, Cooldown);
+ }
}
FixedUpdate(playerControl);
diff --git a/MiraAPI/Hud/CustomPlayerMenu.cs b/MiraAPI/Hud/CustomPlayerMenu.cs
index 27f52ac..56d9f0e 100644
--- a/MiraAPI/Hud/CustomPlayerMenu.cs
+++ b/MiraAPI/Hud/CustomPlayerMenu.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using AmongUs.GameOptions;
using Il2CppInterop.Runtime.Attributes;
@@ -8,6 +9,8 @@ using Reactor.Utilities.Attributes;
using Reactor.Utilities.Extensions;
using UnityEngine;
using UnityEngine.Events;
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
namespace MiraAPI.Hud;
@@ -16,22 +19,18 @@ namespace MiraAPI.Hud;
///
/// Used by Il2Cpp. Do not use constructor, this is a MonoBehaviour.
[RegisterInIl2Cpp]
+[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention")]
+[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Unity Convention")]
+[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")]
public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
{
- public ShapeshifterPanel PanelPrefab;
-
- public float XStart = -0.8f;
-
- public float YStart = 2.15f;
-
- public float XOffset = 1.95f;
-
- public float YOffset = -0.65f;
-
- public UiElement BackButton;
-
- public UiElement DefaultButtonSelected;
-
+ public ShapeshifterPanel panelPrefab;
+ public float xStart = -0.8f;
+ public float yStart = 2.15f;
+ public float xOffset = 1.95f;
+ public float yOffset = -0.65f;
+ public UiElement backButton;
+ public UiElement defaultButtonSelected;
public List potentialVictims;
///
@@ -42,17 +41,17 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
{
var shapeShifterRole = RoleManager.Instance.GetRole(RoleTypes.Shapeshifter);
- var ogMenu = shapeShifterRole.TryCast().ShapeshifterMenu;
+ var ogMenu = shapeShifterRole.TryCast()!.ShapeshifterMenu;
var newMenu = Instantiate(ogMenu);
var customMenu = newMenu.gameObject.AddComponent();
- customMenu.PanelPrefab = newMenu.PanelPrefab;
- customMenu.XStart = newMenu.XStart;
- customMenu.YStart = newMenu.YStart;
- customMenu.XOffset = newMenu.XOffset;
- customMenu.YOffset = newMenu.YOffset;
- customMenu.BackButton = newMenu.BackButton;
- var back = customMenu.BackButton.GetComponent();
+ customMenu.panelPrefab = newMenu.PanelPrefab;
+ customMenu.xStart = newMenu.XStart;
+ customMenu.yStart = newMenu.YStart;
+ customMenu.xOffset = newMenu.XOffset;
+ customMenu.yOffset = newMenu.YOffset;
+ customMenu.backButton = newMenu.BackButton;
+ var back = customMenu.backButton.GetComponent();
back.OnClick.RemoveAllListeners();
back.OnClick.AddListener((UnityAction)(() =>
{
@@ -65,7 +64,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
newMenu.DestroyImmediate();
- customMenu.transform.SetParent(Camera.main.transform, false);
+ customMenu.transform.SetParent(Camera.main!.transform, false);
customMenu.transform.localPosition = new Vector3(0f, 0f, -50f);
return customMenu;
}
@@ -75,6 +74,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
ControllerManager.Instance.CloseOverlayMenu(name);
}
+ ///
public override void Begin(PlayerTask task)
{
throw new NotImplementedException("Use the other Begin method.");
@@ -90,7 +90,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
{
MinigameStubs.Begin(this, null);
- var back = BackButton.GetComponent();
+ var back = backButton.GetComponent();
back.OnClick.AddListener((UnityAction)(() =>
{
onClick(null);
@@ -107,13 +107,13 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
var num = i % 3;
var num2 = i / 3;
var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor;
- var shapeshifterPanel = Instantiate(PanelPrefab, transform);
- shapeshifterPanel.transform.localPosition = new Vector3(XStart + num * XOffset, YStart + num2 * YOffset, -1f);
+ var shapeshifterPanel = Instantiate(panelPrefab, transform);
+ shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f);
shapeshifterPanel.SetPlayer(i, player.Data, (Il2CppSystem.Action)(() => { onClick(player); }));
shapeshifterPanel.NameText.color = flag ? player.Data.Role.NameColor : Color.white;
potentialVictims.Add(shapeshifterPanel);
list2.Add(shapeshifterPanel.Button);
}
- ControllerManager.Instance.OpenOverlayMenu(name, BackButton, DefaultButtonSelected, list2);
+ ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2);
}
}
diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs
index 233f174..ec2cc97 100644
--- a/MiraAPI/MiraApiPlugin.cs
+++ b/MiraAPI/MiraApiPlugin.cs
@@ -1,5 +1,4 @@
using BepInEx;
-using BepInEx.Configuration;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using MiraAPI.PluginLoading;
@@ -17,6 +16,7 @@ namespace MiraAPI;
[BepInAutoPlugin("mira.api", "MiraAPI")]
[BepInProcess("Among Us.exe")]
[BepInDependency(ReactorPlugin.Id)]
+[BepInDependency(ModCompatibility.SubmergedId, BepInDependency.DependencyFlags.SoftDependency)]
[ReactorModFlags(ModFlags.RequireOnAllClients)]
public partial class MiraApiPlugin : BasePlugin
{
diff --git a/MiraAPI/ModCompatibility.cs b/MiraAPI/ModCompatibility.cs
new file mode 100644
index 0000000..23744ab
--- /dev/null
+++ b/MiraAPI/ModCompatibility.cs
@@ -0,0 +1,28 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using BepInEx.Unity.IL2CPP;
+
+namespace MiraAPI;
+
+///
+/// Mod compatibility tools.
+///
+public static class ModCompatibility
+{
+ ///
+ /// The ID for the Submerged mod.
+ ///
+ public const string SubmergedId = "Submerged";
+
+ ///
+ /// Checks if the Submerged mod is loaded.
+ ///
+ /// Submerged mod assembly if loaded, null otherwise.
+ /// True if the Submerged mod is loaded, false otherwise.
+ public static bool SubmergedLoaded([NotNullWhen(true)] out Assembly? submergedAssembly)
+ {
+ var result = IL2CPPChainloader.Instance.Plugins.TryGetValue(SubmergedId, out var plugin);
+ submergedAssembly = result ? plugin?.Instance.GetType().Assembly : null;
+ return result;
+ }
+}
diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs
index 7b3dc3f..435bba5 100644
--- a/MiraAPI/Modifiers/BaseModifier.cs
+++ b/MiraAPI/Modifiers/BaseModifier.cs
@@ -1,6 +1,8 @@
using System;
using MiraAPI.GameOptions;
using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using UnityEngine;
namespace MiraAPI.Modifiers;
@@ -48,9 +50,14 @@ public abstract class BaseModifier : IOptionable
public abstract string ModifierName { get; }
///
- /// Gets a value indicating whether the modifier is hidden on the UI.
+ /// Gets the modifier icon. Useless if HideOnUi is true.
///
- public virtual bool HideOnUi => false;
+ public virtual LoadableAsset? ModifierIcon => null;
+
+ ///
+ /// Gets a value indicating whether the modifier is hidden on the UI. Will be hidden either way if no description is provided.
+ ///
+ public virtual bool HideOnUi => GetDescription() == string.Empty;
///
/// Gets a value indicating whether the modifier is shown in the freeplay menu.
@@ -63,10 +70,10 @@ public abstract class BaseModifier : IOptionable
public virtual bool Unique => true;
///
- /// Gets the HUD information for this modifier. Defaults to the modifier name. Does nothing if is true.
+ /// Gets the HUD description for this modifier. Does nothing if is true. Required to be visible on UI.
///
- /// The information string for the HUD.
- public virtual string GetHudString() => ModifierName;
+ /// The description string for the HUD.
+ public virtual string GetDescription() => string.Empty;
///
/// Called when the modifier is activated.
@@ -104,6 +111,13 @@ public abstract class BaseModifier : IOptionable
{
}
+ ///
+ /// Called when a meeting starts.
+ ///
+ public virtual void OnMeetingStart()
+ {
+ }
+
///
/// Determines whether the player can vent.
///
diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs
index 53a8ac4..1a83838 100644
--- a/MiraAPI/Modifiers/ModifierComponent.cs
+++ b/MiraAPI/Modifiers/ModifierComponent.cs
@@ -5,6 +5,7 @@ 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;
@@ -26,12 +27,12 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
[HideFromIl2Cpp]
public ImmutableList ActiveModifiers { get; private set; } = ImmutableList.Empty;
+ private ModifierDisplayComponent? ModifierDisplay { get; set; }
+
[HideFromIl2Cpp]
private List Modifiers { get; set; } = [];
- private PlayerControl? _player;
-
- private TextMeshPro? _modifierText;
+ private PlayerControl _player = null!;
private readonly List _toRemove = [];
@@ -42,9 +43,18 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
_toRemove.AddRange(Modifiers);
}
- private void Start()
+ private void Awake()
{
_player = GetComponent();
+ if (_player == null)
+ {
+ Logger.Error("ModifierComponent could not find PlayerControl on the same GameObject.");
+ Destroy(this);
+ return;
+ }
+
+ ModifierExtensions.ModifierComponents.Add(_player, this);
+
Modifiers = [];
if (!_player.AmOwner)
@@ -52,20 +62,24 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
return;
}
- _modifierText = Helpers.CreateTextLabel("ModifierText", HudManager.Instance.transform, AspectPosition.EdgeAlignments.RightTop, new Vector3(10.1f, 3.5f, 0), textAlignment: TextAlignmentOptions.Right);
- _modifierText.verticalAlignment = VerticalAlignmentOptions.Top;
+ ModifierDisplay = ModifierDisplayComponent.CreateDisplay();
+ ModifierDisplay?.UpdateModifiersList(Modifiers);
}
private void FixedUpdate()
{
+ var removed = _toRemove.Count > 0;
foreach (var modifier in _toRemove.ToArray())
{
+ _toRemove.Remove(modifier);
modifier.OnDeactivate();
Modifiers.Remove(modifier);
}
+ var added = _toAdd.Count > 0;
foreach (var modifier in _toAdd.ToArray())
{
+ _toAdd.Remove(modifier);
Modifiers.Add(modifier);
modifier.Initialized = true;
modifier.OnActivate();
@@ -76,14 +90,12 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
}
}
- if (_toAdd.Count > 0 || _toRemove.Count > 0)
+ if (removed || added)
{
- if (_player?.AmOwner == true)
+ if (_player.AmOwner && HudManager.InstanceExists)
{
- HudManager.Instance?.SetHudActive(_player, _player.Data.Role, HudManager.Instance.TaskPanel.isActiveAndEnabled);
+ ModifierDisplay?.UpdateModifiersList(Modifiers);
}
- _toAdd.Clear();
- _toRemove.Clear();
ActiveModifiers = Modifiers.ToImmutableList();
}
@@ -92,6 +104,11 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
modifier.FixedUpdate();
}
+
+ if (_player.AmOwner && ModifierDisplay && HudManager.InstanceExists)
+ {
+ ModifierDisplay!.gameObject.SetActive(MeetingHud.Instance || HudManager.Instance.TaskPanel.isActiveAndEnabled);
+ }
}
private void Update()
@@ -100,32 +117,12 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
modifier.Update();
}
+ }
- if (!_modifierText || _player?.AmOwner == false)
- {
- return;
- }
-
- if (MeetingHud.Instance)
- {
- return;
- }
-
- var showText = false;
- var builder = new StringBuilder("Modifiers:\n");
-
- foreach (var modifier in ActiveModifiers)
- {
- if (modifier.HideOnUi)
- {
- continue;
- }
-
- showText = true;
- builder.AppendLine(modifier.GetHudString());
- }
-
- _modifierText!.text = showText ? builder.ToString() : string.Empty;
+ private void OnDestroy()
+ {
+ // Remove this component from the global list when destroyed
+ ModifierExtensions.ModifierComponents.Remove(_player);
}
///
@@ -492,4 +489,61 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
return ActiveModifiers.Exists(x => x.UniqueId == id);
}
+
+ ///
+ /// Checks if a player has an active modifier by its type.
+ ///
+ /// Whether to check inactive modifiers (those pending to be added).
+ /// The predicate to check the modifier.
+ /// The Type of the Modifier.
+ /// True if the Modifier is present, false otherwise.
+ [HideFromIl2Cpp]
+ public bool HasModifier(bool checkInactive, Func? predicate=null) where T : BaseModifier
+ {
+ return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr));
+ bool MatchExpr(BaseModifier bm) => bm is T modifier && (predicate == null || predicate(modifier));
+ }
+
+ ///
+ /// Checks if a player has an active modifier by its type.
+ ///
+ /// The modifier type.
+ /// Whether to check inactive modifiers (those pending to be added).
+ /// The predicate to check the modifier.
+ /// True if the Modifier is present, false otherwise.
+ [HideFromIl2Cpp]
+ public bool HasModifier(Type type, bool checkInactive, Func? predicate=null)
+ {
+ return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr));
+ bool MatchExpr(BaseModifier bm) => bm.GetType() == type && (predicate == null || predicate(bm));
+ }
+
+ ///
+ /// Checks if a player has an active modifier by its type ID.
+ ///
+ /// The modifier's type ID.
+ /// Whether to check inactive modifiers (those pending to be added).
+ /// The predicate to check the modifier.
+ /// True if the modifier is present, false otherwise.
+ [HideFromIl2Cpp]
+ public bool HasModifier(uint id, bool checkInactive, Func? predicate=null)
+ {
+ var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException(
+ $"Cannot get modifier with id {id} because it is not registered.");
+
+ return HasModifier(type, checkInactive, predicate);
+ }
+
+ ///
+ /// Checks if a player has an active modifier by its unique ID.
+ ///
+ /// The modifier's guid.
+ /// Whether to check inactive modifiers (those pending to be added).
+ /// True if the modifier is present, false otherwise.
+ [HideFromIl2Cpp]
+ public bool HasModifier(Guid id, bool checkInactive)
+ {
+ return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr));
+ bool MatchExpr(BaseModifier bm) => bm.UniqueId == id;
+ }
}
diff --git a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs
new file mode 100644
index 0000000..9a82835
--- /dev/null
+++ b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs
@@ -0,0 +1,202 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using HarmonyLib;
+using Il2CppInterop.Runtime.Attributes;
+using MiraAPI.Utilities.Assets;
+using Reactor.Utilities.Attributes;
+using Reactor.Utilities.Extensions;
+using TMPro;
+using UnityEngine;
+using UnityEngine.Events;
+using UnityEngine.UI;
+
+namespace MiraAPI.Modifiers.ModifierDisplay;
+
+///
+/// The code used to display Mira modifiers.
+///
+[RegisterInIl2Cpp]
+public class ModifierDisplayComponent(nint ptr) : MonoBehaviour(ptr)
+{
+ ///
+ /// Gets the instance of the Modifier Display.
+ ///
+ public static ModifierDisplayComponent? Instance { get; private set; }
+
+ private RectTransform _children = null!;
+ private GameObject _modTemplate = null!;
+ private PassiveButton _toggleButton = null!;
+ private TextMeshPro _toggleBtnText = null!;
+
+ private RectTransform _pagination = null!;
+ private TextMeshPro _pageText = null!;
+ private PassiveButton _nextButton = null!;
+ private PassiveButton _backButton = null!;
+
+ private int _currentPage = 0;
+ private const int _itemsPerPage = 3;
+
+ private readonly Dictionary _modifiers = [];
+
+ ///
+ /// Gets a read only dictionary of the created modifier UI components.
+ ///
+ [HideFromIl2Cpp]
+ public ReadOnlyDictionary Modifiers => new(_modifiers);
+
+ ///
+ /// Gets a value indicating whether the modifier display hud is currently open.
+ ///
+ public bool IsOpen { get; private set; }
+
+ private void Awake()
+ {
+ Instance = this;
+
+ _children = transform.GetChild(0).GetComponent();
+ _modTemplate = transform.GetChild(2).gameObject;
+
+ _toggleButton = transform.GetChild(1).GetComponent();
+ _toggleButton.OnClick = new Button.ButtonClickedEvent();
+ _toggleButton.OnClick.AddListener((UnityAction)(() =>
+ {
+ IsOpen = !IsOpen;
+ _children.gameObject.SetActive(!_children.gameObject.activeSelf);
+ }));
+
+ _toggleButton.ClickSound = HudManager.Instance.GameMenu.StreamerModeButton.GetComponent().ClickSound;
+ _toggleButton.HoverSound = HudManager.Instance.GameMenu.StreamerModeButton.GetComponent().HoverSound;
+ _toggleButton.inactiveSprites = _toggleButton.transform.GetChild(1).gameObject;
+ _toggleBtnText = _toggleButton.transform.GetChild(0).GetComponent();
+
+ _pagination = _children.transform.GetChild(0).GetComponent();
+ _pageText = _pagination.transform.GetChild(0).GetComponent();
+ _nextButton = _pagination.transform.GetChild(1).GetComponent();
+ _backButton = _pagination.transform.GetChild(2).GetComponent();
+
+ _toggleBtnText.font = _pageText.font = _nextButton.buttonText.font = _backButton.buttonText.font = HudManager.Instance.TaskPanel.taskText.font;
+ _toggleBtnText.fontMaterial = _pageText.fontMaterial = _nextButton.buttonText.fontMaterial = _backButton.buttonText.fontMaterial = HudManager.Instance.TaskPanel.taskText.fontMaterial;
+
+ _nextButton.OnClick = new Button.ButtonClickedEvent();
+ _nextButton.OnClick.AddListener((UnityAction)(() =>
+ {
+ _currentPage++;
+ UpdatePage();
+ }));
+
+ _backButton.OnClick = new Button.ButtonClickedEvent();
+ _backButton.OnClick.AddListener((UnityAction)(() =>
+ {
+ _currentPage--;
+ UpdatePage();
+ }));
+
+ IsOpen = false;
+ _children.gameObject.SetActive(false);
+ }
+
+ ///
+ /// Use this to refresh the modifiers UI. Useful if you hide modifiers uis based on options.
+ ///
+ public void RefreshModifiers()
+ {
+ UpdateModifiersList([.. PlayerControl.LocalPlayer.GetModifierComponent().ActiveModifiers]);
+ }
+
+ internal void DestroyComponent(ModifierUiComponent component)
+ {
+ _modifiers.Remove(component.Modifier!);
+ component.gameObject.DestroyImmediate();
+
+ RefreshModifiers();
+ }
+
+ [HideFromIl2Cpp]
+ internal static ModifierDisplayComponent CreateDisplay()
+ {
+ var gameObject = Instantiate(MiraAssets.ModifierDisplay.LoadAsset(), HudManager.Instance.transform);
+ var minigame = gameObject.AddComponent();
+ return minigame;
+ }
+
+ [HideFromIl2Cpp]
+ private ModifierUiComponent CreateForModifier(BaseModifier modifier)
+ {
+ var newMod = Instantiate(_modTemplate, _children.transform);
+ newMod.transform.name = modifier.ModifierName;
+ var comp = newMod.AddComponent();
+ comp.Modifier = modifier;
+ return comp;
+ }
+
+ private void UpdatePage()
+ {
+ var totalPages = Mathf.CeilToInt((float)_modifiers.Count / _itemsPerPage);
+ _currentPage = Mathf.Clamp(_currentPage, 0, Mathf.Max(0, totalPages - 1));
+
+ if (_modifiers.Count <= _itemsPerPage)
+ {
+ _modifiers.Do(x => x.Value.gameObject.SetActive(true));
+ _pagination.gameObject.SetActive(false);
+ return;
+ }
+
+ _pagination.gameObject.SetActive(true);
+ if (_pageText != null)
+ {
+ _pageText.text = $"Page {_currentPage + 1}/{Mathf.Max(totalPages, 1)}";
+ }
+
+ _nextButton.gameObject.SetActive(true);
+ _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 modifiers = _modifiers.ToArray();
+
+ for (var i = start; i < end; i++)
+ {
+ modifiers[i].Value.gameObject.SetActive(true);
+ }
+
+ _pagination.SetAsLastSibling();
+ }
+
+ [HideFromIl2Cpp]
+ internal void UpdateModifiersList(List modifiers)
+ {
+ var filteredModifiers = modifiers.Where(x => !x.HideOnUi && x.GetDescription() != string.Empty).ToList();
+ var modifiersToAdd = filteredModifiers.ToList();
+
+ foreach (var mod in _modifiers.ToArray())
+ {
+ if (modifiersToAdd.Contains(mod.Key))
+ {
+ modifiersToAdd.Remove(mod.Key);
+ continue;
+ }
+
+ mod.Value.gameObject.Destroy();
+ _modifiers.Remove(mod.Key);
+ }
+
+ _toggleBtnText.text = $"Modifiers ({filteredModifiers.Count})";
+
+ if (filteredModifiers.Count == 0)
+ {
+ _toggleButton.gameObject.SetActive(false);
+ return;
+ }
+
+ _toggleButton.gameObject.SetActive(true);
+
+ foreach (var mod in modifiersToAdd)
+ {
+ _modifiers.Add(mod, CreateForModifier(mod));
+ }
+
+ UpdatePage();
+ }
+}
diff --git a/MiraAPI/Modifiers/ModifierDisplay/ModifierUiComponent.cs b/MiraAPI/Modifiers/ModifierDisplay/ModifierUiComponent.cs
new file mode 100644
index 0000000..a17dcc3
--- /dev/null
+++ b/MiraAPI/Modifiers/ModifierDisplay/ModifierUiComponent.cs
@@ -0,0 +1,71 @@
+using Il2CppInterop.Runtime.Attributes;
+using Reactor.Utilities.Attributes;
+using TMPro;
+using UnityEngine;
+
+namespace MiraAPI.Modifiers.ModifierDisplay;
+
+///
+/// The code placed on every Modifier HUD object. Used to handle updating.
+///
+[RegisterInIl2Cpp]
+public class ModifierUiComponent(nint ptr) : MonoBehaviour(ptr)
+{
+ ///
+ /// Gets the modifier which this component is for.
+ ///
+ [HideFromIl2Cpp]
+ public BaseModifier? Modifier { get; internal set; }
+
+ private SpriteRenderer modBg = null!;
+ private TextMeshPro desc = null!;
+ private SpriteRenderer icon = null!;
+ private TextMeshPro nameText = null!;
+ private RectTransform descRect = null!;
+
+ private void Awake()
+ {
+ if (Modifier == null) return;
+
+ modBg = transform.GetChild(0).GetComponent();
+ desc = modBg.transform.GetChild(0).GetComponent();
+ icon = modBg.transform.GetChild(1).GetComponent();
+ var nameBg = transform.GetChild(1);
+ nameText = nameBg.GetChild(0).GetComponent();
+ descRect = desc.gameObject.GetComponent();
+
+ nameText.font = desc.font = HudManager.Instance.TaskPanel.taskText.font;
+ nameText.fontMaterial = desc.fontMaterial = HudManager.Instance.TaskPanel.taskText.fontMaterial;
+ }
+
+ // Might not be the best solution, but used to update name/description/icon if updated in the modifier.
+ private void FixedUpdate()
+ {
+ if (Modifier == null || ModifierDisplayComponent.Instance == null || !ModifierDisplayComponent.Instance.IsOpen) return;
+
+ if (Modifier.HideOnUi || Modifier.GetDescription() == string.Empty)
+ {
+ ModifierDisplayComponent.Instance.DestroyComponent(this);
+ return;
+ }
+
+ nameText.text = Modifier.ModifierName;
+ desc.text = Modifier.GetDescription();
+ modBg.gameObject.SetActive(true);
+
+ icon.gameObject.SetActive(Modifier.ModifierIcon != null);
+ if (icon.gameObject && icon.gameObject.activeSelf && Modifier.ModifierIcon != null)
+ {
+ icon.sprite = Modifier.ModifierIcon?.LoadAsset();
+ icon.size = new Vector2(0.7f, 0.7f);
+ icon.tileMode = SpriteTileMode.Continuous;
+ descRect.localPosition = new Vector3(0.38f, -0.1f, -5);
+ descRect.sizeDelta = new Vector2(2.38f, 0.65f);
+ }
+ else
+ {
+ descRect.localPosition = new Vector3(0, -0.1f, -5);
+ descRect.sizeDelta = new Vector2(3.1f, 0.65f);
+ }
+ }
+}
diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs
index ca49808..c028fa3 100644
--- a/MiraAPI/Modifiers/ModifierExtensions.cs
+++ b/MiraAPI/Modifiers/ModifierExtensions.cs
@@ -180,6 +180,17 @@ public static class ModifierExtensions
return player.GetModifierComponent().HasModifier(typeId, predicate);
}
+ ///
+ /// Checks if the player has a specific modifier by its GUID.
+ ///
+ /// The PlayerControl instance.
+ /// The unique ID of the modifier.
+ /// True if the player has the modifier, false otherwise.
+ public static bool HasModifier(this PlayerControl player, Guid uniqueId)
+ {
+ return player.GetModifierComponent().HasModifier(uniqueId);
+ }
+
///
/// Clears all modifiers from a player.
///
@@ -204,17 +215,6 @@ public static class ModifierExtensions
}
}
- ///
- /// Checks if the player has a specific modifier by its GUID.
- ///
- /// The PlayerControl instance.
- /// The unique ID of the modifier.
- /// True if the player has the modifier, false otherwise.
- public static bool HasModifier(this PlayerControl player, Guid uniqueId)
- {
- return player.GetModifierComponent().HasModifier(uniqueId);
- }
-
///
/// Tries to get a modifier by its type.
///
diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs
index 5f6bbff..9f61239 100644
--- a/MiraAPI/Modifiers/ModifierManager.cs
+++ b/MiraAPI/Modifiers/ModifierManager.cs
@@ -103,68 +103,57 @@ public static class ModifierManager
{
var rand = new Random();
- foreach (var priority in PrioritiesToIdsMap.Keys.OrderByDescending(x => x))
+ // Filter and sort modifiers by descending priority.
+ var modifiers = IdToTypeModifierMap
+ .Where(x=>x.Value.IsAssignableTo(typeof(GameModifier)))
+ .Select(x => Activator.CreateInstance(x.Value) as GameModifier)
+ .OfType()
+ .Where(x => x.GetAmountPerGame() > 0 && x.GetAssignmentChance() > 0)
+ .OrderByDescending(x => x.Priority())
+ .ToArray();
+
+ foreach (var modifier in modifiers)
{
- var filteredModifiers = new List();
+ var assignments = modifier.GetAmountPerGame();
- foreach (var id in PrioritiesToIdsMap[priority])
+ var validPlayers = plrs.Where(x => IsGameModifierValid(x, modifier, modifier.TypeId)).ToList();
+ if (validPlayers.Count == 0)
{
- var mod = Activator.CreateInstance(IdToTypeModifierMap[id]) as GameModifier;
- var chance = Math.Clamp(mod!.GetAssignmentChance(), 0, 100);
- var count = mod.GetAmountPerGame();
-
- if (chance == 0 || count == 0)
- {
- continue;
- }
-
- var maxCount = plrs.Count(x => IsGameModifierValid(x, mod, id));
-
- if (maxCount == 0)
- {
- Logger.Warning($"No players are valid for {mod.ModifierName}");
- continue;
- }
-
- var num = Math.Clamp(count, 0, maxCount);
-
- for (var i = 0; i < num; i++)
- {
- var randomNum = rand.Next(100);
-
- if (randomNum < chance)
- {
- filteredModifiers.Add(id);
- }
- }
- }
-
- if (filteredModifiers.Count == 0)
- {
- Logger.Warning($"No filtered modifiers for priority {priority}");
+ Logger.Warning($"No valid players for modifier {modifier.ModifierName}");
continue;
}
- var shuffledList = filteredModifiers.Randomize();
+ assignments = Math.Min(assignments, validPlayers.Count);
+ var availablePlayers = new List(validPlayers);
- if (shuffledList.Count > plrs.Count)
+ for (var i = 0; i < assignments; i++)
{
- shuffledList = shuffledList.GetRange(0, plrs.Count);
- }
-
- foreach (var id in shuffledList)
- {
- var mod = Activator.CreateInstance(IdToTypeModifierMap[id]) as GameModifier;
- var plr = plrs.Where(x => IsGameModifierValid(x, mod!, id)).Random();
-
- if (!plr || plr == null)
+ var chance = Math.Clamp(modifier.GetAssignmentChance(), 0, 100);
+ if (rand.Next(100) >= chance)
{
- Logger.Warning($"Somehow valid players for modifier {mod!.ModifierName} disappeared");
+ continue;
}
- else
+
+ var candidates = availablePlayers
+ .Where(x => IsGameModifierValid(x, modifier, modifier.TypeId))
+ .ToList();
+
+ if (candidates.Count == 0)
{
- plr.RpcAddModifier(id);
+ Logger.Warning(
+ $"No available players for modifier {modifier.ModifierName} at assignment {i + 1}");
+ break;
}
+
+ var plr = candidates.Random();
+ if (plr == null)
+ {
+ Logger.Warning($"Valid player for modifier {modifier.ModifierName} disappeared");
+ continue;
+ }
+
+ plr.RpcAddModifier(modifier.TypeId);
+ availablePlayers.Remove(plr);
}
}
}
diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs
index abcd1f7..13f6754 100644
--- a/MiraAPI/Modifiers/Types/GameModifier.cs
+++ b/MiraAPI/Modifiers/Types/GameModifier.cs
@@ -34,7 +34,7 @@ public abstract class GameModifier : BaseModifier
public virtual bool IsModifierValidOn(RoleBehaviour role) => true;
///
- /// Determines whether the player won the game with this modifier.
+ /// Determines whether the player won the game with this modifier. The modifier with the highest priority will override other modifiers.
///
/// The reason why the game ended.
/// True if the player won, false if they lost. Return null to use the player's default win condition.
diff --git a/MiraAPI/Modifiers/Types/TimedModifier.cs b/MiraAPI/Modifiers/Types/TimedModifier.cs
index ebb0457..d1c8017 100644
--- a/MiraAPI/Modifiers/Types/TimedModifier.cs
+++ b/MiraAPI/Modifiers/Types/TimedModifier.cs
@@ -45,12 +45,12 @@ public abstract class TimedModifier : BaseModifier
public override bool HideOnUi => !TimerActive;
///
- /// Gets the HUD information for Timed Modifier, including the time remaining.
+ /// Gets the HUD description for Timed Modifier, including the time remaining.
///
- /// A string with the hud information + timer.
- public override string GetHudString()
+ /// A string with the time remaining.
+ public override string GetDescription()
{
- return base.GetHudString() + $" ({Math.Round(Duration - TimeRemaining, 0)}s/{Duration}s)";
+ return $"Remaining Time: {Math.Round(Duration - TimeRemaining, 0)}s/{Duration}s";
}
///
diff --git a/MiraAPI/Patches/Events/SpawnInMinigamePatch.cs b/MiraAPI/Patches/Events/SpawnInMinigamePatch.cs
new file mode 100644
index 0000000..b726999
--- /dev/null
+++ b/MiraAPI/Patches/Events/SpawnInMinigamePatch.cs
@@ -0,0 +1,15 @@
+using HarmonyLib;
+using MiraAPI.Events;
+using MiraAPI.Events.Vanilla.Gameplay;
+
+namespace MiraAPI.Patches.Events;
+
+[HarmonyPatch(typeof(SpawnInMinigame), nameof(SpawnInMinigame.Close))]
+internal static class SpawnInMinigamePatch
+{
+ public static void Postfix()
+ {
+ var @event = new RoundStartEvent(false);
+ MiraEventManager.InvokeEvent(@event);
+ }
+}
diff --git a/MiraAPI/Patches/Events/SubmergedSpawnGamePatch.cs b/MiraAPI/Patches/Events/SubmergedSpawnGamePatch.cs
new file mode 100644
index 0000000..d2664b9
--- /dev/null
+++ b/MiraAPI/Patches/Events/SubmergedSpawnGamePatch.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Reflection;
+using HarmonyLib;
+using MiraAPI.Events;
+using MiraAPI.Events.Vanilla.Gameplay;
+
+namespace MiraAPI.Patches.Events;
+
+[HarmonyPatch]
+internal static class SubmergedSpawnGamePatch
+{
+ public static bool Prepare()
+ {
+ return ModCompatibility.SubmergedLoaded(out _);
+ }
+
+ public static IEnumerable TargetMethods()
+ {
+ if (ModCompatibility.SubmergedLoaded(out var subAss) &&
+ subAss.GetType("Submerged.SpawnIn.SubmarineSelectSpawn") is { } selectSpawn)
+ {
+ yield return AccessTools.Method(selectSpawn, "OnDestroy");
+ }
+ }
+
+ public static void Postfix()
+ {
+ var @event = new RoundStartEvent(false);
+ MiraEventManager.InvokeEvent(@event);
+ }
+}
diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs
index 666a1ea..e06e3f0 100644
--- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs
+++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs
@@ -18,7 +18,7 @@ namespace MiraAPI.Patches.Freeplay;
[HarmonyPatch(typeof(TaskAdderGame))]
internal static class TaskAdderPatches
{
- private static Scroller? _scroller;
+ private static Scroller _scroller = null!;
[HarmonyPostfix]
[HarmonyPatch(nameof(TaskAdderGame.Begin))]
@@ -293,9 +293,9 @@ internal static class TaskAdderPatches
chip.GetComponent().maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
}
- if (_scroller && _scroller != null)
+ if (_scroller)
{
- _scroller.CalculateAndSetYBounds(__instance.ActiveItems.Count, 6, 3, 1f);
+ _scroller.CalculateAndSetYBounds(__instance.ActiveItems.Count, 6, 5, 1.4f);
_scroller.SetYBoundsMin(0.0f);
_scroller.ScrollToTop();
}
diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs
index fb6144a..d068f26 100644
--- a/MiraAPI/Patches/IntroCutscenePatches.cs
+++ b/MiraAPI/Patches/IntroCutscenePatches.cs
@@ -56,11 +56,11 @@ public static class IntroCutscenePatches
}
}
- /*
[HarmonyPostfix]
[HarmonyPatch(nameof(IntroCutscene.OnDestroy))]
- public static void GameBeginPatch()
+ public static void GameBeginPatch(IntroCutscene __instance)
{
- CustomGameModeManager.ActiveMode?.Initialize();
- }*/
+ MiraEventManager.InvokeEvent(new IntroEndEvent(__instance));
+ MiraEventManager.InvokeEvent(new RoundStartEvent(true));
+ }
}
diff --git a/MiraAPI/Patches/Menu/HatsTabPatches.cs b/MiraAPI/Patches/Menu/HatsTabPatches.cs
new file mode 100644
index 0000000..daf87b1
--- /dev/null
+++ b/MiraAPI/Patches/Menu/HatsTabPatches.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using AmongUs.Data;
+using HarmonyLib;
+using MiraAPI.Utilities;
+using Reactor.Utilities;
+using Reactor.Utilities.Extensions;
+using TMPro;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace MiraAPI.Patches.Menu;
+
+[HarmonyPatch(typeof(HatsTab))]
+public static class HatsTabPatches
+{
+ private static SortedList> sortedHats = [];
+ private static int currentPage;
+
+ [HarmonyPatch(nameof(HatsTab.OnEnable))]
+ [HarmonyPrefix]
+ public static bool OnEnablePrefix(HatsTab __instance)
+ {
+ __instance.currentHat = HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat);
+ var allHats = HatManager.Instance.GetUnlockedHats().ToImmutableList();
+
+ if (sortedHats.Count == 0)
+ {
+ var comparer = new ControllableComparer(["vanilla"], [], StringComparer.InvariantCulture);
+ sortedHats = new SortedList>(comparer);
+ foreach (var hat in allHats)
+ {
+ if (!sortedHats.ContainsKey(hat.StoreName)) sortedHats[hat.StoreName] = [];
+ sortedHats[hat.StoreName].Add(hat);
+ }
+ }
+
+ GenerateHats(__instance, currentPage);
+
+ return false;
+ }
+
+ [HarmonyPatch(typeof(HatsTab), nameof(HatsTab.Update))]
+ [HarmonyPrefix]
+
+ public static void UpdatePrefix(HatsTab __instance)
+ {
+ if (sortedHats.Count == 0) return;
+
+ if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
+ {
+ currentPage--;
+ currentPage = currentPage < 0 ? sortedHats.Count - 1 : currentPage;
+ GenerateHats(__instance, currentPage);
+ }
+ else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
+ {
+ currentPage++;
+ currentPage = currentPage > sortedHats.Count - 1 ? 0 : currentPage;
+ GenerateHats(__instance, currentPage);
+ }
+ }
+
+ private static IEnumerator? loadRoutine;
+ private static int hatIndex;
+
+ private static void GenerateHats(HatsTab __instance, int page)
+ {
+ if (loadRoutine != null) Coroutines.Stop(loadRoutine);
+
+ hatIndex = 0;
+ foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
+ __instance.ColorChips.Clear();
+ __instance.scroller.Inner.GetComponentsInChildren().Do(x => x.gameObject.Destroy());
+
+ var groupNameText = __instance.GetComponentInChildren(false);
+
+ var (groupName, hats) = sortedHats.ToArray()[page];
+ var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
+ text.enabled = true;
+ text.gameObject.transform.localScale = Vector3.one;
+ text.GetComponent().Destroy();
+ text.EnableStencilMasking();
+ text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages";
+ text.alignment = TextAlignmentOptions.Center;
+ text.fontSize = 3f;
+ text.fontSizeMax = 3f;
+ text.fontSizeMin = 0f;
+ var xLerp = __instance.XRange.Lerp(0.5f);
+ var yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
+ text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
+
+ hatIndex += 5;
+ loadRoutine = Coroutines.Start(CoGenerateChips(__instance, hats));
+ }
+
+ private static IEnumerator CoGenerateChips(HatsTab __instance, List hats)
+ {
+ __instance.scroller.ScrollToTop();
+ var batchSize = 5;
+ for (var i = 0; i < hats.Count; i += batchSize)
+ {
+ var batch = hats.Skip(i).Take(batchSize).ToList();
+
+ foreach (var hat in batch.OrderBy(HatManager.Instance.allHats.IndexOf))
+ {
+ var hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
+ var hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
+ GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), hat);
+ hatIndex += 1;
+ yield return null;
+ }
+
+ __instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
+ yield return new WaitForSeconds(0.01f);
+ }
+ __instance.currentHatIsEquipped = true;
+ loadRoutine = null;
+ }
+
+ private static void GenerateColorChip(HatsTab __instance, Vector2 position, HatData hat)
+ {
+ var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
+ colorChip.gameObject.name = hat.ProductId;
+ colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
+ colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectHat(hat)));
+ colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectHat(HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat))));
+ colorChip.Inner.SetHat(hat, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
+ colorChip.Button.ClickMask = __instance.scroller.Hitbox;
+ colorChip.SelectionHighlight.gameObject.SetActive(false);
+ __instance.UpdateMaterials(colorChip.Inner.FrontLayer, hat);
+ colorChip.Inner.SetMaskType(PlayerMaterial.MaskType.SimpleUI);
+ colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
+ colorChip.Inner.transform.localPosition = hat.ChipOffset + new Vector2(0f, -0.3f);
+ colorChip.Tag = hat;
+ __instance.ColorChips.Add(colorChip);
+ }
+}
diff --git a/MiraAPI/Patches/Menu/MainMenuManagerPatches.cs b/MiraAPI/Patches/Menu/MainMenuManagerPatches.cs
new file mode 100644
index 0000000..6f44ac5
--- /dev/null
+++ b/MiraAPI/Patches/Menu/MainMenuManagerPatches.cs
@@ -0,0 +1,21 @@
+using HarmonyLib;
+using MiraAPI.Utilities.Assets;
+
+namespace MiraAPI.Patches.Menu;
+
+///
+/// General MainMenuManage patches.
+///
+[HarmonyPatch(typeof(MainMenuManager))]
+public static class MainMenuManagerPatches
+{
+ ///
+ /// A postifix on Awake to load all the addressables registered.
+ ///
+ [HarmonyPatch(nameof(MainMenuManager.Awake))]
+ [HarmonyPostfix]
+ public static void AwakePostfix()
+ {
+ AddressablesLoader.LoadAll();
+ }
+}
diff --git a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs
new file mode 100644
index 0000000..fee0ac7
--- /dev/null
+++ b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs
@@ -0,0 +1,118 @@
+using AmongUs.Data;
+using HarmonyLib;
+using Innersloth.Assets;
+using MiraAPI.Utilities;
+using Reactor.Utilities.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using TMPro;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace MiraAPI.Patches.Menu;
+
+[HarmonyPatch(typeof(NameplatesTab))]
+public static class NameplatesTabPatches
+{
+ private static readonly SortedList> SortedNameplates = new(new ControllableComparer(["vanilla"], [], StringComparer.InvariantCulture));
+ private static int currentPage;
+ internal static void AddRange(IEnumerable<(string Key, NamePlateData Visor)> data)
+ {
+ foreach (var item in data)
+ {
+ if (!SortedNameplates.ContainsKey(item.Key)) SortedNameplates.Add(item.Key, []);
+ SortedNameplates[item.Key].Add(item.Visor);
+ }
+ }
+
+ [HarmonyPatch(nameof(NameplatesTab.OnEnable))]
+ [HarmonyPrefix]
+ public static bool OnEnablePrefix(NameplatesTab __instance)
+ {
+ __instance.plateId = HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.namePlate).ProdId;
+
+ if (!SortedNameplates.ContainsKey("Vanilla")) AddRange(DestroyableSingleton.Instance.GetUnlockedNamePlates().Select(x => ("Vanilla", x)));
+ GenerateHats(__instance, currentPage);
+
+ return false;
+ }
+
+ [HarmonyPatch(typeof(NameplatesTab), nameof(NameplatesTab.Update))]
+ [HarmonyPrefix]
+
+ public static void UpdatePrefix(NameplatesTab __instance)
+ {
+ if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
+ {
+ currentPage--;
+ currentPage = currentPage < 0 ? SortedNameplates.Count - 1 : currentPage;
+ GenerateHats(__instance, currentPage);
+ }
+ else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
+ {
+ currentPage++;
+ currentPage = currentPage > SortedNameplates.Count - 1 ? 0 : currentPage;
+ GenerateHats(__instance, currentPage);
+ }
+ }
+
+ private static void GenerateHats(NameplatesTab __instance, int page)
+ {
+ foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
+ __instance.ColorChips.Clear();
+ __instance.scroller.Inner.GetComponentsInChildren().Do(x => x.gameObject.Destroy());
+
+ var groupNameText = __instance.GetComponentInChildren(false);
+
+ int hatIndex = 0;
+
+ var (groupName, nameplates) = SortedNameplates.ToArray()[page];
+ var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
+ text.enabled = true;
+ text.gameObject.transform.localScale = Vector3.one;
+ text.GetComponent().Destroy();
+ text.EnableStencilMasking();
+ text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages";
+ text.alignment = TextAlignmentOptions.Center;
+ text.fontSize = 3f;
+ text.fontSizeMax = 3f;
+ text.fontSizeMin = 0f;
+ float xLerp = __instance.XRange.Lerp(0.5f);
+ float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
+ text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
+
+ hatIndex += 2;
+ foreach (var visor in nameplates.OrderBy(HatManager.Instance.allNamePlates.IndexOf))
+ {
+ float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
+ float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
+ GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), visor);
+ hatIndex += 1;
+ }
+
+ __instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
+ __instance.currentNameplateIsEquipped = true;
+ }
+
+ private static void GenerateColorChip(NameplatesTab __instance, Vector2 position, NamePlateData namePlate)
+ {
+ var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
+ colorChip.gameObject.name = namePlate.ProductId;
+ colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
+ colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectNameplate(namePlate)));
+ colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectNameplate(HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.NamePlate))));
+ colorChip.Button.ClickMask = __instance.scroller.Hitbox;
+ colorChip.ProductId = namePlate.ProdId;
+
+ var x = (NamePlateViewData viewdata) =>
+ {
+ colorChip.transform.GetChild(1).GetComponent().sprite = viewdata?.Image;
+ //(colorChip as NameplateChip).image.sprite = viewdata?.Image;
+ };
+ __instance.StartCoroutine(AddressableAssetExtensions.CoLoadAssetAsync(__instance, namePlate.GetAssetReference(), x));
+ colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
+ colorChip.Tag = namePlate;
+ __instance.ColorChips.Add(colorChip);
+ }
+}
diff --git a/MiraAPI/Patches/Menu/SkinsTabPatches.cs b/MiraAPI/Patches/Menu/SkinsTabPatches.cs
new file mode 100644
index 0000000..b102fb1
--- /dev/null
+++ b/MiraAPI/Patches/Menu/SkinsTabPatches.cs
@@ -0,0 +1,118 @@
+using AmongUs.Data;
+using HarmonyLib;
+using MiraAPI.Utilities;
+using Reactor.Utilities.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using TMPro;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace MiraAPI.Patches.Menu;
+
+[HarmonyPatch(typeof(SkinsTab))]
+public static class SkinsTabPatches
+{
+ private static SortedList> sortedSkins = [];
+ private static int currentPage;
+
+ [HarmonyPatch(nameof(SkinsTab.OnEnable))]
+ [HarmonyPrefix]
+ public static bool OnEnablePrefix(SkinsTab __instance)
+ {
+ __instance.skinId = HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin).ProdId;
+ var allSkins = HatManager.Instance.GetUnlockedSkins().ToImmutableList();
+
+ if (sortedSkins.Count == 0)
+ {
+ var comparer = new ControllableComparer(["vanilla"], [], StringComparer.InvariantCulture);
+ sortedSkins = new SortedList>(comparer);
+ foreach (var skin in allSkins)
+ {
+ if (!sortedSkins.ContainsKey(skin.StoreName)) sortedSkins[skin.StoreName] = [];
+ sortedSkins[skin.StoreName].Add(skin);
+ }
+ }
+
+ GenerateHats(__instance, currentPage);
+
+ return false;
+ }
+
+ [HarmonyPatch(typeof(SkinsTab), nameof(SkinsTab.Update))]
+ [HarmonyPrefix]
+
+ public static void UpdatePrefix(SkinsTab __instance)
+ {
+ if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
+ {
+ currentPage--;
+ currentPage = currentPage < 0 ? sortedSkins.Count - 1 : currentPage;
+ GenerateHats(__instance, currentPage);
+ }
+ else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
+ {
+ currentPage++;
+ currentPage = currentPage > sortedSkins.Count - 1 ? 0 : currentPage;
+ GenerateHats(__instance, currentPage);
+ }
+ }
+
+ private static void GenerateHats(SkinsTab __instance, int page)
+ {
+ foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
+ __instance.ColorChips.Clear();
+ __instance.scroller.Inner.GetComponentsInChildren().Do(x => x.gameObject.Destroy());
+
+ var groupNameText = __instance.GetComponentInChildren(false);
+
+ int hatIndex = 0;
+
+ var (groupName, skins) = sortedSkins.ToArray()[page];
+ var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
+ text.enabled = true;
+ text.gameObject.transform.localScale = Vector3.one;
+ text.GetComponent().Destroy();
+ text.EnableStencilMasking();
+ text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages";
+ text.alignment = TextAlignmentOptions.Center;
+ text.fontSize = 3f;
+ text.fontSizeMax = 3f;
+ text.fontSizeMin = 0f;
+ float xLerp = __instance.XRange.Lerp(0.5f);
+ float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
+ text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
+
+ hatIndex += 5;
+ foreach (var skin in skins.OrderBy(HatManager.Instance.allSkins.IndexOf))
+ {
+ float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
+ float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
+ GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), skin);
+ hatIndex += 1;
+ }
+
+ __instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
+ __instance.currentSkinIsEquipped = true;
+ }
+
+ private static void GenerateColorChip(SkinsTab __instance, Vector2 position, SkinData skin)
+ {
+ var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
+ colorChip.gameObject.name = skin.ProductId;
+ colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
+ colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectSkin(skin)));
+ colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectSkin(HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin))));
+ colorChip.Button.ClickMask = __instance.scroller.Hitbox;
+ colorChip.ProductId = skin.ProductId;
+ colorChip.SelectionHighlight.gameObject.SetActive(false);
+ __instance.UpdateMaterials(colorChip.Inner.FrontLayer, skin);
+ skin.SetPreview(colorChip.Inner.FrontLayer, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
+ colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
+ colorChip.Inner.transform.localPosition = skin.ChipOffset + new Vector2(0f, -0.3f);
+ colorChip.Tag = skin;
+ __instance.ColorChips.Add(colorChip);
+ }
+}
diff --git a/MiraAPI/Patches/Menu/VisorsTabPatches.cs b/MiraAPI/Patches/Menu/VisorsTabPatches.cs
new file mode 100644
index 0000000..81845c6
--- /dev/null
+++ b/MiraAPI/Patches/Menu/VisorsTabPatches.cs
@@ -0,0 +1,114 @@
+using AmongUs.Data;
+using HarmonyLib;
+using MiraAPI.Utilities;
+using Reactor.Utilities.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using TMPro;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace MiraAPI.Patches.Menu;
+
+[HarmonyPatch(typeof(VisorsTab))]
+public static class VisorsTabPatches
+{
+ private static readonly SortedList> SortedVisors = new(new ControllableComparer(["vanilla"], [], StringComparer.InvariantCulture));
+ private static int currentPage;
+ internal static void AddRange(IEnumerable<(string Key, VisorData Visor)> data)
+ {
+ foreach (var item in data)
+ {
+ if (!SortedVisors.ContainsKey(item.Key)) SortedVisors.Add(item.Key, []);
+ SortedVisors[item.Key].Add(item.Visor);
+ }
+ }
+
+ [HarmonyPatch(nameof(VisorsTab.OnEnable))]
+ [HarmonyPrefix]
+ public static bool OnEnablePrefix(VisorsTab __instance)
+ {
+ __instance.visorId = HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor).ProdId;
+
+ if (!SortedVisors.ContainsKey("Vanilla")) AddRange(DestroyableSingleton.Instance.GetUnlockedVisors().Select(x => ("Vanilla", x)));
+ GenerateHats(__instance, currentPage);
+
+ return false;
+ }
+
+ [HarmonyPatch(typeof(VisorsTab), nameof(VisorsTab.Update))]
+ [HarmonyPrefix]
+
+ public static void UpdatePrefix(VisorsTab __instance)
+ {
+ if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
+ {
+ currentPage--;
+ currentPage = currentPage < 0 ? SortedVisors.Count - 1 : currentPage;
+ GenerateHats(__instance, currentPage);
+ }
+ else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
+ {
+ currentPage++;
+ currentPage = currentPage > SortedVisors.Count - 1 ? 0 : currentPage;
+ GenerateHats(__instance, currentPage);
+ }
+ }
+
+ private static void GenerateHats(VisorsTab __instance, int page)
+ {
+ foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
+ __instance.ColorChips.Clear();
+ __instance.scroller.Inner.GetComponentsInChildren().Do(x => x.gameObject.Destroy());
+
+ var groupNameText = __instance.GetComponentInChildren(false);
+
+ int hatIndex = 0;
+
+ var (groupName, visors) = SortedVisors.ToArray()[page];
+ var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
+ text.enabled = true;
+ text.gameObject.transform.localScale = Vector3.one;
+ text.GetComponent().Destroy();
+ text.EnableStencilMasking();
+ text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages";
+ text.alignment = TextAlignmentOptions.Center;
+ text.fontSize = 3f;
+ text.fontSizeMax = 3f;
+ text.fontSizeMin = 0f;
+ float xLerp = __instance.XRange.Lerp(0.5f);
+ float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
+ text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
+
+ hatIndex += 5;
+ foreach (var visor in visors.OrderBy(HatManager.Instance.allVisors.IndexOf))
+ {
+ float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
+ float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
+ GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), visor);
+ hatIndex += 1;
+ }
+
+ __instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
+ __instance.currentVisorIsEquipped = true;
+ }
+
+ private static void GenerateColorChip(VisorsTab __instance, Vector2 position, VisorData visor)
+ {
+ var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
+ colorChip.gameObject.name = visor.ProductId;
+ colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
+ colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectVisor(visor)));
+ colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectVisor(HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor))));
+ colorChip.Button.ClickMask = __instance.scroller.Hitbox;
+ colorChip.ProductId = visor.ProductId;
+ colorChip.SelectionHighlight.gameObject.SetActive(false);
+ __instance.UpdateMaterials(colorChip.Inner.FrontLayer, visor);
+ visor.SetPreview(colorChip.Inner.FrontLayer, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
+ colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
+ colorChip.Inner.transform.localPosition = visor.ChipOffset + new Vector2(0f, -0.3f);
+ colorChip.Tag = visor;
+ __instance.ColorChips.Add(colorChip);
+ }
+}
diff --git a/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs b/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs
index a86383d..eee933c 100644
--- a/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs
+++ b/MiraAPI/Patches/Modifiers/EndGameDidWinPatch.cs
@@ -1,40 +1,63 @@
-using System.Linq;
+using System.Collections.Generic;
+using System.Linq;
using HarmonyLib;
using MiraAPI.Modifiers;
using MiraAPI.Modifiers.Types;
namespace MiraAPI.Patches.Modifiers;
-[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.CoEndGame))]
-public static class EndGameDidWinPatch
+[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnGameEnd))]
+internal static class EndGameDidWinPatch
{
- public static void Prefix()
+ private static List CachedWinners { get; set; } = [];
+
+ public static void Prefix(EndGameResult endGameResult)
{
- var gameOverReason = EndGameResult.CachedGameOverReason;
- EndGameResult.CachedWinners.Clear();
+ CachedWinners.Clear();
+ var gameOverReason = endGameResult.GameOverReason;
var players = GameData.Instance.AllPlayers.ToArray();
for (var i = 0; i < GameData.Instance.PlayerCount; i++)
{
var networkedPlayerInfo = players[i];
- if (networkedPlayerInfo == null)
+ if (!networkedPlayerInfo)
{
continue;
}
var didWin = networkedPlayerInfo.Role.DidWin(gameOverReason);
- var modifierWin = networkedPlayerInfo.Object?.GetModifierComponent().GetModifiers()
- .FirstOrDefault(x => x.DidWin(gameOverReason) != null)?.DidWin(gameOverReason);
- if (modifierWin != null)
+ if (!networkedPlayerInfo.Object)
{
- didWin = modifierWin.Value;
+ if (didWin)
+ {
+ CachedWinners.Add(new CachedPlayerData(networkedPlayerInfo));
+ }
+ continue;
+ }
+
+ foreach (var modifier in networkedPlayerInfo.Object.GetModifiers().OrderByDescending(x => x.Priority()))
+ {
+ var result = modifier.DidWin(gameOverReason);
+ if (!result.HasValue) continue;
+
+ didWin = result.Value;
+ break;
}
if (didWin)
{
- EndGameResult.CachedWinners.Add(new CachedPlayerData(networkedPlayerInfo));
+ CachedWinners.Add(new CachedPlayerData(networkedPlayerInfo));
}
}
}
+
+ public static void Postfix(EndGameResult endGameResult)
+ {
+ EndGameResult.CachedWinners.Clear();
+ foreach (var winner in CachedWinners)
+ {
+ EndGameResult.CachedWinners.Add(winner);
+ }
+ }
}
diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
index 43c5719..8182aa7 100644
--- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
+++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
@@ -250,14 +250,9 @@ public static class LobbyViewPanePatches
var list = new List();
- var roleGroups = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values.OfType()
+ var roleGroups = SelectedMod.CustomRoles.Values.OfType()
.ToLookup(x => x.RoleOptionsGroup);
- if (roleGroups is null)
- {
- return;
- }
-
// sort the groups by priority
var sortedRoleGroups = roleGroups
.OrderBy(x => x.Key.Priority)
@@ -265,7 +260,7 @@ public static class LobbyViewPanePatches
foreach (var grouping in sortedRoleGroups)
{
- if (!grouping.Any())
+ if (!grouping.Any() || grouping.All(x => x.Configuration.HideSettings))
{
continue;
}
@@ -340,7 +335,7 @@ public static class LobbyViewPanePatches
viewSettingsInfoPanelRoleVariant.chanceTitle.color =
viewSettingsInfoPanelRoleVariant.chanceBackground.color =
viewSettingsInfoPanelRoleVariant.background.color =
- customRole.RoleColor.GetAlternateColor();
+ customRole.RoleColor.FindAlternateColor();
instance.settingsInfo.Add(viewSettingsInfoPanelRoleVariant.gameObject);
num -= 0.664f;
}
diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
index 9f2d431..bb41850 100644
--- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
+++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
@@ -83,10 +83,9 @@ public static class RoleSettingMenuPatches
var quotaThing = __instance.categoryHeaderEditRoleOrigin.transform.FindChild("QuotaHeader");
var template = __instance.transform.parent.parent.GetComponent().GameSettingsTab.categoryHeaderOrigin;
-
foreach (var grouping in sortedRoleGroups)
{
- if (!grouping.Any())
+ if (!grouping.Any() || grouping.All(x=>x.Configuration.HideSettings))
{
continue;
}
diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs
index feb2921..0f53f9f 100644
--- a/MiraAPI/Patches/PlayerControlPatches.cs
+++ b/MiraAPI/Patches/PlayerControlPatches.cs
@@ -15,14 +15,11 @@ namespace MiraAPI.Patches;
/// General patches for the PlayerControl class.
///
[HarmonyPatch(typeof(PlayerControl))]
-public static class PlayerControlPatches
+internal static class PlayerControlPatches
{
- ///
- /// Adds the modifier component to the player on start.
- ///
- /// PlayerControl instance.
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.Start))]
+ // ReSharper disable once InconsistentNaming
public static void PlayerControlStartPostfix(PlayerControl __instance)
{
if (__instance.gameObject.TryGetComponent(out var modifierComp))
@@ -39,12 +36,9 @@ public static class PlayerControlPatches
__instance.gameObject.AddComponent();
}
- ///
- /// Calls the OnDeath method for all active modifiers.
- ///
- /// PlayerControl instance.
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.Die))]
+ // ReSharper disable once InconsistentNaming
public static void PlayerControlDiePostfix(PlayerControl __instance, DeathReason reason)
{
var deathEvent = new PlayerDeathEvent(__instance, reason);
@@ -58,13 +52,9 @@ public static class PlayerControlPatches
}
}
- ///
- /// Used to trigger the .
- ///
- /// PlayerControl instance.
- /// The task id.
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.CompleteTask))]
+ // ReSharper disable once InconsistentNaming
public static void PlayerCompleteTaskPostfix(PlayerControl __instance, uint idx)
{
var playerTask = __instance.myTasks.ToArray().First(playerTask => playerTask.Id == idx);
@@ -75,14 +65,9 @@ public static class PlayerControlPatches
}
}
- ///
- /// Used to trigger the .
- ///
- /// The source player.
- /// The target.
- /// Whether the kill succeeded.
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerControl.RpcMurderPlayer))]
+ // ReSharper disable once InconsistentNaming
public static void PlayerControlMurderPrefix(PlayerControl __instance, PlayerControl target, ref bool didSucceed)
{
var beforeMurderEvent = new BeforeMurderEvent(__instance, target);
@@ -91,12 +76,9 @@ public static class PlayerControlPatches
didSucceed = beforeMurderEvent.IsCancelled;
}
- ///
- /// FixedUpdate handler for custom roles and custom buttons.
- ///
- /// PlayerControl instance.
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.FixedUpdate))]
+ // ReSharper disable once InconsistentNaming
public static void PlayerControlFixedUpdatePostfix(PlayerControl __instance)
{
if (__instance.Data?.Role is ICustomRole customRole)
@@ -124,15 +106,4 @@ public static class PlayerControlPatches
button.FixedUpdateHandler(__instance);
}
}
-
- ///
- /// Clear from modifier component cache.
- ///
- /// PlayerControl instance.
- [HarmonyPrefix]
- [HarmonyPatch(nameof(PlayerControl.OnDestroy))]
- public static void PlayerControlOnDestroyPrefix(PlayerControl __instance)
- {
- ModifierExtensions.ModifierComponents.Remove(__instance);
- }
}
diff --git a/MiraAPI/Patches/Roles/EjectionPatches.cs b/MiraAPI/Patches/Roles/EjectionPatches.cs
index 48d6abb..73bf0a7 100644
--- a/MiraAPI/Patches/Roles/EjectionPatches.cs
+++ b/MiraAPI/Patches/Roles/EjectionPatches.cs
@@ -1,5 +1,8 @@
-using HarmonyLib;
+using System.Collections.Generic;
+using System.Reflection;
+using HarmonyLib;
using MiraAPI.Events;
+using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Events.Vanilla.Meeting;
using MiraAPI.Roles;
@@ -9,7 +12,7 @@ namespace MiraAPI.Patches.Roles;
/// Patches for custom ejection messages.
///
[HarmonyPatch(typeof(ExileController))]
-public static class EjectionPatches
+internal static class EjectionPatches
{
[HarmonyPostfix]
[HarmonyPatch(nameof(ExileController.Begin))]
@@ -24,11 +27,21 @@ public static class EjectionPatches
return;
}
- if (role.GetCustomEjectionMessage(__instance.initData.networkedPlayer) == null)
+ if (!GameManager.Instance.LogicOptions.GetConfirmImpostor() || role.GetCustomEjectionMessage(__instance.initData.networkedPlayer) == null)
{
return;
}
__instance.completeString = role.GetCustomEjectionMessage(__instance.initData.networkedPlayer);
}
+
+ [HarmonyPatch(typeof(ExileController), nameof(ExileController.WrapUp))]
+ public static class WrapUpPatch
+ {
+ public static void Postfix()
+ {
+ var @event = new RoundStartEvent(false);
+ MiraEventManager.InvokeEvent(@event);
+ }
+ }
}
diff --git a/MiraAPI/Patches/Roles/RoleManagerPatches.cs b/MiraAPI/Patches/Roles/RoleManagerPatches.cs
index f326239..57512e3 100644
--- a/MiraAPI/Patches/Roles/RoleManagerPatches.cs
+++ b/MiraAPI/Patches/Roles/RoleManagerPatches.cs
@@ -5,18 +5,58 @@ using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
+using UnityEngine;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(RoleManager))]
public static class RoleManagerPatches
{
- [HarmonyPostfix]
+ [HarmonyPrefix]
[HarmonyPatch(nameof(RoleManager.SetRole))]
- public static void SetRolePatch(RoleManager __instance, PlayerControl targetPlayer, RoleTypes roleType)
+ public static bool SetRolePatch(RoleManager __instance, PlayerControl targetPlayer, RoleTypes roleType)
{
+ if (!targetPlayer)
+ {
+ return false;
+ }
+ var data = targetPlayer.Data;
+ if (data == null)
+ {
+ Debug.LogError("It shouldn't be possible, but " + targetPlayer.name + " still doesn't have PlayerData during role selection.");
+ return false;
+ }
+ if (data.Role)
+ {
+ data.Role.Deinitialize(targetPlayer);
+ Object.Destroy(data.Role.gameObject);
+ }
+ var roleBehaviour = Object.Instantiate(__instance.AllRoles.First(r => r.Role == roleType), data.gameObject.transform);
+ roleBehaviour.Initialize(targetPlayer);
+ targetPlayer.Data.Role = roleBehaviour;
+ targetPlayer.Data.RoleType = roleType;
+
+ // basically everything is the same except this one if statement
+ // innersloth decided to check the roleType manually instead of the role behaviour.
+ if (!roleBehaviour.IsDead)
+ {
+ targetPlayer.Data.RoleWhenAlive = new Il2CppSystem.Nullable(roleType);
+ }
+ roleBehaviour.AdjustTasks(targetPlayer);
+ switch (roleBehaviour.IsDead)
+ {
+ case true when !targetPlayer.Data.IsDead:
+ targetPlayer.Die(DeathReason.Kill, false);
+ return false;
+ case false when targetPlayer.Data.IsDead:
+ targetPlayer.Revive();
+ break;
+ }
+
var @event = new SetRoleEvent(targetPlayer, roleType);
MiraEventManager.InvokeEvent(@event);
+
+ return false;
}
[HarmonyPostfix]
diff --git a/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs b/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs
index 0248aaa..fe04962 100644
--- a/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs
+++ b/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs
@@ -4,6 +4,9 @@ using Il2CppSystem.Text;
namespace MiraAPI.Patches.Stubs;
+///
+/// Stub methods for the RoleBehaviour class. Needed because of Il2Cpp limitations on injected classes.
+///
[HarmonyPatch]
public static class RoleBehaviourStubs
{
@@ -57,7 +60,7 @@ public static class RoleBehaviourStubs
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool CanUse(RoleBehaviour instance, IUsable console)
{
- return default;
+ return false;
}
///
@@ -84,7 +87,7 @@ public static class RoleBehaviourStubs
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool DidWin(RoleBehaviour instance, GameOverReason gameOverReason)
{
- return default;
+ return false;
}
///
@@ -97,7 +100,7 @@ public static class RoleBehaviourStubs
[MethodImpl(MethodImplOptions.NoInlining)]
public static PlayerControl FindClosestTarget(RoleBehaviour instance)
{
- return null;
+ return null!;
}
///
@@ -110,7 +113,7 @@ public static class RoleBehaviourStubs
[MethodImpl(MethodImplOptions.NoInlining)]
public static float GetAbilityDistance(RoleBehaviour instance)
{
- return default;
+ return 0;
}
///
@@ -124,7 +127,7 @@ public static class RoleBehaviourStubs
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsValidTarget(RoleBehaviour instance, NetworkedPlayerInfo target)
{
- return default;
+ return false;
}
///
diff --git a/MiraAPI/Patches/VentPatches.cs b/MiraAPI/Patches/VentCanUsePatch.cs
similarity index 78%
rename from MiraAPI/Patches/VentPatches.cs
rename to MiraAPI/Patches/VentCanUsePatch.cs
index 2f8991d..b285411 100644
--- a/MiraAPI/Patches/VentPatches.cs
+++ b/MiraAPI/Patches/VentCanUsePatch.cs
@@ -8,17 +8,13 @@ using UnityEngine;
namespace MiraAPI.Patches;
///
-/// Vent patches to make sure the player is able to use the vent.
+/// Used to change vent behaviour for the event system, custom roles, and modifiers.
///
-[HarmonyPatch(typeof(Vent))]
-public static class VentPatches
+[HarmonyPatch(typeof(Vent), nameof(Vent.CanUse))]
+internal static class VentCanUsePatch
{
- ///
- /// CanUse patch.
- ///
- [HarmonyPostfix]
- [HarmonyPatch(nameof(Vent.CanUse))]
- public static void VentCanUsePostfix(Vent __instance, ref float __result, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] ref bool canUse, [HarmonyArgument(2)] ref bool couldUse)
+ // ReSharper disable InconsistentNaming
+ public static void Postfix(Vent __instance, ref float __result, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] ref bool canUse, [HarmonyArgument(2)] ref bool couldUse)
{
var @event = new PlayerCanUseEvent(__instance.Cast());
MiraEventManager.InvokeEvent(@event);
diff --git a/MiraAPI/Patches/Voting/MeetingHudPatches.cs b/MiraAPI/Patches/Voting/MeetingHudPatches.cs
index 6070b1e..316fc7f 100644
--- a/MiraAPI/Patches/Voting/MeetingHudPatches.cs
+++ b/MiraAPI/Patches/Voting/MeetingHudPatches.cs
@@ -1,15 +1,18 @@
-using System.Linq;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Meeting;
using MiraAPI.Events.Vanilla.Meeting.Voting;
+using MiraAPI.Modifiers;
using MiraAPI.Utilities;
using MiraAPI.Voting;
namespace MiraAPI.Patches.Voting;
[HarmonyPatch(typeof(MeetingHud))]
+[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony Convention")]
internal static class MeetingHudPatches
{
[HarmonyPostfix]
@@ -26,11 +29,62 @@ internal static class MeetingHudPatches
{
voteData.SetRemainingVotes(0);
}
+
+ foreach (var modifier in plr.GetModifierComponent().ActiveModifiers)
+ {
+ modifier.OnMeetingStart();
+ }
}
+
var @event = new StartMeetingEvent(__instance);
MiraEventManager.InvokeEvent(@event);
}
+ [HarmonyPostfix]
+ [HarmonyPatch(nameof(MeetingHud.OnDestroy))]
+ public static void OnDestroyPatch(MeetingHud __instance)
+ {
+ MiraEventManager.InvokeEvent(new EndMeetingEvent(__instance));
+ }
+
+ [HarmonyPostfix]
+ [HarmonyPatch(nameof(MeetingHud.VotingComplete))]
+ public static void VotingCompletePatch(MeetingHud __instance)
+ {
+ MiraEventManager.InvokeEvent(new VotingCompleteEvent(__instance));
+ }
+
+ // this is necessary because the actual ForceSkipAll method is inlined.
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(MeetingHud.Update))]
+ public static void ForceSkipPatch(MeetingHud __instance)
+ {
+ if (__instance.state is not (MeetingHud.VoteStates.NotVoted or MeetingHud.VoteStates.Voted))
+ {
+ return;
+ }
+
+ var logicOptionsNormal = GameManager.Instance.LogicOptions.Cast();
+ var votingTime = logicOptionsNormal.GetVotingTime();
+ if (votingTime <= 0)
+ {
+ return;
+ }
+
+ var num2 = __instance.discussionTimer - logicOptionsNormal.GetDiscussionTime();
+
+ if (!AmongUsClient.Instance.AmHost || num2 < votingTime)
+ {
+ return;
+ }
+
+ foreach (var plr in PlayerControl.AllPlayerControls)
+ {
+ var voteData = plr.GetVoteData();
+ voteData.SetRemainingVotes(0);
+ }
+ }
+
[HarmonyPrefix]
[HarmonyPatch(nameof(MeetingHud.Select))]
public static bool SelectPatch(MeetingHud __instance, int suspectStateIdx)
diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs
index 885414d..12ff874 100644
--- a/MiraAPI/PluginLoading/MiraPluginManager.cs
+++ b/MiraAPI/PluginLoading/MiraPluginManager.cs
@@ -43,6 +43,9 @@ public sealed class MiraPluginManager
var info = new MiraPluginInfo(miraPlugin, pluginInfo);
var roles = new List();
+ var oldConfigSetting = info.PluginConfig.SaveOnConfigSet;
+ info.PluginConfig.SaveOnConfigSet = false;
+
foreach (var type in assembly.GetTypes())
{
if (type.GetCustomAttribute() != null)
@@ -96,6 +99,9 @@ public sealed class MiraPluginManager
RegisterColorClasses(type);
}
+ info.PluginConfig.Save();
+ info.PluginConfig.SaveOnConfigSet = oldConfigSetting;
+
info.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
QueuedRoleRegistrations.Add(info, roles);
@@ -103,7 +109,6 @@ public sealed class MiraPluginManager
Logger.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API.");
};
IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors;
- IL2CPPChainloader.Instance.Finished += MiraEventManager.SortAllHandlers;
IL2CPPChainloader.Instance.Finished += () =>
{
CustomButtonManager.Buttons = new ReadOnlyCollection(CustomButtonManager.CustomButtons);
diff --git a/MiraAPI/Resources/mirabundle-win-x86.bundle b/MiraAPI/Resources/mirabundle-win-x86.bundle
new file mode 100644
index 0000000..59afd15
Binary files /dev/null and b/MiraAPI/Resources/mirabundle-win-x86.bundle differ
diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs
index ab890fb..8aaf288 100644
--- a/MiraAPI/Roles/CustomRoleManager.cs
+++ b/MiraAPI/Roles/CustomRoleManager.cs
@@ -59,6 +59,9 @@ public static class CustomRoleManager
{
roles.ForEach(x => RoleIds.Add(x, GetNextRoleId()));
+ var oldConfigSetting = pluginInfo.PluginConfig.SaveOnConfigSet;
+ pluginInfo.PluginConfig.SaveOnConfigSet = false;
+
foreach (var roleType in roles)
{
ClassInjector.RegisterTypeInIl2Cpp(roleType);
@@ -71,6 +74,9 @@ public static class CustomRoleManager
pluginInfo.CustomRoles.Add((ushort)role.Role, role);
}
+
+ pluginInfo.PluginConfig.Save();
+ pluginInfo.PluginConfig.SaveOnConfigSet = oldConfigSetting;
}
private static RoleBehaviour? RegisterRole(Type roleType, MiraPluginInfo parentMod)
diff --git a/MiraAPI/Utilities/Assets/AddressablesLoader.cs b/MiraAPI/Utilities/Assets/AddressablesLoader.cs
new file mode 100644
index 0000000..ab75377
--- /dev/null
+++ b/MiraAPI/Utilities/Assets/AddressablesLoader.cs
@@ -0,0 +1,222 @@
+using Il2CppInterop.Runtime.Attributes;
+using MiraAPI.Patches.Menu;
+using Reactor.Utilities;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine.AddressableAssets;
+using UnityEngine.ResourceManagement.AsyncOperations;
+
+namespace MiraAPI.Utilities.Assets;
+
+///
+/// The component for loading Addressables, and cosmetics.
+///
+public static class AddressablesLoader
+{
+ private static bool _isInitialized;
+
+ private static readonly List<(string Location, string ProviderSuffix)> CatalogLocations = [];
+ private static readonly List LoadedLocations = [];
+
+ private static readonly List RegisteredHatKeys = [];
+ private static readonly List<(string, string)> RegisteredVisorKeys = [];
+ private static readonly List<(string, string)> RegisteredNameplateKeys = [];
+ private static readonly List RegisteredSkinKeys = [];
+
+ ///
+ /// Registers a specific addressables package to load asynchronously at the start of the game, when possible.
+ ///
+ /// The location, remote or otherwise, of the addressables.
+ /// The suffix of the provider for an addressables package.
+ public static void RegisterCatalog(string location, string providerSuffix = "")
+ {
+ if (_isInitialized)
+ {
+ Logger.Error("AddressablesLoader has already been initialized, cannot register more catalogs.");
+ return;
+ }
+
+ CatalogLocations.Add((location, providerSuffix));
+ }
+
+ ///
+ /// Registers a specific addressables key as only containing 's to load.
+ ///
+ /// The key/label/group for a List .
+ public static void RegisterHats(string addressablesKey)
+ {
+ if (_isInitialized)
+ {
+ Logger.Error("AddressablesLoader has already been initialized, cannot register more keys for hats.");
+ return;
+ }
+
+ RegisteredHatKeys.Add(addressablesKey);
+ }
+
+ ///
+ /// Registers a specific addressables key as only containing 's to load.
+ ///
+ /// The key/label/group for a List .
+ public static void RegisterSkins(string addressablesKey)
+ {
+ if (_isInitialized)
+ {
+ Logger.Error("AddressablesLoader has already been initialized, cannot register more keys for skins.");
+ return;
+ }
+
+ RegisteredSkinKeys.Add(addressablesKey);
+ }
+
+ ///
+ /// Registers a specific addressables key as only containing 's to load.
+ ///
+ /// The key/label/group for a List .
+ /// /// The title of the group for visors.
+ public static void RegisterNameplates(string addressablesKey, string groupTitle)
+ {
+ if (_isInitialized)
+ {
+ Logger.Error("AddressablesLoader has already been initialized, cannot register more keys for nameplates.");
+ return;
+ }
+
+ RegisteredNameplateKeys.Add((addressablesKey, groupTitle));
+ }
+
+ ///
+ /// Registers a specific addressables key as only containing 's to load.
+ ///
+ /// The key/label/group for a List .
+ /// The title of the group for visors.
+ public static void RegisterVisors(string addressablesKey, string groupTitle)
+ {
+ if (_isInitialized)
+ {
+ Logger.Error("AddressablesLoader has already been initialized, cannot register more keys for visors.");
+ return;
+ }
+
+ RegisteredVisorKeys.Add((addressablesKey, groupTitle));
+ }
+
+ internal static void LoadAll()
+ {
+ if (_isInitialized)
+ {
+ Logger.Error("AddressablesLoader has already been initialized.");
+ return;
+ }
+
+ _isInitialized = true;
+ foreach (var (location, providerSuffix) in CatalogLocations)
+ {
+ Coroutines.Start(CoLoadAddressables(location, providerSuffix));
+ }
+
+ Coroutines.Start(LoadCosmetics());
+ }
+
+ internal static IEnumerator CoLoadAddressables(string location, string suffix = "")
+ {
+ while (!AmongUsClient.Instance) yield return null;
+ // Load the local/remote content catalog
+ var catalogOperation = Addressables.LoadContentCatalog(location, suffix);
+ yield return catalogOperation;
+
+ // Check for errors
+ if (catalogOperation.Status != AsyncOperationStatus.Succeeded)
+ {
+ Logger.Error($"Failed to load catalog {location}.");
+ }
+
+ Logger.Info($"Loaded addressables {location}.");
+ LoadedLocations.Add(location);
+ }
+
+ [HideFromIl2Cpp]
+ internal static IEnumerator LoadCosmetics()
+ {
+ while (!AmongUsClient.Instance || CatalogLocations.Select(x=>x.Location).Any(x=>!LoadedLocations.Contains(x))) yield return null;
+
+ var hatBehaviours = DiscoverData(RegisteredHatKeys);
+ var skinBehaviours = DiscoverData(RegisteredSkinKeys);
+ var namePlateBehaviours = DiscoverAndReportData(RegisteredNameplateKeys);
+ var visorBehaviours = DiscoverAndReportData(RegisteredVisorKeys);
+
+ var hatData = new List();
+ hatData.AddRange(DestroyableSingleton.Instance.allHats);
+ hatData.ForEach(x => x.StoreName = "Vanilla");
+ DestroyableSingleton.Instance.allHats = PrepareArray(hatData, hatBehaviours);
+
+ var skinData = new List();
+ skinData.AddRange(DestroyableSingleton.Instance.allSkins);
+ skinData.ForEach(x => x.StoreName = "Vanilla");
+ DestroyableSingleton.Instance.allSkins = PrepareArray(skinData, skinBehaviours);
+
+ var visorData = new List();
+ visorData.AddRange(DestroyableSingleton.Instance.allVisors);
+ VisorsTabPatches.AddRange(visorBehaviours);
+ DestroyableSingleton.Instance.allVisors = PrepareArray(visorData, visorBehaviours.Select(x=>x.Data).ToList());
+
+ var namePlateData = new List();
+ namePlateData.AddRange(DestroyableSingleton.Instance.allNamePlates);
+ NameplatesTabPatches.AddRange(namePlateBehaviours);
+ DestroyableSingleton.Instance.allNamePlates = PrepareArray(namePlateData, namePlateBehaviours.Select(x => x.Data).ToList());
+ }
+
+ private static T[] PrepareArray(List data, List behaviours) where T : CosmeticData
+ {
+ var count = data.Count;
+ for (int i = 0; i < behaviours.Count; i++)
+ {
+ behaviours[i].displayOrder = count + i;
+ data.Add(behaviours[i]);
+ }
+ return data.ToArray();
+ }
+
+ private static List DiscoverData(List tags)
+ {
+ var behaviours = new List();
+
+ foreach (var tag in tags)
+ {
+ try
+ {
+ var allLocations = Addressables.LoadResourceLocationsAsync(tag).WaitForCompletion();
+ var assets = Addressables.LoadAssetsAsync(allLocations, null, false).WaitForCompletion();
+ var array = new Il2CppSystem.Collections.Generic.List(assets.Pointer);
+ behaviours.AddRange(array.ToArray());
+ }
+ catch
+ {
+ Logger.Error($"Failed to find tag {tag}");
+ }
+ }
+ return behaviours;
+ }
+
+ private static List<(string Category, T Data)> DiscoverAndReportData(List<(string Tag, string Category)> tags)
+ {
+ var behaviours = new List<(string, T)>();
+
+ foreach (var tag in tags)
+ {
+ try
+ {
+ var allLocations = Addressables.LoadResourceLocationsAsync(tag.Tag).WaitForCompletion();
+ var assets = Addressables.LoadAssetsAsync(allLocations, null, false).WaitForCompletion();
+ var array = new Il2CppSystem.Collections.Generic.List(assets.Pointer);
+ behaviours.AddRange(array.ToArray().Select(x => (tag.Category, x)));
+ }
+ catch
+ {
+ Logger.Error($"Failed to find tag {tag}");
+ }
+ }
+ return behaviours;
+ }
+}
diff --git a/MiraAPI/Utilities/Assets/LoadableAddressableAsset.cs b/MiraAPI/Utilities/Assets/LoadableAddressableAsset.cs
new file mode 100644
index 0000000..59c6de1
--- /dev/null
+++ b/MiraAPI/Utilities/Assets/LoadableAddressableAsset.cs
@@ -0,0 +1,55 @@
+using System;
+using Reactor.Utilities;
+using UnityEngine.AddressableAssets;
+
+namespace MiraAPI.Utilities.Assets;
+
+///
+/// A utility class for loading an asset from ALL addressable locations.
+///
+/// The guid of the asset.
+/// The type of the asset to be loaded.
+public class LoadableAddressableAsset(string uid) : LoadableAsset where T : UnityEngine.Object
+{
+ private readonly Action>? _gcAction;
+
+ ///
+ /// Initializes a new instance of the class, with added garbageCollector logic,
+ /// to help with https://github.com/BepInEx/Il2CppInterop/issues/40.
+ ///
+ /// The guid of the asset.
+ /// A lambda that allows GCHandle.Alloc calls without garbage collection interferences.
+ public LoadableAddressableAsset(string uid, Action> garbageCollection) : this(uid)
+ {
+ _gcAction = garbageCollection;
+ }
+
+ ///
+ /// Loads the asset from addressables.
+ ///
+ /// The asset.
+ /// The asset did not load properly.
+ public override T LoadAsset()
+ {
+ if (LoadedAsset != null)
+ {
+ return LoadedAsset;
+ }
+
+ var remoteReference = new AssetReference(uid);
+
+ if (!GC.TryStartNoGCRegion(4096, true)) Logger.Error("Could not start NoGCRegion of size 4kb, there is the possibility of injected unmanaged classes being garbage collected as per BepInEx/Il2CppInterop/issues/40");
+
+ LoadedAsset = remoteReference.LoadAssetAsync().WaitForCompletion();
+ if (LoadedAsset == null)
+ {
+ GC.EndNoGCRegion();
+ throw new InvalidOperationException($"INVALID ASSET: {uid}");
+ }
+
+ _gcAction?.Invoke(this);
+ GC.EndNoGCRegion();
+
+ return LoadedAsset;
+ }
+}
diff --git a/MiraAPI/Utilities/Assets/LoadableAddressableAssets.cs b/MiraAPI/Utilities/Assets/LoadableAddressableAssets.cs
new file mode 100644
index 0000000..e7d88a9
--- /dev/null
+++ b/MiraAPI/Utilities/Assets/LoadableAddressableAssets.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using Reactor.Utilities;
+using UnityEngine.AddressableAssets;
+
+namespace MiraAPI.Utilities.Assets;
+
+///
+/// A utility class for loading groups of assets from ALL addressable locations.
+///
+/// The type of the asset to be loaded.
+public class LoadableAddressableAssets(string key) where T : UnityEngine.Object
+{
+ private readonly Action>? _gcAction;
+
+ ///
+ /// Gets or sets reference to the loaded asset. Intended to be used for caching purposes.
+ ///
+ protected List? LoadedAssets { get; set; }
+
+ ///
+ /// Initializes a new instance of the class, with added garbageCollector logic,
+ /// to help with https://github.com/BepInEx/Il2CppInterop/issues/40.
+ ///
+ /// The key of the assets.
+ /// A lambda that allows GCHandle.Alloc calls without garbage collection interferences.
+ public LoadableAddressableAssets(string key, Action> garbageCollection) : this(key)
+ {
+ _gcAction = garbageCollection;
+ }
+
+ ///
+ /// Loads the asset from addressables.
+ ///
+ /// The asset.
+ /// The asset did not load properly.
+ public ReadOnlyCollection LoadAssets()
+ {
+ if (LoadedAssets != null)
+ {
+ return LoadedAssets.AsReadOnly();
+ }
+
+ var locations = Addressables.LoadResourceLocationsAsync(key).WaitForCompletion();
+
+ if (!GC.TryStartNoGCRegion(4096, true)) Logger.Error("Could not start NoGCRegion of size 4kb, there is the possibility of injected unmanaged classes being garbage collected as per BepInEx/Il2CppInterop/issues/40");
+
+ var assetsIList = Addressables.LoadAssetsAsync(locations, null, false).WaitForCompletion();
+ if (assetsIList == null)
+ {
+ GC.EndNoGCRegion();
+ throw new InvalidOperationException($"INVALID ASSET/s: {key}");
+ }
+
+ var assetsList = new Il2CppSystem.Collections.Generic.List(assetsIList.Pointer);
+ LoadedAssets = assetsList.ToArray().ToList();
+
+ _gcAction?.Invoke(this);
+ GC.EndNoGCRegion();
+
+ return LoadedAssets.AsReadOnly();
+ }
+}
diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs
index d2e855f..283d57c 100644
--- a/MiraAPI/Utilities/Assets/MiraAssets.cs
+++ b/MiraAPI/Utilities/Assets/MiraAssets.cs
@@ -1,9 +1,13 @@
-using UnityEngine;
+using Reactor.Utilities;
+using UnityEngine;
namespace MiraAPI.Utilities.Assets;
public static class MiraAssets
{
+ public static readonly AssetBundle MiraAssetBundle = AssetBundleManager.Load("mirabundle");
+ public static readonly LoadableAsset ModifierDisplay = new LoadableBundleAsset("Modifiers", MiraAssetBundle);
+
public static readonly LoadableResourceAsset Empty = new("MiraAPI.Resources.Empty.png");
public static readonly LoadableResourceAsset NextButton = new("MiraAPI.Resources.NextButton.png");
diff --git a/MiraAPI/Utilities/Assets/PreloadedAsset.cs b/MiraAPI/Utilities/Assets/PreloadedAsset.cs
index ed28ebf..5f7cd99 100644
--- a/MiraAPI/Utilities/Assets/PreloadedAsset.cs
+++ b/MiraAPI/Utilities/Assets/PreloadedAsset.cs
@@ -3,17 +3,17 @@
namespace MiraAPI.Utilities.Assets;
///
-/// An implementation for creating loadable assets from preloaded assets.
+/// An implementation for creating loadable assets from existing assets.
///
///
///
public class PreloadedAsset(T asset) : LoadableAsset where T : UnityEngine.Object
{
- private readonly T loadedAsset = asset ?? throw new ArgumentNullException(nameof(asset));
+ private readonly T _loadedAsset = asset ?? throw new ArgumentNullException(nameof(asset));
///
public override T LoadAsset()
{
- return loadedAsset;
+ return _loadedAsset;
}
}
diff --git a/MiraAPI/Utilities/ColorExtensions.cs b/MiraAPI/Utilities/ColorExtensions.cs
index 74c5a22..d773b47 100644
--- a/MiraAPI/Utilities/ColorExtensions.cs
+++ b/MiraAPI/Utilities/ColorExtensions.cs
@@ -8,10 +8,10 @@ namespace MiraAPI.Utilities;
public static class ColorExtensions
{
///
- /// Gets the relative luminance of a color (WCAG 2.1 specification).
+ /// Gets the relative luminance of a color (WCAG 2.1 specification). 0 is black, 1 is white.
///
/// Color to calculate luminance for.
- /// Relative luminance value.
+ /// Luminance value between 0 and 1.
public static float GetRelativeLuminance(this Color color)
{
var r = color.r <= 0.03928f ? color.r / 12.92f : Mathf.Pow((color.r + 0.055f) / 1.055f, 2.4f);
diff --git a/MiraAPI/Utilities/ControllableComparer.cs b/MiraAPI/Utilities/ControllableComparer.cs
new file mode 100644
index 0000000..8c7806c
--- /dev/null
+++ b/MiraAPI/Utilities/ControllableComparer.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MiraAPI.Utilities;
+
+internal sealed class ControllableComparer : IComparer where T : IComparable
+{
+ private readonly T[] _forcedToBottom;
+ private readonly T[] _forcedToTop;
+ private readonly IComparer _fallbackComparer;
+ public ControllableComparer(T[] forcedToBottom, T[] forcedToTop, IComparer fallbackComparer)
+ {
+ _forcedToBottom = forcedToBottom;
+ _forcedToTop = forcedToTop;
+ _fallbackComparer = fallbackComparer;
+ }
+
+ ///
+ public int Compare(T? x, T? y)
+ {
+ if ((_forcedToBottom.Contains(x) && _forcedToBottom.Contains(y)) || (_forcedToTop.Contains(x) && _forcedToTop.Contains(y)))
+ return _fallbackComparer.Compare(x, y);
+
+ if (_forcedToBottom.Contains(x))
+ return 1;
+ if (_forcedToBottom.Contains(y))
+ return -1;
+
+ if (_forcedToTop.Contains(x))
+ return -1;
+ if (_forcedToTop.Contains(y))
+ return 1;
+
+ return _fallbackComparer.Compare(x, y);
+ }
+}
diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs
index 4139aee..7a0d9a7 100644
--- a/MiraAPI/Utilities/Extensions.cs
+++ b/MiraAPI/Utilities/Extensions.cs
@@ -283,12 +283,13 @@ public static class Extensions
if (count + length > chunkSize)
{
- chunks.Enqueue(current.ToArray());
+ chunks.Enqueue([.. current]);
current.Clear();
count = 0;
}
current.Add(netData);
+ count += length;
}
if (current.Count > 0)
diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs
index 09c6bf2..f122b5c 100644
--- a/MiraAPI/Utilities/Helpers.cs
+++ b/MiraAPI/Utilities/Helpers.cs
@@ -22,7 +22,7 @@ public static class Helpers
/// A list of alive players.
public static List GetAlivePlayers()
{
- return PlayerControl.AllPlayerControls.ToArray().Where(x => !x.Data.IsDead).ToList();
+ return [.. GameData.Instance.AllPlayers.ToArray().Where(x => !x.IsDead && !x.Disconnected && x.Object).Select(x=>x.Object)];
}
///
diff --git a/MiraAPI/Voting/PlayerVoteData.cs b/MiraAPI/Voting/PlayerVoteData.cs
index b7a1b84..f673bc4 100644
--- a/MiraAPI/Voting/PlayerVoteData.cs
+++ b/MiraAPI/Voting/PlayerVoteData.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Il2CppInterop.Runtime.Attributes;
using Reactor.Utilities.Attributes;
using UnityEngine;
@@ -19,6 +20,7 @@ public class PlayerVoteData(nint cppPtr) : MonoBehaviour(cppPtr)
///
/// Gets the list of votes the owner has.
///
+ [HideFromIl2Cpp]
public List Votes { get; private set; } = [];
///