From bc7f772de9be6aeb0c957cb4f0130e165e8c542b Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sat, 28 Jun 2025 18:44:31 -0400 Subject: [PATCH] Add ModdedPlayerOption and fix role options scroll --- MiraAPI.Example/Options/ExampleOptions2.cs | 2 + MiraAPI/GameOptions/IModdedOption.cs | 3 +- MiraAPI/GameOptions/ModdedOptionsManager.cs | 2 + .../OptionTypes/ModdedEnumOption.cs | 4 +- .../OptionTypes/ModdedNumberOption.cs | 1 + .../GameOptions/OptionTypes/ModdedOption.cs | 2 + .../OptionTypes/ModdedPlayerOption.cs | 134 ++++++++++++++++++ .../OptionTypes/ModdedToggleOption.cs | 2 +- .../Patches/Options/GameOptionsMenuPatch.cs | 1 + MiraAPI/Patches/Options/OptionsPatches.cs | 13 +- .../Patches/Options/PlayerOptionPatches.cs | 128 +++++++++++++++++ .../Patches/Options/RoleSettingMenuPatches.cs | 18 +-- MiraAPI/Utilities/Extensions.cs | 18 +++ 13 files changed, 315 insertions(+), 13 deletions(-) create mode 100644 MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs create mode 100644 MiraAPI/Patches/Options/PlayerOptionPatches.cs 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. ///