From f43191c4f2bdfb838edda6cac8685504084b98cc Mon Sep 17 00:00:00 2001 From: ang-xd Date: Fri, 27 Jun 2025 19:16:56 -0400 Subject: [PATCH 1/4] Fix role option visibility & shift 5 increments on chance --- MiraAPI/Patches/Options/OptionsPatches.cs | 10 ++++- .../Patches/Options/RoleSettingMenuPatches.cs | 44 +++++++++++++++---- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/MiraAPI/Patches/Options/OptionsPatches.cs b/MiraAPI/Patches/Options/OptionsPatches.cs index 1a388d8..70a8ff4 100644 --- a/MiraAPI/Patches/Options/OptionsPatches.cs +++ b/MiraAPI/Patches/Options/OptionsPatches.cs @@ -2,6 +2,8 @@ using Il2CppSystem; using MiraAPI.Roles; using MiraAPI.Utilities; +using UnityEngine; +using Object = Il2CppSystem.Object; namespace MiraAPI.Patches.Options; @@ -22,7 +24,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 +50,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; diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 71ecdc1..6ce680d 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -30,6 +30,8 @@ public static class RoleSettingMenuPatches private static float ScrollerNum { get; set; } = 0.522f; + private static RoleBehaviour? CurrentRole { get; set; } + [HarmonyPostfix] [HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))] public static void SetQuotaTabPostfix(RolesSettingsMenu __instance) @@ -45,6 +47,7 @@ public static class RoleSettingMenuPatches RoleOptionSettings.ForEach(Object.Destroy); Headers.Clear(); RoleOptionSettings.Clear(); + CurrentRole = null; __instance.roleChances = new Il2CppSystem.Collections.Generic.List(); __instance.advancedSettingChildren = new Il2CppSystem.Collections.Generic.List(); @@ -262,9 +265,36 @@ 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) return false; + + var filteredOptions = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups + .Where(x => x.GroupVisible() && x.OptionableType == CurrentRole.GetType()) + .SelectMany(x => x.Options) + .ToList() ?? []; + + var hasImage = CurrentRole.RoleScreenshot != null; + var num = hasImage ? -0.872f : -1; + + foreach (var opt in filteredOptions) + { + 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] @@ -328,16 +358,14 @@ 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() ?? []; foreach (var option in filteredOptions) @@ -348,7 +376,6 @@ public static class RoleSettingMenuPatches __instance.stringOptionOrigin, __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 +393,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 +532,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"; From bc7f772de9be6aeb0c957cb4f0130e165e8c542b Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sat, 28 Jun 2025 18:44:31 -0400 Subject: [PATCH 2/4] 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. /// From eba2a037d3319dbc5d97557276e06fe644755955 Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sat, 28 Jun 2025 19:21:47 -0400 Subject: [PATCH 3/4] Bug fixes and chat pasting --- MiraAPI.Example/ExampleColors.cs | 5 ++- MiraAPI/Colors/CustomColor.cs | 33 +++++++++++++++---- MiraAPI/Patches/ChatControllerPatch.cs | 34 ++++++++++++++++++++ MiraAPI/Patches/Menu/HatsTabPatches.cs | 2 +- MiraAPI/Patches/Menu/NameplatesTabPatches.cs | 2 +- MiraAPI/Patches/Menu/SkinsTabPatches.cs | 2 +- MiraAPI/Patches/Menu/VisorsTabPatches.cs | 2 +- 7 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 MiraAPI/Patches/ChatControllerPatch.cs 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/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/Patches/ChatControllerPatch.cs b/MiraAPI/Patches/ChatControllerPatch.cs new file mode 100644 index 0000000..0108ece --- /dev/null +++ b/MiraAPI/Patches/ChatControllerPatch.cs @@ -0,0 +1,34 @@ +using HarmonyLib; +using UnityEngine; + +namespace MiraAPI.Patches; + +/// +/// Allows players to paste text into chat. +/// Source: https://github.com/CallOfCreator/NewMod/blob/main/NewMod/Patches/ClipboardPatch.cs +/// +[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/Menu/HatsTabPatches.cs b/MiraAPI/Patches/Menu/HatsTabPatches.cs index daf87b1..f985027 100644 --- a/MiraAPI/Patches/Menu/HatsTabPatches.cs +++ b/MiraAPI/Patches/Menu/HatsTabPatches.cs @@ -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..bb5482a 100644 --- a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs +++ b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs @@ -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..2e9ef2e 100644 --- a/MiraAPI/Patches/Menu/SkinsTabPatches.cs +++ b/MiraAPI/Patches/Menu/SkinsTabPatches.cs @@ -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..d88cdab 100644 --- a/MiraAPI/Patches/Menu/VisorsTabPatches.cs +++ b/MiraAPI/Patches/Menu/VisorsTabPatches.cs @@ -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; } From de77a9fa92e6e1a590da73741becde6ed054e4a8 Mon Sep 17 00:00:00 2001 From: ang-xd Date: Sat, 28 Jun 2025 20:55:22 -0400 Subject: [PATCH 4/4] Add page counter in options screen --- MiraAPI/Patches/Options/GameSettingMenuPatches.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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;