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/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..8431d1f
--- /dev/null
+++ b/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs
@@ -0,0 +1,134 @@
+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/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/OptionsPatches.cs b/MiraAPI/Patches/Options/OptionsPatches.cs
index 70a8ff4..469a053 100644
--- a/MiraAPI/Patches/Options/OptionsPatches.cs
+++ b/MiraAPI/Patches/Options/OptionsPatches.cs
@@ -1,5 +1,9 @@
-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;
@@ -113,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)
@@ -129,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;
}
@@ -178,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 6ce680d..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;
@@ -31,6 +32,7 @@ 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))]
@@ -48,6 +50,7 @@ public static class RoleSettingMenuPatches
Headers.Clear();
RoleOptionSettings.Clear();
CurrentRole = null;
+ CurrentRoleOptions = null;
__instance.roleChances = new Il2CppSystem.Collections.Generic.List();
__instance.advancedSettingChildren = new Il2CppSystem.Collections.Generic.List();
@@ -268,17 +271,12 @@ public static class RoleSettingMenuPatches
public static bool UpdatePatch(RolesSettingsMenu __instance)
{
if (GameSettingMenuPatches.SelectedModIdx == 0) return true;
- if (CurrentRole == null) return false;
-
- var filteredOptions = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
- .Where(x => x.GroupVisible() && x.OptionableType == CurrentRole.GetType())
- .SelectMany(x => x.Options)
- .ToList() ?? [];
+ if (CurrentRole == null || CurrentRoleOptions == null) return false;
var hasImage = CurrentRole.RoleScreenshot != null;
var num = hasImage ? -0.872f : -1;
- foreach (var opt in filteredOptions)
+ foreach (var opt in CurrentRoleOptions)
{
if (opt.OptionBehaviour == null) continue;
@@ -306,6 +304,8 @@ public static class RoleSettingMenuPatches
return;
}
+ CurrentRole = null;
+ CurrentRoleOptions = null;
__instance.scrollBar.SetScrollBounds(__instance);
}
@@ -361,19 +361,21 @@ public static class RoleSettingMenuPatches
CurrentRole = role;
__instance.advancedSettingChildren.Clear();
-
// 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)
.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.SetClickMask(__instance.ButtonClickMask);
diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs
index 64e02f9..57887d8 100644
--- a/MiraAPI/Utilities/Extensions.cs
+++ b/MiraAPI/Utilities/Extensions.cs
@@ -154,6 +154,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.
///