diff --git a/Directory.Build.props b/Directory.Build.props
index f6b4be7..c8f52ba 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -8,7 +8,7 @@
true
- 0.2.0
+ 0.2.1
dev
All Of Us, XtraCube, Angxl
diff --git a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs
index 3b7363a..4edd4f8 100644
--- a/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs
+++ b/MiraAPI.Example/Buttons/Freezer/FreezeButton.cs
@@ -6,6 +6,7 @@ using MiraAPI.Hud;
using MiraAPI.Modifiers;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
+using Rewired;
using UnityEngine;
namespace MiraAPI.Example.Buttons.Freezer;
@@ -19,6 +20,8 @@ public class FreezeButton : CustomActionButton
public override int MaxUses => (int)OptionGroupSingleton.Instance.FreezeUses;
public override LoadableAsset Sprite => ExampleAssets.ExampleButton;
+ public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.T;
+ public override ModifierKey Modifier1 => ModifierKey.Control;
protected override void OnClick()
{
diff --git a/MiraAPI.Example/Buttons/MeetingButton.cs b/MiraAPI.Example/Buttons/MeetingButton.cs
index c4d59e3..c0acc20 100644
--- a/MiraAPI.Example/Buttons/MeetingButton.cs
+++ b/MiraAPI.Example/Buttons/MeetingButton.cs
@@ -2,6 +2,7 @@
using MiraAPI.Hud;
using MiraAPI.Modifiers;
using MiraAPI.Utilities.Assets;
+using Rewired;
using UnityEngine;
namespace MiraAPI.Example.Buttons;
@@ -15,6 +16,9 @@ public class MeetingButton : CustomActionButton
public override int MaxUses => 3;
public override LoadableAsset Sprite => ExampleAssets.ExampleButton;
+ public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.P;
+ public override ModifierKey Modifier1 => ModifierKey.Control;
+ public override ModifierKey Modifier2 => ModifierKey.Shift;
public override bool Enabled(RoleBehaviour? role)
{
diff --git a/MiraAPI.Example/Buttons/NeutralKillerButton.cs b/MiraAPI.Example/Buttons/NeutralKillerButton.cs
index d38253a..02fc908 100644
--- a/MiraAPI.Example/Buttons/NeutralKillerButton.cs
+++ b/MiraAPI.Example/Buttons/NeutralKillerButton.cs
@@ -3,6 +3,7 @@ using MiraAPI.Example.Roles;
using MiraAPI.GameEnd;
using MiraAPI.Hud;
using MiraAPI.Utilities.Assets;
+using Rewired;
using UnityEngine;
namespace MiraAPI.Example.Buttons;
@@ -12,6 +13,7 @@ public class NeutralKillerButton : CustomActionButton
public override string Name => "Win Game";
public override float Cooldown => 0f;
public override LoadableAsset Sprite => ExampleAssets.ExampleButton;
+ public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.K;
protected override void OnClick()
{
CustomGameOver.Trigger([PlayerControl.LocalPlayer.Data]);
diff --git a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs
index 76a5b9e..c4af2ca 100644
--- a/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs
+++ b/MiraAPI.Example/Buttons/Teleporter/TeleportButton.cs
@@ -5,6 +5,7 @@ using MiraAPI.GameOptions;
using MiraAPI.Hud;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
+using Rewired;
using UnityEngine;
namespace MiraAPI.Example.Buttons.Teleporter;
@@ -21,7 +22,7 @@ public class TeleportButton : CustomActionButton
public override LoadableAsset Sprite => ExampleAssets.TeleportButton;
public override Color TextOutlineColor => new Color32(221, 176, 152, 255);
public static bool IsZoom { get; private set; }
-
+ public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.M;
public override bool Enabled(RoleBehaviour? role)
{
return role is TeleporterRole;
diff --git a/MiraAPI.Example/ExampleColors.cs b/MiraAPI.Example/ExampleColors.cs
index 6833d3c..d61eb49 100644
--- a/MiraAPI.Example/ExampleColors.cs
+++ b/MiraAPI.Example/ExampleColors.cs
@@ -6,7 +6,10 @@ namespace MiraAPI.Example;
[RegisterCustomColors]
public static class ExampleColors
{
- public static CustomColor Cerulean { get; } = new("Cerulean", new Color(0.0f, 0.48f, 0.65f));
+ public static CustomColor Cerulean { get; } = new("Cerulean", new Color(0.0f, 0.48f, 0.65f))
+ {
+ ColorBrightness = CustomColorBrightness.Lighter,
+ };
public static CustomColor Rose { get; } = new("Rose", new Color(0.98f, 0.26f, 0.62f));
diff --git a/MiraAPI.Example/Options/ExampleOptions2.cs b/MiraAPI.Example/Options/ExampleOptions2.cs
index 9abf375..bc3c910 100644
--- a/MiraAPI.Example/Options/ExampleOptions2.cs
+++ b/MiraAPI.Example/Options/ExampleOptions2.cs
@@ -10,6 +10,8 @@ public class ExampleOptions2 : AbstractOptionGroup
public override uint GroupPriority => 0; // This group will be displayed first. The default value is uint.MaxValue.
+ public ModdedPlayerOption PlayerOption2 { get; } = new("YOU MUST CHOSE A PLAYER", false);
+ public ModdedPlayerOption PlayerOption { get; } = new("Eh whatever");
public ModdedToggleOption ToggleOpt1 { get; } = new("Toggle Option 1", false);
public ModdedToggleOption ToggleOpt2 { get; } = new("Toggle Option 2", false)
diff --git a/MiraAPI/Colors/CustomColor.cs b/MiraAPI/Colors/CustomColor.cs
index 7bbe87a..89663d1 100644
--- a/MiraAPI/Colors/CustomColor.cs
+++ b/MiraAPI/Colors/CustomColor.cs
@@ -13,19 +13,24 @@ namespace MiraAPI.Colors;
public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor)
{
///
- /// Gets the main color.
+ /// Gets or sets the main color.
///
- public Color32 MainColor { get; } = mainColor;
+ public Color32 MainColor { get; set; } = mainColor;
///
- /// Gets the shadow color.
+ /// Gets or sets the shadow color.
///
- public Color32 ShadowColor { get; } = shadowColor;
+ public Color32 ShadowColor { get; set; } = shadowColor;
///
- /// Gets the name of the color.
+ /// Gets or sets the name of the color.
///
- public StringNames Name { get; } = name;
+ public StringNames Name { get; set; } = name;
+
+ ///
+ /// Gets or sets whether the color is lighter or darker. Can be used to help with colorblind stuff.
+ ///
+ public CustomColorBrightness ColorBrightness { get; set; } = CustomColorBrightness.Darker;
///
public CustomColor(StringNames name, Color32 mainColor) : this(name, mainColor, mainColor.GetShadowColor(60))
@@ -42,3 +47,19 @@ public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 sha
{
}
}
+
+///
+/// Can be used to help with colorblind stuff.
+///
+public enum CustomColorBrightness
+{
+ ///
+ /// A lighter color.
+ ///
+ Lighter,
+
+ ///
+ /// A darker color.
+ ///
+ Darker,
+}
diff --git a/MiraAPI/Events/Vanilla/Gameplay/BeforeRoundStartEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/BeforeRoundStartEvent.cs
new file mode 100644
index 0000000..f9d41ee
--- /dev/null
+++ b/MiraAPI/Events/Vanilla/Gameplay/BeforeRoundStartEvent.cs
@@ -0,0 +1,21 @@
+namespace MiraAPI.Events.Vanilla.Gameplay;
+
+///
+/// Event invoked on IntroCutscene.OnDestroy and ExileController.WrapUp to determine if it should run the round event or not.
+///
+public class BeforeRoundStartEvent : MiraCancelableEvent
+{
+ ///
+ /// 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 BeforeRoundStartEvent(bool triggeredByIntro)
+ {
+ TriggeredByIntro = triggeredByIntro;
+ }
+}
diff --git a/MiraAPI/Events/Vanilla/Gameplay/RoundStartEvent.cs b/MiraAPI/Events/Vanilla/Gameplay/RoundStartEvent.cs
index a50fde5..470f85a 100644
--- a/MiraAPI/Events/Vanilla/Gameplay/RoundStartEvent.cs
+++ b/MiraAPI/Events/Vanilla/Gameplay/RoundStartEvent.cs
@@ -1,7 +1,7 @@
namespace MiraAPI.Events.Vanilla.Gameplay;
///
-/// Start Round event, invoked on IntroCutscene.OnDestroy and ExileController.WrapUp.
+/// Start Round event, invoked on IntroCutscene.OnDestroy and ExileController.WrapUp if the event isn't cancelled.
///
public class RoundStartEvent : MiraEvent
{
diff --git a/MiraAPI/Events/Vanilla/Meeting/Voting/DummyVoteEvent.cs b/MiraAPI/Events/Vanilla/Meeting/Voting/DummyVoteEvent.cs
new file mode 100644
index 0000000..2f5cba0
--- /dev/null
+++ b/MiraAPI/Events/Vanilla/Meeting/Voting/DummyVoteEvent.cs
@@ -0,0 +1,44 @@
+using System;
+using MiraAPI.Utilities;
+using MiraAPI.Voting;
+
+namespace MiraAPI.Events.Vanilla.Meeting.Voting;
+
+///
+/// Invoked before the dummy selects their vote. If canceled, the dummy won't vote.
+///
+public class DummyVoteEvent : MiraCancelableEvent
+{
+ ///
+ /// Gets the dummy that needs to vote.
+ ///
+ public DummyBehaviour Dummy { get; }
+
+ ///
+ /// Gets the instance of the dummy's vote data.
+ ///
+ public PlayerVoteData VoteData { get; }
+
+ ///
+ /// Gets or sets a value indicating whether the dummy can vote to skip the meeting.
+ ///
+ public bool CanSkip { get; set; }
+
+ ///
+ /// Gets or sets a predicate for validating whether the dummy can vote on a player.
+ /// By default, excludes dead and disconnected players.
+ ///
+ public Predicate PlayerIsValid { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The voting dummy.
+ public DummyVoteEvent(DummyBehaviour dummy)
+ {
+ Dummy = dummy;
+ VoteData = dummy.myPlayer.GetVoteData();
+ CanSkip = true;
+ PlayerIsValid = x => !x.Data.IsDead && !x.Data.Disconnected;
+ }
+}
diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs
index 95173fb..947b6ff 100644
--- a/MiraAPI/GameOptions/IModdedOption.cs
+++ b/MiraAPI/GameOptions/IModdedOption.cs
@@ -62,9 +62,10 @@ public interface IModdedOption
/// The ToggleOption template.
/// The NumberOption template.
/// The StringOption template.
+ /// The PlayerOption template.
/// >The Transform container for the option.
/// >The created OptionBehaviour object.
- OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container);
+ OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container);
///
/// Gets the value as a float.
diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs
index 0adc461..86e2006 100644
--- a/MiraAPI/GameOptions/ModdedOptionsManager.cs
+++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs
@@ -6,6 +6,7 @@ using System.Reflection;
using BepInEx.Configuration;
using HarmonyLib;
using MiraAPI.GameOptions.Attributes;
+using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Networking;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities;
@@ -22,6 +23,7 @@ public static class ModdedOptionsManager
private static readonly Dictionary OptionAttributes = [];
private static readonly Dictionary TypeToGroup = [];
+ internal static readonly Dictionary CreatedPlayerOptions = [];
internal static readonly Dictionary ModdedOptions = [];
internal static readonly List Groups = [];
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs
index 18f13c6..0bec22f 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedEnumOption.cs
@@ -44,7 +44,7 @@ public class ModdedEnumOption : ModdedOption
}
///
- public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
+ public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
@@ -135,7 +135,7 @@ public class ModdedEnumOption : ModdedOption where T : Enum
}
///
- public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
+ public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs
index a01ffe8..53378e1 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs
@@ -93,6 +93,7 @@ public class ModdedNumberOption : ModdedOption
ToggleOption toggleOpt,
NumberOption numberOpt,
StringOption stringOpt,
+ PlayerOption playerOpt,
Transform container)
{
var numberOption = Object.Instantiate(numberOpt, Vector3.zero, Quaternion.identity, container);
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
index 3c98d24..311f294 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
@@ -37,6 +37,7 @@ public abstract class ModdedOption : IModdedOption
{
if (_parentMod != null || value == null) return;
_parentMod = value;
+
var entry = _parentMod.GetConfigFile().Bind(ConfigDefinition, DefaultValue);
Value = entry.Value;
}
@@ -177,6 +178,7 @@ public abstract class ModdedOption : IModdedOption
ToggleOption toggleOpt,
NumberOption numberOpt,
StringOption stringOpt,
+ PlayerOption playerOpt,
Transform container);
///
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs
new file mode 100644
index 0000000..6069a2c
--- /dev/null
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs
@@ -0,0 +1,138 @@
+using System;
+using System.Linq;
+using Il2CppSystem.Collections.Generic;
+using MiraAPI.Networking;
+using MiraAPI.Utilities;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace MiraAPI.GameOptions.OptionTypes;
+
+///
+/// An option for selecting an ingame player. It returns an index of the values list, NOT A PLAYER ID. To get a player out of this, index the Values list.
+///
+public class ModdedPlayerOption : ModdedOption
+{
+ ///
+ /// Gets or sets the included players.
+ ///
+ public List Values { get; set; }
+
+ ///
+ /// Gets or sets a filter for the players included in the option.
+ ///
+ public Func? PlayerFilter { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the option includes a "None" option. If None is selected, value will return -1.
+ ///
+ public bool AllowNone { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The title of the option.
+ /// Whether the option includes a none option. If None is selected, value will return -1.
+ public ModdedPlayerOption(string title, bool allowNone = true) : base(title, allowNone ? -1 : 0, false)
+ {
+ Data = ScriptableObject.CreateInstance();
+ var data = (PlayerSelectionGameSetting)Data;
+
+ data.Title = StringName;
+ data.Type = global::OptionTypes.Player;
+
+ Values = new List();
+ AllowNone = allowNone;
+ Value = DefaultValue;
+ }
+
+ ///
+ public override OptionBehaviour CreateOption(
+ ToggleOption toggleOpt,
+ NumberOption numberOpt,
+ StringOption stringOpt,
+ PlayerOption playerOpt,
+ Transform container)
+ {
+ var playerOption = Object.Instantiate(playerOpt, container);
+
+ var filteredList = GetFilteredPlayers();
+ Values = filteredList.ToIl2CppList();
+
+ playerOption.SetUpFromData(Data, 20);
+ playerOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged;
+
+ playerOption.Title = StringName;
+ playerOption.TitleText.text = TranslationController.Instance.GetString(StringName);
+ playerOption.Values = Values;
+ playerOption.Value = Value;
+ playerOption.playerIndex = filteredList.FindIndex(p => p.PlayerId == Value);
+
+ OptionBehaviour = playerOption;
+ ModdedOptionsManager.CreatedPlayerOptions.TryAdd(playerOption, this);
+
+ playerOption.SetValueText();
+ playerOption.AdjustButtonsActiveState();
+
+ return playerOption;
+ }
+
+ ///
+ /// Returns a player ran through the filter.
+ ///
+ /// A list of filtered players.
+ public System.Collections.Generic.List GetFilteredPlayers()
+ {
+ return GameData.Instance.AllPlayers.ToArray()
+ .Where(x => PlayerFilter?.Invoke(x) ?? true)
+ .ToList();
+ }
+
+ ///
+ /// Returns a player from the value. If AllowNone is enabled and none is enabled this will return null.
+ ///
+ /// A player or null if chosen none.
+ public NetworkedPlayerInfo? GetPlayerValue()
+ {
+ return Value < 0 ? null : Values[Value];
+ }
+
+ ///
+ public override float GetFloatData()
+ {
+ return Value;
+ }
+
+ ///
+ public override NetData GetNetData()
+ {
+ return new NetData(Id, BitConverter.GetBytes(Value));
+ }
+
+ ///
+ public override void HandleNetData(byte[] data)
+ {
+ SetValue(BitConverter.ToInt32(data));
+ }
+
+ ///
+ public override int GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
+ {
+ return optionBehaviour.GetInt();
+ }
+
+ ///
+ protected override void OnValueChanged(int newValue)
+ {
+ HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(newValue), false);
+ if (!OptionBehaviour)
+ {
+ return;
+ }
+
+ if (OptionBehaviour is PlayerOption opt)
+ {
+ opt.Value = newValue;
+ }
+ }
+}
diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs
index 26ecc74..2cdd0e1 100644
--- a/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedToggleOption.cs
@@ -26,7 +26,7 @@ public class ModdedToggleOption : ModdedOption
}
///
- public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
+ public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var toggleOption = Object.Instantiate(toggleOpt, Vector3.zero, Quaternion.identity, container);
diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs
index 988aac9..ac48c4a 100644
--- a/MiraAPI/Hud/CustomActionButton.cs
+++ b/MiraAPI/Hud/CustomActionButton.cs
@@ -5,6 +5,7 @@ using MiraAPI.Events.Mira;
using MiraAPI.Patches;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
+using Rewired;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
@@ -52,6 +53,28 @@ public abstract class CustomActionButton
///
public virtual int MaxUses => 0;
+ ///
+ /// Gets or sets the current key binding for this button.
+ ///
+ public abstract KeyboardKeyCode Defaultkeybind { get; }
+
+ ///
+ /// The first optional modifier key (e.g., Control, Shift, Alt) that must be held with the main key to activate the button.
+ ///
+ public virtual ModifierKey Modifier1 => ModifierKey.None;
+
+ ///
+ /// The second optional modifier key.
+ /// Set to if unused.
+ ///
+ public virtual ModifierKey Modifier2 => ModifierKey.None;
+
+ ///
+ /// The third optional modifier key.
+ /// Set to if unused.
+ ///
+ public virtual ModifierKey Modifier3 => ModifierKey.None;
+
///
/// Gets the button's text outline color.
///
diff --git a/MiraAPI/Keybinds/KeybindManager.cs b/MiraAPI/Keybinds/KeybindManager.cs
new file mode 100644
index 0000000..3f95f4f
--- /dev/null
+++ b/MiraAPI/Keybinds/KeybindManager.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using Rewired;
+
+namespace MiraAPI.Keybinds
+{
+ ///
+ /// Lets you register and manage mod keybinds with conflict checks and rebinding support.
+ ///
+ public static class KeybindManager
+ {
+ ///
+ /// Stores all info for a registered keybind.
+ ///
+ public class KeybindEntry
+ {
+ /// Unique identifier for this keybind.
+ public string Id;
+
+ /// Text shown in the Among Us rebinding menu to describe this keybind.
+ public string Description;
+
+ ///
+ /// The method that runs when this keybind is pressed.
+ ///
+ public Action Handler;
+
+ ///
+ /// The key currently assigned to this keybind.
+ ///
+ public KeyboardKeyCode Key;
+
+ ///
+ /// If true, this keybind will be checked for conflicts with other exclusive keybinds using the same key.
+ ///
+ public bool Exclusive;
+
+ ///
+ /// The first optional modifier key (e.g., Control, Shift, Alt) that must be held with the main key.
+ ///
+ public ModifierKey Modifier1 = ModifierKey.None;
+
+ ///
+ /// The second optional modifier key, if needed. Set to ModifierKey.None if unused.
+ ///
+ public ModifierKey Modifier2 = ModifierKey.None;
+
+ ///
+ /// The third optional modifier key, if needed. Set to ModifierKey.None if unused.
+ ///
+ public ModifierKey Modifier3 = ModifierKey.None;
+ }
+
+ private static readonly List Registered = new();
+
+ ///
+ /// Registers a new mod keybind.
+ ///
+ /// Unique identifier for the keybind.
+ /// Description to display in UI.
+ /// Default key to use if no rebind exists.
+ /// Method to invoke when the keybind is pressed.
+ /// If true, conflicts with other exclusive keybinds on the same key.
+ /// Optional first modifier key (e.g., Control, Shift, Alt) that must be held with the main key.
+ /// Optional second modifier key. Use ModifierKey.None if unused.
+ /// Optional third modifier key. Use ModifierKey.None if unused.
+ /// Returns the key that was registered for this keybind.
+ public static KeyboardKeyCode Register(string id, string description, KeyboardKeyCode key, Action handler, bool exclusive = true, ModifierKey modifier1 = ModifierKey.None, ModifierKey modifier2 = ModifierKey.None, ModifierKey modifier3 = ModifierKey.None)
+ {
+ Registered.Add(new KeybindEntry
+ {
+ Id = id,
+ Description = description,
+ Key = key,
+ Handler = handler,
+ Exclusive = exclusive,
+ Modifier1 = modifier1,
+ Modifier2 = modifier2,
+ Modifier3 = modifier3,
+ });
+
+ return key;
+ }
+
+ ///
+ /// Returns all conflicts where exclusive keybinds use the same key.
+ ///
+ /// A list of pairs of keybinds that conflict.
+ public static List<(KeybindEntry, KeybindEntry)> GetConflicts()
+ {
+ var conflicts = new List<(KeybindEntry, KeybindEntry)>();
+ for (int i = 0; i < Registered.Count; i++)
+ {
+ for (int j = i + 1; j < Registered.Count; j++)
+ {
+ var a = Registered[i];
+ var b = Registered[j];
+ if (a.Key == b.Key && a.Exclusive && b.Exclusive)
+ {
+ conflicts.Add((a, b));
+ }
+ }
+ }
+ return conflicts;
+ }
+
+ ///
+ /// Returns all currently registered keybind entries.
+ ///
+ /// A list of all registered keybind entries.
+ public static List GetEntries()
+ {
+ return Registered;
+ }
+ }
+}
diff --git a/MiraAPI/MiraApiConfig.cs b/MiraAPI/MiraApiConfig.cs
new file mode 100644
index 0000000..10c9776
--- /dev/null
+++ b/MiraAPI/MiraApiConfig.cs
@@ -0,0 +1,23 @@
+using BepInEx;
+using BepInEx.Configuration;
+using BepInEx.Unity.IL2CPP;
+using HarmonyLib;
+using MiraAPI.PluginLoading;
+using Reactor;
+using Reactor.Networking;
+using Reactor.Networking.Attributes;
+using Reactor.Utilities;
+using UnityEngine;
+
+namespace MiraAPI;
+
+///
+/// Mira API Config File Handler
+///
+public class MiraApiConfig(ConfigFile config)
+{
+ ///
+ /// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers.
+ ///
+ public ConfigEntry ModifiersHudLeftSide { get; private set; } = config.Bind("Displays", "Show Modifiers HUD on Left Side", false);
+}
diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs
index 8654cf6..aa7f3c6 100644
--- a/MiraAPI/MiraApiPlugin.cs
+++ b/MiraAPI/MiraApiPlugin.cs
@@ -30,6 +30,10 @@ public partial class MiraApiPlugin : BasePlugin
///
public static Color DefaultHeaderColor { get; } = new Color32(77, 77, 77, 255);
+ ///
+ /// Gets the Mira Api Config Handler.
+ ///
+ public MiraApiConfig? MiraConfig { get; private set; }
private static MiraPluginManager? PluginManager { get; set; }
internal Harmony Harmony { get; } = new(Id);
@@ -40,6 +44,7 @@ public partial class MiraApiPlugin : BasePlugin
ReactorCredits.Register("Mira API", Version, true, ReactorCredits.AlwaysShow);
+ MiraConfig = new MiraApiConfig(Config);
PluginManager = new MiraPluginManager();
PluginManager.Initialize();
}
diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs
index 52e30b6..0e31779 100644
--- a/MiraAPI/Modifiers/BaseModifier.cs
+++ b/MiraAPI/Modifiers/BaseModifier.cs
@@ -64,6 +64,11 @@ public abstract class BaseModifier : IOptionable
///
public virtual bool ShowInFreeplay => false;
+ ///
+ /// Gets a value indicating the color that should be used for the modifier within freeplay.
+ ///
+ public virtual Color FreeplayFileColor => Color.gray;
+
///
/// Gets a value indicating whether the modifier is unique. If true, the player can only have one instance of this modifier.
///
diff --git a/MiraAPI/Modifiers/ModifierComponent.cs b/MiraAPI/Modifiers/ModifierComponent.cs
index 5c29dc2..4f556f5 100644
--- a/MiraAPI/Modifiers/ModifierComponent.cs
+++ b/MiraAPI/Modifiers/ModifierComponent.cs
@@ -6,6 +6,7 @@ using System.Linq;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Modifiers.ModifierDisplay;
using MiraAPI.Modifiers.Types;
+using MiraAPI.Patches.Roles;
using Reactor.Utilities;
using Reactor.Utilities.Attributes;
using UnityEngine;
@@ -126,7 +127,18 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
if (_player.AmOwner && ModifierDisplay && HudManager.InstanceExists)
{
- ModifierDisplay!.gameObject.SetActive(MeetingHud.Instance || HudManager.Instance.TaskPanel.isActiveAndEnabled);
+ var taskPanelOpen = HudManager.Instance.TaskPanel.open;
+ var roleTabOpen = HudManagerPatches.RoleTab != null && HudManagerPatches.RoleTab.open;
+ var leftSideHud = PluginSingleton.Instance.MiraConfig!.ModifiersHudLeftSide.Value;
+
+ if (leftSideHud && (taskPanelOpen || roleTabOpen))
+ {
+ ModifierDisplay!.gameObject.SetActive(false);
+ }
+ else
+ {
+ ModifierDisplay!.gameObject.SetActive(MeetingHud.Instance || HudManager.Instance.TaskPanel.isActiveAndEnabled);
+ }
}
}
diff --git a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs
index f1a614e..0836935 100644
--- a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs
+++ b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs
@@ -1,9 +1,12 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using HarmonyLib;
using Il2CppInterop.Runtime.Attributes;
+using MiraAPI.Patches.Roles;
using MiraAPI.Utilities.Assets;
+using Reactor.Utilities;
using Reactor.Utilities.Attributes;
using Reactor.Utilities.Extensions;
using TMPro;
@@ -94,6 +97,16 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
IsOpen = false;
_children.gameObject.SetActive(false);
+
+ if (PluginSingleton.Instance.MiraConfig!.ModifiersHudLeftSide.Value)
+ {
+ var aspect = GetComponent();
+ _toggleButton.transform.localPosition = new Vector3(-1f, 2.7f, 0f);
+ _children.transform.localPosition = new Vector3(-0.2f, -0.05f, 0f);
+ aspect.Alignment = AspectPosition.EdgeAlignments.LeftTop;
+ aspect.DistanceFromEdge = new Vector3(1.8f, 2.55f, -20f);
+ aspect.AdjustPosition();
+ }
}
///
@@ -109,7 +122,16 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
///
public void ToggleTab()
{
- IsOpen = !IsOpen;
+ SetOpened(!IsOpen);
+ }
+
+ ///
+ /// Use this to set the visibility of the modifiers tab.
+ ///
+ /// Whether it should be visible or not.
+ public void SetOpened(bool val)
+ {
+ IsOpen = val;
_children.gameObject.SetActive(IsOpen);
RefreshModifiers();
}
@@ -118,7 +140,6 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
{
_modifiers.Remove(component.Modifier!);
component.gameObject.DestroyImmediate();
-
RefreshModifiers();
}
@@ -194,13 +215,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
_toggleBtnText.text = $"Modifiers ({filteredModifiers.Count})";
- if (filteredModifiers.Count == 0)
- {
- _toggleButton.gameObject.SetActive(false);
- return;
- }
-
- _toggleButton.gameObject.SetActive(true);
+ _toggleButton.gameObject.SetActive(filteredModifiers.Count != 0);
foreach (var mod in modifiersToAdd)
{
diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs
index 1b38efd..f34d11f 100644
--- a/MiraAPI/Modifiers/ModifierManager.cs
+++ b/MiraAPI/Modifiers/ModifierManager.cs
@@ -5,6 +5,7 @@ using System.Runtime.Serialization;
using MiraAPI.Modifiers.Types;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
+using MiraAPI.Utilities;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using Random = System.Random;
@@ -21,6 +22,13 @@ public static class ModifierManager
///
public static bool MiraAssignsModifiers { get; set; } = true;
+ ///
+ /// Gets the list of all registered modifiers.
+ ///
+ public static IReadOnlyList Modifiers { get; internal set; } = [];
+
+ internal static List InternalModifiers { get; } = [];
+
private static readonly Dictionary IdToTypeModifierMap = [];
private static readonly Dictionary TypeToIdModifierMap = [];
private static readonly Dictionary> PrioritiesToIdsMap = [];
@@ -80,6 +88,7 @@ public static class ModifierManager
}
info.InternalModifiers.Add(modifier);
+ InternalModifiers.Add(modifier);
if (modifier is not GameModifier gameModifier)
{
@@ -117,6 +126,7 @@ public static class ModifierManager
.Select(x => Activator.CreateInstance(x.Value) as GameModifier)
.OfType()
.Where(x => x.GetAmountPerGame() > 0 && x.GetAssignmentChance() > 0)
+ .Shuffle()
.OrderByDescending(x => x.Priority())
.ToArray();
diff --git a/MiraAPI/Patches/ChatControllerPatch.cs b/MiraAPI/Patches/ChatControllerPatch.cs
new file mode 100644
index 0000000..e65be7f
--- /dev/null
+++ b/MiraAPI/Patches/ChatControllerPatch.cs
@@ -0,0 +1,35 @@
+using HarmonyLib;
+using UnityEngine;
+
+namespace MiraAPI.Patches;
+
+#pragma warning disable SA1629
+///
+/// Allows players to paste text into chat.
+/// Source: https://github.com/CallOfCreator/NewMod/blob/main/NewMod/Patches/ClipboardPatch.cs
+///
+#pragma warning restore SA1629
+[HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))]
+public static class ChatControllerPatch
+{
+ public static void Prefix(ChatController __instance)
+ {
+ if (!HudManager.Instance.Chat.IsOpenOrOpening) return;
+
+ var ctrlPressed = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
+
+ if (!ctrlPressed || !Input.GetKeyDown(KeyCode.V)) return;
+
+ var clipboard = GUIUtility.systemCopyBuffer;
+
+ if (string.IsNullOrWhiteSpace(clipboard)) return;
+ clipboard = clipboard.Replace("<", string.Empty)
+ .Replace(">", string.Empty)
+ .Replace("\r", string.Empty);
+
+ if (!Input.GetKey(KeyCode.LeftShift) && !Input.GetKey(KeyCode.RightShift))
+ clipboard = clipboard.Replace("\n", string.Empty);
+
+ __instance.freeChatField.textArea.SetText(__instance.freeChatField.textArea.text + clipboard);
+ }
+}
diff --git a/MiraAPI/Patches/Events/CanUsePatches.cs b/MiraAPI/Patches/Events/CanUsePatches.cs
index 143f176..6206591 100644
--- a/MiraAPI/Patches/Events/CanUsePatches.cs
+++ b/MiraAPI/Patches/Events/CanUsePatches.cs
@@ -12,14 +12,16 @@ public static class CanUsePatches
public static IEnumerable TargetMethods()
{
yield return AccessTools.Method(typeof(Console), nameof(Console.CanUse));
- yield return AccessTools.Method(typeof(MapConsole), nameof(MapConsole.CanUse));
- yield return AccessTools.Method(typeof(SystemConsole), nameof(SystemConsole.CanUse));
- yield return AccessTools.Method(typeof(Ladder), nameof(Ladder.CanUse));
- yield return AccessTools.Method(typeof(PlatformConsole), nameof(PlatformConsole.CanUse));
- yield return AccessTools.Method(typeof(OpenDoorConsole), nameof(OpenDoorConsole.CanUse));
+ yield return AccessTools.Method(typeof(DeconControl), nameof(DeconControl.CanUse));
yield return AccessTools.Method(typeof(DoorConsole), nameof(DoorConsole.CanUse));
+ yield return AccessTools.Method(typeof(Ladder), nameof(Ladder.CanUse));
+ yield return AccessTools.Method(typeof(MapConsole), nameof(MapConsole.CanUse));
+ yield return AccessTools.Method(typeof(OpenDoorConsole), nameof(OpenDoorConsole.CanUse));
yield return AccessTools.Method(typeof(OptionsConsole), nameof(OptionsConsole.CanUse));
+ yield return AccessTools.Method(typeof(PlatformConsole), nameof(PlatformConsole.CanUse));
+ yield return AccessTools.Method(typeof(SystemConsole), nameof(SystemConsole.CanUse));
yield return AccessTools.Method(typeof(ZiplineConsole), nameof(ZiplineConsole.CanUse));
+ // vent is not necessary as it has its own patch
}
[HarmonyPriority(Priority.Last)]
diff --git a/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs b/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs
new file mode 100644
index 0000000..12c95c1
--- /dev/null
+++ b/MiraAPI/Patches/Events/FreeplayRoundStartPatch.cs
@@ -0,0 +1,17 @@
+using HarmonyLib;
+using MiraAPI.Events;
+using MiraAPI.Events.Vanilla.Gameplay;
+
+namespace MiraAPI.Patches.Events;
+
+[HarmonyPatch(typeof(TutorialManager._RunTutorial_d__3), nameof(TutorialManager._RunTutorial_d__3.MoveNext))]
+public static class FreeplayRoundStartPatch
+{
+ public static void Postfix(TutorialManager._RunTutorial_d__3 __instance, ref bool __result)
+ {
+ if (!__result)
+ {
+ MiraEventManager.InvokeEvent(new RoundStartEvent(true));
+ }
+ }
+}
diff --git a/MiraAPI/Patches/Events/SpawnInMinigamePatch.cs b/MiraAPI/Patches/Events/SpawnInMinigamePatch.cs
index b726999..65768d4 100644
--- a/MiraAPI/Patches/Events/SpawnInMinigamePatch.cs
+++ b/MiraAPI/Patches/Events/SpawnInMinigamePatch.cs
@@ -9,7 +9,12 @@ internal static class SpawnInMinigamePatch
{
public static void Postfix()
{
- var @event = new RoundStartEvent(false);
+ var @event = new BeforeRoundStartEvent(false);
MiraEventManager.InvokeEvent(@event);
+
+ if (@event.IsCancelled) return;
+
+ var @event2 = new RoundStartEvent(false);
+ MiraEventManager.InvokeEvent(@event2);
}
}
diff --git a/MiraAPI/Patches/Events/SubmergedSpawnGamePatch.cs b/MiraAPI/Patches/Events/SubmergedSpawnGamePatch.cs
index d2664b9..07c358a 100644
--- a/MiraAPI/Patches/Events/SubmergedSpawnGamePatch.cs
+++ b/MiraAPI/Patches/Events/SubmergedSpawnGamePatch.cs
@@ -25,7 +25,12 @@ internal static class SubmergedSpawnGamePatch
public static void Postfix()
{
- var @event = new RoundStartEvent(false);
+ var @event = new BeforeRoundStartEvent(false);
MiraEventManager.InvokeEvent(@event);
+
+ if (@event.IsCancelled) return;
+
+ var @event2 = new RoundStartEvent(false);
+ MiraEventManager.InvokeEvent(@event2);
}
}
diff --git a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs
index 184bafb..07d37f5 100644
--- a/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs
+++ b/MiraAPI/Patches/Freeplay/TaskAddButtonPatches.cs
@@ -22,15 +22,4 @@ internal static class TaskAddButtonPatches
return true;
}
-
- [HarmonyPostfix]
- [HarmonyPatch(nameof(TaskAddButton.Role), MethodType.Setter)]
- public static void RoleGetterPatch(TaskAddButton __instance)
- {
- if (__instance.role is ICustomRole { Team: ModdedRoleTeams.Custom } customRole)
- {
- __instance.FileImage.color = customRole.IntroConfiguration?.IntroTeamColor ?? Color.gray;
- }
- __instance.RolloverHandler.OutColor = __instance.FileImage.color;
- }
}
diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs
index f90ffba..ae49ed4 100644
--- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs
+++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs
@@ -10,6 +10,7 @@ using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using TMPro;
using UnityEngine;
+using UnityEngine.Events;
using UnityEngine.UI;
using Object = UnityEngine.Object;
@@ -19,7 +20,6 @@ namespace MiraAPI.Patches.Freeplay;
internal static class TaskAdderPatches
{
private static Scroller _scroller = null!;
-
[HarmonyPostfix]
[HarmonyPatch(nameof(TaskAdderGame.Begin))]
public static void AddRolesFolder(TaskAdderGame __instance)
@@ -32,6 +32,11 @@ internal static class TaskAdderPatches
_scroller.allowY = true;
_scroller.Inner = inner.transform;
+ var scrollerHelper = __instance.TaskParent.gameObject.AddComponent();
+ scrollerHelper.scroller = _scroller;
+ scrollerHelper.verticalAxis = RewiredConstsEnum.Action.TaskRVertical;
+ scrollerHelper.scrollSpeed = 10f;
+
GameObject hitbox = new("Hitbox")
{
layer = 5,
@@ -295,7 +300,7 @@ internal static class TaskAdderPatches
if (_scroller)
{
- _scroller.CalculateAndSetYBounds(__instance.ActiveItems.Count, 6, 5, 1.4f);
+ _scroller.CalculateAndSetYBounds(__instance.ActiveItems.Count, 6, 4.5f, 1.65f);
_scroller.SetYBoundsMin(0.0f);
_scroller.ScrollToTop();
}
@@ -332,7 +337,8 @@ internal static class TaskAdderPatches
taskAddButton.SafePositionWorld = instance.SafePositionWorld;
taskAddButton.Text.text = modifier.ModifierName;
taskAddButton.Text.EnableMasking();
-
+ taskAddButton.FileImage.color = modifier.FreeplayFileColor;
+ taskAddButton.RolloverHandler.OutColor = modifier.FreeplayFileColor;
taskAddButton.Button.OnClick = new Button.ButtonClickedEvent();
taskAddButton.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() =>
{
@@ -384,6 +390,20 @@ internal static class TaskAdderPatches
instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3);
taskAddButton2.Role = roleBehaviour;
+ if (roleBehaviour is ICustomRole custom)
+ {
+ var customColor = custom.IntroConfiguration?.IntroTeamColor ?? Color.gray;
+ if (custom.Team is ModdedRoleTeams.Crewmate)
+ {
+ customColor = Palette.CrewmateBlue;
+ }
+ else if (custom.Team is ModdedRoleTeams.Impostor)
+ {
+ customColor = Palette.ImpostorRed;
+ }
+ taskAddButton2.FileImage.color = customColor;
+ taskAddButton2.RolloverHandler.OutColor = customColor;
+ }
if (taskAddButton2.Button == null)
{
continue;
diff --git a/MiraAPI/Patches/GameStartManagerPatch.cs b/MiraAPI/Patches/GameStartManagerPatch.cs
new file mode 100644
index 0000000..7ebb05b
--- /dev/null
+++ b/MiraAPI/Patches/GameStartManagerPatch.cs
@@ -0,0 +1,36 @@
+using HarmonyLib;
+using MiraAPI.Keybinds;
+using UnityEngine;
+
+namespace MiraAPI.Patches;
+
+///
+/// Checks keybind conflicts in the lobby and disables secondary ones.
+///
+[HarmonyPatch(typeof(GameStartManager))]
+public static class GameStartManagerPatch
+{
+ public static bool conflictShown = false;
+
+ [HarmonyPatch(nameof(GameStartManager.Start))]
+ [HarmonyPostfix]
+ public static void StartPostfix(GameStartManager __instance)
+ {
+ if (conflictShown) return;
+
+ var conflicts = KeybindManager.GetConflicts();
+ if (conflicts.Count == 0) return;
+
+ conflictShown = true;
+
+ var message = "The following keybinds are disabled due to conflicts:\n\n";
+
+ foreach (var (a, b) in conflicts)
+ {
+ message += $"• {b.Description} (conflicts with {a.Description})\n";
+ b.Handler = null;
+ }
+
+ HudManager.Instance.ShowPopUp(message.TrimEnd());
+ }
+}
diff --git a/MiraAPI/Patches/Hud/ButtonResetPatches.cs b/MiraAPI/Patches/Hud/ButtonResetPatches.cs
index 0859bea..29b190d 100644
--- a/MiraAPI/Patches/Hud/ButtonResetPatches.cs
+++ b/MiraAPI/Patches/Hud/ButtonResetPatches.cs
@@ -1,37 +1,25 @@
using HarmonyLib;
using MiraAPI.Hud;
+using Reactor.Utilities;
namespace MiraAPI.Patches.Hud;
-///
-/// Reset button patches.
-///
-[HarmonyPatch]
+[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))]
+[HarmonyPatch(typeof(ExileController), nameof(ExileController.ReEnableGameplay))]
public static class ButtonResetPatches
{
- ///
- /// Resets the cooldown and effect of all custom buttons when the meeting starts.
- ///
- [HarmonyPostfix]
- [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))]
- public static void MeetingHudStartPostfix()
+ public static void Postfix()
{
foreach (var customActionButton in CustomButtonManager.CustomButtons)
{
- customActionButton.ResetCooldownAndOrEffect();
- }
- }
-
- ///
- /// Resets the cooldown and effect of all custom buttons after the exile screen is closed.
- ///
- [HarmonyPostfix]
- [HarmonyPatch(typeof(ExileController), nameof(ExileController.ReEnableGameplay))]
- public static void ExileControllerReEnableGameplayPostfix()
- {
- foreach (var customActionButton in CustomButtonManager.CustomButtons)
- {
- customActionButton.ResetCooldownAndOrEffect();
+ try
+ {
+ customActionButton.ResetCooldownAndOrEffect();
+ }
+ catch (System.Exception ex)
+ {
+ Logger.Error($"Error resetting cooldown and effect for button {customActionButton.GetType().Name}: {ex}");
+ }
}
}
}
diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs
index 27a8026..acb4482 100644
--- a/MiraAPI/Patches/HudManagerPatches.cs
+++ b/MiraAPI/Patches/HudManagerPatches.cs
@@ -1,6 +1,11 @@
-using HarmonyLib;
+using System;
+using System.Collections;
+using HarmonyLib;
using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using Reactor.Utilities;
using Reactor.Utilities.Extensions;
+using Rewired;
using UnityEngine;
using Object = UnityEngine.Object;
@@ -77,7 +82,14 @@ public static class HudManagerPatches
continue;
}
- button.CreateButton(location);
+ try
+ {
+ button.CreateButton(location);
+ }
+ catch (System.Exception e)
+ {
+ Logger.Error($"Failed to create custom button {button.GetType().Name}: {e}");
+ }
}
gridArrange.Start();
@@ -105,7 +117,36 @@ public static class HudManagerPatches
foreach (var button in CustomButtonManager.CustomButtons)
{
- button.SetActive(isActive, role);
+ try
+ {
+ button.SetActive(isActive, role);
+ }
+ catch (System.Exception e)
+ {
+ Logger.Error($"Failed to set custom button {button.GetType().Name} active: {e}");
+ }
+ }
+ }
+
+ [HarmonyPatch(nameof(HudManager.Update))]
+ [HarmonyPostfix]
+ public static void StartPostfix()
+ {
+ if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started) return;
+ if (PlayerControl.LocalPlayer.Data.IsDead) return;
+
+ foreach (var entry in KeybindManager.GetEntries())
+ {
+ var player = ReInput.players.GetPlayer(0);
+ var keyboard = player.controllers.Keyboard;
+
+ if (player.GetButtonDown(entry.Id) &&
+ (entry.Modifier1 == ModifierKey.None || keyboard.GetModifierKey(entry.Modifier1)) &&
+ (entry.Modifier2 == ModifierKey.None || keyboard.GetModifierKey(entry.Modifier2)) &&
+ (entry.Modifier3 == ModifierKey.None || keyboard.GetModifierKey(entry.Modifier3)))
+ {
+ entry.Handler?.Invoke();
+ }
}
}
}
diff --git a/MiraAPI/Patches/IntroCutscenePatches.cs b/MiraAPI/Patches/IntroCutscenePatches.cs
index d068f26..af7ce8c 100644
--- a/MiraAPI/Patches/IntroCutscenePatches.cs
+++ b/MiraAPI/Patches/IntroCutscenePatches.cs
@@ -61,6 +61,11 @@ public static class IntroCutscenePatches
public static void GameBeginPatch(IntroCutscene __instance)
{
MiraEventManager.InvokeEvent(new IntroEndEvent(__instance));
+
+ var @event = new BeforeRoundStartEvent(true);
+ MiraEventManager.InvokeEvent(@event);
+
+ if (@event.IsCancelled) return;
MiraEventManager.InvokeEvent(new RoundStartEvent(true));
}
}
diff --git a/MiraAPI/Patches/Keybinds/KeybindsPatch.cs b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs
new file mode 100644
index 0000000..e609086
--- /dev/null
+++ b/MiraAPI/Patches/Keybinds/KeybindsPatch.cs
@@ -0,0 +1,20 @@
+using HarmonyLib;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities;
+using Rewired;
+
+namespace MiraAPI.Patches.Keybinds
+{
+ [HarmonyPatch(typeof(InputManager_Base), nameof(InputManager_Base.Awake))]
+ public static class KeybindMenuPatch
+ {
+ [HarmonyPrefix]
+ private static void StartPrefix(InputManager_Base __instance)
+ {
+ foreach (var entry in KeybindManager.GetEntries())
+ {
+ __instance.userData.RegisterModBind(entry.Id, entry.Description, entry.Key, modifier1: entry.Modifier1, modifier2: entry.Modifier2, modifier3: entry.Modifier3);
+ }
+ }
+ }
+}
diff --git a/MiraAPI/Patches/Menu/HatsTabPatches.cs b/MiraAPI/Patches/Menu/HatsTabPatches.cs
index daf87b1..8d36e9c 100644
--- a/MiraAPI/Patches/Menu/HatsTabPatches.cs
+++ b/MiraAPI/Patches/Menu/HatsTabPatches.cs
@@ -84,7 +84,7 @@ public static class HatsTabPatches
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent().Destroy();
text.EnableStencilMasking();
- text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages";
+ text.text = $"{groupName} ({currentPage + 1}/{sortedHats.Count})\nPress Ctrl or Tab to cycle pages";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 3f;
text.fontSizeMax = 3f;
@@ -114,7 +114,7 @@ public static class HatsTabPatches
yield return null;
}
- __instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
+ __instance.SetScrollerBounds();
yield return new WaitForSeconds(0.01f);
}
__instance.currentHatIsEquipped = true;
diff --git a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs
index d66c8f3..11657a0 100644
--- a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs
+++ b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs
@@ -73,7 +73,7 @@ public static class NameplatesTabPatches
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent().Destroy();
text.EnableStencilMasking();
- text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages";
+ text.text = $"{groupName} ({currentPage + 1}/{SortedNameplates.Count})\nPress Ctrl or Tab to cycle pages";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 3f;
text.fontSizeMax = 3f;
@@ -91,7 +91,7 @@ public static class NameplatesTabPatches
hatIndex += 1;
}
- __instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
+ __instance.SetScrollerBounds();
__instance.currentNameplateIsEquipped = true;
}
diff --git a/MiraAPI/Patches/Menu/SkinsTabPatches.cs b/MiraAPI/Patches/Menu/SkinsTabPatches.cs
index b102fb1..9378d8b 100644
--- a/MiraAPI/Patches/Menu/SkinsTabPatches.cs
+++ b/MiraAPI/Patches/Menu/SkinsTabPatches.cs
@@ -76,7 +76,7 @@ public static class SkinsTabPatches
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent().Destroy();
text.EnableStencilMasking();
- text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages";
+ text.text = $"{groupName} ({currentPage + 1}/{sortedSkins.Count})\nPress Ctrl or Tab to cycle pages";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 3f;
text.fontSizeMax = 3f;
@@ -94,7 +94,7 @@ public static class SkinsTabPatches
hatIndex += 1;
}
- __instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
+ __instance.SetScrollerBounds();
__instance.currentSkinIsEquipped = true;
}
diff --git a/MiraAPI/Patches/Menu/VisorsTabPatches.cs b/MiraAPI/Patches/Menu/VisorsTabPatches.cs
index 81845c6..92d5b8e 100644
--- a/MiraAPI/Patches/Menu/VisorsTabPatches.cs
+++ b/MiraAPI/Patches/Menu/VisorsTabPatches.cs
@@ -72,7 +72,7 @@ public static class VisorsTabPatches
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent().Destroy();
text.EnableStencilMasking();
- text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages";
+ text.text = $"{groupName} ({currentPage + 1}/{SortedVisors.Count})\nPress Ctrl or Tab to cycle pages";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 3f;
text.fontSizeMax = 3f;
@@ -90,7 +90,7 @@ public static class VisorsTabPatches
hatIndex += 1;
}
- __instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
+ __instance.SetScrollerBounds();
__instance.currentVisorIsEquipped = true;
}
diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs
index 0d88920..155b50d 100644
--- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs
+++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs
@@ -220,6 +220,7 @@ internal static class GameOptionsMenuPatch
menu.checkboxOrigin,
menu.numberOptionOrigin,
menu.stringOptionOrigin,
+ menu.playerOptionOrigin,
menu.settingsContainer));
foreach (var newOpt in options)
diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
index 7313206..fd9c78b 100644
--- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
+++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
@@ -288,7 +288,7 @@ internal static class GameSettingMenuPatches
{
if (_text is not null && SelectedModIdx == 0)
{
- _text.text = "Default";
+ _text.text = $"(Page 0/{MiraPluginManager.Instance.RegisteredPlugins.Length})\nDefault";
_text.fontSizeMax = 3.2f;
SelectedMod = null;
}
@@ -298,7 +298,7 @@ internal static class GameSettingMenuPatches
SelectedMod = MiraPluginManager.Instance.RegisteredPlugins[SelectedModIdx - 1];
var name = SelectedMod.MiraPlugin.OptionsTitleText;
- _text.text = name[..Math.Min(name.Length, 25)];
+ _text.text = $"(Page {SelectedModIdx}/{MiraPluginManager.Instance.RegisteredPlugins.Length})\n" + name[..Math.Min(name.Length, 25)];
}
bool replaceWithModifiers = true;
diff --git a/MiraAPI/Patches/Options/OptionsPatches.cs b/MiraAPI/Patches/Options/OptionsPatches.cs
index 1a388d8..469a053 100644
--- a/MiraAPI/Patches/Options/OptionsPatches.cs
+++ b/MiraAPI/Patches/Options/OptionsPatches.cs
@@ -1,7 +1,13 @@
-using HarmonyLib;
+using System.Collections.Generic;
+using System.Linq;
+using HarmonyLib;
using Il2CppSystem;
+using MiraAPI.GameOptions;
+using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Roles;
using MiraAPI.Utilities;
+using UnityEngine;
+using Object = Il2CppSystem.Object;
namespace MiraAPI.Patches.Options;
@@ -22,7 +28,9 @@ public static class OptionsPatches
__instance.roleMaxCount = 1;
}
- __instance.roleChance += 10;
+ var increment = Input.GetKey(KeyCode.LeftShift) ? 5 : 10;
+ __instance.roleChance += increment;
+
if (__instance.roleChance > 100)
{
__instance.roleChance = 0;
@@ -46,7 +54,9 @@ public static class OptionsPatches
__instance.roleMaxCount = 1;
}
- __instance.roleChance -= 10;
+ var increment = Input.GetKey(KeyCode.LeftShift) ? 5 : 10;
+ __instance.roleChance -= increment;
+
if (__instance.roleChance < 0)
{
__instance.roleChance = 100;
@@ -107,6 +117,7 @@ public static class OptionsPatches
[HarmonyPrefix]
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.AdjustButtonsActiveState))]
[HarmonyPatch(typeof(StringOption), nameof(StringOption.AdjustButtonsActiveState))]
+ [HarmonyPatch(typeof(PlayerOption), nameof(PlayerOption.AdjustButtonsActiveState))]
[HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.AdjustChanceButtonsActiveState))]
[HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.AdjustCountButtonsActiveState))]
public static bool AdjustButtonsPrefix(OptionBehaviour __instance)
@@ -123,6 +134,11 @@ public static class OptionsPatches
stringOption.MinusBtn.SetInteractable(true);
stringOption.PlusBtn.SetInteractable(true);
}
+ if (__instance.TryCast() is { } playerOption)
+ {
+ playerOption.MinusBtn.SetInteractable(true);
+ playerOption.PlusBtn.SetInteractable(true);
+ }
return false;
}
@@ -172,6 +188,7 @@ public static class OptionsPatches
[HarmonyPrefix]
[HarmonyPatch(typeof(StringOption), nameof(StringOption.UpdateValue))]
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.UpdateValue))]
+ [HarmonyPatch(typeof(PlayerOption), nameof(PlayerOption.UpdateValue))]
public static bool UpdateValuePrefix(OptionBehaviour __instance)
{
return !__instance.IsCustom();
diff --git a/MiraAPI/Patches/Options/PlayerOptionPatches.cs b/MiraAPI/Patches/Options/PlayerOptionPatches.cs
new file mode 100644
index 0000000..2b77bc1
--- /dev/null
+++ b/MiraAPI/Patches/Options/PlayerOptionPatches.cs
@@ -0,0 +1,128 @@
+using System;
+using System.Linq;
+using HarmonyLib;
+using MiraAPI.GameOptions;
+using MiraAPI.GameOptions.OptionTypes;
+using MiraAPI.Utilities;
+using Reactor.Utilities;
+using UnityEngine;
+using Object = Il2CppSystem.Object;
+
+namespace MiraAPI.Patches.Options;
+
+[HarmonyPatch(typeof(PlayerOption))]
+public static class PlayerOptionPatches
+{
+ // Updates the list of players according to the filter.
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(PlayerOption.FixedUpdate))]
+ public static bool PlayerOptionUpdatePatch(PlayerOption __instance)
+ {
+ if (!__instance.IsCustom())
+ {
+ return true;
+ }
+
+ if (!ModdedOptionsManager.CreatedPlayerOptions.TryGetValue(__instance, out var moddedPlrOpt)) return false;
+
+ var filteredPlayers = moddedPlrOpt.GetFilteredPlayers();
+ if (filteredPlayers.Count != __instance.Values.Count)
+ {
+ filteredPlayers.Sort((a, b) => a.PlayerId.CompareTo(b.PlayerId));
+
+ __instance.Values = filteredPlayers.ToIl2CppList();
+ moddedPlrOpt.Values = __instance.Values;
+ __instance.playerIndex = filteredPlayers.FindIndex(p => p.PlayerId == __instance.Value);
+ __instance.SetValueText();
+ }
+
+ __instance.CheckValueChanged();
+
+ return false;
+ }
+
+ // Disable going under 0 if you aren't allowed to choose None.
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(PlayerOption.UpdatePlayerIndex))]
+ public static bool UpdatePlayerIndexPatch(PlayerOption __instance, int index)
+ {
+ if (!__instance.IsCustom() || ModdedOptionsManager.CreatedPlayerOptions[__instance].AllowNone)
+ {
+ return true;
+ }
+
+ var newIndex = Mathf.Clamp(index, 0, __instance.Values.Count - 1);
+ __instance.playerIndex = newIndex;
+ __instance.Value = __instance.Values[newIndex].PlayerId;
+ return false;
+ }
+
+ // Replace "Round Robin" with "None" in the Host Options Menu.
+ [HarmonyPostfix]
+ [HarmonyPatch(nameof(PlayerOption.SetValueText))]
+ public static void SetValueTextPatch(PlayerOption __instance)
+ {
+ if (__instance.playerIndex < 0)
+ {
+ __instance.ValueText.text = TranslationController.Instance.GetString(StringNames.FilterNone);
+ }
+ }
+
+ // Innersloth hardcode literally replaces the value with the HNS impostor option OnEnable, so I have to patch that out. Smh.
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(PlayerOption.OnEnable))]
+ public static bool OnEnablePatch(PlayerOption __instance)
+ {
+ return !__instance.IsCustom();
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(PlayerOption.GetInt))]
+ public static bool GetIntPatch(PlayerOption __instance, ref int __result)
+ {
+ if (!__instance.IsCustom())
+ {
+ return true;
+ }
+
+ __result = __instance.Value;
+ return false;
+ }
+
+ // Used to replace "Round Robin" with "None" for option notification & view pane.
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(PlayerSelectionGameSetting), nameof(PlayerSelectionGameSetting.GetValueString))]
+ public static bool GetStringPatch(PlayerSelectionGameSetting __instance, float value, ref string __result)
+ {
+ var option = ModdedOptionsManager.ModdedOptions.FirstOrDefault(x => x.Value.Data == __instance).Value;
+ if (option == null) return true;
+
+ var networkedPlayerInfo = GameData.Instance.AllPlayers.ToArray().FirstOrDefault(p => p.PlayerId == (int)value);
+ if (networkedPlayerInfo != null)
+ {
+ __result = networkedPlayerInfo.PlayerName;
+ return false;
+ }
+
+ __result = TranslationController.Instance.GetString(StringNames.FilterNone);
+ return false;
+ }
+
+ // "UpdateValue" was inlined and I needed to replace it, so I just rewrote the method where it's called from.
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(PlayerOption.CheckValueChanged))]
+ public static bool UpdateValuePatch(PlayerOption __instance)
+ {
+ if (!__instance.IsCustom())
+ {
+ return true;
+ }
+
+ if (__instance.oldValue == __instance.Value) return false;
+
+ __instance.oldValue = __instance.Value;
+ __instance.SetValueText();
+
+ return false;
+ }
+}
diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
index 71ecdc1..80f96c9 100644
--- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
+++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
+using MiraAPI.GameOptions;
using MiraAPI.Networking;
using MiraAPI.Roles;
using MiraAPI.Utilities;
@@ -30,6 +31,9 @@ public static class RoleSettingMenuPatches
private static float ScrollerNum { get; set; } = 0.522f;
+ private static RoleBehaviour? CurrentRole { get; set; }
+ private static List? CurrentRoleOptions { get; set; }
+
[HarmonyPostfix]
[HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))]
public static void SetQuotaTabPostfix(RolesSettingsMenu __instance)
@@ -45,6 +49,8 @@ public static class RoleSettingMenuPatches
RoleOptionSettings.ForEach(Object.Destroy);
Headers.Clear();
RoleOptionSettings.Clear();
+ CurrentRole = null;
+ CurrentRoleOptions = null;
__instance.roleChances = new Il2CppSystem.Collections.Generic.List();
__instance.advancedSettingChildren = new Il2CppSystem.Collections.Generic.List();
@@ -262,9 +268,31 @@ public static class RoleSettingMenuPatches
[HarmonyPrefix]
[HarmonyPatch(nameof(RolesSettingsMenu.Update))]
- public static bool UpdatePatch()
+ public static bool UpdatePatch(RolesSettingsMenu __instance)
{
- return GameSettingMenuPatches.SelectedModIdx == 0;
+ if (GameSettingMenuPatches.SelectedModIdx == 0) return true;
+ if (CurrentRole == null || CurrentRoleOptions == null) return false;
+
+ var hasImage = CurrentRole.RoleScreenshot != null;
+ var num = hasImage ? -0.872f : -1;
+
+ foreach (var opt in CurrentRoleOptions)
+ {
+ if (opt.OptionBehaviour == null) continue;
+
+ if (!opt.Visible.Invoke())
+ {
+ opt.OptionBehaviour.gameObject.SetActive(false);
+ continue;
+ }
+
+ opt.OptionBehaviour.transform.localPosition = new Vector3(hasImage ? 2.17f : 1.1f, num, -2f);
+ opt.OptionBehaviour.gameObject.SetActive(true);
+ num += -0.45f;
+ }
+
+ __instance.scrollBar.SetYBoundsMax(-num - 3);
+ return false;
}
[HarmonyPostfix]
@@ -276,6 +304,8 @@ public static class RoleSettingMenuPatches
return;
}
+ CurrentRole = null;
+ CurrentRoleOptions = null;
__instance.scrollBar.SetScrollBounds(__instance);
}
@@ -328,27 +358,26 @@ public static class RoleSettingMenuPatches
optBehaviour.gameObject.DestroyImmediate();
}
+ CurrentRole = role;
__instance.advancedSettingChildren.Clear();
- var hasImage = role.RoleScreenshot != null;
- var num = hasImage ? -0.872f : -1;
-
// TODO: create sub groups under the role settings.
var filteredOptions = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.GroupVisible() && x.OptionableType == role.GetType())
.SelectMany(x => x.Options)
- .Where(x => x.Visible())
.ToList() ?? [];
+ CurrentRoleOptions = filteredOptions;
+
foreach (var option in filteredOptions)
{
var newOpt = option.CreateOption(
__instance.checkboxOrigin,
__instance.numberOptionOrigin,
__instance.stringOptionOrigin,
+ GameSettingMenu.Instance.GameSettingsTab.playerOptionOrigin,
__instance.AdvancedRolesSettings.transform);
- newOpt.transform.localPosition = new Vector3(hasImage ? 2.17f : 1.1f, num, -2f);
newOpt.SetClickMask(__instance.ButtonClickMask);
SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true);
@@ -366,11 +395,10 @@ public static class RoleSettingMenuPatches
newOpt.LabelBackground.enabled = false;
__instance.advancedSettingChildren.Add(newOpt);
- num += -0.45f;
+ newOpt.gameObject.SetActive(false);
newOpt.Initialize();
}
- __instance.scrollBar.SetYBoundsMax(-num - 3);
__instance.scrollBar.ScrollToTop();
}
@@ -506,7 +534,7 @@ public static class RoleSettingMenuPatches
if (GameSettingMenuPatches.SelectedMod is null ||
GameSettingMenuPatches.SelectedMod.InternalOptionGroups
- .Exists(x => x.OptionableType == role.GetType()))
+ .Exists(x => x.GroupVisible() && x.OptionableType == role.GetType() && x.Options.Any(y => y.Visible())))
{
var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform);
newButton.name = "ConfigButton";
diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs
index c93aa6d..a0b88cd 100644
--- a/MiraAPI/Patches/PlayerControlPatches.cs
+++ b/MiraAPI/Patches/PlayerControlPatches.cs
@@ -7,6 +7,7 @@ using MiraAPI.Hud;
using MiraAPI.Modifiers;
using MiraAPI.Utilities;
using MiraAPI.Voting;
+using Reactor.Utilities;
using Reactor.Utilities.Extensions;
namespace MiraAPI.Patches;
@@ -94,12 +95,19 @@ internal static class PlayerControlPatches
continue;
}
- if (!button.Enabled(__instance.Data?.Role))
+ try
{
- continue;
- }
+ if (!button.Enabled(__instance.Data?.Role))
+ {
+ continue;
+ }
- button.FixedUpdateHandler(__instance);
+ button.FixedUpdateHandler(__instance);
+ }
+ catch (System.Exception e)
+ {
+ Logger.Error($"Failed to update custom button {button.GetType().Name}: {e}");
+ }
}
}
}
diff --git a/MiraAPI/Patches/Roles/EjectionPatches.cs b/MiraAPI/Patches/Roles/EjectionPatches.cs
index 85641eb..009da27 100644
--- a/MiraAPI/Patches/Roles/EjectionPatches.cs
+++ b/MiraAPI/Patches/Roles/EjectionPatches.cs
@@ -38,8 +38,13 @@ internal static class EjectionPatches
{
public static void Postfix()
{
- var @event = new RoundStartEvent(false);
+ var @event = new BeforeRoundStartEvent(false);
MiraEventManager.InvokeEvent(@event);
+
+ if (@event.IsCancelled) return;
+
+ var @event2 = new RoundStartEvent(false);
+ MiraEventManager.InvokeEvent(@event2);
}
}
}
diff --git a/MiraAPI/Patches/Roles/GameStartupPatch.cs b/MiraAPI/Patches/Roles/GameStartupPatch.cs
index 6ab234d..9f88bdd 100644
--- a/MiraAPI/Patches/Roles/GameStartupPatch.cs
+++ b/MiraAPI/Patches/Roles/GameStartupPatch.cs
@@ -23,7 +23,11 @@ public static class GameStartupPatch
_runOnce = true;
- if (MiraPluginManager.Instance.QueuedRoleRegistrations.Count <= 0) return;
+ if (MiraPluginManager.Instance.QueuedRoleRegistrations.Count <= 0)
+ {
+ return;
+ }
+
foreach (var queue in MiraPluginManager.Instance.QueuedRoleRegistrations)
{
CustomRoleManager.RegisterRoleTypes(queue.Value, queue.Key);
diff --git a/MiraAPI/Patches/Roles/HudManagerPatches.cs b/MiraAPI/Patches/Roles/HudManagerPatches.cs
index 29affce..348f739 100644
--- a/MiraAPI/Patches/Roles/HudManagerPatches.cs
+++ b/MiraAPI/Patches/Roles/HudManagerPatches.cs
@@ -12,7 +12,7 @@ namespace MiraAPI.Patches.Roles;
public static class HudManagerPatches
{
// Custom role tab.
- private static TaskPanelBehaviour? _roleTab;
+ public static TaskPanelBehaviour? RoleTab;
///
/// Fixes Kill Button not showing for Neutral killing role.
@@ -38,9 +38,9 @@ public static class HudManagerPatches
__instance.SabotageButton.gameObject.SetActive(isActive && customRole.Configuration.CanUseSabotage);
}
- if (_roleTab)
+ if (RoleTab)
{
- _roleTab?.gameObject.SetActive(isActive);
+ RoleTab?.gameObject.SetActive(isActive);
}
}
@@ -62,16 +62,16 @@ public static class HudManagerPatches
if (role is ICustomRole { Configuration.RoleHintType: RoleHintType.RoleTab } customRole)
{
- if (_roleTab == null)
+ if (RoleTab == null)
{
- _roleTab = CustomRoleManager.CreateRoleTab(customRole);
+ RoleTab = CustomRoleManager.CreateRoleTab(customRole);
}
- CustomRoleManager.UpdateRoleTab(_roleTab, customRole);
+ CustomRoleManager.UpdateRoleTab(RoleTab, customRole);
}
- else if (_roleTab)
+ else if (RoleTab)
{
- _roleTab?.gameObject.Destroy();
+ RoleTab?.gameObject.Destroy();
}
}
}
diff --git a/MiraAPI/Patches/ServerDropdownPatch.cs b/MiraAPI/Patches/ServerDropdownPatch.cs
index 0fb96e3..36da2d7 100644
--- a/MiraAPI/Patches/ServerDropdownPatch.cs
+++ b/MiraAPI/Patches/ServerDropdownPatch.cs
@@ -1,6 +1,7 @@
using HarmonyLib;
using UnityEngine;
using UnityEngine.Events;
+using UnityEngine.SceneManagement;
namespace MiraAPI.Patches;
@@ -14,6 +15,8 @@ public static class ServerDropdownPatch
foreach (var regionInfo in DestroyableSingleton.Instance.AvailableRegions)
{
+ var findingGame = SceneManager.GetActiveScene().name is "FindAGame";
+
if (DestroyableSingleton.Instance.CurrentRegion.Equals(regionInfo))
{
__instance.defaultButtonSelected = __instance.firstOption;
@@ -27,6 +30,10 @@ public static class ServerDropdownPatch
var region = regionInfo;
var serverListButton = __instance.ButtonPool.Get();
var x = num % 2 == 0 ? -2 : 2;
+ if (findingGame)
+ {
+ x += 2;
+ }
var y = -0.55f * (num / 2);
serverListButton.transform.localPosition = new Vector3(x, __instance.y_posButton + y, -1f);
serverListButton.transform.localScale = Vector3.one;
@@ -39,7 +46,7 @@ public static class ServerDropdownPatch
serverListButton.Button.OnClick.AddListener((UnityAction)(() => { __instance.ChooseOption(region); }));
__instance.controllerSelectable.Add(serverListButton.Button);
__instance.background.transform.localPosition = new Vector3(
- 0f,
+ findingGame ? 2f : 0f,
__instance.initialYPos + (-0.3f * (num / 2)),
0f);
__instance.background.size = new Vector2(__instance.background.size.x, 1.2f + (0.6f * (num / 2)));
diff --git a/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs b/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs
index a276946..7652d4f 100644
--- a/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs
+++ b/MiraAPI/Patches/Voting/DummyBehaviourPatch.cs
@@ -1,16 +1,79 @@
-using HarmonyLib;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using BepInEx.Unity.IL2CPP.Utils.Collections;
+using HarmonyLib;
+using MiraAPI.Events;
+using MiraAPI.Events.Vanilla.Meeting.Voting;
using MiraAPI.Utilities;
+using MiraAPI.Voting;
+using Reactor.Utilities;
+using Reactor.Utilities.Extensions;
+using UnityEngine;
namespace MiraAPI.Patches.Voting;
-[HarmonyPatch(typeof(DummyBehaviour), nameof(DummyBehaviour.Update))]
-internal static class DummyBehaviourPatch
+[HarmonyPatch(typeof(DummyBehaviour))]
+internal static class DummyBehaviourPatches
{
- public static void Postfix(DummyBehaviour __instance)
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(DummyBehaviour.Update))]
+ public static bool DummyUpdatePatch(DummyBehaviour __instance)
{
- if (MeetingHud.Instance && __instance.myPlayer.GetVoteData())
+ NetworkedPlayerInfo data = __instance.myPlayer.Data;
+ if (data == null || data.IsDead) return false;
+
+ if (MeetingHud.Instance)
{
- __instance.voted = __instance.myPlayer.GetVoteData().VotesRemaining == 0;
+ if (!__instance.voted)
+ {
+ __instance.StartCoroutine(CoDoVote(__instance).WrapToIl2Cpp());
+ }
+ }
+ else
+ {
+ __instance.voted = false;
+ }
+
+ return false;
+ }
+
+ private static IEnumerator CoDoVote(DummyBehaviour dummy)
+ {
+ Logger.Warning(dummy.voted);
+ dummy.voted = true;
+ yield return new WaitForSeconds(dummy.voteTime.Next());
+
+ var dummyVoteEvent = new DummyVoteEvent(dummy);
+ MiraEventManager.InvokeEvent(dummyVoteEvent);
+
+ if (dummyVoteEvent.IsCancelled)
+ {
+ dummy.voted = false;
+ yield break;
+ }
+
+ List potentialSuspects = new();
+ potentialSuspects.AddRange(PlayerControl.AllPlayerControls
+ .ToArray()
+ .Where(p => p != dummy.myPlayer)
+ .Where(p => dummyVoteEvent.PlayerIsValid(p))
+ .Select(p => p.PlayerId));
+
+ if (dummyVoteEvent.CanSkip)
+ {
+ potentialSuspects.Add(253);
+ }
+
+ if (CanVote(dummy))
+ {
+ VotingUtils.RpcCastVote(PlayerControl.LocalPlayer, dummy.myPlayer.PlayerId, potentialSuspects.Random());
}
}
+
+ private static bool CanVote(this DummyBehaviour dummy)
+ {
+ var dummyVoteData = dummy.myPlayer.GetVoteData();
+ return dummyVoteData.VotesRemaining > 0;
+ }
}
diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs
index 9dae0fd..02e9102 100644
--- a/MiraAPI/PluginLoading/MiraPluginManager.cs
+++ b/MiraAPI/PluginLoading/MiraPluginManager.cs
@@ -11,12 +11,14 @@ using MiraAPI.GameEnd;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.Hud;
+using MiraAPI.Keybinds;
using MiraAPI.Modifiers;
using MiraAPI.Presets;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Networking;
using Reactor.Utilities;
+using UnityEngine;
namespace MiraAPI.PluginLoading;
@@ -129,7 +131,16 @@ public sealed class MiraPluginManager
CustomButtonManager.Buttons = new ReadOnlyCollection(CustomButtonManager.CustomButtons);
// Cache all the registered plugins into an array for easy access
- RegisteredPlugins = [.._registeredPlugins.Values];
+ RegisteredPlugins = [.. _registeredPlugins.Values];
+
+ ModifierManager.Modifiers = new ReadOnlyCollection(ModifierManager.InternalModifiers);
+
+ foreach (var button in CustomButtonManager.Buttons)
+ {
+ KeybindManager.Register($"{button.Name}_Keybind", $"Keybind for {button.Name}", button.Defaultkeybind, button.ClickHandler, modifier1: button.Modifier1, modifier2: button.Modifier2, modifier3: button.Modifier3);
+
+ Logger.Info($"Registered keybind for button '{button.GetType().Name}' with default key {button.Defaultkeybind}.");
+ }
};
}
diff --git a/MiraAPI/Presets/SavePresetPopup.cs b/MiraAPI/Presets/SavePresetPopup.cs
index 9449857..87959e7 100644
--- a/MiraAPI/Presets/SavePresetPopup.cs
+++ b/MiraAPI/Presets/SavePresetPopup.cs
@@ -96,6 +96,3 @@ public class SavePresetPopup(nint cppPtr) : Minigame(cppPtr)
popup.Begin(null);
}
}
-
-
-
diff --git a/MiraAPI/Resources/mirabundle-win.bundle b/MiraAPI/Resources/mirabundle-win.bundle
index d73dcbe..06d8340 100644
Binary files a/MiraAPI/Resources/mirabundle-win.bundle and b/MiraAPI/Resources/mirabundle-win.bundle differ
diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs
index 20d61d0..5e1c3ae 100644
--- a/MiraAPI/Roles/CustomRoleManager.cs
+++ b/MiraAPI/Roles/CustomRoleManager.cs
@@ -35,6 +35,16 @@ public static class CustomRoleManager
public static readonly LoadableAsset ImpostorIntroSound =
CustomRoleUtils.GetIntroSound(RoleTypes.Impostor)!;
+ ///
+ /// Gets the list of custom roles as RoleBehaviour objects.
+ ///
+ public static IReadOnlyList CustomRoleBehaviours { get; private set; } = [];
+
+ ///
+ /// Gets the list of custom roles as ICustomRole objects.
+ ///
+ public static IReadOnlyList CustomMiraRoles { get; private set; } = [];
+
internal static readonly Dictionary CustomRoles = [];
internal static readonly Dictionary RoleIds = [];
@@ -53,6 +63,9 @@ public static class CustomRoleManager
{
RoleManager.GhostRoles.Add(role.Role);
}
+
+ CustomRoleBehaviours = CustomRoles.Values.ToList();
+ CustomMiraRoles = CustomRoles.Values.OfType().ToList();
}
internal static void RegisterRoleTypes(List roles, MiraPluginInfo pluginInfo)
diff --git a/MiraAPI/Utilities/EnumerableExtensions.cs b/MiraAPI/Utilities/EnumerableExtensions.cs
new file mode 100644
index 0000000..5e0bc35
--- /dev/null
+++ b/MiraAPI/Utilities/EnumerableExtensions.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MiraAPI.Utilities;
+
+///
+/// Extensions for IEnumerable.
+///
+public static class EnumerableExtensions
+{
+ ///
+ /// Shuffle the elements of the source enumerable using a new instance of Random.
+ ///
+ /// The source enumerable.
+ /// The type of the elements.
+ /// >The shuffled enumerable.
+ public static IEnumerable Shuffle(this IEnumerable source)
+ {
+ return source.Shuffle(new Random());
+ }
+
+ ///
+ /// Shuffle the elements of the source enumerable using the provided Random instance.
+ ///
+ /// The source enumerable.
+ /// The Random instance to use for shuffling.
+ /// The type of the elements.
+ /// >The shuffled enumerable.
+ public static IEnumerable Shuffle(this IEnumerable source, Random rng)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ ArgumentNullException.ThrowIfNull(rng);
+
+ return source.ShuffleIterator(rng);
+ }
+
+ private static IEnumerable ShuffleIterator(this IEnumerable source, Random rng)
+ {
+ var buffer = source.ToList();
+ for (var i = 0; i < buffer.Count; i++)
+ {
+ var j = rng.Next(i, buffer.Count);
+ yield return buffer[j];
+
+ buffer[j] = buffer[i];
+ }
+ }
+}
diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs
index 64e02f9..863df73 100644
--- a/MiraAPI/Utilities/Extensions.cs
+++ b/MiraAPI/Utilities/Extensions.cs
@@ -10,6 +10,8 @@ using MiraAPI.Networking;
using MiraAPI.Roles;
using MiraAPI.Voting;
using Reactor.Utilities;
+using Rewired;
+using Rewired.Data;
using TMPro;
using UnityEngine;
@@ -92,7 +94,7 @@ public static class Extensions
/// The current timer value.
/// The maximum timer value.
/// The format string to use for the timer text.
- public static void SetCooldownFormat(this ActionButton button, float timer, float maxTimer, string format="0")
+ public static void SetCooldownFormat(this ActionButton button, float timer, float maxTimer, string format = "0")
{
var num = Mathf.Clamp(timer / maxTimer, 0f, 1f);
button.isCoolingDown = num > 0f;
@@ -113,7 +115,7 @@ public static class Extensions
/// The current timer value.
/// The maximum timer value.
/// The format string to use for the timer text.
- public static void SetFillUpFormat(this ActionButton button, float timer, float maxTimer, string format="0")
+ public static void SetFillUpFormat(this ActionButton button, float timer, float maxTimer, string format = "0")
{
var num = Mathf.Clamp(timer / maxTimer, 0f, 1f);
button.isCoolingDown = num > 0f;
@@ -154,6 +156,24 @@ public static class Extensions
return TutorialManager.InstanceExists || AmongUsClient.Instance.HostId == playerControl.OwnerId;
}
+ ///
+ /// Used to convert a System.Collections.Generic.List to Il2cppSystem.
+ ///
+ /// The list.
+ /// The type in the list.
+ /// The converted list.
+ public static Il2CppSystem.Collections.Generic.List ToIl2CppList(this List systemList)
+ {
+ var il2cppList = new Il2CppSystem.Collections.Generic.List();
+
+ foreach (var item in systemList)
+ {
+ il2cppList.Add(item);
+ }
+
+ return il2cppList;
+ }
+
///
/// Determines if a float is an integer.
///
@@ -495,6 +515,7 @@ public static class Extensions
/// Whether impostors should be included in the search.
/// The radius to search within.
/// Whether colliders should be ignored when searching.
+ /// Determines if Ghosts are included.
/// Optional predicate to test if the object is valid.
/// The closest player if there is one, false otherwise.
public static PlayerControl? GetClosestPlayer(
@@ -502,16 +523,16 @@ public static class Extensions
bool includeImpostors,
float distance,
bool ignoreColliders = false,
+ bool includeGhosts = false,
Predicate? predicate = null)
{
var filteredPlayers = Helpers.GetClosestPlayers(playerControl, distance, ignoreColliders)
.Where(
playerInfo => !playerInfo.Data.Disconnected &&
playerInfo.PlayerId != playerControl.PlayerId &&
- !playerInfo.Data.IsDead &&
+ (includeGhosts || !playerInfo.Data.IsDead) &&
(includeImpostors || !playerInfo.Data.Role.IsImpostor))
.ToList();
-
return predicate != null ? filteredPlayers.Find(predicate) : filteredPlayers.FirstOrDefault();
}
@@ -526,4 +547,49 @@ public static class Extensions
renderer.material.SetColor(ShaderID.OutlineColor, color ?? Color.clear);
renderer.material.SetColor(ShaderID.AddColor, color ?? Color.clear);
}
+
+ // Inspired by: https://github.com/eDonnes124/Town-Of-Us-R/blob/master/source/Patches/Keybinds.cs#L29
+
+ ///
+ /// Registers a new mod keybind as a user-assignable button action in Rewired.
+ ///
+ /// The Rewired user data to add the action to.
+ /// The internal name of the action.
+ /// Text shown in the rebinding UI.
+ /// The default key to assign to this action.
+ /// Category ID to group actions in Rewired (default is 0).
+ /// The element identifier ID (default is -1, meaning none specified).
+ /// The for this action (default is Button).
+ /// The first optional modifier key (e.g., Control, Shift, Alt) that must be held together with the main key.
+ /// The second optional modifier key. Set to if not used.
+ /// The third optional modifier key. Set to if not used.
+ /// The action ID of the newly registered action.
+ public static int RegisterModBind(this UserData userData, string actionName, string description, KeyboardKeyCode key, int category = 0, int elementIdentifierId = -1, InputActionType type = InputActionType.Button, ModifierKey modifier1 = ModifierKey.None, ModifierKey modifier2 = ModifierKey.None, ModifierKey modifier3 = ModifierKey.None)
+ {
+ userData.AddAction(category);
+
+ var action = userData.GetAction(userData.actions.Count - 1)!;
+
+ action.name = actionName;
+ action.descriptiveName = description;
+ action.categoryId = category;
+ action.type = type;
+ action.userAssignable = true;
+
+ var map = new ActionElementMap
+ {
+ _elementIdentifierId = elementIdentifierId,
+ _actionId = action.id,
+ _elementType = ControllerElementType.Button,
+ _axisContribution = Pole.Positive,
+ _keyboardKeyCode = key,
+ _modifierKey1 = modifier1,
+ _modifierKey2 = modifier2,
+ _modifierKey3 = modifier3,
+ };
+ userData.keyboardMaps[0].actionElementMaps.Add(map);
+ userData.joystickMaps[0].actionElementMaps.Add(map);
+
+ return action.id;
+ }
}
diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs
index 7d8ce9b..2bf7b37 100644
--- a/MiraAPI/Utilities/Helpers.cs
+++ b/MiraAPI/Utilities/Helpers.cs
@@ -1,7 +1,11 @@
-using System.Collections;
+using System;
+using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using Il2CppSystem.IO;
+using MiraAPI.Keybinds;
+using Rewired;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
@@ -19,7 +23,7 @@ public static class Helpers
/// A list of alive players.
public static List GetAlivePlayers()
{
- return [.. GameData.Instance.AllPlayers.ToArray().Where(x => !x.IsDead && !x.Disconnected && x.Object).Select(x=>x.Object)];
+ return [.. GameData.Instance.AllPlayers.ToArray().Where(x => !x.IsDead && !x.Disconnected && x.Object).Select(x => x.Object)];
}
///
@@ -36,10 +40,10 @@ public static class Helpers
case 100:
return true;
default:
- {
- var num = Random.RandomRangeInt(1, 101);
- return num <= probability;
- }
+ {
+ var num = UnityEngine.Random.RandomRangeInt(1, 101);
+ return num <= probability;
+ }
}
}
@@ -398,6 +402,25 @@ public static class Helpers
public static string RandomString(int length, string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
{
return new string(Enumerable.Repeat(chars, length)
- .Select(s => s[Random.RandomRangeInt(0, s.Length)]).ToArray());
+ .Select(s => s[UnityEngine.Random.RandomRangeInt(0, s.Length)]).ToArray());
+ }
+
+ ///
+ /// Finds and returns an unused KeyCode that is not equal to the excluded key.
+ ///
+ /// The KeyCode to skip during the search.
+ ///
+ /// The first available KeyCode not currently used by any registered keybind,
+ /// or KeyCode.None if none are available.
+ ///
+ public static KeyboardKeyCode FindAvailableKey(KeyboardKeyCode exclude)
+ {
+ foreach (KeyboardKeyCode key in Enum.GetValues(typeof(KeyboardKeyCode)))
+ {
+ if (key == exclude) continue;
+ bool used = KeybindManager.GetEntries().Exists(e => e.Key == key);
+ if (!used) return key;
+ }
+ return KeyboardKeyCode.None;
}
}