From 613ebafa8a4d53516dfb691d1aa57de9fee00e25 Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:24:18 -0400 Subject: [PATCH 01/29] Add match info guide scale setting --- MiraAPI/MiraApiSettings.cs | 10 ++++++++++ MiraAPI/Resources/Locale/en_US.xml | 1 + 2 files changed, 11 insertions(+) diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs index 1560955..a7893c2 100644 --- a/MiraAPI/MiraApiSettings.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -69,6 +69,10 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) OldUiButtonScaleFactor = TopRightButtonsFactorSlider.Value; } + else if (configEntry == MatchInfoGuideScaleFactor && MatchInfoGuide.Instance) + { + RoleGuidePatches.ApplyGuideScale(MatchInfoGuide.Instance, MatchInfoGuideScaleFactor.Value); + } else if (configEntry == WikiOnBottomRow) { SetUpButtonPositions(); @@ -198,6 +202,12 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) [LocalSliderSetting(min: 0.3f, max: 2f, suffixType: MiraNumberSuffixes.Multiplier, formatString: "0.00", displayValue: true)] public ConfigEntry TopRightButtonsFactorSlider { get; private set; } = config.Bind("MiraApi.VisualsUi", "MiraApi.TopRightButtonScaleFactor", 1f); + /// + /// Gets the scale of the match info guide. + /// + [LocalSliderSetting(min: 0.5f, max: 1.5f, suffixType: MiraNumberSuffixes.Multiplier, formatString: "0.00", displayValue: true)] + public ConfigEntry MatchInfoGuideScaleFactor { get; private set; } = config.Bind("MiraApi.VisualsUi", "MiraApi.MatchInfoGuideScaleFactor", 1f); + /// /// Gets the fps specified by the player. /// diff --git a/MiraAPI/Resources/Locale/en_US.xml b/MiraAPI/Resources/Locale/en_US.xml index ba60484..9560412 100644 --- a/MiraAPI/Resources/Locale/en_US.xml +++ b/MiraAPI/Resources/Locale/en_US.xml @@ -8,6 +8,7 @@ Show Keybinds on Buttons Button Scale Factor Top-Right UI Scale Factor + Match Info Guide Scale Factor Max FPS Display Wiki on Bottom Row Enable Sabotage Flashes From cc4daf5d8ba8089068b1d79b0ad1436791a66ba7 Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:40:58 -0400 Subject: [PATCH 02/29] change maximum to 1.00 instead of 1.50 --- MiraAPI/MiraApiSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs index a7893c2..7db2c54 100644 --- a/MiraAPI/MiraApiSettings.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -205,7 +205,7 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) /// /// Gets the scale of the match info guide. /// - [LocalSliderSetting(min: 0.5f, max: 1.5f, suffixType: MiraNumberSuffixes.Multiplier, formatString: "0.00", displayValue: true)] + [LocalSliderSetting(min: 0.5f, max: 1f, suffixType: MiraNumberSuffixes.Multiplier, formatString: "0.00", displayValue: true)] public ConfigEntry MatchInfoGuideScaleFactor { get; private set; } = config.Bind("MiraApi.VisualsUi", "MiraApi.MatchInfoGuideScaleFactor", 1f); /// From 57471289e1e5459d387fa4d3fdb0e18fadabbcda Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:43:48 -0400 Subject: [PATCH 03/29] Scale advanced wiki page content --- MiraAPI/Utilities/Helpers.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 773a4ae..53bade5 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -30,6 +30,7 @@ namespace MiraAPI.Utilities; /// public static class Helpers { + internal const float MatchInfoGuideBaseScale = 1.3f; private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); public static ReadOnlyCollection? GetModdedOptionsForType(Type classType) @@ -833,7 +834,8 @@ public static class Helpers grid.localScale = new Vector3(1.3f, 1.3f, 1); } - obj.transform.SetParent(parent.Inner.transform); + obj.transform.SetParent(parent.Inner.transform, false); + obj.transform.localScale = Vector3.one / MatchInfoGuideBaseScale; obj.transform.localPosition = new Vector3(0f, 0.5f, 0f); var scrollHeight = variableTextSizing ? (desc.textBounds.size.y - 2.5f) + maxTextSize + 0.475f From 6f36aa91a53faf4da4911d7f4998040b189a169f Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:44:20 -0400 Subject: [PATCH 04/29] also add filtering/hiding of entries not pertaining to search --- MiraAPI/Patches/RoleGuidePatches.cs | 101 ++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 13 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 92080e8..c0081a4 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -7,6 +7,7 @@ using System.Linq; using AmongUs.GameOptions; using HarmonyLib; using Innersloth.Assets; +using MiraAPI.LocalSettings; using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; using MiraAPI.Roles; @@ -28,7 +29,9 @@ public static class RoleGuidePatches { private const int RolesTabIndex = 2; private const int ModifiersTabIndex = 3; - private const float SearchDebounceSeconds = 0.3f; + private const float MinGuideScaleFactor = 0.5f; + private const float MaxGuideScaleFactor = 1f; + private const float SearchDebounceSeconds = 0.05f; private static readonly List RoleEntries = []; private static readonly List ModifierEntries = []; @@ -113,13 +116,29 @@ public static class RoleGuidePatches [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Awake))] public static void Awake(MatchInfoGuide __instance) { - __instance.transitionOpen.targetSize = 1.3f; + __instance.transitionOpen.targetSize = Helpers.MatchInfoGuideBaseScale; if (_guide != __instance) { ResetState(); } } + public static void ApplyGuideScale(MatchInfoGuide instance, float scaleFactor) + { + if (!instance || !instance.transitionOpen) + { + return; + } + + var targetSize = Helpers.MatchInfoGuideBaseScale * + Mathf.Clamp(scaleFactor, MinGuideScaleFactor, MaxGuideScaleFactor); + instance.transitionOpen.targetSize = targetSize; + if (instance.MatchInfoParent.activeSelf) + { + instance.transitionOpen.transform.localScale = Vector3.one * targetSize; + } + } + /// /// Clears all cached state tied to a previous instance. /// Every game spawns a fresh guide, so anything we cached from the old one is a destroyed Unity object. @@ -171,6 +190,7 @@ public static class RoleGuidePatches [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Awake))] private static void AwakePostfix(MatchInfoGuide __instance) { + ApplyGuideScale(__instance, LocalSettingsTabSingleton.Instance.MatchInfoGuideScaleFactor.Value); Coroutines.Start(CoPreload(__instance)); } @@ -232,6 +252,8 @@ public static class RoleGuidePatches return false; } + ApplyGuideScale(__instance, LocalSettingsTabSingleton.Instance.MatchInfoGuideScaleFactor.Value); + if (Minigame.Instance != null) { Minigame.Instance.Close(); @@ -746,6 +768,8 @@ public static class RoleGuidePatches public int Amount { get; set; } public int Chance { get; set; } public float Likelihood { get; set; } + public bool CanShow { get; set; } + public bool CountsReady { get; set; } public bool Visible { get; set; } public string GetSortKey(SortingGroups grouping) @@ -935,14 +959,18 @@ public static class RoleGuidePatches entry.Amount = amount; entry.Chance = chance; entry.Likelihood = amount * chance; - entry.Visible = ShouldShowPanel(forceShow, amount == 0 || chance == 0, isHidden); + entry.CanShow = ShouldShowPanel(forceShow, amount == 0 || chance == 0, isHidden); + entry.CountsReady = true; + entry.Visible = entry.CanShow; } private static List UpdateEntries(List entries, bool modifiers) { + var searchText = GetSearchText(); foreach (var entry in entries) { ComputeCounts(entry); + entry.Visible = entry.CanShow && (modifiers || MatchesSearch(entry, searchText)); if (entry.Panel) { SetPanelCount(entry.Panel!, entry.Amount, entry.Chance, entry.ModId); @@ -953,16 +981,7 @@ public static class RoleGuidePatches } } - var searchText = GetSearchText(); - var sorted = entries.Where(p => p.Visible) - .OrderByDescending(p => p.Title.Equals(searchText, StringComparison.OrdinalIgnoreCase)) - .ThenByDescending(p => - p.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase) || - p.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase)); - var shown = (_sortMethod is SortingMethod.Alphabetical - ? sorted.ThenBy(p => p.GetSortKey(_sortGrouping)) - : sorted.ThenByDescending(p => p.GetSortKey(_sortGrouping))) - .ToList(); + var shown = SortEntries(entries, searchText); var instance = MatchInfoGuide.Instance; if (modifiers) @@ -979,6 +998,26 @@ public static class RoleGuidePatches return shown; } + private static List SortEntries(List entries, string searchText) + { + var sorted = entries.Where(p => p.Visible) + .OrderByDescending(p => p.Title.Equals(searchText, StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(p => + p.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase) || + p.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase)); + return (_sortMethod is SortingMethod.Alphabetical + ? sorted.ThenBy(p => p.GetSortKey(_sortGrouping)) + : sorted.ThenByDescending(p => p.GetSortKey(_sortGrouping))) + .ToList(); + } + + private static bool MatchesSearch(DetailedPanel entry, string searchText) + { + return searchText.Length == 0 || + entry.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase) || + entry.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase); + } + private static void RefreshActiveTab(bool scrollToTop) { StartPopulate(); @@ -1009,9 +1048,45 @@ public static class RoleGuidePatches } _appliedSearch = text; + if (TryRefreshRoleSearch(text)) + { + yield break; + } + RefreshActiveTab(true); } + private static bool TryRefreshRoleSearch(string searchText) + { + var guide = MatchInfoGuide.Instance; + if (!_guide || guide != _guide || guide.activeTabIndex != RolesTabIndex || + _populate != null || RoleEntries.Count == 0 || + RoleEntries.Any(entry => !entry.CountsReady || !entry.Panel)) + { + return false; + } + + foreach (var entry in RoleEntries) + { + entry.Visible = entry.CanShow && MatchesSearch(entry, searchText); + if (entry.Panel!.gameObject.activeSelf != entry.Visible) + { + entry.Panel.gameObject.SetActive(entry.Visible); + } + } + + var shown = SortEntries(RoleEntries, searchText); + foreach (var entry in shown) + { + entry.Panel!.transform.SetAsLastSibling(); + } + + guide.MatchInfoRoleScroller.SetYBoundsMax(GetGridScrollBounds(shown.Count)); + guide.rolesEnabledMessage.SetActive(shown.Count == 0); + _rolesScroller.ScrollToTop(); + return true; + } + private static void ResetSearchBox() { if (_searchDebounce != null) From b6414fe5a0487c45ed59af8632374be5e9dc35e1 Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:52:16 -0400 Subject: [PATCH 05/29] Filter/hide roles by search in cleaner way --- MiraAPI/Patches/RoleGuidePatches.cs | 122 +++++++++++++++++++++++++--- 1 file changed, 109 insertions(+), 13 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 92080e8..410639b 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -7,6 +7,7 @@ using System.Linq; using AmongUs.GameOptions; using HarmonyLib; using Innersloth.Assets; +using MiraAPI.LocalSettings; using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; using MiraAPI.Roles; @@ -28,7 +29,9 @@ public static class RoleGuidePatches { private const int RolesTabIndex = 2; private const int ModifiersTabIndex = 3; - private const float SearchDebounceSeconds = 0.3f; + private const float MinGuideScaleFactor = 0.5f; + private const float MaxGuideScaleFactor = 1f; + private const float SearchDebounceSeconds = 0.05f; private static readonly List RoleEntries = []; private static readonly List ModifierEntries = []; @@ -113,13 +116,29 @@ public static class RoleGuidePatches [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Awake))] public static void Awake(MatchInfoGuide __instance) { - __instance.transitionOpen.targetSize = 1.3f; + __instance.transitionOpen.targetSize = Helpers.MatchInfoGuideBaseScale; if (_guide != __instance) { ResetState(); } } + public static void ApplyGuideScale(MatchInfoGuide instance, float scaleFactor) + { + if (!instance || !instance.transitionOpen) + { + return; + } + + var targetSize = Helpers.MatchInfoGuideBaseScale * + Mathf.Clamp(scaleFactor, MinGuideScaleFactor, MaxGuideScaleFactor); + instance.transitionOpen.targetSize = targetSize; + if (instance.MatchInfoParent.activeSelf) + { + instance.transitionOpen.transform.localScale = Vector3.one * targetSize; + } + } + /// /// Clears all cached state tied to a previous instance. /// Every game spawns a fresh guide, so anything we cached from the old one is a destroyed Unity object. @@ -171,6 +190,7 @@ public static class RoleGuidePatches [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Awake))] private static void AwakePostfix(MatchInfoGuide __instance) { + ApplyGuideScale(__instance, LocalSettingsTabSingleton.Instance.MatchInfoGuideScaleFactor.Value); Coroutines.Start(CoPreload(__instance)); } @@ -232,6 +252,8 @@ public static class RoleGuidePatches return false; } + ApplyGuideScale(__instance, LocalSettingsTabSingleton.Instance.MatchInfoGuideScaleFactor.Value); + if (Minigame.Instance != null) { Minigame.Instance.Close(); @@ -746,6 +768,8 @@ public static class RoleGuidePatches public int Amount { get; set; } public int Chance { get; set; } public float Likelihood { get; set; } + public bool CanShow { get; set; } + public bool CountsReady { get; set; } public bool Visible { get; set; } public string GetSortKey(SortingGroups grouping) @@ -935,14 +959,18 @@ public static class RoleGuidePatches entry.Amount = amount; entry.Chance = chance; entry.Likelihood = amount * chance; - entry.Visible = ShouldShowPanel(forceShow, amount == 0 || chance == 0, isHidden); + entry.CanShow = ShouldShowPanel(forceShow, amount == 0 || chance == 0, isHidden); + entry.CountsReady = true; + entry.Visible = entry.CanShow; } private static List UpdateEntries(List entries, bool modifiers) { + var searchText = GetSearchText(); foreach (var entry in entries) { ComputeCounts(entry); + entry.Visible = entry.CanShow && (modifiers || MatchesSearch(entry, searchText)); if (entry.Panel) { SetPanelCount(entry.Panel!, entry.Amount, entry.Chance, entry.ModId); @@ -953,16 +981,7 @@ public static class RoleGuidePatches } } - var searchText = GetSearchText(); - var sorted = entries.Where(p => p.Visible) - .OrderByDescending(p => p.Title.Equals(searchText, StringComparison.OrdinalIgnoreCase)) - .ThenByDescending(p => - p.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase) || - p.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase)); - var shown = (_sortMethod is SortingMethod.Alphabetical - ? sorted.ThenBy(p => p.GetSortKey(_sortGrouping)) - : sorted.ThenByDescending(p => p.GetSortKey(_sortGrouping))) - .ToList(); + var shown = SortEntries(entries, searchText); var instance = MatchInfoGuide.Instance; if (modifiers) @@ -979,6 +998,26 @@ public static class RoleGuidePatches return shown; } + private static List SortEntries(List entries, string searchText) + { + var sorted = entries.Where(p => p.Visible) + .OrderByDescending(p => p.Title.Equals(searchText, StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(p => + p.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase) || + p.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase)); + return (_sortMethod is SortingMethod.Alphabetical + ? sorted.ThenBy(p => p.GetSortKey(_sortGrouping)) + : sorted.ThenByDescending(p => p.GetSortKey(_sortGrouping))) + .ToList(); + } + + private static bool MatchesSearch(DetailedPanel entry, string searchText) + { + return searchText.Length == 0 || + entry.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase) || + entry.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase); + } + private static void RefreshActiveTab(bool scrollToTop) { StartPopulate(); @@ -1009,9 +1048,66 @@ public static class RoleGuidePatches } _appliedSearch = text; + if (TryRefreshRoleSearch(text)) + { + yield break; + } + RefreshActiveTab(true); } + private static bool TryRefreshRoleSearch(string searchText) + { + var guide = MatchInfoGuide.Instance; + if (!_guide || guide != _guide) + { + return false; + } + + if (guide.activeTabIndex != RolesTabIndex || _populate != null || !AreRolePanelsReady()) + { + return false; + } + + foreach (var entry in RoleEntries) + { + entry.Visible = entry.CanShow && MatchesSearch(entry, searchText); + if (entry.Panel!.gameObject.activeSelf != entry.Visible) + { + entry.Panel.gameObject.SetActive(entry.Visible); + } + } + + var shown = SortEntries(RoleEntries, searchText); + foreach (var entry in shown) + { + entry.Panel!.transform.SetAsLastSibling(); + } + + guide.MatchInfoRoleScroller.SetYBoundsMax(GetGridScrollBounds(shown.Count)); + guide.rolesEnabledMessage.SetActive(shown.Count == 0); + _rolesScroller.ScrollToTop(); + return true; + } + + private static bool AreRolePanelsReady() + { + if (RoleEntries.Count == 0) + { + return false; + } + + foreach (var entry in RoleEntries) + { + if (!entry.CountsReady || !entry.Panel) + { + return false; + } + } + + return true; + } + private static void ResetSearchBox() { if (_searchDebounce != null) From afcb4dc1809f2a0bcb19b7b0d042bbdc847dc6b7 Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:00:40 -0400 Subject: [PATCH 06/29] change No Roles Enabled to No Results Found for more general meaning across search settings --- MiraAPI/Resources/Locale/en_US.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/MiraAPI/Resources/Locale/en_US.xml b/MiraAPI/Resources/Locale/en_US.xml index 9560412..c210db0 100644 --- a/MiraAPI/Resources/Locale/en_US.xml +++ b/MiraAPI/Resources/Locale/en_US.xml @@ -60,4 +60,5 @@ Outlive the timer or eliminate the crew. Filter Grouping Order + No Results Found From 8e4ff7c179990e0517cd6fc7d24e7ad8c8299f8a Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:00:54 -0400 Subject: [PATCH 07/29] add filtering/hiding to modifiers page --- MiraAPI/Patches/RoleGuidePatches.cs | 104 ++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 14 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 410639b..1aa2526 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -54,6 +54,7 @@ public static class RoleGuidePatches private static SpriteRenderer _searchSortingFilterHover; private static Scroller _rolesScroller; private static Scroller _modifiersScroller; + private static GameObject _modifierNoResultsMessage; private static Scroller _advancedInfoTabScroller; private static TextBoxTMP _searchBoxTmp; private static GameObject _currentAdvancedTabObject; @@ -169,6 +170,7 @@ public static class RoleGuidePatches _searchBoxTmp = null!; _rolesScroller = null!; _modifiersScroller = null!; + _modifierNoResultsMessage = null!; _advancedInfoTabScroller = null!; _currentAdvancedTabObject = null!; _loadingSpinner = null; @@ -383,6 +385,10 @@ public static class RoleGuidePatches _sortMethod = MiraApiPlugin.WikiSortingMethod.Value; _sortGrouping = MiraApiPlugin.WikiSortingGroup.Value; + var noResultsText = instance.rolesEnabledMessage.GetComponentInChildren(); + if (noResultsText) + SetNoResultsText(instance.rolesEnabledMessage); + if (_populate != null) { Coroutines.Stop(_populate); @@ -451,6 +457,16 @@ public static class RoleGuidePatches instance.settingsTabs[RolesTabIndex], instance.settingsTabs[RolesTabIndex].transform.parent); modifiersTab.name = "ModifiersPanel"; + var clonedMessage = FindClonedTransform( + instance.rolesEnabledMessage.transform, + instance.settingsTabs[RolesTabIndex].transform, + modifiersTab.transform); + _modifierNoResultsMessage = clonedMessage + ? clonedMessage.gameObject + : Object.Instantiate(instance.rolesEnabledMessage, modifiersTab.transform, true); + _modifierNoResultsMessage.name = "NoResultsFound"; + _modifierNoResultsMessage.SetActive(false); + SetNoResultsText(_modifierNoResultsMessage); modifiersTab.transform.FindChild("MaskArea")?.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); modifiersTab.transform.GetAllChildren() @@ -757,6 +773,57 @@ public static class RoleGuidePatches public static void OpenRolesTab() => OpenTab(RolesTabIndex); + private static Transform? FindClonedTransform(Transform source, Transform sourceRoot, Transform cloneRoot) + { + var childIndices = new List(); + var current = source; + while (current != sourceRoot && current.parent) + { + childIndices.Add(current.GetSiblingIndex()); + current = current.parent; + } + + if (current != sourceRoot) + { + return null; + } + + var result = cloneRoot; + for (var i = childIndices.Count - 1; i >= 0; i--) + { + var index = childIndices[i]; + if (index >= result.childCount) + { + return null; + } + + result = result.GetChild(index); + } + + return result; + } + + private static void SetNoResultsText(GameObject message) + { + var text = message.GetComponentInChildren(); + if (!text) + { + return; + } + + text.GetComponent()?.Destroy(); + text.text = MiraLocaleManager.Get("MiraApi.Wiki.NoResultsFound", "No Results Found"); + } + + private static void SetNoResultsVisible(bool modifiers, bool visible) + { + MatchInfoGuide.Instance.rolesEnabledMessage.SetActive(!modifiers && visible); + if (_modifierNoResultsMessage) + { + _modifierNoResultsMessage.SetActive(modifiers && visible); + } + } + public sealed class DetailedPanel(string title, string category, string modId, RoleBehaviour? role, GameModifier? modifier) { public MatchInfoRolePanel? Panel { get; set; } @@ -970,7 +1037,7 @@ public static class RoleGuidePatches foreach (var entry in entries) { ComputeCounts(entry); - entry.Visible = entry.CanShow && (modifiers || MatchesSearch(entry, searchText)); + entry.Visible = entry.CanShow && MatchesSearch(entry, searchText); if (entry.Panel) { SetPanelCount(entry.Panel!, entry.Amount, entry.Chance, entry.ModId); @@ -987,12 +1054,12 @@ public static class RoleGuidePatches if (modifiers) { _modifiersScroller.SetYBoundsMax(GetGridScrollBounds(shown.Count)); - /*instance.rolesEnabledMessage.SetActive(shown.Count == 0);*/ + SetNoResultsVisible(true, shown.Count == 0); } else { instance.MatchInfoRoleScroller.SetYBoundsMax(GetGridScrollBounds(shown.Count)); - instance.rolesEnabledMessage.SetActive(shown.Count == 0); + SetNoResultsVisible(false, shown.Count == 0); } return shown; @@ -1048,7 +1115,7 @@ public static class RoleGuidePatches } _appliedSearch = text; - if (TryRefreshRoleSearch(text)) + if (TryRefreshSearch(text)) { yield break; } @@ -1056,7 +1123,7 @@ public static class RoleGuidePatches RefreshActiveTab(true); } - private static bool TryRefreshRoleSearch(string searchText) + private static bool TryRefreshSearch(string searchText) { var guide = MatchInfoGuide.Instance; if (!_guide || guide != _guide) @@ -1064,12 +1131,18 @@ public static class RoleGuidePatches return false; } - if (guide.activeTabIndex != RolesTabIndex || _populate != null || !AreRolePanelsReady()) + var entries = guide.activeTabIndex switch + { + RolesTabIndex => RoleEntries, + ModifiersTabIndex => ModifierEntries, + _ => null, + }; + if (entries == null || _populate != null || !ArePanelsReady(entries)) { return false; } - foreach (var entry in RoleEntries) + foreach (var entry in entries) { entry.Visible = entry.CanShow && MatchesSearch(entry, searchText); if (entry.Panel!.gameObject.activeSelf != entry.Visible) @@ -1078,26 +1151,29 @@ public static class RoleGuidePatches } } - var shown = SortEntries(RoleEntries, searchText); + var shown = SortEntries(entries, searchText); foreach (var entry in shown) { entry.Panel!.transform.SetAsLastSibling(); } - guide.MatchInfoRoleScroller.SetYBoundsMax(GetGridScrollBounds(shown.Count)); - guide.rolesEnabledMessage.SetActive(shown.Count == 0); - _rolesScroller.ScrollToTop(); + var scroller = guide.activeTabIndex == ModifiersTabIndex + ? _modifiersScroller + : guide.MatchInfoRoleScroller; + scroller.SetYBoundsMax(GetGridScrollBounds(shown.Count)); + SetNoResultsVisible(guide.activeTabIndex == ModifiersTabIndex, shown.Count == 0); + scroller.ScrollToTop(); return true; } - private static bool AreRolePanelsReady() + private static bool ArePanelsReady(List entries) { - if (RoleEntries.Count == 0) + if (entries.Count == 0) { return false; } - foreach (var entry in RoleEntries) + foreach (var entry in entries) { if (!entry.CountsReady || !entry.Panel) { From 1c8f7318d37d545c0f36959d2d7b5ed9f135cc5e Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:04:02 -0400 Subject: [PATCH 08/29] add no results found text to modifiers page --- MiraAPI/Patches/RoleGuidePatches.cs | 63 +++++++---------------------- 1 file changed, 15 insertions(+), 48 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 1aa2526..7f6a903 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -385,9 +385,7 @@ public static class RoleGuidePatches _sortMethod = MiraApiPlugin.WikiSortingMethod.Value; _sortGrouping = MiraApiPlugin.WikiSortingGroup.Value; - var noResultsText = instance.rolesEnabledMessage.GetComponentInChildren(); - if (noResultsText) - SetNoResultsText(instance.rolesEnabledMessage); + SetNoResultsText(instance.rolesEnabledMessage); if (_populate != null) { @@ -457,13 +455,10 @@ public static class RoleGuidePatches instance.settingsTabs[RolesTabIndex], instance.settingsTabs[RolesTabIndex].transform.parent); modifiersTab.name = "ModifiersPanel"; - var clonedMessage = FindClonedTransform( - instance.rolesEnabledMessage.transform, - instance.settingsTabs[RolesTabIndex].transform, - modifiersTab.transform); - _modifierNoResultsMessage = clonedMessage - ? clonedMessage.gameObject - : Object.Instantiate(instance.rolesEnabledMessage, modifiersTab.transform, true); + _modifierNoResultsMessage = Object.Instantiate( + instance.rolesEnabledMessage, + instance.rolesEnabledMessage.transform.parent, + true); _modifierNoResultsMessage.name = "NoResultsFound"; _modifierNoResultsMessage.SetActive(false); SetNoResultsText(_modifierNoResultsMessage); @@ -773,39 +768,9 @@ public static class RoleGuidePatches public static void OpenRolesTab() => OpenTab(RolesTabIndex); - private static Transform? FindClonedTransform(Transform source, Transform sourceRoot, Transform cloneRoot) - { - var childIndices = new List(); - var current = source; - while (current != sourceRoot && current.parent) - { - childIndices.Add(current.GetSiblingIndex()); - current = current.parent; - } - - if (current != sourceRoot) - { - return null; - } - - var result = cloneRoot; - for (var i = childIndices.Count - 1; i >= 0; i--) - { - var index = childIndices[i]; - if (index >= result.childCount) - { - return null; - } - - result = result.GetChild(index); - } - - return result; - } - private static void SetNoResultsText(GameObject message) { - var text = message.GetComponentInChildren(); + var text = message.GetComponentInChildren(true); if (!text) { return; @@ -815,12 +780,11 @@ public static class RoleGuidePatches text.text = MiraLocaleManager.Get("MiraApi.Wiki.NoResultsFound", "No Results Found"); } - private static void SetNoResultsVisible(bool modifiers, bool visible) + private static void SetNoResultsVisible(GameObject message, bool visible) { - MatchInfoGuide.Instance.rolesEnabledMessage.SetActive(!modifiers && visible); - if (_modifierNoResultsMessage) + if (message) { - _modifierNoResultsMessage.SetActive(modifiers && visible); + message.SetActive(visible); } } @@ -1054,12 +1018,12 @@ public static class RoleGuidePatches if (modifiers) { _modifiersScroller.SetYBoundsMax(GetGridScrollBounds(shown.Count)); - SetNoResultsVisible(true, shown.Count == 0); + SetNoResultsVisible(_modifierNoResultsMessage, shown.Count == 0); } else { instance.MatchInfoRoleScroller.SetYBoundsMax(GetGridScrollBounds(shown.Count)); - SetNoResultsVisible(false, shown.Count == 0); + SetNoResultsVisible(instance.rolesEnabledMessage, shown.Count == 0); } return shown; @@ -1161,7 +1125,10 @@ public static class RoleGuidePatches ? _modifiersScroller : guide.MatchInfoRoleScroller; scroller.SetYBoundsMax(GetGridScrollBounds(shown.Count)); - SetNoResultsVisible(guide.activeTabIndex == ModifiersTabIndex, shown.Count == 0); + var noResultsMessage = guide.activeTabIndex == ModifiersTabIndex + ? _modifierNoResultsMessage + : guide.rolesEnabledMessage; + SetNoResultsVisible(noResultsMessage, shown.Count == 0); scroller.ScrollToTop(); return true; } From 1e453cf6afe673ce6f53dfde81924278722174eb Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:26:00 -0400 Subject: [PATCH 09/29] add very scuffed pagination of game settings --- MiraAPI/Patches/RoleGuidePatches.cs | 458 ++++++++++++++++++++++- MiraAPI/Resources/Locale/en_US.xml | 4 + MiraAPI/Utilities/GuideOptionsPatches.cs | 203 ++++++++++ 3 files changed, 662 insertions(+), 3 deletions(-) create mode 100644 MiraAPI/Utilities/GuideOptionsPatches.cs diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 7f6a903..9d3fbc8 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -27,6 +27,7 @@ namespace MiraAPI.Patches; [HarmonyPatch] public static class RoleGuidePatches { + private const int GeneralTabIndex = 1; private const int RolesTabIndex = 2; private const int ModifiersTabIndex = 3; private const float MinGuideScaleFactor = 0.5f; @@ -174,6 +175,7 @@ public static class RoleGuidePatches _advancedInfoTabScroller = null!; _currentAdvancedTabObject = null!; _loadingSpinner = null; + ResetGeneralState(); SearchIconButton = null!; SearchSortingOrderButton = null!; SearchSortingGroupButton = null!; @@ -468,6 +470,7 @@ public static class RoleGuidePatches .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5); _modifiersScroller = modifiersTab.GetComponent(); instance.settingsTabs.Add(modifiersTab); + InitializeGeneralPanel(instance); var wikiTab = Object.Instantiate( instance.settingsTabs[RolesTabIndex], @@ -1051,6 +1054,12 @@ public static class RoleGuidePatches private static void RefreshActiveTab(bool scrollToTop) { + if (IsGeneralTabActive) + { + RefreshGeneralTab(scrollToTop); + return; + } + StartPopulate(); if (scrollToTop) { @@ -1079,6 +1088,12 @@ public static class RoleGuidePatches } _appliedSearch = text; + if (IsGeneralTabActive) + { + RefreshGeneralTab(true); + yield break; + } + if (TryRefreshSearch(text)) { yield break; @@ -1186,6 +1201,12 @@ public static class RoleGuidePatches public static void SwitchFilter() { + if (IsGeneralTabActive) + { + ChangeGeneralPage(-1); + return; + } + var stepUp = (SortingFilter)((int)_sortFilter + 1); if (Enum.IsDefined(stepUp)) { @@ -1232,6 +1253,11 @@ public static class RoleGuidePatches public static void SwitchMethod() { + if (IsGeneralTabActive) + { + return; + } + var stepUp = (SortingMethod)((int)_sortMethod + 1); if (Enum.IsDefined(stepUp)) { @@ -1266,6 +1292,12 @@ public static class RoleGuidePatches public static void SwitchGrouping() { + if (IsGeneralTabActive) + { + ChangeGeneralPage(1); + return; + } + var stepUp = (SortingGroups)((int)_sortGrouping + 1); if (Enum.IsDefined(stepUp)) { @@ -1405,8 +1437,8 @@ public static class RoleGuidePatches _titleText.text = MiraLocaleManager.Get("MiraApi.Wiki.PlayersTab"); _titleIcon.sprite = MiraAssets.IconPlayerList.LoadAsset(); break; - case 1: - _titleText.text = MiraLocaleManager.Get("MiraApi.Wiki.SettingsTab"); + case GeneralTabIndex: + _titleText.text = GetGeneralTitle(); _titleIcon.sprite = MiraAssets.IconSettings.LoadAsset(); break; case RolesTabIndex: @@ -1428,6 +1460,15 @@ public static class RoleGuidePatches _advancedInfoTabScroller?.gameObject.SetActive(false); SearchComponentContainer?.SetActive(true); + if (tabIndex == GeneralTabIndex) + { + EnterGeneralTab(); + } + else + { + LeaveGeneralTab(); + } + UpdateSpinner(); } @@ -1474,6 +1515,7 @@ public static class RoleGuidePatches { instance.settingsTabs[RolesTabIndex].SetActive(false); _modifiersScroller?.gameObject.SetActive(false); + LeaveGeneralTab(); if (_currentAdvancedTabObject) { _currentAdvancedTabObject.SetActive(false); @@ -1680,6 +1722,416 @@ public static class RoleGuidePatches { return customRole.RoleFactionTitle; } + + // ---- General (non-role) option pages: paginated General tab + cross-page search ---- + private static Scroller? _generalScroller; + private static GameObject? _generalNoResults; + private static readonly List GeneralRows = []; + private static GameObject? _generalRowTemplate; + private static Vector3 _generalRowOrigin; + private static Vector3 _generalRowColumnStep; + private static float _generalRowHeight = -0.65f; // negative: rows go down + private static int _generalColumns = 2; + private static float _generalVisibleHeight; + private static bool _generalScrollerFailed; + + /// One vanilla-looking settings row (label box + value box) that is reused between pages. + private sealed class GeneralRow(GameObject root, TextMeshPro label, TextMeshPro value) + { + public GameObject Root { get; } = root; + public TextMeshPro Label { get; } = label; + public TextMeshPro Value { get; } = value; + } + private static List _generalPages = []; + private static int _generalPage; // 0 = vanilla settings, 1..N = _generalPages[page - 1] + private static bool _generalSearching; + + // Saved state of the sort buttons so they can be restored when leaving the General tab. + private static bool _sortButtonsRepurposed; + private static string _savedFilterLabel = string.Empty; + private static string _savedGroupLabel = string.Empty; + private static Vector3 _savedFilterIconScale = Vector3.one; + private static Vector3 _savedGroupIconScale = Vector3.one; + + private static bool IsGeneralTabActive => + _uiInitialized && MatchInfoGuide.Instance && MatchInfoGuide.Instance.activeTabIndex == GeneralTabIndex; + + private static void ResetGeneralState() + { + _generalScroller = null; + _generalNoResults = null; + GeneralRows.Clear(); + _generalRowTemplate = null; + _generalScrollerFailed = false; + _generalPages = []; + _generalPage = 0; + _generalSearching = false; + _sortButtonsRepurposed = false; + } + + private static void InitializeGeneralPanel(MatchInfoGuide instance) + { + _generalNoResults = Object.Instantiate( + instance.rolesEnabledMessage, + instance.rolesEnabledMessage.transform.parent, + true); + _generalNoResults.name = "GeneralNoResultsFound"; + _generalNoResults.SetActive(false); + SetNoResultsText(_generalNoResults); + } + + /// + /// Clones the vanilla settings tab the first time it is needed, so extra pages are drawn with the exact same + /// rows, fonts, masking and scroll area as the vanilla list. Done lazily because the vanilla rows are only + /// created when the guide first opens. The clone is NOT added to settingsTabs; its visibility is managed + /// here so the guide's tab count and tab buttons stay untouched. + /// + private static bool EnsureGeneralScroller(MatchInfoGuide guide) + { + if (_generalScroller != null && _generalRowTemplate) + { + return true; + } + + if (_generalScrollerFailed) + { + return false; + } + + var vanillaTab = guide.settingsTabs[GeneralTabIndex]; + var vanillaScroller = vanillaTab.GetComponent() ?? vanillaTab.GetComponentInChildren(true); + if (!vanillaScroller || vanillaScroller.Inner.childCount < 2) + { + return false; // vanilla rows not built yet; try again next refresh + } + + // Read the vanilla grid: first row, second entry (same row => 2 columns), third entry (next row). + var vInner = vanillaScroller.Inner; + var p0 = vInner.GetChild(0).localPosition; + var p1 = vInner.GetChild(1).localPosition; + var sameRow = Mathf.Abs(p1.y - p0.y) < 0.01f; + var vanillaCount = vInner.childCount; + _generalColumns = sameRow ? 2 : 1; + _generalRowOrigin = p0; + _generalRowColumnStep = sameRow ? new Vector3(p1.x - p0.x, 0f, 0f) : Vector3.zero; + if (sameRow) + { + _generalRowHeight = vanillaCount >= 3 ? vInner.GetChild(2).localPosition.y - p0.y : -0.65f; + } + else + { + _generalRowHeight = p1.y - p0.y; + } + + if (Mathf.Abs(_generalRowHeight) < 0.05f) + { + _generalRowHeight = -0.65f; + } + + // Self-calibrate the visible height from how the vanilla list was bounded. + var vanillaRows = Mathf.CeilToInt(vanillaCount / (float)_generalColumns); + var visible = (vanillaRows * Mathf.Abs(_generalRowHeight)) - vanillaScroller.ContentYBounds.max; + _generalVisibleHeight = visible > 0.1f && visible < vanillaRows * Mathf.Abs(_generalRowHeight) + ? visible + : 3.3f * Mathf.Abs(_generalRowHeight); + + var clone = Object.Instantiate(vanillaTab, vanillaTab.transform.parent); + clone.name = "GeneralOptionsPanel"; + var cloneScroller = clone.GetComponent() ?? clone.GetComponentInChildren(true); + var cInner = cloneScroller.Inner; + + // Keep the first row as an inactive prototype, delete the rest. + var template = cInner.GetChild(0).gameObject; + for (var i = cInner.childCount - 1; i >= 1; i--) + { + var child = cInner.GetChild(i).gameObject; + child.SetActive(false); + child.Destroy(); + } + + template.SetActive(false); + _generalRowTemplate = template; + _generalScroller = cloneScroller; + clone.SetActive(false); + return true; + } + + private static GeneralRow? CreateGeneralRow() + { + if (_generalScroller == null || !_generalRowTemplate) + { + return null; + } + + var go = Object.Instantiate(_generalRowTemplate!, _generalScroller.Inner); + go.name = "GeneralOptionRow"; + + // The game would re-translate these and overwrite our text. + foreach (var translator in go.GetComponentsInChildren(true)) + { + translator.Destroy(); + } + + // Label is the left-most text, value is the right-most (same as the vanilla entries). + var texts = go.GetComponentsInChildren(true).OrderBy(t => t.transform.position.x).ToArray(); + if (texts.Length < 2) + { + Warning("Vanilla settings row has fewer than two text fields; cannot clone it for the general pages."); + go.Destroy(); + _generalScrollerFailed = true; + return null; + } + + return new GeneralRow(go, texts[0], texts[^1]); + } + + private static void EnterGeneralTab() + { + if (!_uiInitialized) + { + return; + } + + RefreshGeneralTab(true); + } + + private static void LeaveGeneralTab() + { + if (_generalScroller != null) + { + _generalScroller.gameObject.SetActive(false); + } + + if (_generalNoResults) + { + _generalNoResults!.SetActive(false); + } + + RestoreSortButtons(); + } + + private static void ChangeGeneralPage(int delta) + { + var total = _generalPages.Count + 1; + if (total <= 1) + { + return; + } + + _generalPage = (_generalPage + delta + total) % total; + RefreshGeneralTab(true); + } + + /// + /// Rebuilds pages from live option data and redraws the General tab. + /// Pages are rebuilt every time because values, group visibility and the gamemode can change. + /// + private static void RefreshGeneralTab(bool scrollToTop) + { + var guide = MatchInfoGuide.Instance; + if (!_uiInitialized || !guide) + { + return; + } + + _generalPages = GuideOptionPages.BuildPages(guide.GetBoolString); + if (_generalPage > _generalPages.Count) + { + _generalPage = 0; + } + + var vanillaTab = guide.settingsTabs[GeneralTabIndex]; + + // No MiraAPI general options in this match: leave the vanilla tab exactly as it always was. + if (_generalPages.Count == 0) + { + _generalSearching = false; + _generalScroller?.gameObject.SetActive(false); + _generalNoResults?.SetActive(false); + vanillaTab.SetActive(true); + RestoreSortButtons(); + UpdateGeneralTitle(); + return; + } + + var searchText = GetSearchText().Trim(); + _generalSearching = searchText.Length > 0; + + if (_generalSearching) + { + ShowGeneralEntries(GuideOptionPages.Search(_generalPages, searchText), true, scrollToTop); + vanillaTab.SetActive(false); + RepurposeSortButtons(false); + } + else if (_generalPage == 0) + { + _generalScroller?.gameObject.SetActive(false); + _generalNoResults?.SetActive(false); + vanillaTab.SetActive(true); + RepurposeSortButtons(true); + } + else + { + ShowGeneralEntries(_generalPages[_generalPage - 1].Entries, false, scrollToTop); + vanillaTab.SetActive(false); + RepurposeSortButtons(true); + } + + UpdateGeneralTitle(); + } + + private static void ShowGeneralEntries(IReadOnlyList entries, bool showGroup, bool scrollToTop) + { + var guide = MatchInfoGuide.Instance; + if (!guide || !EnsureGeneralScroller(guide)) + { + // Could not clone the vanilla rows: fall back to the plain vanilla tab rather than a blank page. + guide?.settingsTabs[GeneralTabIndex].SetActive(true); + return; + } + + while (GeneralRows.Count < entries.Count) + { + var row = CreateGeneralRow(); + if (row == null) + { + break; + } + + GeneralRows.Add(row); + } + + for (var i = 0; i < GeneralRows.Count; i++) + { + var row = GeneralRows[i]; + if (i >= entries.Count) + { + row.Root.SetActive(false); + continue; + } + + var entry = entries[i]; + row.Label.text = showGroup ? $"{entry.GroupName}\n{entry.Title}" : entry.Title; + row.Value.text = entry.Value; + row.Root.transform.localPosition = _generalRowOrigin + + (_generalRowColumnStep * (i % _generalColumns)) + + new Vector3(0f, _generalRowHeight * (i / _generalColumns), 0f); + row.Root.SetActive(true); + } + + var rows = Mathf.CeilToInt(Mathf.Min(entries.Count, GeneralRows.Count) / (float)_generalColumns); + _generalScroller!.gameObject.SetActive(true); + _generalScroller.SetYBoundsMax(Mathf.Clamp((rows * Mathf.Abs(_generalRowHeight)) - _generalVisibleHeight, 0f, 999f)); + _generalNoResults?.SetActive(entries.Count == 0); + if (scrollToTop) + { + _generalScroller.ScrollToTop(); + } + } + + private static string GetGeneralTitle() + { + var baseTitle = MiraLocaleManager.Get("MiraApi.Wiki.SettingsTab"); + if (!_uiInitialized || _generalPages.Count == 0) + { + return baseTitle; + } + + if (_generalSearching) + { + return $"{baseTitle}: {MiraLocaleManager.Get("MiraApi.Wiki.SearchResults", "Search Results")}"; + } + + var counter = $"({_generalPage + 1}/{_generalPages.Count + 1})"; + return _generalPage == 0 + ? $"{baseTitle} {counter}" + : $"{baseTitle}: {_generalPages[_generalPage - 1].Title} {counter}"; + } + + private static void UpdateGeneralTitle() + { + if (_titleText && IsGeneralTabActive) + { + _titleText.text = GetGeneralTitle(); + } + } + + /// + /// Turns the Filter button into "previous page" and the Grouping button into "next page", + /// and hides the Order button (it only sorts roles/modifiers). + /// + /// False while searching, since search results are a single page. + private static void RepurposeSortButtons(bool paginationVisible) + { + if (!SearchSortingFilterButton || !SearchSortingGroupButton || !SearchSortingOrderButton) + { + return; + } + + if (!_sortButtonsRepurposed) + { + _savedFilterLabel = FilterSortingTmp.text; + _savedGroupLabel = GroupSortingTmp.text; + _savedFilterIconScale = _searchSortingFilterIdle.transform.localScale; + _savedGroupIconScale = _searchSortingGroupIdle.transform.localScale; + + SetPaginationSprites(_searchSortingFilterIdle, _searchSortingFilterHover, true); + SetPaginationSprites(_searchSortingGroupIdle, _searchSortingGroupHover, false); + FilterSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.PrevPage", "Prev"); + GroupSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.NextPage", "Next"); + _sortButtonsRepurposed = true; + } + + SearchSortingOrderButton.gameObject.SetActive(false); + SearchSortingFilterButton.gameObject.SetActive(paginationVisible); + SearchSortingGroupButton.gameObject.SetActive(paginationVisible); + } + + private static void SetPaginationSprites(SpriteRenderer idle, SpriteRenderer hover, bool flip) + { + var idleSprite = MiraAssets.NextButton.LoadAsset(); + var hoverSprite = MiraAssets.NextButtonActive.LoadAsset(); + + // Match the on-screen size of the sort icon being replaced, since the arrow art differs in size. + var currentHeight = idle.sprite ? idle.sprite.bounds.size.y * idle.transform.localScale.y : 0f; + var newHeight = idleSprite.bounds.size.y; + var scale = currentHeight > 0f && newHeight > 0f ? currentHeight / newHeight : 1f; + + idle.sprite = idleSprite; + hover.sprite = hoverSprite; + idle.flipX = hover.flipX = flip; + idle.transform.localScale = hover.transform.localScale = new Vector3(scale, scale, 1f); + } + + private static void RestoreSortButtons() + { + if (!_sortButtonsRepurposed) + { + return; + } + + _sortButtonsRepurposed = false; + if (!SearchSortingFilterButton || !SearchSortingGroupButton || !SearchSortingOrderButton) + { + return; + } + + _searchSortingFilterIdle.sprite = GetSortFilterSprite(_sortFilter, false); + _searchSortingFilterHover.sprite = GetSortFilterSprite(_sortFilter, true); + _searchSortingGroupIdle.sprite = GetSortGroupSprite(_sortGrouping, false); + _searchSortingGroupHover.sprite = GetSortGroupSprite(_sortGrouping, true); + _searchSortingFilterIdle.flipX = _searchSortingFilterHover.flipX = false; + _searchSortingGroupIdle.flipX = _searchSortingGroupHover.flipX = false; + _searchSortingFilterIdle.transform.localScale = + _searchSortingFilterHover.transform.localScale = _savedFilterIconScale; + _searchSortingGroupIdle.transform.localScale = + _searchSortingGroupHover.transform.localScale = _savedGroupIconScale; + FilterSortingTmp.text = _savedFilterLabel; + GroupSortingTmp.text = _savedGroupLabel; + SearchSortingOrderButton.gameObject.SetActive(true); + SearchSortingFilterButton.gameObject.SetActive(true); + SearchSortingGroupButton.gameObject.SetActive(true); + } } public enum SortingGroups @@ -1700,4 +2152,4 @@ public enum SortingFilter EnabledOnly, DisabledOnly, AllRoles, -} +} \ No newline at end of file diff --git a/MiraAPI/Resources/Locale/en_US.xml b/MiraAPI/Resources/Locale/en_US.xml index c210db0..7d27154 100644 --- a/MiraAPI/Resources/Locale/en_US.xml +++ b/MiraAPI/Resources/Locale/en_US.xml @@ -60,5 +60,9 @@ Outlive the timer or eliminate the crew. Filter Grouping Order + Vanilla + Search Results + Previous + Next No Results Found diff --git a/MiraAPI/Utilities/GuideOptionsPatches.cs b/MiraAPI/Utilities/GuideOptionsPatches.cs new file mode 100644 index 0000000..c84cb17 --- /dev/null +++ b/MiraAPI/Utilities/GuideOptionsPatches.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using MiraAPI.GameModes; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.PluginLoading; +using MiraAPI.Translation; +using Reactor.Utilities; + +namespace MiraAPI.Utilities; + +internal sealed record GuideOptionEntry(string Title, string Value, string GroupName, string ModAbbreviation); + +internal sealed class GuideOptionPage(AbstractOptionGroup group, string modAbbreviation, IReadOnlyList entries) +{ + public AbstractOptionGroup Group { get; } = group; + + public string Title { get; } = group.GroupName.Translate(); + + public string ModAbbreviation { get; } = modAbbreviation; + + public IReadOnlyList Entries { get; } = entries; +} + +internal static class GuideOptionPages +{ + private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); + public static List BuildPages(Func formatBool) + { + var pages = new List(); + var mode = CustomGameModeManager.ActiveMode; + + // Same plugin set the in-game options menu lists (skips mods that hide themselves from it). + foreach (var plugin in MiraPluginManager.Instance.PluginsWithOptionsOrGameModes + .OrderBy(p => p.MiraPlugin.GetAbbreviatedModName(), StringComparer.OrdinalIgnoreCase)) + { + var modAbbreviation = plugin.MiraPlugin.GetAbbreviatedModName(); + + var groups = plugin.InternalOptionGroups + .Where(g => IsGeneralGroup(g, mode) && SafeGroupVisible(g)) + .OrderBy(g => g.GroupPriority) + .ThenBy(g => g.GroupName, StringComparer.OrdinalIgnoreCase); + + foreach (var group in groups) + { + var groupName = group.GroupName.Translate(); + var entries = new List(); + + foreach (var option in group.Options) + { + if (!SafeOptionVisible(option)) + { + continue; + } + + var entry = TryCreateEntry(option, groupName, modAbbreviation, formatBool); + if (entry != null) + { + entries.Add(entry); + } + } + + if (entries.Count > 0) + { + pages.Add(new GuideOptionPage(group, modAbbreviation, entries)); + } + } + } + + return pages; + } + public static List Search(IEnumerable pages, string query) + { + query = query.Trim(); + if (query.Length == 0) + { + return []; + } + + const StringComparison cmp = StringComparison.OrdinalIgnoreCase; + return pages + .SelectMany(p => p.Entries) + .Select(e => (Entry: e, Rank: Rank(e, query, cmp))) + .Where(x => x.Rank < int.MaxValue) + .OrderBy(x => x.Rank) + .Select(x => x.Entry) + .ToList(); + } + + private static int Rank(GuideOptionEntry entry, string query, StringComparison cmp) + { + if (entry.Title.Equals(query, cmp)) + { + return 0; + } + + if (entry.Title.Contains(query, cmp)) + { + return 1; + } + + if (entry.GroupName.Contains(query, cmp) || entry.ModAbbreviation.Contains(query, cmp)) + { + return 2; + } + + return entry.Value.Contains(query, cmp) ? 3 : int.MaxValue; + } + private static bool IsGeneralGroup(AbstractOptionGroup group, AbstractGameMode? mode) + { + if (group.ParentMenu is not (MenuCategory.Game or MenuCategory.CustomOne or MenuCategory.CustomTwo)) + { + return false; + } + + if (group.OptionableType == null) + { + return true; + } + + return mode != null && typeof(AbstractGameMode).IsAssignableFrom(group.OptionableType) && + mode.GetType().IsAssignableTo(group.OptionableType); + } + + private static bool SafeGroupVisible(AbstractOptionGroup group) + { + try + { + return group.GroupVisible(); + } + catch (Exception e) + { + Logger.Warning($"GroupVisible threw for {group.GetType().Name}: {e.Message}"); + return false; + } + } + + private static bool SafeOptionVisible(IModdedOption option) + { + try + { + return option.Visible(); + } + catch (Exception e) + { + Logger.Warning($"Visible threw for option '{option.Title}': {e.Message}"); + return false; + } + } + + private static GuideOptionEntry? TryCreateEntry( + IModdedOption option, + string groupName, + string modAbbreviation, + Func formatBool) + { + try + { + var title = TranslationController.Instance.GetString(option.StringName); + var value = FormatValue(option, formatBool); + return new GuideOptionEntry(title, value, groupName, modAbbreviation); + } + catch (Exception e) + { + Logger.Warning($"Could not format option '{option.Title}' for the guide: {e.Message}"); + return null; + } + } + + private static string FormatValue(IModdedOption option, Func formatBool) + { + switch (option) + { + case ModdedToggleOption toggle: + return formatBool(toggle.Value); + + case ModdedEnumOption enumOption: + return MiraLocaleManager.Get( + enumOption.Values[enumOption.Value], + enumOption.Values[enumOption.Value]); + + case ModdedNumberOption number: + if (number is { NegativeWordValue: not "#", Value: -1 }) + { + return number.NegativeWordValue; + } + + if (number is { ZeroWordValue: not "#", Value: 0 }) + { + return number.ZeroWordValue; + } + + return TrailingZeroRegex.Replace(number.Data.GetValueString(number.Value), string.Empty); + + default: + return option.Data.Type == global::OptionTypes.Checkbox + ? formatBool(Math.Abs(option.GetFloatData() - 1f) < 0.0001f) + : option.Data.GetValueString(option.GetFloatData()); + } + } +} \ No newline at end of file From 5ec02eec9c6cfaf45c1afff1ef11a855a79d62ce Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:50:45 -0400 Subject: [PATCH 10/29] add CreateAdvancedSettingsPage to print the settings --- MiraAPI/Patches/RoleGuidePatches.cs | 178 ++++------------------------ MiraAPI/Utilities/Helpers.cs | 115 +++++++++++++++++- 2 files changed, 140 insertions(+), 153 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 9d3fbc8..3bdcc4c 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -1726,22 +1726,7 @@ public static class RoleGuidePatches // ---- General (non-role) option pages: paginated General tab + cross-page search ---- private static Scroller? _generalScroller; private static GameObject? _generalNoResults; - private static readonly List GeneralRows = []; - private static GameObject? _generalRowTemplate; - private static Vector3 _generalRowOrigin; - private static Vector3 _generalRowColumnStep; - private static float _generalRowHeight = -0.65f; // negative: rows go down - private static int _generalColumns = 2; - private static float _generalVisibleHeight; - private static bool _generalScrollerFailed; - - /// One vanilla-looking settings row (label box + value box) that is reused between pages. - private sealed class GeneralRow(GameObject root, TextMeshPro label, TextMeshPro value) - { - public GameObject Root { get; } = root; - public TextMeshPro Label { get; } = label; - public TextMeshPro Value { get; } = value; - } + private static GameObject? _generalPageObject; private static List _generalPages = []; private static int _generalPage; // 0 = vanilla settings, 1..N = _generalPages[page - 1] private static bool _generalSearching; @@ -1760,17 +1745,27 @@ public static class RoleGuidePatches { _generalScroller = null; _generalNoResults = null; - GeneralRows.Clear(); - _generalRowTemplate = null; - _generalScrollerFailed = false; + _generalPageObject = null; _generalPages = []; _generalPage = 0; _generalSearching = false; _sortButtonsRepurposed = false; } + /// + /// Clones the roles tab to make the Settings tab. Used to display Options initialized by MiraAPI mods not related to roles or modifiers. + /// private static void InitializeGeneralPanel(MatchInfoGuide instance) { + var rolesTab = instance.settingsTabs[RolesTabIndex]; + var generalTab = Object.Instantiate(rolesTab, rolesTab.transform.parent); + generalTab.name = "GeneralOptionsPanel"; + generalTab.transform.FindChild("MaskArea")?.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); + generalTab.transform.GetAllChildren() + .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5); + _generalScroller = generalTab.GetComponent(); + generalTab.SetActive(false); + _generalNoResults = Object.Instantiate( instance.rolesEnabledMessage, instance.rolesEnabledMessage.transform.parent, @@ -1780,111 +1775,6 @@ public static class RoleGuidePatches SetNoResultsText(_generalNoResults); } - /// - /// Clones the vanilla settings tab the first time it is needed, so extra pages are drawn with the exact same - /// rows, fonts, masking and scroll area as the vanilla list. Done lazily because the vanilla rows are only - /// created when the guide first opens. The clone is NOT added to settingsTabs; its visibility is managed - /// here so the guide's tab count and tab buttons stay untouched. - /// - private static bool EnsureGeneralScroller(MatchInfoGuide guide) - { - if (_generalScroller != null && _generalRowTemplate) - { - return true; - } - - if (_generalScrollerFailed) - { - return false; - } - - var vanillaTab = guide.settingsTabs[GeneralTabIndex]; - var vanillaScroller = vanillaTab.GetComponent() ?? vanillaTab.GetComponentInChildren(true); - if (!vanillaScroller || vanillaScroller.Inner.childCount < 2) - { - return false; // vanilla rows not built yet; try again next refresh - } - - // Read the vanilla grid: first row, second entry (same row => 2 columns), third entry (next row). - var vInner = vanillaScroller.Inner; - var p0 = vInner.GetChild(0).localPosition; - var p1 = vInner.GetChild(1).localPosition; - var sameRow = Mathf.Abs(p1.y - p0.y) < 0.01f; - var vanillaCount = vInner.childCount; - _generalColumns = sameRow ? 2 : 1; - _generalRowOrigin = p0; - _generalRowColumnStep = sameRow ? new Vector3(p1.x - p0.x, 0f, 0f) : Vector3.zero; - if (sameRow) - { - _generalRowHeight = vanillaCount >= 3 ? vInner.GetChild(2).localPosition.y - p0.y : -0.65f; - } - else - { - _generalRowHeight = p1.y - p0.y; - } - - if (Mathf.Abs(_generalRowHeight) < 0.05f) - { - _generalRowHeight = -0.65f; - } - - // Self-calibrate the visible height from how the vanilla list was bounded. - var vanillaRows = Mathf.CeilToInt(vanillaCount / (float)_generalColumns); - var visible = (vanillaRows * Mathf.Abs(_generalRowHeight)) - vanillaScroller.ContentYBounds.max; - _generalVisibleHeight = visible > 0.1f && visible < vanillaRows * Mathf.Abs(_generalRowHeight) - ? visible - : 3.3f * Mathf.Abs(_generalRowHeight); - - var clone = Object.Instantiate(vanillaTab, vanillaTab.transform.parent); - clone.name = "GeneralOptionsPanel"; - var cloneScroller = clone.GetComponent() ?? clone.GetComponentInChildren(true); - var cInner = cloneScroller.Inner; - - // Keep the first row as an inactive prototype, delete the rest. - var template = cInner.GetChild(0).gameObject; - for (var i = cInner.childCount - 1; i >= 1; i--) - { - var child = cInner.GetChild(i).gameObject; - child.SetActive(false); - child.Destroy(); - } - - template.SetActive(false); - _generalRowTemplate = template; - _generalScroller = cloneScroller; - clone.SetActive(false); - return true; - } - - private static GeneralRow? CreateGeneralRow() - { - if (_generalScroller == null || !_generalRowTemplate) - { - return null; - } - - var go = Object.Instantiate(_generalRowTemplate!, _generalScroller.Inner); - go.name = "GeneralOptionRow"; - - // The game would re-translate these and overwrite our text. - foreach (var translator in go.GetComponentsInChildren(true)) - { - translator.Destroy(); - } - - // Label is the left-most text, value is the right-most (same as the vanilla entries). - var texts = go.GetComponentsInChildren(true).OrderBy(t => t.transform.position.x).ToArray(); - if (texts.Length < 2) - { - Warning("Vanilla settings row has fewer than two text fields; cannot clone it for the general pages."); - go.Destroy(); - _generalScrollerFailed = true; - return null; - } - - return new GeneralRow(go, texts[0], texts[^1]); - } - private static void EnterGeneralTab() { if (!_uiInitialized) @@ -1983,45 +1873,29 @@ public static class RoleGuidePatches private static void ShowGeneralEntries(IReadOnlyList entries, bool showGroup, bool scrollToTop) { var guide = MatchInfoGuide.Instance; - if (!guide || !EnsureGeneralScroller(guide)) + if (_generalScroller == null || !guide || !guide.MatchInfoRolePanelPrefab) { - // Could not clone the vanilla rows: fall back to the plain vanilla tab rather than a blank page. + // Never leave the tab blank: fall back to the plain vanilla list. guide?.settingsTabs[GeneralTabIndex].SetActive(true); return; } - while (GeneralRows.Count < entries.Count) + if (_generalPageObject) { - var row = CreateGeneralRow(); - if (row == null) - { - break; - } - - GeneralRows.Add(row); + _generalPageObject!.SetActive(false); + _generalPageObject.Destroy(); + _generalPageObject = null; } - for (var i = 0; i < GeneralRows.Count; i++) + var rows = new List>(entries.Count); + foreach (var entry in entries) { - var row = GeneralRows[i]; - if (i >= entries.Count) - { - row.Root.SetActive(false); - continue; - } - - var entry = entries[i]; - row.Label.text = showGroup ? $"{entry.GroupName}\n{entry.Title}" : entry.Title; - row.Value.text = entry.Value; - row.Root.transform.localPosition = _generalRowOrigin + - (_generalRowColumnStep * (i % _generalColumns)) + - new Vector3(0f, _generalRowHeight * (i / _generalColumns), 0f); - row.Root.SetActive(true); + var label = showGroup ? $"{entry.GroupName}\n{entry.Title}" : entry.Title; + rows.Add(new KeyValuePair(label, entry.Value)); } - var rows = Mathf.CeilToInt(Mathf.Min(entries.Count, GeneralRows.Count) / (float)_generalColumns); - _generalScroller!.gameObject.SetActive(true); - _generalScroller.SetYBoundsMax(Mathf.Clamp((rows * Mathf.Abs(_generalRowHeight)) - _generalVisibleHeight, 0f, 999f)); + _generalScroller.gameObject.SetActive(true); + _generalPageObject = Helpers.CreateAdvancedSettingsPage(guide, _generalScroller, rows); _generalNoResults?.SetActive(entries.Count == 0); if (scrollToTop) { diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 53bade5..26d68df 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -844,6 +844,119 @@ public static class Helpers return obj; } + /// + /// Creates a page of vanilla-looking settings rows (label box on the left, value box on the right, two columns) + /// inside a roles-style scroller. Use this for lists of "Name: Value" settings that are not roles or modifiers. + /// + /// The guide object. + /// The the page is parented to. + /// The rows to show: key is the label, value is the displayed value. + /// The that holds all the rows. Destroy it to remove the page. + public static GameObject CreateAdvancedSettingsPage( + MatchInfoGuide guide, + Scroller parent, + IReadOnlyList> settings) + { + const float rowHeight = 0.4f; + const float rowPitch = 0.52f; + const float contentTop = 0.95f; + const float visibleHeight = 1.5f; + const int columns = 2; + + var inner = parent.Inner; + var layout = inner.GetComponent(); + if (layout) + { + layout.enabled = false; + } + + var columnWidth = layout ? Mathf.Clamp(layout.cellSize.x, 2f, 3.2f) : 2.7f; + var columnGap = layout ? Mathf.Clamp(layout.spacing.x, 0.1f, 0.6f) : 0.3f; + var labelWidth = columnWidth * 0.62f; + var valueWidth = columnWidth * 0.30f; + var boxGap = columnWidth - labelWidth - valueWidth; + var left = -((columns * columnWidth) + ((columns - 1) * columnGap)) / 2f; + + var prefab = guide.MatchInfoRolePanelPrefab; + var vanillaFont = HudManager.Instance.TaskPanel.taskText; + + var boxMaterial = new Material(prefab.roleIcon.sharedMaterial); + boxMaterial.SetInt(PlayerMaterial.MaskLayer, 50); + var boxSprite = MiraAssets.BlankSprite.LoadAsset(); + var layer = inner.gameObject.layer; + + var page = new GameObject("AdvancedSettingsPage") { layer = layer }; + page.transform.SetParent(inner, false); + page.transform.localPosition = Vector3.zero; + + void MakeBox(Vector3 pos, Vector2 size, Color color) + { + var box = new GameObject("Box") { layer = layer }; + box.transform.SetParent(page.transform, false); + box.transform.localPosition = pos; + var sr = box.AddComponent(); + sr.sprite = boxSprite; + sr.drawMode = SpriteDrawMode.Sliced; + sr.size = size; + sr.color = color; + sr.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + sr.sharedMaterial = boxMaterial; + } + + void MakeText(string text, Vector3 pos, Vector2 size) + { + var tmp = Object.Instantiate(prefab.roleName, page.transform); + tmp.richText = true; + tmp.font = vanillaFont.font; + tmp.fontMaterial = vanillaFont.fontMaterial; + tmp.text = text; + tmp.enableAutoSizing = true; + tmp.fontSizeMin = 0.9f; + tmp.fontSizeMax = 2.2f; + tmp.fontStyle = FontStyles.Normal; + tmp.enableWordWrapping = true; + tmp.alignment = TextAlignmentOptions.Center; + tmp.color = Color.white; + tmp.rectTransform.sizeDelta = size; + tmp.transform.localPosition = pos; + tmp.EnableStencilMasking(); + } + + for (var i = 0; i < settings.Count; i++) + { + var col = i % columns; + var row = i / columns; + var originX = left + (col * (columnWidth + columnGap)); + var centerY = contentTop - (rowHeight / 2f) - (row * rowPitch); + + var labelX = originX + (labelWidth / 2f); + var valueX = originX + labelWidth + boxGap + (valueWidth / 2f); + + MakeBox( + new Vector3(labelX, centerY, 0f), + new Vector2(labelWidth, rowHeight), + new Color(0.16f, 0.16f, 0.16f, 1f)); + MakeBox(new Vector3(valueX, centerY, -0.01f), new Vector2(valueWidth, rowHeight), Color.white); + MakeBox( + new Vector3(valueX, centerY, -0.02f), + new Vector2(valueWidth - 0.04f, rowHeight - 0.04f), + Color.black); + + MakeText( + $"{settings[i].Key}", + new Vector3(labelX, centerY, -0.1f), + new Vector2(labelWidth - 0.12f, rowHeight - 0.04f)); + MakeText( + settings[i].Value, + new Vector3(valueX, centerY, -0.1f), + new Vector2(valueWidth - 0.1f, rowHeight - 0.04f)); + } + + var rows = Mathf.CeilToInt(settings.Count / (float)columns); + parent.SetYBoundsMax(Mathf.Clamp((rows * rowPitch) - visibleHeight, 0f, 999f)); + return page; + } + private static void SetupAbilityPanel(MatchInfoRolePanel panel, AdvancedWikiAbilityDescription ability, bool variableTextSizing = false) { panel.roleCount.text = ability.AbilityType; @@ -868,4 +981,4 @@ public static class Helpers panel.roleCount.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f); panel.roleIcon.transform.localScale = new Vector3(3.5f, 3.5f, 1f); } -} +} \ No newline at end of file From e3358507bfa60fc5b5716ae2a82f54853b11dcb8 Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:20:21 -0400 Subject: [PATCH 11/29] Update UI to clone Options Num Rows to display settings --- .../Patches/Options/GameOptionsMenuPatch.cs | 1 + .../Patches/Options/GameSettingMenuPatches.cs | 6 + MiraAPI/Utilities/Helpers.cs | 203 ++++++++++++------ 3 files changed, 142 insertions(+), 68 deletions(-) diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index e65f4c3..ecf574c 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -38,6 +38,7 @@ internal static class GameOptionsMenuPatch [HarmonyPatch(nameof(GameOptionsMenu.Initialize))] public static bool InitPatch(GameOptionsMenu __instance) { + Helpers.CacheAdvancedSettingsRowPrefab(__instance.numberOptionOrigin); __instance.Children ??= new(); __instance.Children.Clear(); diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index e6ae2d5..6c1ede7 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using MiraAPI.Utilities; namespace MiraAPI.Patches.Options; @@ -13,6 +14,11 @@ internal static class GameSettingMenuPatches [HarmonyPatch(nameof(GameSettingMenu.OnEnable))] public static void OnEnablePrefix(GameSettingMenu __instance) { + if (__instance.GameSettingsTab && __instance.GameSettingsTab.numberOptionOrigin) + { + Helpers.CacheAdvancedSettingsRowPrefab(__instance.GameSettingsTab.numberOptionOrigin); + } + // Add Menu State object if (!__instance.gameObject.GetComponent()) { diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 26d68df..29a57a4 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using HarmonyLib; +using Il2CppInterop.Runtime; using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.GameOptions; using MiraAPI.GameOptions.OptionTypes; @@ -32,6 +33,7 @@ public static class Helpers { internal const float MatchInfoGuideBaseScale = 1.3f; private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); + private static NumberOption? _advancedSettingsRowPrefab; public static ReadOnlyCollection? GetModdedOptionsForType(Type classType) { @@ -845,8 +847,60 @@ public static class Helpers } /// - /// Creates a page of vanilla-looking settings rows (label box on the left, value box on the right, two columns) - /// inside a roles-style scroller. Use this for lists of "Name: Value" settings that are not roles or modifiers. + /// Caches a persistent clone of the vanilla settings row for use in the Match Info Guide. + /// + /// The vanilla settings row prefab. + internal static void CacheAdvancedSettingsRowPrefab(NumberOption? rowPrefab) + { + if (!rowPrefab) + { + foreach (var candidate in Resources.FindObjectsOfTypeAll(Il2CppType.Of())) + { + var menu = candidate.TryCast(); + var settingsTab = menu ? menu!.GameSettingsTab : null; + if (settingsTab && settingsTab!.numberOptionOrigin) + { + rowPrefab = settingsTab.numberOptionOrigin; + break; + } + } + } + + if (!rowPrefab) + { + foreach (var candidate in Resources.FindObjectsOfTypeAll(Il2CppType.Of())) + { + var option = candidate.TryCast(); + if (!option) + { + continue; + } + + if (option!.TitleText && option.ValueText && option.PlusBtn && option.MinusBtn) + { + rowPrefab = option; + break; + } + } + } + + if (_advancedSettingsRowPrefab || !rowPrefab) + { + return; + } + + var cachedPrefab = Object.Instantiate(rowPrefab!.gameObject); + cachedPrefab.name = "MiraAPIAdvancedSettingsRowPrefab"; + cachedPrefab.transform.SetParent(null, false); + cachedPrefab.transform.localPosition = Vector3.zero; + cachedPrefab.transform.localRotation = Quaternion.identity; + cachedPrefab.SetActive(false); + Object.DontDestroyOnLoad(cachedPrefab); + _advancedSettingsRowPrefab = cachedPrefab.GetComponent(); + } + + /// + /// Creates a page of vanilla settings rows inside a roles-style scroller. /// /// The guide object. /// The the page is parented to. @@ -857,10 +911,9 @@ public static class Helpers Scroller parent, IReadOnlyList> settings) { - const float rowHeight = 0.4f; - const float rowPitch = 0.52f; + const float rowPitch = 0.57f; const float contentTop = 0.95f; - const float visibleHeight = 1.5f; + const float visibleHeight = 1.8f; const int columns = 2; var inner = parent.Inner; @@ -872,88 +925,102 @@ public static class Helpers var columnWidth = layout ? Mathf.Clamp(layout.cellSize.x, 2f, 3.2f) : 2.7f; var columnGap = layout ? Mathf.Clamp(layout.spacing.x, 0.1f, 0.6f) : 0.3f; - var labelWidth = columnWidth * 0.62f; - var valueWidth = columnWidth * 0.30f; - var boxGap = columnWidth - labelWidth - valueWidth; - var left = -((columns * columnWidth) + ((columns - 1) * columnGap)) / 2f; - - var prefab = guide.MatchInfoRolePanelPrefab; - var vanillaFont = HudManager.Instance.TaskPanel.taskText; - - var boxMaterial = new Material(prefab.roleIcon.sharedMaterial); - boxMaterial.SetInt(PlayerMaterial.MaskLayer, 50); - var boxSprite = MiraAssets.BlankSprite.LoadAsset(); var layer = inner.gameObject.layer; - var page = new GameObject("AdvancedSettingsPage") { layer = layer }; page.transform.SetParent(inner, false); page.transform.localPosition = Vector3.zero; + page.SetActive(false); - void MakeBox(Vector3 pos, Vector2 size, Color color) + var gameSettingsMenu = GameSettingMenu.Instance; + CacheAdvancedSettingsRowPrefab(gameSettingsMenu ? gameSettingsMenu.GameSettingsTab.numberOptionOrigin : null); + var rowPrefab = _advancedSettingsRowPrefab; + if (!rowPrefab) { - var box = new GameObject("Box") { layer = layer }; - box.transform.SetParent(page.transform, false); - box.transform.localPosition = pos; - var sr = box.AddComponent(); - sr.sprite = boxSprite; - sr.drawMode = SpriteDrawMode.Sliced; - sr.size = size; - sr.color = color; - sr.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; - sr.sharedMaterial = boxMaterial; + Error("Could not render MiraAPI settings rows because the vanilla NumberOption prefab was not cached."); + return page; } - void MakeText(string text, Vector3 pos, Vector2 size) + var spriteBounds = rowPrefab!.GetComponentsInChildren(true) + .Where(renderer => + renderer.enabled && + renderer.sprite && + renderer.transform != rowPrefab.PlusBtn.transform && + !renderer.transform.IsChildOf(rowPrefab.PlusBtn.transform) && + renderer.transform != rowPrefab.MinusBtn.transform && + !renderer.transform.IsChildOf(rowPrefab.MinusBtn.transform)) + .Select(renderer => renderer.bounds) + .ToArray(); + if (spriteBounds.Length == 0) { - var tmp = Object.Instantiate(prefab.roleName, page.transform); - tmp.richText = true; - tmp.font = vanillaFont.font; - tmp.fontMaterial = vanillaFont.fontMaterial; - tmp.text = text; - tmp.enableAutoSizing = true; - tmp.fontSizeMin = 0.9f; - tmp.fontSizeMax = 2.2f; - tmp.fontStyle = FontStyles.Normal; - tmp.enableWordWrapping = true; - tmp.alignment = TextAlignmentOptions.Center; - tmp.color = Color.white; - tmp.rectTransform.sizeDelta = size; - tmp.transform.localPosition = pos; - tmp.EnableStencilMasking(); + Error("Could not render MiraAPI settings rows because the vanilla NumberOption prefab has no sprites."); + return page; } + var prefabBounds = spriteBounds.Skip(1).Aggregate( + spriteBounds[0], + (bounds, next) => + { + bounds.Encapsulate(next); + return bounds; + }); + var rowScale = (columnWidth / prefabBounds.size.x) * 1.1f; + var rowHeight = prefabBounds.size.y * rowScale; + for (var i = 0; i < settings.Count; i++) { - var col = i % columns; - var row = i / columns; - var originX = left + (col * (columnWidth + columnGap)); - var centerY = contentTop - (rowHeight / 2f) - (row * rowPitch); + var column = i % columns; + var rowIndex = i / columns; + var originX = -((columns * columnWidth) + ((columns - 1) * columnGap)) / 2f + + (column * (columnWidth + columnGap)); + var centerY = contentTop - (rowHeight / 2f) - (rowIndex * rowPitch); + var row = Object.Instantiate(rowPrefab, page.transform); + row.enabled = false; + row.PlusBtn.gameObject.SetActive(false); + row.MinusBtn.gameObject.SetActive(false); - var labelX = originX + (labelWidth / 2f); - var valueX = originX + labelWidth + boxGap + (valueWidth / 2f); + foreach (var button in row.GetComponentsInChildren(true)) + { + button.enabled = false; + } - MakeBox( - new Vector3(labelX, centerY, 0f), - new Vector2(labelWidth, rowHeight), - new Color(0.16f, 0.16f, 0.16f, 1f)); - MakeBox(new Vector3(valueX, centerY, -0.01f), new Vector2(valueWidth, rowHeight), Color.white); - MakeBox( - new Vector3(valueX, centerY, -0.02f), - new Vector2(valueWidth - 0.04f, rowHeight - 0.04f), - Color.black); + foreach (var collider in row.GetComponentsInChildren(true)) + { + collider.enabled = false; + } - MakeText( - $"{settings[i].Key}", - new Vector3(labelX, centerY, -0.1f), - new Vector2(labelWidth - 0.12f, rowHeight - 0.04f)); - MakeText( - settings[i].Value, - new Vector3(valueX, centerY, -0.1f), - new Vector2(valueWidth - 0.1f, rowHeight - 0.04f)); + row.transform.localScale = rowPrefab.transform.localScale * rowScale; + row.transform.localPosition = new Vector3( + originX + (columnWidth / 2f) - (prefabBounds.center.x * rowScale), + centerY - (prefabBounds.center.y * rowScale), + 0f); + row.TitleText.text = settings[i].Key; + row.TitleText.fontStyle = FontStyles.Bold; + row.ValueText.text = settings[i].Value; + + foreach (var text in row.GetComponentsInChildren(true)) + { + if (text.TryGetComponent(out var translator)) + { + translator.enabled = false; + Object.Destroy(translator); + } + + text.fontMaterial.SetFloat(guide.MatchInfoRolePanelPrefab.STENCIL_NAME, 50f); + } + + foreach (var renderer in row.GetComponentsInChildren(true)) + { + renderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + renderer.material.SetInt(PlayerMaterial.MaskLayer, 50); + } + + row.gameObject.SetActive(true); } var rows = Mathf.CeilToInt(settings.Count / (float)columns); - parent.SetYBoundsMax(Mathf.Clamp((rows * rowPitch) - visibleHeight, 0f, 999f)); + var contentHeight = rows == 0 ? 0f : rowHeight + ((rows - 1) * rowPitch); + parent.SetYBoundsMax(Mathf.Clamp(contentHeight - visibleHeight, 0f, 999f)); + page.SetActive(true); return page; } From 6d40113ea0469323a9d9041c8a908eb7bb10dda2 Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:36:57 -0400 Subject: [PATCH 12/29] fix scroller and option content clipping --- MiraAPI/Utilities/Helpers.cs | 44 +++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 29a57a4..3685089 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -911,9 +911,8 @@ public static class Helpers Scroller parent, IReadOnlyList> settings) { - const float rowPitch = 0.57f; const float contentTop = 0.95f; - const float visibleHeight = 1.8f; + const float horizontalOffset = -0.2f; const int columns = 2; var inner = parent.Inner; @@ -965,13 +964,16 @@ public static class Helpers }); var rowScale = (columnWidth / prefabBounds.size.x) * 1.1f; var rowHeight = prefabBounds.size.y * rowScale; + var rowPitch = 0.45f * rowScale; + var contentBottom = float.PositiveInfinity; for (var i = 0; i < settings.Count; i++) { var column = i % columns; var rowIndex = i / columns; var originX = -((columns * columnWidth) + ((columns - 1) * columnGap)) / 2f + - (column * (columnWidth + columnGap)); + (column * (columnWidth + columnGap)) + + horizontalOffset; var centerY = contentTop - (rowHeight / 2f) - (rowIndex * rowPitch); var row = Object.Instantiate(rowPrefab, page.transform); row.enabled = false; @@ -994,7 +996,6 @@ public static class Helpers centerY - (prefabBounds.center.y * rowScale), 0f); row.TitleText.text = settings[i].Key; - row.TitleText.fontStyle = FontStyles.Bold; row.ValueText.text = settings[i].Value; foreach (var text in row.GetComponentsInChildren(true)) @@ -1005,21 +1006,48 @@ public static class Helpers Object.Destroy(translator); } - text.fontMaterial.SetFloat(guide.MatchInfoRolePanelPrefab.STENCIL_NAME, 50f); + text.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); + text.fontMaterial.SetFloat(ShaderID.Stencil, 50f); } foreach (var renderer in row.GetComponentsInChildren(true)) { renderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; renderer.material.SetInt(PlayerMaterial.MaskLayer, 50); + if (renderer.enabled && + renderer.sprite && + renderer.transform != row.PlusBtn.transform && + !renderer.transform.IsChildOf(row.PlusBtn.transform) && + renderer.transform != row.MinusBtn.transform && + !renderer.transform.IsChildOf(row.MinusBtn.transform)) + { + contentBottom = Mathf.Min( + contentBottom, + inner.InverseTransformPoint(renderer.bounds.min).y); + } + } + + foreach (var text in row.GetComponentsInChildren(true)) + { + if (text.enabled) + { + contentBottom = Mathf.Min( + contentBottom, + inner.InverseTransformPoint(text.bounds.min).y); + } } row.gameObject.SetActive(true); } - var rows = Mathf.CeilToInt(settings.Count / (float)columns); - var contentHeight = rows == 0 ? 0f : rowHeight + ((rows - 1) * rowPitch); - parent.SetYBoundsMax(Mathf.Clamp(contentHeight - visibleHeight, 0f, 999f)); + var maskArea = parent.transform.FindChild("MaskArea")?.GetComponent(); + var maskBottom = maskArea + ? inner.InverseTransformPoint(maskArea!.bounds.min).y + : -0.75f; + var scrollRange = float.IsPositiveInfinity(contentBottom) + ? 0f + : Mathf.Max(0f, maskBottom - contentBottom + 0.1f); + parent.SetYBoundsMax(Mathf.Clamp(scrollRange, 0f, 999f)); page.SetActive(true); return page; } From 75142dbcb836f340307cbc24c68116379970e67e Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:01:36 -0400 Subject: [PATCH 13/29] rename sm properties for easier reading --- MiraAPI/Patches/RoleGuidePatches.cs | 258 +++++++++++------------ MiraAPI/Utilities/GuideOptionsPatches.cs | 4 +- 2 files changed, 129 insertions(+), 133 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 3bdcc4c..427dcf9 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -27,7 +27,7 @@ namespace MiraAPI.Patches; [HarmonyPatch] public static class RoleGuidePatches { - private const int GeneralTabIndex = 1; + private const int SettingsTabIndex = 1; private const int RolesTabIndex = 2; private const int ModifiersTabIndex = 3; private const float MinGuideScaleFactor = 0.5f; @@ -56,9 +56,9 @@ public static class RoleGuidePatches private static Scroller _rolesScroller; private static Scroller _modifiersScroller; private static GameObject _modifierNoResultsMessage; - private static Scroller _advancedInfoTabScroller; + private static Scroller _wikiScroller; private static TextBoxTMP _searchBoxTmp; - private static GameObject _currentAdvancedTabObject; + private static GameObject _currentWikiPage; private static TextMeshPro _titleText; private static SpriteRenderer _titleIcon; private static bool _suppressSearchRefresh; @@ -172,10 +172,10 @@ public static class RoleGuidePatches _rolesScroller = null!; _modifiersScroller = null!; _modifierNoResultsMessage = null!; - _advancedInfoTabScroller = null!; - _currentAdvancedTabObject = null!; + _wikiScroller = null!; + _currentWikiPage = null!; _loadingSpinner = null; - ResetGeneralState(); + ResetSettingsState(); SearchIconButton = null!; SearchSortingOrderButton = null!; SearchSortingGroupButton = null!; @@ -470,24 +470,24 @@ public static class RoleGuidePatches .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5); _modifiersScroller = modifiersTab.GetComponent(); instance.settingsTabs.Add(modifiersTab); - InitializeGeneralPanel(instance); + InitializeSettingsPanel(instance); var wikiTab = Object.Instantiate( instance.settingsTabs[RolesTabIndex], instance.settingsTabs[RolesTabIndex].transform.parent); - wikiTab.name = "AdvancedWikiPanels"; + wikiTab.name = "WikiPanels"; var wikiMask = wikiTab.transform.FindChild("MaskArea"); wikiMask.transform.localPosition = new Vector3(-0.0184f, 0.425f, -0.1f); wikiMask.transform.localScale = new Vector3(1, 1.3f, 1); wikiTab.transform.GetAllChildren() .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5); - _advancedInfoTabScroller = wikiTab.GetComponent(); + _wikiScroller = wikiTab.GetComponent(); var wikiScrollHitbox = wikiTab.transform.FindChild("Hitbox"); - _advancedInfoTabScroller.ScrollbarYBounds.max = 1.2f; + _wikiScroller.ScrollbarYBounds.max = 1.2f; wikiScrollHitbox.GetComponent().size = new Vector2(5.75f, 2.4f); wikiScrollHitbox.transform.localPosition += new Vector3(0, 0.3f); - _advancedInfoTabScroller.ScrollbarY.trackGraphic.transform.localPosition = new Vector3(2.8364f, 0.3649f, -0.2f); - _advancedInfoTabScroller.ScrollbarY.trackGraphic.transform.localScale = new Vector3(0.0234f, 2.068f, 0.5598f); + _wikiScroller.ScrollbarY.trackGraphic.transform.localPosition = new Vector3(2.8364f, 0.3649f, -0.2f); + _wikiScroller.ScrollbarY.trackGraphic.transform.localScale = new Vector3(0.0234f, 2.068f, 0.5598f); var spinnerParent = instance.settingsTabs[RolesTabIndex].transform.parent; var spinnerPos = spinnerParent.InverseTransformPoint(instance.MatchInfoRoleMaskArea.transform.position); @@ -750,7 +750,7 @@ public static class RoleGuidePatches private static void OpenTab(int tabIndex, float delay = 0f) { - _advancedInfoTabScroller?.gameObject.SetActive(false); + _wikiScroller?.gameObject.SetActive(false); MatchInfoGuide.Instance.SetActiveTab(tabIndex); HidePanels(tabIndex == ModifiersTabIndex ? ModifierEntries : RoleEntries); StartPopulate(delay); @@ -838,7 +838,7 @@ public static class RoleGuidePatches { _loadingSpinner.gameObject.SetActive( _loading && MatchInfoGuide.Instance.activeTabIndex is RolesTabIndex or ModifiersTabIndex && - !(_advancedInfoTabScroller && _advancedInfoTabScroller.gameObject.activeSelf)); + !(_wikiScroller && _wikiScroller.gameObject.activeSelf)); } } @@ -1054,9 +1054,9 @@ public static class RoleGuidePatches private static void RefreshActiveTab(bool scrollToTop) { - if (IsGeneralTabActive) + if (IsSettingsTabActive) { - RefreshGeneralTab(scrollToTop); + RefreshSettingsTab(scrollToTop); return; } @@ -1088,9 +1088,9 @@ public static class RoleGuidePatches } _appliedSearch = text; - if (IsGeneralTabActive) + if (IsSettingsTabActive) { - RefreshGeneralTab(true); + RefreshSettingsTab(true); yield break; } @@ -1201,9 +1201,9 @@ public static class RoleGuidePatches public static void SwitchFilter() { - if (IsGeneralTabActive) + if (IsSettingsTabActive) { - ChangeGeneralPage(-1); + ChangeSettingsPage(-1); return; } @@ -1253,7 +1253,7 @@ public static class RoleGuidePatches public static void SwitchMethod() { - if (IsGeneralTabActive) + if (IsSettingsTabActive) { return; } @@ -1292,9 +1292,9 @@ public static class RoleGuidePatches public static void SwitchGrouping() { - if (IsGeneralTabActive) + if (IsSettingsTabActive) { - ChangeGeneralPage(1); + ChangeSettingsPage(1); return; } @@ -1437,8 +1437,8 @@ public static class RoleGuidePatches _titleText.text = MiraLocaleManager.Get("MiraApi.Wiki.PlayersTab"); _titleIcon.sprite = MiraAssets.IconPlayerList.LoadAsset(); break; - case GeneralTabIndex: - _titleText.text = GetGeneralTitle(); + case SettingsTabIndex: + _titleText.text = GetSettingsTitle(); _titleIcon.sprite = MiraAssets.IconSettings.LoadAsset(); break; case RolesTabIndex: @@ -1458,15 +1458,15 @@ public static class RoleGuidePatches _titleIcon.SetSizeLimit(0.55f); } - _advancedInfoTabScroller?.gameObject.SetActive(false); + _wikiScroller?.gameObject.SetActive(false); SearchComponentContainer?.SetActive(true); - if (tabIndex == GeneralTabIndex) + if (tabIndex == SettingsTabIndex) { - EnterGeneralTab(); + EnterSettingsTab(); } else { - LeaveGeneralTab(); + LeaveSettingsTab(); } UpdateSpinner(); @@ -1511,15 +1511,15 @@ public static class RoleGuidePatches return MiraAssets.IconNeutral.LoadAsset(); } - private static bool TryPrepareAdvancedWiki(MatchInfoGuide instance) + private static bool TryPrepareWikiPage(MatchInfoGuide instance) { instance.settingsTabs[RolesTabIndex].SetActive(false); _modifiersScroller?.gameObject.SetActive(false); - LeaveGeneralTab(); - if (_currentAdvancedTabObject) + LeaveSettingsTab(); + if (_currentWikiPage) { - _currentAdvancedTabObject.SetActive(false); - _currentAdvancedTabObject.Destroy(); + _currentWikiPage.SetActive(false); + _currentWikiPage.Destroy(); } if (SearchComponentContainer) @@ -1527,18 +1527,18 @@ public static class RoleGuidePatches SearchComponentContainer.SetActive(false); } - if (_advancedInfoTabScroller == null) + if (_wikiScroller == null) { Warning("Wiki tab is null."); return false; } - _advancedInfoTabScroller.gameObject.SetActive(true); + _wikiScroller.gameObject.SetActive(true); UpdateSpinner(); return true; } - private static IEnumerator CoDisplayAdvancedWikiLater(float delay, Action display) + private static IEnumerator CoDisplayWikiPageLater(float delay, Action display) { yield return new WaitForSeconds(delay); if (_guide && MatchInfoGuide.Instance && MatchInfoGuide.Instance.MatchInfoParent.activeSelf) @@ -1552,18 +1552,18 @@ public static class RoleGuidePatches var remaining = _openedAt + instance.transitionOpen.duration - Time.time; if (remaining > 0f) { - Coroutines.Start(CoDisplayAdvancedWikiLater(remaining, () => DisplayAdvancedWiki(instance, modifier))); + Coroutines.Start(CoDisplayWikiPageLater(remaining, () => DisplayAdvancedWiki(instance, modifier))); return; } var name = modifier.ModifierName; - Info($"Opening advanced tab for {name}."); - if (!TryPrepareAdvancedWiki(instance)) + Info($"Opening wiki page for {name}."); + if (!TryPrepareWikiPage(instance)) { return; } - _currentAdvancedTabObject = modifier.GetAdvancedWiki(instance, _titleText, _titleIcon, _advancedInfoTabScroller); + _currentWikiPage = modifier.GetAdvancedWiki(instance, _titleText, _titleIcon, _wikiScroller); } public static void DisplayAdvancedWiki(MatchInfoGuide instance, RoleBehaviour role) @@ -1571,19 +1571,19 @@ public static class RoleGuidePatches var remaining = _openedAt + instance.transitionOpen.duration - Time.time; if (remaining > 0f) { - Coroutines.Start(CoDisplayAdvancedWikiLater(remaining, () => DisplayAdvancedWiki(instance, role))); + Coroutines.Start(CoDisplayWikiPageLater(remaining, () => DisplayAdvancedWiki(instance, role))); return; } - Info($"Opening advanced tab for {role.GetRoleName()}."); - if (!TryPrepareAdvancedWiki(instance)) + Info($"Opening wiki page for {role.GetRoleName()}."); + if (!TryPrepareWikiPage(instance)) { return; } if (role is ICustomRole custom) { - _currentAdvancedTabObject = custom.GetAdvancedWiki(instance, _titleText, _titleIcon, _advancedInfoTabScroller); + _currentWikiPage = custom.GetAdvancedWiki(instance, _titleText, _titleIcon, _wikiScroller); } else { @@ -1599,11 +1599,11 @@ public static class RoleGuidePatches } } - _currentAdvancedTabObject = Helpers.CreateAdvancedWikiPage( + _currentWikiPage = Helpers.CreateAdvancedWikiPage( instance, _titleText, _titleIcon, - _advancedInfoTabScroller, + _wikiScroller, role.Role.ToString(), titleTxt, role.RoleIconColor, @@ -1723,100 +1723,96 @@ public static class RoleGuidePatches return customRole.RoleFactionTitle; } - // ---- General (non-role) option pages: paginated General tab + cross-page search ---- - private static Scroller? _generalScroller; - private static GameObject? _generalNoResults; - private static GameObject? _generalPageObject; - private static List _generalPages = []; - private static int _generalPage; // 0 = vanilla settings, 1..N = _generalPages[page - 1] - private static bool _generalSearching; + // Settings page state. + private static Scroller? _settingsScroller; + private static GameObject? _settingsNoResults; + private static GameObject? _settingsPageObject; + private static List _settingsPages = []; + private static int _settingsPage; // 0 = vanilla settings, 1..N = _settingsPages[page - 1] + private static bool _settingsSearching; - // Saved state of the sort buttons so they can be restored when leaving the General tab. + // Save the sort-button appearance while they are used for page navigation. private static bool _sortButtonsRepurposed; private static string _savedFilterLabel = string.Empty; private static string _savedGroupLabel = string.Empty; private static Vector3 _savedFilterIconScale = Vector3.one; private static Vector3 _savedGroupIconScale = Vector3.one; - private static bool IsGeneralTabActive => - _uiInitialized && MatchInfoGuide.Instance && MatchInfoGuide.Instance.activeTabIndex == GeneralTabIndex; + private static bool IsSettingsTabActive => + _uiInitialized && MatchInfoGuide.Instance && MatchInfoGuide.Instance.activeTabIndex == SettingsTabIndex; - private static void ResetGeneralState() + private static void ResetSettingsState() { - _generalScroller = null; - _generalNoResults = null; - _generalPageObject = null; - _generalPages = []; - _generalPage = 0; - _generalSearching = false; + _settingsScroller = null; + _settingsNoResults = null; + _settingsPageObject = null; + _settingsPages = []; + _settingsPage = 0; + _settingsSearching = false; _sortButtonsRepurposed = false; } - /// - /// Clones the roles tab to make the Settings tab. Used to display Options initialized by MiraAPI mods not related to roles or modifiers. - /// - private static void InitializeGeneralPanel(MatchInfoGuide instance) + private static void InitializeSettingsPanel(MatchInfoGuide instance) { var rolesTab = instance.settingsTabs[RolesTabIndex]; - var generalTab = Object.Instantiate(rolesTab, rolesTab.transform.parent); - generalTab.name = "GeneralOptionsPanel"; - generalTab.transform.FindChild("MaskArea")?.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); - generalTab.transform.GetAllChildren() + var settingsTab = Object.Instantiate(rolesTab, rolesTab.transform.parent); + settingsTab.name = "SettingsOptionsPanel"; + settingsTab.transform.FindChild("MaskArea")?.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); + settingsTab.transform.GetAllChildren() .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5); - _generalScroller = generalTab.GetComponent(); - generalTab.SetActive(false); + _settingsScroller = settingsTab.GetComponent(); + settingsTab.SetActive(false); - _generalNoResults = Object.Instantiate( + _settingsNoResults = Object.Instantiate( instance.rolesEnabledMessage, instance.rolesEnabledMessage.transform.parent, true); - _generalNoResults.name = "GeneralNoResultsFound"; - _generalNoResults.SetActive(false); - SetNoResultsText(_generalNoResults); + _settingsNoResults.name = "SettingsNoResultsFound"; + _settingsNoResults.SetActive(false); + SetNoResultsText(_settingsNoResults); } - private static void EnterGeneralTab() + private static void EnterSettingsTab() { if (!_uiInitialized) { return; } - RefreshGeneralTab(true); + RefreshSettingsTab(true); } - private static void LeaveGeneralTab() + private static void LeaveSettingsTab() { - if (_generalScroller != null) + if (_settingsScroller != null) { - _generalScroller.gameObject.SetActive(false); + _settingsScroller.gameObject.SetActive(false); } - if (_generalNoResults) + if (_settingsNoResults) { - _generalNoResults!.SetActive(false); + _settingsNoResults!.SetActive(false); } RestoreSortButtons(); } - private static void ChangeGeneralPage(int delta) + private static void ChangeSettingsPage(int delta) { - var total = _generalPages.Count + 1; + var total = _settingsPages.Count + 1; if (total <= 1) { return; } - _generalPage = (_generalPage + delta + total) % total; - RefreshGeneralTab(true); + _settingsPage = (_settingsPage + delta + total) % total; + RefreshSettingsTab(true); } /// - /// Rebuilds pages from live option data and redraws the General tab. - /// Pages are rebuilt every time because values, group visibility and the gamemode can change. + /// Updates the Settings tab with the current option values. /// - private static void RefreshGeneralTab(bool scrollToTop) + private static void RefreshSettingsTab(bool scrollToTop) { var guide = MatchInfoGuide.Instance; if (!_uiInitialized || !guide) @@ -1824,67 +1820,67 @@ public static class RoleGuidePatches return; } - _generalPages = GuideOptionPages.BuildPages(guide.GetBoolString); - if (_generalPage > _generalPages.Count) + _settingsPages = GuideOptionPages.BuildPages(guide.GetBoolString); + if (_settingsPage > _settingsPages.Count) { - _generalPage = 0; + _settingsPage = 0; } - var vanillaTab = guide.settingsTabs[GeneralTabIndex]; + var vanillaTab = guide.settingsTabs[SettingsTabIndex]; - // No MiraAPI general options in this match: leave the vanilla tab exactly as it always was. - if (_generalPages.Count == 0) + // Keep the vanilla tab when there are no MiraAPI settings pages. + if (_settingsPages.Count == 0) { - _generalSearching = false; - _generalScroller?.gameObject.SetActive(false); - _generalNoResults?.SetActive(false); + _settingsSearching = false; + _settingsScroller?.gameObject.SetActive(false); + _settingsNoResults?.SetActive(false); vanillaTab.SetActive(true); RestoreSortButtons(); - UpdateGeneralTitle(); + UpdateSettingsTitle(); return; } var searchText = GetSearchText().Trim(); - _generalSearching = searchText.Length > 0; + _settingsSearching = searchText.Length > 0; - if (_generalSearching) + if (_settingsSearching) { - ShowGeneralEntries(GuideOptionPages.Search(_generalPages, searchText), true, scrollToTop); + ShowSettingsEntries(GuideOptionPages.Search(_settingsPages, searchText), true, scrollToTop); vanillaTab.SetActive(false); RepurposeSortButtons(false); } - else if (_generalPage == 0) + else if (_settingsPage == 0) { - _generalScroller?.gameObject.SetActive(false); - _generalNoResults?.SetActive(false); + _settingsScroller?.gameObject.SetActive(false); + _settingsNoResults?.SetActive(false); vanillaTab.SetActive(true); RepurposeSortButtons(true); } else { - ShowGeneralEntries(_generalPages[_generalPage - 1].Entries, false, scrollToTop); + ShowSettingsEntries(_settingsPages[_settingsPage - 1].Entries, false, scrollToTop); vanillaTab.SetActive(false); RepurposeSortButtons(true); } - UpdateGeneralTitle(); + UpdateSettingsTitle(); } - private static void ShowGeneralEntries(IReadOnlyList entries, bool showGroup, bool scrollToTop) + private static void ShowSettingsEntries(IReadOnlyList entries, bool showGroup, bool scrollToTop) { var guide = MatchInfoGuide.Instance; - if (_generalScroller == null || !guide || !guide.MatchInfoRolePanelPrefab) + if (_settingsScroller == null || !guide || !guide.MatchInfoRolePanelPrefab) { - // Never leave the tab blank: fall back to the plain vanilla list. - guide?.settingsTabs[GeneralTabIndex].SetActive(true); + // Use the vanilla settings tab if the custom panel is unavailable. + guide?.settingsTabs[SettingsTabIndex].SetActive(true); return; } - if (_generalPageObject) + if (_settingsPageObject) { - _generalPageObject!.SetActive(false); - _generalPageObject.Destroy(); - _generalPageObject = null; + _settingsPageObject!.SetActive(false); + _settingsPageObject.Destroy(); + _settingsPageObject = null; } var rows = new List>(entries.Count); @@ -1894,39 +1890,39 @@ public static class RoleGuidePatches rows.Add(new KeyValuePair(label, entry.Value)); } - _generalScroller.gameObject.SetActive(true); - _generalPageObject = Helpers.CreateAdvancedSettingsPage(guide, _generalScroller, rows); - _generalNoResults?.SetActive(entries.Count == 0); + _settingsScroller.gameObject.SetActive(true); + _settingsPageObject = Helpers.CreateAdvancedSettingsPage(guide, _settingsScroller, rows); + _settingsNoResults?.SetActive(entries.Count == 0); if (scrollToTop) { - _generalScroller.ScrollToTop(); + _settingsScroller.ScrollToTop(); } } - private static string GetGeneralTitle() + private static string GetSettingsTitle() { var baseTitle = MiraLocaleManager.Get("MiraApi.Wiki.SettingsTab"); - if (!_uiInitialized || _generalPages.Count == 0) + if (!_uiInitialized || _settingsPages.Count == 0) { return baseTitle; } - if (_generalSearching) + if (_settingsSearching) { return $"{baseTitle}: {MiraLocaleManager.Get("MiraApi.Wiki.SearchResults", "Search Results")}"; } - var counter = $"({_generalPage + 1}/{_generalPages.Count + 1})"; - return _generalPage == 0 + var counter = $"({_settingsPage + 1}/{_settingsPages.Count + 1})"; + return _settingsPage == 0 ? $"{baseTitle} {counter}" - : $"{baseTitle}: {_generalPages[_generalPage - 1].Title} {counter}"; + : $"{baseTitle}: {_settingsPages[_settingsPage - 1].Title} {counter}"; } - private static void UpdateGeneralTitle() + private static void UpdateSettingsTitle() { - if (_titleText && IsGeneralTabActive) + if (_titleText && IsSettingsTabActive) { - _titleText.text = GetGeneralTitle(); + _titleText.text = GetSettingsTitle(); } } diff --git a/MiraAPI/Utilities/GuideOptionsPatches.cs b/MiraAPI/Utilities/GuideOptionsPatches.cs index c84cb17..0e22103 100644 --- a/MiraAPI/Utilities/GuideOptionsPatches.cs +++ b/MiraAPI/Utilities/GuideOptionsPatches.cs @@ -39,7 +39,7 @@ internal static class GuideOptionPages var modAbbreviation = plugin.MiraPlugin.GetAbbreviatedModName(); var groups = plugin.InternalOptionGroups - .Where(g => IsGeneralGroup(g, mode) && SafeGroupVisible(g)) + .Where(g => IsSettingsGroup(g, mode) && SafeGroupVisible(g)) .OrderBy(g => g.GroupPriority) .ThenBy(g => g.GroupName, StringComparer.OrdinalIgnoreCase); @@ -108,7 +108,7 @@ internal static class GuideOptionPages return entry.Value.Contains(query, cmp) ? 3 : int.MaxValue; } - private static bool IsGeneralGroup(AbstractOptionGroup group, AbstractGameMode? mode) + private static bool IsSettingsGroup(AbstractOptionGroup group, AbstractGameMode? mode) { if (group.ParentMenu is not (MenuCategory.Game or MenuCategory.CustomOne or MenuCategory.CustomTwo)) { From a01ff6168c4a39e6ff843c132ec3c98ab686f853 Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:08:44 -0400 Subject: [PATCH 14/29] Override Vanilla View to match others --- MiraAPI/GameModes/GameModeOption.cs | 10 +- .../Patches/Options/GameOptionsMenuPatch.cs | 60 ++++++++++ MiraAPI/Patches/RoleGuidePatches.cs | 104 +++++++++++++----- 3 files changed, 139 insertions(+), 35 deletions(-) diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index c58030a..2d1cc88 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -4,6 +4,7 @@ using System.Linq; using HarmonyLib; using MiraAPI.GameOptions; using MiraAPI.Networking; +using MiraAPI.Patches; using MiraAPI.Patches.GameModes; using MiraAPI.Patches.Options; using MiraAPI.Translation; @@ -128,6 +129,9 @@ public static class GameModeOption MenuState.Instance.QueuedRoleMenuRefresh[roleMenu.Key] = true; } } + + GameOptionsMenuPatch.RefreshForGameMode(); + RoleGuidePatches.RefreshSettingsForGameMode(); } // TODO: could make Values a dict of AbstractGameMode too @@ -143,12 +147,6 @@ public static class GameModeOption Info($"Game mode changed to {option.GetInt()}"); RpcSyncGamemode(PlayerControl.LocalPlayer, option.GetInt()); - if (GameSettingMenu.Instance && CustomGameModeManager.ActiveMode != null) - { - GameOptionsMenuPatch.ToggleGamemodeOptions(CustomGameModeManager.ActiveMode, __instance); - GameSettingMenu.Instance.RoleSettingsButton.gameObject.SetActive(CustomGameModeManager.ActiveMode.ShowNormalRoleSettings); - } - return false; } diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index ecf574c..f403961 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -188,6 +188,66 @@ internal static class GameOptionsMenuPatch instance.scrollBar.SetYBoundsMax(-num - 1.65f); } + internal static void RefreshForGameMode() + { + if (CustomGameModeManager.ActiveMode == null || + !GameSettingMenu.Instance || + !GameSettingMenu.Instance.GameSettingsTab || + !MenuState.Instance || + MenuState.Instance.CurrentModIdx != 0 || + GameModeGroups.Count == 0) + { + return; + } + + ToggleGamemodeOptions(CustomGameModeManager.ActiveMode, GameSettingMenu.Instance.GameSettingsTab); + GameSettingMenu.Instance.RoleSettingsButton.gameObject.SetActive( + CustomGameModeManager.ActiveMode.ShowNormalRoleSettings); + } + + internal static List> GetActiveGameSettingsRows(Func formatBool) + { + var menu = GameSettingMenu.Instance ? GameSettingMenu.Instance.GameSettingsTab : null; + if (!menu || menu.Children == null) + { + return []; + } + + var rows = new List>(); + foreach (var option in menu.Children) + { + if (!option || !option!.gameObject.activeInHierarchy || + option == menu.MapPicker || + (GameModeOption.OptionBehaviour && option == GameModeOption.OptionBehaviour) || + !option.Data) + { + continue; + } + + var title = option switch + { + ToggleOption toggle => toggle.TitleText.text, + NumberOption number => number.TitleText.text, + StringOption str => str.TitleText.text, + PlayerOption player => player.TitleText.text, + _ => TranslationController.Instance.GetString(option.Data.Title), + }; + + var value = option switch + { + ToggleOption toggle => formatBool(toggle.GetBool()), + NumberOption number => number.ValueText.text, + StringOption str => str.ValueText.text, + PlayerOption player => player.ValueText.text, + _ => option.Data.GetValueString(option.GetFloat()), + }; + + rows.Add(new KeyValuePair(title, value)); + } + + return rows; + } + private static void CreateSettings(GameOptionsMenu instance, Transform container) { var num = 0.713f; diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 427dcf9..82c18f4 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -10,6 +10,7 @@ using Innersloth.Assets; using MiraAPI.LocalSettings; using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; +using MiraAPI.Patches.Options; using MiraAPI.Roles; using MiraAPI.Translation; using MiraAPI.Utilities; @@ -1820,26 +1821,19 @@ public static class RoleGuidePatches return; } + var vanillaTab = guide.settingsTabs[SettingsTabIndex]; + vanillaTab.SetActive(false); + var vanillaEntries = GameOptionsMenuPatch.GetActiveGameSettingsRows(guide.GetBoolString); + if (vanillaEntries.Count == 0) + { + vanillaEntries = BuildVanillaSettingsEntries(guide); + } _settingsPages = GuideOptionPages.BuildPages(guide.GetBoolString); if (_settingsPage > _settingsPages.Count) { _settingsPage = 0; } - var vanillaTab = guide.settingsTabs[SettingsTabIndex]; - - // Keep the vanilla tab when there are no MiraAPI settings pages. - if (_settingsPages.Count == 0) - { - _settingsSearching = false; - _settingsScroller?.gameObject.SetActive(false); - _settingsNoResults?.SetActive(false); - vanillaTab.SetActive(true); - RestoreSortButtons(); - UpdateSettingsTitle(); - return; - } - var searchText = GetSearchText().Trim(); _settingsSearching = searchText.Length > 0; @@ -1851,9 +1845,7 @@ public static class RoleGuidePatches } else if (_settingsPage == 0) { - _settingsScroller?.gameObject.SetActive(false); - _settingsNoResults?.SetActive(false); - vanillaTab.SetActive(true); + ShowSettingsRows(vanillaEntries, scrollToTop); RepurposeSortButtons(true); } else @@ -1866,7 +1858,62 @@ public static class RoleGuidePatches UpdateSettingsTitle(); } + internal static void RefreshSettingsForGameMode() + { + if (IsSettingsTabActive) + { + RefreshSettingsTab(false); + } + } + + private static List> BuildVanillaSettingsEntries(MatchInfoGuide guide) + { + foreach (var setting in guide.NormalModeSettings.ToArray()) + { + if (setting) + { + setting.Destroy(); + } + } + + guide.NormalModeSettings.Clear(); + guide.CreateNormalModeSettings(); + + var entries = new List>(guide.NormalModeSettings.Count); + foreach (var setting in guide.NormalModeSettings) + { + if (!setting) + { + continue; + } + + var texts = setting.GetComponentsInChildren(true); + if (texts.Length < 2) + { + Warning($"Could not read vanilla settings row '{setting.name}'."); + continue; + } + + entries.Add(new KeyValuePair(texts[0].text, texts[1].text)); + } + + return entries; + } + private static void ShowSettingsEntries(IReadOnlyList entries, bool showGroup, bool scrollToTop) + { + var rows = new List>(entries.Count); + foreach (var entry in entries) + { + var label = showGroup ? $"{entry.GroupName}\n{entry.Title}" : entry.Title; + rows.Add(new KeyValuePair(label, entry.Value)); + } + + ShowSettingsRows(rows, scrollToTop); + _settingsNoResults?.SetActive(entries.Count == 0); + } + + private static void ShowSettingsRows(IReadOnlyList> rows, bool scrollToTop) { var guide = MatchInfoGuide.Instance; if (_settingsScroller == null || !guide || !guide.MatchInfoRolePanelPrefab) @@ -1883,16 +1930,9 @@ public static class RoleGuidePatches _settingsPageObject = null; } - var rows = new List>(entries.Count); - foreach (var entry in entries) - { - var label = showGroup ? $"{entry.GroupName}\n{entry.Title}" : entry.Title; - rows.Add(new KeyValuePair(label, entry.Value)); - } - _settingsScroller.gameObject.SetActive(true); _settingsPageObject = Helpers.CreateAdvancedSettingsPage(guide, _settingsScroller, rows); - _settingsNoResults?.SetActive(entries.Count == 0); + _settingsNoResults?.SetActive(rows.Count == 0); if (scrollToTop) { _settingsScroller.ScrollToTop(); @@ -1902,20 +1942,26 @@ public static class RoleGuidePatches private static string GetSettingsTitle() { var baseTitle = MiraLocaleManager.Get("MiraApi.Wiki.SettingsTab"); - if (!_uiInitialized || _settingsPages.Count == 0) + if (!_uiInitialized) { return baseTitle; } + if (_settingsPage == 0 || _settingsPages.Count == 0) + { + var vanillaTitle = $"{MiraLocaleManager.Get("MiraApi.Wiki.VanillaSettings", "Vanilla")} Settings"; + return _settingsPages.Count == 0 + ? vanillaTitle + : $"{vanillaTitle} ({_settingsPage + 1}/{_settingsPages.Count + 1})"; + } + if (_settingsSearching) { return $"{baseTitle}: {MiraLocaleManager.Get("MiraApi.Wiki.SearchResults", "Search Results")}"; } var counter = $"({_settingsPage + 1}/{_settingsPages.Count + 1})"; - return _settingsPage == 0 - ? $"{baseTitle} {counter}" - : $"{baseTitle}: {_settingsPages[_settingsPage - 1].Title} {counter}"; + return $"{_settingsPages[_settingsPage - 1].Title} Settings {counter}"; } private static void UpdateSettingsTitle() From 754161d4396da999ef90b361cae3faba839c81db Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:44:45 -0400 Subject: [PATCH 15/29] Implement ShowInMatchInfoGuide --- MiraAPI.Example/Options/ExampleOptions.cs | 1 + MiraAPI.Example/Options/ExampleOptions2.cs | 1 + MiraAPI/GameOptions/AbstractOptionGroup.cs | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/MiraAPI.Example/Options/ExampleOptions.cs b/MiraAPI.Example/Options/ExampleOptions.cs index cba80e6..1effde1 100644 --- a/MiraAPI.Example/Options/ExampleOptions.cs +++ b/MiraAPI.Example/Options/ExampleOptions.cs @@ -10,6 +10,7 @@ public class ExampleOptions : AbstractOptionGroup { public override string GroupName => "options.exampleOptions1"; public override Color GroupColor => Color.green; + public override bool ShowInMatchInfoGuide => true; [ModdedToggleOption("options.exampleOptions1.toggleOpt1")] public bool ToggleOpt { get; set; } = false; diff --git a/MiraAPI.Example/Options/ExampleOptions2.cs b/MiraAPI.Example/Options/ExampleOptions2.cs index 24a061c..58885f5 100644 --- a/MiraAPI.Example/Options/ExampleOptions2.cs +++ b/MiraAPI.Example/Options/ExampleOptions2.cs @@ -11,6 +11,7 @@ public class ExampleOptions2 : AbstractOptionGroup public override uint GroupPriority => 0; // This group will be displayed first. The default value is uint.MaxValue. public override MenuCategory ParentMenu => MenuCategory.CustomTwo; + public override bool ShowInMatchInfoGuide => true; public ModdedPlayerOption PlayerOption2 { get; } = new("options.exampleOptions2.youMustChoseAPlayer", false); public ModdedPlayerOption PlayerOption { get; } = new("options.exampleOptions2.ehWhatever"); diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs index c2977c2..1478602 100644 --- a/MiraAPI/GameOptions/AbstractOptionGroup.cs +++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs @@ -49,6 +49,11 @@ public abstract class AbstractOptionGroup /// public virtual Func GroupVisible => () => true; + /// + /// Gets a value indicating whether this group's options are shown in the Match Info Guide. + /// + public virtual bool ShowInMatchInfoGuide => true; + /// /// Gets the group . This is used to color the group in the options menu. /// From 542bf72ece0b11e4d8330a7e4d444b89e147cc92 Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:45:01 -0400 Subject: [PATCH 16/29] Sort Pages by Priority of menus and groups --- MiraAPI/Utilities/GuideOptionsPatches.cs | 61 ++++++++++++++---------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/MiraAPI/Utilities/GuideOptionsPatches.cs b/MiraAPI/Utilities/GuideOptionsPatches.cs index 0e22103..3ae9f0e 100644 --- a/MiraAPI/Utilities/GuideOptionsPatches.cs +++ b/MiraAPI/Utilities/GuideOptionsPatches.cs @@ -32,45 +32,54 @@ internal static class GuideOptionPages var pages = new List(); var mode = CustomGameModeManager.ActiveMode; - // Same plugin set the in-game options menu lists (skips mods that hide themselves from it). - foreach (var plugin in MiraPluginManager.Instance.PluginsWithOptionsOrGameModes - .OrderBy(p => p.MiraPlugin.GetAbbreviatedModName(), StringComparer.OrdinalIgnoreCase)) + var groups = MiraPluginManager.Instance.PluginsWithOptionsOrGameModes + .SelectMany(plugin => plugin.InternalOptionGroups + .Where(group => group.ShowInMatchInfoGuide && IsSettingsGroup(group, mode) && SafeGroupVisible(group)) + .Select(group => (Group: group, ModAbbreviation: plugin.MiraPlugin.GetAbbreviatedModName()))) + .OrderBy(item => GetMenuOrder(item.Group.ParentMenu)) + .ThenBy(item => item.Group.GroupPriority) + .ThenBy(item => item.Group.GroupName, StringComparer.OrdinalIgnoreCase) + .ThenBy(item => item.ModAbbreviation, StringComparer.OrdinalIgnoreCase); + + foreach (var item in groups) { - var modAbbreviation = plugin.MiraPlugin.GetAbbreviatedModName(); + var groupName = item.Group.GroupName.Translate(); + var entries = new List(); - var groups = plugin.InternalOptionGroups - .Where(g => IsSettingsGroup(g, mode) && SafeGroupVisible(g)) - .OrderBy(g => g.GroupPriority) - .ThenBy(g => g.GroupName, StringComparer.OrdinalIgnoreCase); - - foreach (var group in groups) + foreach (var option in item.Group.Options) { - var groupName = group.GroupName.Translate(); - var entries = new List(); - - foreach (var option in group.Options) + if (!SafeOptionVisible(option)) { - if (!SafeOptionVisible(option)) - { - continue; - } - - var entry = TryCreateEntry(option, groupName, modAbbreviation, formatBool); - if (entry != null) - { - entries.Add(entry); - } + continue; } - if (entries.Count > 0) + var entry = TryCreateEntry(option, groupName, item.ModAbbreviation, formatBool); + if (entry != null) { - pages.Add(new GuideOptionPage(group, modAbbreviation, entries)); + entries.Add(entry); } } + + if (entries.Count > 0) + { + pages.Add(new GuideOptionPage(item.Group, item.ModAbbreviation, entries)); + } } return pages; } + + private static int GetMenuOrder(MenuCategory menu) + { + return menu switch + { + MenuCategory.Game => 0, + MenuCategory.CustomOne => 1, + MenuCategory.CustomTwo => 2, + _ => int.MaxValue, + }; + } + public static List Search(IEnumerable pages, string query) { query = query.Trim(); From 5197b90360e8607621fb1d654300f922686ef983 Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:57:12 -0400 Subject: [PATCH 17/29] layer sorting by extra mods --- MiraAPI/Patches/RoleGuidePatches.cs | 65 ++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 82c18f4..e3a53d2 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -68,6 +68,7 @@ public static class RoleGuidePatches private static float _openedAt; private static bool _uiInitialized; private static MatchInfoGuide? _guide; + private static int _activeGuideTabIndex = -1; private static IEnumerator? _populate; private static bool _loading; private static LoadingRing? _loadingSpinner; @@ -75,7 +76,7 @@ public static class RoleGuidePatches private static SortingMethod _sortMethod = SortingMethod.Alphabetical; private static SortingGroups _sortGrouping = SortingGroups.Ungrouped; - private static bool IsModifiersTabActive => MatchInfoGuide.Instance.activeTabIndex == ModifiersTabIndex; + private static bool IsModifiersTabActive => _activeGuideTabIndex == ModifiersTabIndex; [HarmonyPostfix] [HarmonyPriority(Priority.First)] @@ -163,6 +164,7 @@ public static class RoleGuidePatches RoleEntries.Clear(); ModifierEntries.Clear(); _guide = null; + _activeGuideTabIndex = -1; _uiInitialized = false; _loading = false; _appliedSearch = string.Empty; @@ -670,7 +672,7 @@ public static class RoleGuidePatches GroupSortingTmp = Object.Instantiate( OrderSortingTmp, sortingGroupButton.transform); - OrderSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.GroupingLabel"); + GroupSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.GroupingLabel"); GroupSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0); FilterSortingTmp = Object.Instantiate( @@ -752,6 +754,7 @@ public static class RoleGuidePatches private static void OpenTab(int tabIndex, float delay = 0f) { _wikiScroller?.gameObject.SetActive(false); + _activeGuideTabIndex = tabIndex; MatchInfoGuide.Instance.SetActiveTab(tabIndex); HidePanels(tabIndex == ModifiersTabIndex ? ModifierEntries : RoleEntries); StartPopulate(delay); @@ -838,7 +841,7 @@ public static class RoleGuidePatches if (_loadingSpinner != null) { _loadingSpinner.gameObject.SetActive( - _loading && MatchInfoGuide.Instance.activeTabIndex is RolesTabIndex or ModifiersTabIndex && + _loading && _activeGuideTabIndex is RolesTabIndex or ModifiersTabIndex && !(_wikiScroller && _wikiScroller.gameObject.activeSelf)); } } @@ -920,6 +923,7 @@ public static class RoleGuidePatches { if (entry.Panel) { + SetupPanelButton(entry.Panel!, () => OpenAdvancedWiki(entry)); return; } @@ -938,18 +942,29 @@ public static class RoleGuidePatches if (entry.Modifier != null) { panel.SetModifierPanel(entry.Modifier, entry.Amount, entry.Chance); - SetupPanelButton(panel, () => DisplayAdvancedWiki(MatchInfoGuide.Instance, entry.Modifier)); } else { panel.SetRolePanel(entry.Role!, entry.Amount, entry.Chance); - SetupPanelButton(panel, () => DisplayAdvancedWiki(MatchInfoGuide.Instance, entry.Role!)); } + SetupPanelButton(panel, () => OpenAdvancedWiki(entry)); panel.gameObject.SetActive(false); entry.Panel = panel; } + private static void OpenAdvancedWiki(DetailedPanel entry) + { + if (entry.Modifier != null) + { + DisplayAdvancedWiki(MatchInfoGuide.Instance, entry.Modifier); + } + else if (entry.Role != null) + { + DisplayAdvancedWiki(MatchInfoGuide.Instance, entry.Role); + } + } + private static void PreloadPanel(DetailedPanel entry) { if (!entry.Panel) @@ -1111,7 +1126,7 @@ public static class RoleGuidePatches return false; } - var entries = guide.activeTabIndex switch + var entries = _activeGuideTabIndex switch { RolesTabIndex => RoleEntries, ModifiersTabIndex => ModifierEntries, @@ -1137,11 +1152,11 @@ public static class RoleGuidePatches entry.Panel!.transform.SetAsLastSibling(); } - var scroller = guide.activeTabIndex == ModifiersTabIndex + var scroller = _activeGuideTabIndex == ModifiersTabIndex ? _modifiersScroller : guide.MatchInfoRoleScroller; scroller.SetYBoundsMax(GetGridScrollBounds(shown.Count)); - var noResultsMessage = guide.activeTabIndex == ModifiersTabIndex + var noResultsMessage = _activeGuideTabIndex == ModifiersTabIndex ? _modifierNoResultsMessage : guide.rolesEnabledMessage; SetNoResultsVisible(noResultsMessage, shown.Count == 0); @@ -1371,10 +1386,22 @@ public static class RoleGuidePatches private static void SetupPanelButton(MatchInfoRolePanel panel, Action onClick) { - var collider = panel.roleIcon.gameObject.AddComponent(); + var collider = panel.roleIcon.gameObject.GetComponent(); + if (!collider) + { + collider = panel.roleIcon.gameObject.AddComponent(); + } + collider.size = new Vector2(0.13f, 0.13f); collider.offset = new Vector2(0, 0); - var passiveButton = panel.roleIcon.gameObject.AddComponent(); + collider.enabled = true; + var passiveButton = panel.roleIcon.gameObject.GetComponent(); + if (!passiveButton) + { + passiveButton = panel.roleIcon.gameObject.AddComponent(); + } + + passiveButton.enabled = true; passiveButton.ClickSound = HudManager.Instance.MapButton.ClickSound; passiveButton.OnMouseOver = new UnityEvent(); passiveButton.OnMouseOver.AddListener( @@ -1430,6 +1457,8 @@ public static class RoleGuidePatches [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.SetActiveTab))] private static void SetActiveTab(int tabIndex) { + _activeGuideTabIndex = tabIndex; + if (_titleText && _titleIcon) { switch (tabIndex) @@ -1740,7 +1769,7 @@ public static class RoleGuidePatches private static Vector3 _savedGroupIconScale = Vector3.one; private static bool IsSettingsTabActive => - _uiInitialized && MatchInfoGuide.Instance && MatchInfoGuide.Instance.activeTabIndex == SettingsTabIndex; + _uiInitialized && MatchInfoGuide.Instance && _activeGuideTabIndex == SettingsTabIndex; private static void ResetSettingsState() { @@ -1947,19 +1976,17 @@ public static class RoleGuidePatches return baseTitle; } - if (_settingsPage == 0 || _settingsPages.Count == 0) - { - var vanillaTitle = $"{MiraLocaleManager.Get("MiraApi.Wiki.VanillaSettings", "Vanilla")} Settings"; - return _settingsPages.Count == 0 - ? vanillaTitle - : $"{vanillaTitle} ({_settingsPage + 1}/{_settingsPages.Count + 1})"; - } - if (_settingsSearching) { return $"{baseTitle}: {MiraLocaleManager.Get("MiraApi.Wiki.SearchResults", "Search Results")}"; } + if (_settingsPage == 0 || _settingsPages.Count == 0) + { + var vanillaTitle = $"{MiraLocaleManager.Get("MiraApi.Wiki.VanillaSettings", "Vanilla")} Settings"; + return $"{vanillaTitle} ({_settingsPage + 1}/{_settingsPages.Count + 1})"; + } + var counter = $"({_settingsPage + 1}/{_settingsPages.Count + 1})"; return $"{_settingsPages[_settingsPage - 1].Title} Settings {counter}"; } From 901503f762f0e52151cd2c7482b4a7d844ba304e Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:00:23 -0400 Subject: [PATCH 18/29] fix double dummy roles being spawned on the panel --- MiraAPI/Patches/RoleGuidePatches.cs | 175 +++++++++++++++++++++++----- 1 file changed, 147 insertions(+), 28 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 154fb07..42d4be4 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -7,6 +7,7 @@ using System.Linq; using AmongUs.GameOptions; using HarmonyLib; using Innersloth.Assets; +using MiraAPI.GameModes; using MiraAPI.LocalSettings; using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; @@ -37,6 +38,7 @@ public static class RoleGuidePatches private static readonly List RoleEntries = []; private static readonly List ModifierEntries = []; + private static readonly List UntrackedRolePanelDiagnostics = []; private static GameObject searchComponentContainer; public static PassiveButton SearchIconButton; @@ -70,12 +72,18 @@ public static class RoleGuidePatches private static int _activeGuideTabIndex = -1; private static IEnumerator? _populate; private static bool _loading; + private static Type? _roleEntriesMode; + private static bool _roleEntriesInitialized; + private static bool _rolePanelsCleaned; private static LoadingRing? _loadingSpinner; private static SortingFilter _sortFilter = SortingFilter.EnabledOnly; private static SortingMethod _sortMethod = SortingMethod.Alphabetical; private static SortingGroups _sortGrouping = SortingGroups.Ungrouped; - private static bool IsModifiersTabActive => _activeGuideTabIndex == ModifiersTabIndex; + private static int ActiveGuideTabIndex => + _guide && MatchInfoGuide.Instance == _guide ? MatchInfoGuide.Instance.activeTabIndex : _activeGuideTabIndex; + + private static bool IsModifiersTabActive => ActiveGuideTabIndex == ModifiersTabIndex; [HarmonyPostfix] [HarmonyPriority(Priority.First)] @@ -162,6 +170,10 @@ public static class RoleGuidePatches RoleEntries.Clear(); ModifierEntries.Clear(); + UntrackedRolePanelDiagnostics.Clear(); + _roleEntriesMode = null; + _roleEntriesInitialized = false; + _rolePanelsCleaned = false; _guide = null; _activeGuideTabIndex = -1; _uiInitialized = false; @@ -223,11 +235,7 @@ public static class RoleGuidePatches } _guide = instance; - if (RoleEntries.Count == 0) - { - BuildEntries(); - } - + BuildEntries(); if (_populate != null) { yield break; @@ -690,19 +698,42 @@ public static class RoleGuidePatches private static void BuildEntries() { - foreach (var modifier in ModifierManager.Modifiers.OfType()) + if (ModifierEntries.Count == 0) { - ModifierEntries.Add(new DetailedPanel( - modifier.ModifierName, - modifier.ModifierCategoryTitle, - modifier.ParentMod.MiraPlugin.GetAbbreviatedModName(), - null, - modifier)); + foreach (var modifier in ModifierManager.Modifiers.OfType()) + { + ModifierEntries.Add(new DetailedPanel( + modifier.ModifierName, + modifier.ModifierCategoryTitle, + modifier.ParentMod.MiraPlugin.GetAbbreviatedModName(), + null, + modifier)); + } } + var activeMode = CustomGameModeManager.ActiveMode; + var activeModeType = activeMode?.GetType(); + if (_roleEntriesInitialized && _roleEntriesMode == activeModeType) + { + return; + } + + foreach (var entry in RoleEntries) + { + if (entry.Panel) + { + entry.Panel!.gameObject.Destroy(); + } + } + + RoleEntries.Clear(); + _rolePanelsCleaned = false; foreach (var roleBehaviour in CustomRoleManager.AllStoredRoleBehaviours.Where(x => x.Role is not RoleTypes.Crewmate and not RoleTypes.Impostor and - not RoleTypes.CrewmateGhost and not RoleTypes.ImpostorGhost)) + not RoleTypes.CrewmateGhost and not RoleTypes.ImpostorGhost && + CustomRoleUtils.CanSpawnOnCurrentMode(x)) + .GroupBy(x => x.Role) + .Select(group => group.First())) { var modId = roleBehaviour is ICustomRole custom ? custom.ParentMod.MiraPlugin.GetAbbreviatedModName() @@ -714,6 +745,9 @@ public static class RoleGuidePatches roleBehaviour, null)); } + + _roleEntriesMode = activeModeType; + _roleEntriesInitialized = true; } private static void SetupTabButton(MatchInfoGuideTabButton button, Sprite inactive, Sprite selected, Sprite hover) @@ -756,10 +790,54 @@ public static class RoleGuidePatches _wikiScroller?.gameObject.SetActive(false); _activeGuideTabIndex = tabIndex; MatchInfoGuide.Instance.SetActiveTab(tabIndex); + if (tabIndex == RolesTabIndex) + { + BuildEntries(); + if (!_rolePanelsCleaned) + { + _rolePanelsCleaned = RemoveUntrackedRolePanels(); + } + } + HidePanels(tabIndex == ModifiersTabIndex ? ModifierEntries : RoleEntries); StartPopulate(delay); } + private static bool RemoveUntrackedRolePanels() + { + if (_rolesScroller?.Inner == null) + { + return false; + } + + var trackedPanels = RoleEntries + .Where(entry => entry.Panel) + .Select(entry => entry.Panel!) + .ToHashSet(); + foreach (var panel in _rolesScroller.Inner.GetComponentsInChildren(true)) + { + if (!trackedPanels.Contains(panel)) + { + var panelName = panel.roleName ? panel.roleName.text : panel.name; + UntrackedRolePanelDiagnostics.Add(panelName); + panel.gameObject.Destroy(); + } + } + + if (UntrackedRolePanelDiagnostics.Count > 0) + { + var panelNames = string.Join(", ", UntrackedRolePanelDiagnostics.Take(10)); + var additionalCount = UntrackedRolePanelDiagnostics.Count - 10; + Logger.Warning( + $"Removed or blocked {UntrackedRolePanelDiagnostics.Count} untracked Match Info role panel(s) in " + + $"{CustomGameModeManager.ActiveMode?.GetType().Name ?? "vanilla"} mode: {panelNames}" + + (additionalCount > 0 ? $", and {additionalCount} more" : string.Empty)); + UntrackedRolePanelDiagnostics.Clear(); + } + + return true; + } + private static void HidePanels(List entries) { foreach (var entry in entries) @@ -847,7 +925,7 @@ public static class RoleGuidePatches if (_loadingSpinner != null) { _loadingSpinner.gameObject.SetActive( - _loading && _activeGuideTabIndex is RolesTabIndex or ModifiersTabIndex && + _loading && ActiveGuideTabIndex is RolesTabIndex or ModifiersTabIndex && !(_wikiScroller && _wikiScroller.gameObject.activeSelf)); } } @@ -892,8 +970,16 @@ public static class RoleGuidePatches }); _loading = false; UpdateSpinner(); - yield return CoForEachBudgeted(entries, EnsurePanel); - yield return CoForEachBudgeted(modifiers ? RoleEntries : ModifierEntries, PreloadPanel); + if (!modifiers) + { + RemoveUntrackedRolePanels(); + yield return null; + if (_guide) + { + RemoveUntrackedRolePanels(); + } + } + _populate = null; } @@ -1125,7 +1211,8 @@ public static class RoleGuidePatches return false; } - var entries = _activeGuideTabIndex switch + var activeTabIndex = ActiveGuideTabIndex; + var entries = activeTabIndex switch { RolesTabIndex => RoleEntries, ModifiersTabIndex => ModifierEntries, @@ -1151,11 +1238,11 @@ public static class RoleGuidePatches entry.Panel!.transform.SetAsLastSibling(); } - var scroller = _activeGuideTabIndex == ModifiersTabIndex + var scroller = activeTabIndex == ModifiersTabIndex ? _modifiersScroller : guide.MatchInfoRoleScroller; scroller.SetYBoundsMax(GetGridScrollBounds(shown.Count)); - var noResultsMessage = _activeGuideTabIndex == ModifiersTabIndex + var noResultsMessage = activeTabIndex == ModifiersTabIndex ? _modifierNoResultsMessage : guide.rolesEnabledMessage; SetNoResultsVisible(noResultsMessage, shown.Count == 0); @@ -1634,6 +1721,17 @@ public static class RoleGuidePatches [HarmonyPatch(typeof(MatchInfoRolePanel), nameof(MatchInfoRolePanel.SetPanel))] public static bool SetPanel(MatchInfoRolePanel __instance, RoleBehaviour role, int numPerGame, int chancePerGame) { + var rolesInner = _rolesScroller ? _rolesScroller.Inner : null; + if (rolesInner && + __instance.transform.IsChildOf(rolesInner) && + !RoleEntries.Any(entry => entry.Panel == __instance)) + { + UntrackedRolePanelDiagnostics.Add( + role is ICustomRole untrackedRole ? untrackedRole.RoleName : role.NiceName); + __instance.gameObject.Destroy(); + return false; + } + __instance.roleCount.text = string.Format( CultureInfo.InvariantCulture, "{0} at {1}%", @@ -1754,9 +1852,11 @@ public static class RoleGuidePatches private static string _savedGroupLabel = string.Empty; private static Vector3 _savedFilterIconScale = Vector3.one; private static Vector3 _savedGroupIconScale = Vector3.one; + private static Vector3 _savedFilterLabelPosition = Vector3.zero; + private static Vector3 _savedGroupLabelPosition = Vector3.zero; private static bool IsSettingsTabActive => - _uiInitialized && MatchInfoGuide.Instance && _activeGuideTabIndex == SettingsTabIndex; + _uiInitialized && MatchInfoGuide.Instance && ActiveGuideTabIndex == SettingsTabIndex; private static void ResetSettingsState() { @@ -1918,18 +2018,25 @@ public static class RoleGuidePatches private static void ShowSettingsEntries(IReadOnlyList entries, bool showGroup, bool scrollToTop) { - var rows = new List>(entries.Count); - foreach (var entry in entries) - { - var label = showGroup ? $"{entry.GroupName}\n{entry.Title}" : entry.Title; - rows.Add(new KeyValuePair(label, entry.Value)); - } - + var orderedEntries = showGroup + ? entries.GroupBy(entry => entry.Group).SelectMany(group => group) + : entries; + var rows = orderedEntries + .Select(entry => new AdvancedSettingsRow(entry.Title, entry.Value, showGroup ? entry.Group : null)) + .ToList(); ShowSettingsRows(rows, scrollToTop); _settingsNoResults?.SetActive(entries.Count == 0); } private static void ShowSettingsRows(IReadOnlyList> rows, bool scrollToTop) + { + var advancedRows = rows + .Select(row => new AdvancedSettingsRow(row.Key, row.Value, null)) + .ToList(); + ShowSettingsRows(advancedRows, scrollToTop); + } + + private static void ShowSettingsRows(List rows, bool scrollToTop) { var guide = MatchInfoGuide.Instance; if (_settingsScroller == null || !guide || !guide.MatchInfoRolePanelPrefab) @@ -2004,9 +2111,19 @@ public static class RoleGuidePatches _savedGroupLabel = GroupSortingTmp.text; _savedFilterIconScale = _searchSortingFilterIdle.transform.localScale; _savedGroupIconScale = _searchSortingGroupIdle.transform.localScale; + _savedFilterLabelPosition = FilterSortingTmp.transform.localPosition; + _savedGroupLabelPosition = GroupSortingTmp.transform.localPosition; SetPaginationSprites(_searchSortingFilterIdle, _searchSortingFilterHover, true); SetPaginationSprites(_searchSortingGroupIdle, _searchSortingGroupHover, false); + FilterSortingTmp.transform.localPosition = new Vector3( + -0.15f, + _savedFilterLabelPosition.y, + _savedFilterLabelPosition.z); + GroupSortingTmp.transform.localPosition = new Vector3( + 0.15f, + _savedGroupLabelPosition.y, + _savedGroupLabelPosition.z); FilterSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.PrevPage", "Prev"); GroupSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.NextPage", "Next"); _sortButtonsRepurposed = true; @@ -2058,6 +2175,8 @@ public static class RoleGuidePatches _searchSortingGroupHover.transform.localScale = _savedGroupIconScale; FilterSortingTmp.text = _savedFilterLabel; GroupSortingTmp.text = _savedGroupLabel; + FilterSortingTmp.transform.localPosition = _savedFilterLabelPosition; + GroupSortingTmp.transform.localPosition = _savedGroupLabelPosition; SearchSortingOrderButton.gameObject.SetActive(true); SearchSortingFilterButton.gameObject.SetActive(true); SearchSortingGroupButton.gameObject.SetActive(true); From 7b77d0d102ac6c9d69ccc6869def8357355718d9 Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:08:28 -0400 Subject: [PATCH 19/29] use Option Headers as Dividers to Search Options Results --- MiraAPI/Utilities/GuideOptionsPatches.cs | 22 ++-- MiraAPI/Utilities/Helpers.cs | 137 ++++++++++++++++++++++- 2 files changed, 148 insertions(+), 11 deletions(-) diff --git a/MiraAPI/Utilities/GuideOptionsPatches.cs b/MiraAPI/Utilities/GuideOptionsPatches.cs index 3ae9f0e..a26699f 100644 --- a/MiraAPI/Utilities/GuideOptionsPatches.cs +++ b/MiraAPI/Utilities/GuideOptionsPatches.cs @@ -11,7 +11,12 @@ using Reactor.Utilities; namespace MiraAPI.Utilities; -internal sealed record GuideOptionEntry(string Title, string Value, string GroupName, string ModAbbreviation); +internal sealed record GuideOptionEntry( + string Title, + string Value, + AbstractOptionGroup Group, + string GroupName, + string ModAbbreviation); internal sealed class GuideOptionPage(AbstractOptionGroup group, string modAbbreviation, IReadOnlyList entries) { @@ -53,7 +58,7 @@ internal static class GuideOptionPages continue; } - var entry = TryCreateEntry(option, groupName, item.ModAbbreviation, formatBool); + var entry = TryCreateEntry(option, item.Group, groupName, item.ModAbbreviation, formatBool); if (entry != null) { entries.Add(entry); @@ -90,11 +95,11 @@ internal static class GuideOptionPages const StringComparison cmp = StringComparison.OrdinalIgnoreCase; return pages - .SelectMany(p => p.Entries) - .Select(e => (Entry: e, Rank: Rank(e, query, cmp))) - .Where(x => x.Rank < int.MaxValue) - .OrderBy(x => x.Rank) - .Select(x => x.Entry) + .SelectMany(page => page.Entries + .Select(entry => (Entry: entry, Rank: Rank(entry, query, cmp))) + .Where(result => result.Rank < int.MaxValue) + .OrderBy(result => result.Rank) + .Select(result => result.Entry)) .ToList(); } @@ -161,6 +166,7 @@ internal static class GuideOptionPages private static GuideOptionEntry? TryCreateEntry( IModdedOption option, + AbstractOptionGroup group, string groupName, string modAbbreviation, Func formatBool) @@ -169,7 +175,7 @@ internal static class GuideOptionPages { var title = TranslationController.Instance.GetString(option.StringName); var value = FormatValue(option, formatBool); - return new GuideOptionEntry(title, value, groupName, modAbbreviation); + return new GuideOptionEntry(title, value, group, groupName, modAbbreviation); } catch (Exception e) { diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 618dca3..3ce4291 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -26,6 +26,8 @@ using Random = UnityEngine.Random; namespace MiraAPI.Utilities; +internal readonly record struct AdvancedSettingsRow(string Title, string Value, AbstractOptionGroup? Group); + /// /// A class that contains helper methods. /// @@ -920,6 +922,17 @@ public static class Helpers MatchInfoGuide guide, Scroller parent, IReadOnlyList> settings) + { + return CreateAdvancedSettingsPage( + guide, + parent, + settings.Select(setting => new AdvancedSettingsRow(setting.Key, setting.Value, null)).ToList()); + } + + internal static GameObject CreateAdvancedSettingsPage( + MatchInfoGuide guide, + Scroller parent, + List settings) { const float contentTop = 0.95f; const float horizontalOffset = -0.2f; @@ -976,15 +989,127 @@ public static class Helpers var rowHeight = prefabBounds.size.y * rowScale; var rowPitch = 0.45f * rowScale; var contentBottom = float.PositiveInfinity; + var previousGroup = (AbstractOptionGroup?)null; + var rowIndex = 0; + var column = 0; + var groupSpacing = 0f; for (var i = 0; i < settings.Count; i++) { - var column = i % columns; - var rowIndex = i / columns; + if (settings[i].Group != previousGroup && settings[i].Group is { } group) + { + if (previousGroup != null) + { + groupSpacing += 0.2f; + } + + if (column != 0) + { + rowIndex++; + column = 0; + } + + var headerSource = group.Header; + if (!headerSource && gameSettingsMenu && gameSettingsMenu.GameSettingsTab) + { + headerSource = gameSettingsMenu.GameSettingsTab.categoryHeaderOrigin; + } + + if (!headerSource) + { + foreach (var candidate in Resources.FindObjectsOfTypeAll(Il2CppType.Of())) + { + var candidateHeader = candidate.TryCast(); + if (candidateHeader && candidateHeader!.Background && candidateHeader.Title) + { + headerSource = candidateHeader; + break; + } + } + } + + if (headerSource) + { + const float headerScale = 0.5f; + var headerWidth = (columns * columnWidth + (columns - 1) * columnGap) / headerScale; + var header = Object.Instantiate(headerSource!, page.transform, false); + header.transform.localScale = Vector3.one * headerScale; + header.transform.localPosition = new Vector3( + horizontalOffset - 0.4f, + contentTop - (rowIndex * rowPitch) - groupSpacing, + -1f); + header.SetHeader(MiraLocaleManager.GetOrCreateLocaleString(group.GroupName), 20); + if (header.Title) + { + var titleTranslator = header.Title.GetComponent(); + if (titleTranslator) + { + Object.Destroy(titleTranslator); + } + + header.Title.text = group.GroupName.Translate(); + header.Title.color = Color.white; + header.Title.horizontalAlignment = HorizontalAlignmentOptions.Left; + var titleTransform = header.Title.rectTransform; + var titlePosition = titleTransform.localPosition; + titlePosition.x = 0.5f; + titleTransform.localPosition = titlePosition; + titleTransform.SetSizeWithCurrentAnchors( + RectTransform.Axis.Horizontal, + headerWidth - 0.3f); + } + + if (header.Background) + { + header.Background.sprite = MiraAssets.CategoryHeader.LoadAsset(); + header.Background.drawMode = SpriteDrawMode.Sliced; + header.Background.color = MiraApiPlugin.DefaultHeaderColor; + header.Background.transform.localPosition = new Vector3(0.5f, -0.1833f, 0f); + header.Background.size = new Vector2(headerWidth, header.Background.size.y); + } + + if (header.Divider) + { + header.Divider.color = MiraApiPlugin.DefaultHeaderColor; + } + + header.gameObject.SetActive(true); + foreach (var renderer in header.GetComponentsInChildren(true)) + { + renderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + renderer.material.SetInt(PlayerMaterial.MaskLayer, 50); + if (renderer.enabled && renderer.sprite) + { + contentBottom = Mathf.Min( + contentBottom, + inner.InverseTransformPoint(renderer.bounds.min).y); + } + } + + foreach (var text in header.GetComponentsInChildren(true)) + { + text.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); + text.fontMaterial.SetFloat(ShaderID.Stencil, 50f); + if (text.enabled) + { + text.ForceMeshUpdate(); + contentBottom = Mathf.Min( + contentBottom, + inner.InverseTransformPoint(text.bounds.min).y); + } + } + + rowIndex++; + } + + previousGroup = group; + } + var originX = -((columns * columnWidth) + ((columns - 1) * columnGap)) / 2f + (column * (columnWidth + columnGap)) + horizontalOffset; var centerY = contentTop - (rowHeight / 2f) - (rowIndex * rowPitch); + centerY -= groupSpacing; var row = Object.Instantiate(rowPrefab, page.transform); row.enabled = false; row.PlusBtn.gameObject.SetActive(false); @@ -1005,7 +1130,7 @@ public static class Helpers originX + (columnWidth / 2f) - (prefabBounds.center.x * rowScale), centerY - (prefabBounds.center.y * rowScale), 0f); - row.TitleText.text = settings[i].Key; + row.TitleText.text = settings[i].Title; row.ValueText.text = settings[i].Value; foreach (var text in row.GetComponentsInChildren(true)) @@ -1048,6 +1173,12 @@ public static class Helpers } row.gameObject.SetActive(true); + column++; + if (column == columns) + { + column = 0; + rowIndex++; + } } var maskArea = parent.transform.FindChild("MaskArea")?.GetComponent(); From 06ca48c0880577088a686cee44daff220ae1ba8d Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:08:59 -0400 Subject: [PATCH 20/29] Fix No Results Found text overlaying other UI elements. and add No Results found message to Settings tab --- MiraAPI/Patches/RoleGuidePatches.cs | 32 ++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 42d4be4..261e119 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -871,9 +871,9 @@ public static class RoleGuidePatches text.text = MiraLocaleManager.Get("MiraApi.Wiki.NoResultsFound", "No Results Found"); } - private static void SetNoResultsVisible(GameObject message, bool visible) + private static void SetNoResultsVisible(GameObject? message, bool visible) { - if (message) + if (message != null && message) { message.SetActive(visible); } @@ -1532,6 +1532,7 @@ public static class RoleGuidePatches private static void SetActiveTab(int tabIndex) { _activeGuideTabIndex = tabIndex; + HideNoResultsMessages(); if (_titleText && _titleIcon) { @@ -1576,6 +1577,18 @@ public static class RoleGuidePatches UpdateSpinner(); } + private static void HideNoResultsMessages() + { + var guide = _guide; + if (guide != null && guide) + { + SetNoResultsVisible(guide.rolesEnabledMessage, false); + } + + SetNoResultsVisible(_modifierNoResultsMessage, false); + SetNoResultsVisible(_settingsNoResults, false); + } + public static Sprite GetRandomModifierIcon() { var activeMods = PlayerControl.LocalPlayer.GetModifiers() @@ -2025,7 +2038,7 @@ public static class RoleGuidePatches .Select(entry => new AdvancedSettingsRow(entry.Title, entry.Value, showGroup ? entry.Group : null)) .ToList(); ShowSettingsRows(rows, scrollToTop); - _settingsNoResults?.SetActive(entries.Count == 0); + _settingsNoResults?.SetActive(_settingsSearching && entries.Count == 0); } private static void ShowSettingsRows(IReadOnlyList> rows, bool scrollToTop) @@ -2055,7 +2068,7 @@ public static class RoleGuidePatches _settingsScroller.gameObject.SetActive(true); _settingsPageObject = Helpers.CreateAdvancedSettingsPage(guide, _settingsScroller, rows); - _settingsNoResults?.SetActive(rows.Count == 0); + _settingsNoResults?.SetActive(false); if (scrollToTop) { _settingsScroller.ScrollToTop(); @@ -2082,7 +2095,12 @@ public static class RoleGuidePatches } var counter = $"({_settingsPage + 1}/{_settingsPages.Count + 1})"; - return $"{_settingsPages[_settingsPage - 1].Title} Settings {counter}"; + var pageTitle = _settingsPages[_settingsPage - 1].Title; + var titleAlreadyDescribesSettings = pageTitle.EndsWith("settings", StringComparison.OrdinalIgnoreCase) || + pageTitle.EndsWith("options", StringComparison.OrdinalIgnoreCase); + return titleAlreadyDescribesSettings + ? $"{pageTitle} {counter}" + : $"{pageTitle} Settings {counter}"; } private static void UpdateSettingsTitle() @@ -2117,11 +2135,11 @@ public static class RoleGuidePatches SetPaginationSprites(_searchSortingFilterIdle, _searchSortingFilterHover, true); SetPaginationSprites(_searchSortingGroupIdle, _searchSortingGroupHover, false); FilterSortingTmp.transform.localPosition = new Vector3( - -0.15f, + -1.05f, _savedFilterLabelPosition.y, _savedFilterLabelPosition.z); GroupSortingTmp.transform.localPosition = new Vector3( - 0.15f, + 0.95f, _savedGroupLabelPosition.y, _savedGroupLabelPosition.z); FilterSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.PrevPage", "Prev"); From 1db6a80768f8e97cd5131f629914a3d9f70a3009 Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:18:55 -0400 Subject: [PATCH 21/29] add the no results found text again --- MiraAPI/Patches/RoleGuidePatches.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 261e119..459808f 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -470,7 +470,7 @@ public static class RoleGuidePatches modifiersTab.name = "ModifiersPanel"; _modifierNoResultsMessage = Object.Instantiate( instance.rolesEnabledMessage, - instance.rolesEnabledMessage.transform.parent, + modifiersTab.transform, true); _modifierNoResultsMessage.name = "NoResultsFound"; _modifierNoResultsMessage.SetActive(false); @@ -1895,7 +1895,7 @@ public static class RoleGuidePatches _settingsNoResults = Object.Instantiate( instance.rolesEnabledMessage, - instance.rolesEnabledMessage.transform.parent, + settingsTab.transform, true); _settingsNoResults.name = "SettingsNoResultsFound"; _settingsNoResults.SetActive(false); From cac15bf07fce8c3eb1f19edd106cc3faed9936b8 Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:25:20 -0400 Subject: [PATCH 22/29] adjust positons of tab labels --- MiraAPI/Patches/RoleGuidePatches.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 459808f..44f9d46 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -2135,12 +2135,12 @@ public static class RoleGuidePatches SetPaginationSprites(_searchSortingFilterIdle, _searchSortingFilterHover, true); SetPaginationSprites(_searchSortingGroupIdle, _searchSortingGroupHover, false); FilterSortingTmp.transform.localPosition = new Vector3( - -1.05f, - _savedFilterLabelPosition.y, + -1.25f, + 0f, _savedFilterLabelPosition.z); GroupSortingTmp.transform.localPosition = new Vector3( - 0.95f, - _savedGroupLabelPosition.y, + 0f, + 0f, _savedGroupLabelPosition.z); FilterSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.PrevPage", "Prev"); GroupSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.NextPage", "Next"); From c1978bbb883e3e940b3b768f71c5bc7ed0dbd0f2 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:09:08 -0500 Subject: [PATCH 23/29] Add sprites for settings in wiki --- MiraAPI/GameOptions/AbstractOptionGroup.cs | 15 ++++++++++++++- MiraAPI/Patches/RoleGuidePatches.cs | 18 +++++++++++++++++- MiraAPI/Utilities/GuideOptionsPatches.cs | 6 +++++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs index 1478602..b62b803 100644 --- a/MiraAPI/GameOptions/AbstractOptionGroup.cs +++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Il2CppInterop.Runtime.Attributes; using MiraAPI.Modifiers; using MiraAPI.Roles; +using MiraAPI.Utilities.Assets; using UnityEngine; namespace MiraAPI.GameOptions; @@ -26,6 +28,12 @@ public abstract class AbstractOptionGroup /// public abstract string GroupName { get; } + /// + /// Gets the sprite of the group that is displayed in the match info guide. + /// + [HideFromIl2Cpp] + public virtual LoadableAsset Sprite => MiraAssets.IconSettings; + /// /// Gets the Optionable type of the group. /// @@ -52,7 +60,12 @@ public abstract class AbstractOptionGroup /// /// Gets a value indicating whether this group's options are shown in the Match Info Guide. /// - public virtual bool ShowInMatchInfoGuide => true; + public virtual bool ShowInMatchInfoGuide => ParentMenu is MenuCategory.Game; + + /// + /// Gets the name title of the group when it is in the match info guide. + /// + public virtual string RoleGuideTitle => GroupName; /// /// Gets the group . This is used to color the group in the options menu. diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 44f9d46..395a16d 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -1544,7 +1544,7 @@ public static class RoleGuidePatches break; case SettingsTabIndex: _titleText.text = GetSettingsTitle(); - _titleIcon.sprite = MiraAssets.IconSettings.LoadAsset(); + _titleIcon.sprite = GetSettingsIcon(); break; case RolesTabIndex: _titleText.text = MiraLocaleManager.Get("Roles"); @@ -2103,11 +2103,27 @@ public static class RoleGuidePatches : $"{pageTitle} Settings {counter}"; } + private static Sprite GetSettingsIcon() + { + if (!_uiInitialized || _settingsSearching || _settingsPage == 0 || _settingsPages.Count == 0) + { + return MiraAssets.IconSettings; + } + + return _settingsPages[_settingsPage - 1].Sprite; + } + private static void UpdateSettingsTitle() { if (_titleText && IsSettingsTabActive) { _titleText.text = GetSettingsTitle(); + if (_titleIcon) + { + _titleIcon.sprite = GetSettingsIcon(); + + _titleIcon.SetSizeLimit(0.55f); + } } } diff --git a/MiraAPI/Utilities/GuideOptionsPatches.cs b/MiraAPI/Utilities/GuideOptionsPatches.cs index a26699f..63f5617 100644 --- a/MiraAPI/Utilities/GuideOptionsPatches.cs +++ b/MiraAPI/Utilities/GuideOptionsPatches.cs @@ -7,7 +7,9 @@ using MiraAPI.GameOptions; using MiraAPI.GameOptions.OptionTypes; using MiraAPI.PluginLoading; using MiraAPI.Translation; +using MiraAPI.Utilities.Assets; using Reactor.Utilities; +using UnityEngine; namespace MiraAPI.Utilities; @@ -22,7 +24,9 @@ internal sealed class GuideOptionPage(AbstractOptionGroup group, string modAbbre { public AbstractOptionGroup Group { get; } = group; - public string Title { get; } = group.GroupName.Translate(); + public string Title { get; } = group.RoleGuideTitle; + + public LoadableAsset Sprite { get; } = group.Sprite; public string ModAbbreviation { get; } = modAbbreviation; From a7585ee2fe54e68117d8a1010c19479bdd183f62 Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:37:00 -0400 Subject: [PATCH 24/29] add setting searching by mod name --- MiraAPI/Utilities/GuideOptionsPatches.cs | 80 +++++++++++++++++++++--- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/MiraAPI/Utilities/GuideOptionsPatches.cs b/MiraAPI/Utilities/GuideOptionsPatches.cs index 63f5617..dceebc3 100644 --- a/MiraAPI/Utilities/GuideOptionsPatches.cs +++ b/MiraAPI/Utilities/GuideOptionsPatches.cs @@ -18,9 +18,14 @@ internal sealed record GuideOptionEntry( string Value, AbstractOptionGroup Group, string GroupName, - string ModAbbreviation); + string ModAbbreviation, + IReadOnlyList ModSearchTerms); -internal sealed class GuideOptionPage(AbstractOptionGroup group, string modAbbreviation, IReadOnlyList entries) +internal sealed class GuideOptionPage( + AbstractOptionGroup group, + string modAbbreviation, + IReadOnlyList modSearchTerms, + IReadOnlyList entries) { public AbstractOptionGroup Group { get; } = group; @@ -30,6 +35,8 @@ internal sealed class GuideOptionPage(AbstractOptionGroup group, string modAbbre public string ModAbbreviation { get; } = modAbbreviation; + public IReadOnlyList ModSearchTerms { get; } = modSearchTerms; + public IReadOnlyList Entries { get; } = entries; } @@ -44,7 +51,10 @@ internal static class GuideOptionPages var groups = MiraPluginManager.Instance.PluginsWithOptionsOrGameModes .SelectMany(plugin => plugin.InternalOptionGroups .Where(group => group.ShowInMatchInfoGuide && IsSettingsGroup(group, mode) && SafeGroupVisible(group)) - .Select(group => (Group: group, ModAbbreviation: plugin.MiraPlugin.GetAbbreviatedModName()))) + .Select(group => ( + Plugin: plugin, + Group: group, + ModAbbreviation: plugin.MiraPlugin.GetAbbreviatedModName()))) .OrderBy(item => GetMenuOrder(item.Group.ParentMenu)) .ThenBy(item => item.Group.GroupPriority) .ThenBy(item => item.Group.GroupName, StringComparer.OrdinalIgnoreCase) @@ -53,6 +63,7 @@ internal static class GuideOptionPages foreach (var item in groups) { var groupName = item.Group.GroupName.Translate(); + var modSearchTerms = GetModSearchTerms(item.Plugin, item.Group); var entries = new List(); foreach (var option in item.Group.Options) @@ -62,7 +73,13 @@ internal static class GuideOptionPages continue; } - var entry = TryCreateEntry(option, item.Group, groupName, item.ModAbbreviation, formatBool); + var entry = TryCreateEntry( + option, + item.Group, + groupName, + item.ModAbbreviation, + modSearchTerms, + formatBool); if (entry != null) { entries.Add(entry); @@ -71,7 +88,7 @@ internal static class GuideOptionPages if (entries.Count > 0) { - pages.Add(new GuideOptionPage(item.Group, item.ModAbbreviation, entries)); + pages.Add(new GuideOptionPage(item.Group, item.ModAbbreviation, modSearchTerms, entries)); } } @@ -98,7 +115,21 @@ internal static class GuideOptionPages } const StringComparison cmp = StringComparison.OrdinalIgnoreCase; - return pages + var pageList = pages.ToList(); + var normalizedQuery = NormalizeSearchText(query); + var matchingModPages = normalizedQuery.Length < 3 + ? Array.Empty() + : pageList + .Where(page => page.ModSearchTerms.Any(term => + NormalizeSearchText(term).Contains(normalizedQuery, StringComparison.Ordinal))) + .ToArray(); + + if (matchingModPages.Length > 0) + { + return matchingModPages.SelectMany(page => page.Entries).ToList(); + } + + return pageList .SelectMany(page => page.Entries .Select(entry => (Entry: entry, Rank: Rank(entry, query, cmp))) .Where(result => result.Rank < int.MaxValue) @@ -107,6 +138,40 @@ internal static class GuideOptionPages .ToList(); } + private static IReadOnlyList GetModSearchTerms(MiraPluginInfo plugin, AbstractOptionGroup group) + { + var terms = new HashSet(StringComparer.OrdinalIgnoreCase) + { + plugin.PluginId, + plugin.PluginInfo.Metadata.Name, + plugin.MiraPlugin.OptionsTitleText, + plugin.MiraPlugin.ParentLocaleId, + plugin.MiraPlugin.GetAbbreviatedModName(), + }; + + AddNamespace(terms, plugin.MiraPlugin.GetType()); + AddNamespace(terms, group.GetType()); + foreach (var option in group.Options) + { + AddNamespace(terms, option.GetType()); + } + + return terms.Where(term => !string.IsNullOrWhiteSpace(term)).ToArray(); + } + + private static void AddNamespace(ISet terms, Type type) + { + if (!string.IsNullOrWhiteSpace(type.Namespace)) + { + terms.Add(type.Namespace); + } + } + + private static string NormalizeSearchText(string value) + { + return new string(value.Where(char.IsLetterOrDigit).Select(char.ToLowerInvariant).ToArray()); + } + private static int Rank(GuideOptionEntry entry, string query, StringComparison cmp) { if (entry.Title.Equals(query, cmp)) @@ -173,13 +238,14 @@ internal static class GuideOptionPages AbstractOptionGroup group, string groupName, string modAbbreviation, + IReadOnlyList modSearchTerms, Func formatBool) { try { var title = TranslationController.Instance.GetString(option.StringName); var value = FormatValue(option, formatBool); - return new GuideOptionEntry(title, value, group, groupName, modAbbreviation); + return new GuideOptionEntry(title, value, group, groupName, modAbbreviation, modSearchTerms); } catch (Exception e) { From 5e9967606dbbdf5088dc43d0d1e76da05f0807cd Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:37:11 -0400 Subject: [PATCH 25/29] enlarge setting content by base --- MiraAPI/Utilities/Helpers.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 3ce4291..7e0df84 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -985,7 +985,7 @@ public static class Helpers bounds.Encapsulate(next); return bounds; }); - var rowScale = (columnWidth / prefabBounds.size.x) * 1.1f; + var rowScale = (columnWidth / prefabBounds.size.x) * 1.22f; var rowHeight = prefabBounds.size.y * rowScale; var rowPitch = 0.45f * rowScale; var contentBottom = float.PositiveInfinity; @@ -1035,7 +1035,7 @@ public static class Helpers var header = Object.Instantiate(headerSource!, page.transform, false); header.transform.localScale = Vector3.one * headerScale; header.transform.localPosition = new Vector3( - horizontalOffset - 0.4f, + horizontalOffset - 0.5f, contentTop - (rowIndex * rowPitch) - groupSpacing, -1f); header.SetHeader(MiraLocaleManager.GetOrCreateLocaleString(group.GroupName), 20); @@ -1143,6 +1143,10 @@ public static class Helpers text.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); text.fontMaterial.SetFloat(ShaderID.Stencil, 50f); + if (text.enabled) + { + text.ForceMeshUpdate(); + } } foreach (var renderer in row.GetComponentsInChildren(true)) @@ -1185,9 +1189,12 @@ public static class Helpers var maskBottom = maskArea ? inner.InverseTransformPoint(maskArea!.bounds.min).y : -0.75f; - var scrollRange = float.IsPositiveInfinity(contentBottom) + var contentOverflow = float.IsPositiveInfinity(contentBottom) ? 0f - : Mathf.Max(0f, maskBottom - contentBottom + 0.1f); + : maskBottom - contentBottom; + var scrollRange = contentOverflow > 0.05f + ? contentOverflow + 0.05f + : 0f; parent.SetYBoundsMax(Mathf.Clamp(scrollRange, 0f, 999f)); page.SetActive(true); return page; From 39d77a301b7977dc2b4f33962ac21c5e88a17917 Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:01:27 -0400 Subject: [PATCH 26/29] add properties to merge groups of options on one page --- MiraAPI/GameOptions/AbstractOptionGroup.cs | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs index b62b803..52d8fdf 100644 --- a/MiraAPI/GameOptions/AbstractOptionGroup.cs +++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs @@ -67,6 +67,32 @@ public abstract class AbstractOptionGroup /// public virtual string RoleGuideTitle => GroupName; + /// + /// Gets the key used to combine this group with other option groups on one Match Info Guide page. + /// Groups are combined only with groups from the same plugin that use the same non-empty key. + /// + public virtual string? MatchInfoGuidePageKey => null; + + /// + /// Gets the title of the Match Info Guide page. For groups sharing a page key, the first + /// group in menu order supplies the page title. + /// + public virtual string MatchInfoGuidePageTitle => RoleGuideTitle; + + /// + /// Gets the ordering priority of this Match Info Guide page. Lower values appear first + /// among pages with the same menu category and group priority. Groups sharing a page key + /// use the lowest page-order value among those groups. + /// + public virtual int MatchInfoGuidePageOrder => int.MaxValue; + + /// + /// Gets the number of columns used to lay out options on this Match Info Guide page. + /// For a combined page, groups are placed into these columns. Values are limited to 1-6; + /// for groups sharing a page key, the first group in menu order supplies this value. + /// + public virtual int MatchInfoGuidePageColumns => 2; + /// /// Gets the group . This is used to color the group in the options menu. /// From 602665079c78c1ca5405428a7efe91d44b669233 Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:03:09 -0400 Subject: [PATCH 27/29] wire up headers to pages with more than 2 columns --- MiraAPI/Patches/RoleGuidePatches.cs | 31 +++++- MiraAPI/Utilities/Helpers.cs | 141 +++++++++++++++++++++++----- 2 files changed, 142 insertions(+), 30 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 395a16d..ab28562 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -1957,6 +1957,7 @@ public static class RoleGuidePatches { vanillaEntries = BuildVanillaSettingsEntries(guide); } + _settingsPages = GuideOptionPages.BuildPages(guide.GetBoolString); if (_settingsPage > _settingsPages.Count) { @@ -1979,7 +1980,13 @@ public static class RoleGuidePatches } else { - ShowSettingsEntries(_settingsPages[_settingsPage - 1].Entries, false, scrollToTop); + var page = _settingsPages[_settingsPage - 1]; + ShowSettingsEntries( + page.Entries, + page.Groups.Count > 1, + scrollToTop, + page.Columns, + page.Groups.Count > 1); vanillaTab.SetActive(false); RepurposeSortButtons(true); } @@ -2029,7 +2036,12 @@ public static class RoleGuidePatches return entries; } - private static void ShowSettingsEntries(IReadOnlyList entries, bool showGroup, bool scrollToTop) + private static void ShowSettingsEntries( + List entries, + bool showGroup, + bool scrollToTop, + int columns = 2, + bool groupsAsColumns = false) { var orderedEntries = showGroup ? entries.GroupBy(entry => entry.Group).SelectMany(group => group) @@ -2037,7 +2049,7 @@ public static class RoleGuidePatches var rows = orderedEntries .Select(entry => new AdvancedSettingsRow(entry.Title, entry.Value, showGroup ? entry.Group : null)) .ToList(); - ShowSettingsRows(rows, scrollToTop); + ShowSettingsRows(rows, scrollToTop, columns, groupsAsColumns); _settingsNoResults?.SetActive(_settingsSearching && entries.Count == 0); } @@ -2049,7 +2061,11 @@ public static class RoleGuidePatches ShowSettingsRows(advancedRows, scrollToTop); } - private static void ShowSettingsRows(List rows, bool scrollToTop) + private static void ShowSettingsRows( + List rows, + bool scrollToTop, + int columns = 2, + bool groupsAsColumns = false) { var guide = MatchInfoGuide.Instance; if (_settingsScroller == null || !guide || !guide.MatchInfoRolePanelPrefab) @@ -2067,7 +2083,12 @@ public static class RoleGuidePatches } _settingsScroller.gameObject.SetActive(true); - _settingsPageObject = Helpers.CreateAdvancedSettingsPage(guide, _settingsScroller, rows); + _settingsPageObject = Helpers.CreateAdvancedSettingsPage( + guide, + _settingsScroller, + rows, + columns, + groupsAsColumns); _settingsNoResults?.SetActive(false); if (scrollToTop) { diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 7e0df84..214e166 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -926,18 +926,23 @@ public static class Helpers return CreateAdvancedSettingsPage( guide, parent, - settings.Select(setting => new AdvancedSettingsRow(setting.Key, setting.Value, null)).ToList()); + settings.Select(setting => new AdvancedSettingsRow(setting.Key, setting.Value, null)).ToList(), + 2, + false); } internal static GameObject CreateAdvancedSettingsPage( MatchInfoGuide guide, Scroller parent, - List settings) + List settings, + int columns = 2, + bool groupsAsColumns = false) { const float contentTop = 0.95f; const float horizontalOffset = -0.2f; - const int columns = 2; + const float headerContentGap = 0.2f; + columns = Mathf.Clamp(columns, 1, 6); var inner = parent.Inner; var layout = inner.GetComponent(); if (layout) @@ -945,8 +950,14 @@ public static class Helpers layout.enabled = false; } - var columnWidth = layout ? Mathf.Clamp(layout.cellSize.x, 2f, 3.2f) : 2.7f; var columnGap = layout ? Mathf.Clamp(layout.spacing.x, 0.1f, 0.6f) : 0.3f; + const int baseColumnCount = 2; + var baseColumnWidth = layout ? Mathf.Clamp(layout.cellSize.x, 2f, 3.2f) : 2.7f; + var availableWidth = baseColumnWidth * baseColumnCount + columnGap * (baseColumnCount - 1); + var columnWidth = Mathf.Clamp( + (availableWidth - (columns - 1) * columnGap) / columns, + 1f, + 3.2f); var layer = inner.gameObject.layer; var page = new GameObject("AdvancedSettingsPage") { layer = layer }; page.transform.SetParent(inner, false); @@ -988,6 +999,50 @@ public static class Helpers var rowScale = (columnWidth / prefabBounds.size.x) * 1.22f; var rowHeight = prefabBounds.size.y * rowScale; var rowPitch = 0.45f * rowScale; + var groupedSettings = groupsAsColumns + ? settings + .Where(setting => setting.Group != null) + .GroupBy(setting => setting.Group!) + .ToList() + : []; + groupsAsColumns = groupedSettings.Count > 1; + if (groupsAsColumns) + { + rowScale = columnWidth / prefabBounds.size.x; + rowHeight = prefabBounds.size.y * rowScale; + rowPitch = 0.45f * rowScale; + } + + var groupColumns = new Dictionary(); + var groupTopOffsets = new Dictionary(); + var groupRows = new Dictionary(); + if (groupsAsColumns) + { + var bandCount = Mathf.CeilToInt(groupedSettings.Count / (float)columns); + var bandHeights = new float[bandCount]; + for (var i = 0; i < groupedSettings.Count; i++) + { + var group = groupedSettings[i].Key; + var band = i / columns; + groupColumns[group] = i % columns; + bandHeights[band] = Mathf.Max( + bandHeights[band], + rowPitch * (groupedSettings[i].Count() + 1) + headerContentGap + 0.25f); + } + + var bandOffset = 0f; + for (var band = 0; band < bandCount; band++) + { + for (var i = band * columns; i < Mathf.Min((band + 1) * columns, groupedSettings.Count); i++) + { + groupTopOffsets[groupedSettings[i].Key] = bandOffset; + groupRows[groupedSettings[i].Key] = 0; + } + + bandOffset += bandHeights[band]; + } + } + var contentBottom = float.PositiveInfinity; var previousGroup = (AbstractOptionGroup?)null; var rowIndex = 0; @@ -998,15 +1053,22 @@ public static class Helpers { if (settings[i].Group != previousGroup && settings[i].Group is { } group) { - if (previousGroup != null) + if (groupsAsColumns) { - groupSpacing += 0.2f; + column = groupColumns[group]; } - - if (column != 0) + else { - rowIndex++; - column = 0; + if (previousGroup != null) + { + groupSpacing += 0.2f; + } + + if (column != 0) + { + rowIndex++; + column = 0; + } } var headerSource = group.Header; @@ -1030,14 +1092,31 @@ public static class Helpers if (headerSource) { - const float headerScale = 0.5f; - var headerWidth = (columns * columnWidth + (columns - 1) * columnGap) / headerScale; + const float headerScale = 0.46f; + var headerWidth = groupsAsColumns + ? columnWidth / headerScale + : (columns * columnWidth + (columns - 1) * columnGap) / headerScale; var header = Object.Instantiate(headerSource!, page.transform, false); header.transform.localScale = Vector3.one * headerScale; - header.transform.localPosition = new Vector3( - horizontalOffset - 0.5f, - contentTop - (rowIndex * rowPitch) - groupSpacing, - -1f); + if (groupsAsColumns) + { + var totalWidth = columns * columnWidth + (columns - 1) * columnGap; + var groupColumn = groupColumns[group]; + var groupLeft = -totalWidth / 2f + horizontalOffset + + groupColumn * (columnWidth + columnGap); + header.transform.localPosition = new Vector3( + groupLeft + (columnWidth / 2f) - 0.25f, + contentTop - groupTopOffsets[group], + -1f); + } + else + { + header.transform.localPosition = new Vector3( + horizontalOffset - 0.5f, + contentTop - (rowIndex * rowPitch) - groupSpacing, + -1f); + } + header.SetHeader(MiraLocaleManager.GetOrCreateLocaleString(group.GroupName), 20); if (header.Title) { @@ -1063,14 +1142,14 @@ public static class Helpers { header.Background.sprite = MiraAssets.CategoryHeader.LoadAsset(); header.Background.drawMode = SpriteDrawMode.Sliced; - header.Background.color = MiraApiPlugin.DefaultHeaderColor; + header.Background.color = group.GroupColor; header.Background.transform.localPosition = new Vector3(0.5f, -0.1833f, 0f); header.Background.size = new Vector2(headerWidth, header.Background.size.y); } if (header.Divider) { - header.Divider.color = MiraApiPlugin.DefaultHeaderColor; + header.Divider.color = group.GroupColor; } header.gameObject.SetActive(true); @@ -1099,7 +1178,10 @@ public static class Helpers } } - rowIndex++; + if (!groupsAsColumns) + { + rowIndex++; + } } previousGroup = group; @@ -1108,8 +1190,10 @@ public static class Helpers var originX = -((columns * columnWidth) + ((columns - 1) * columnGap)) / 2f + (column * (columnWidth + columnGap)) + horizontalOffset; - var centerY = contentTop - (rowHeight / 2f) - (rowIndex * rowPitch); - centerY -= groupSpacing; + var centerY = groupsAsColumns && settings[i].Group is { } rowGroup + ? contentTop - groupTopOffsets[rowGroup] - + (rowPitch * (groupRows[rowGroup] + 1)) - headerContentGap - (rowHeight / 2f) + : contentTop - (rowHeight / 2f) - (rowIndex * rowPitch) - groupSpacing; var row = Object.Instantiate(rowPrefab, page.transform); row.enabled = false; row.PlusBtn.gameObject.SetActive(false); @@ -1177,11 +1261,18 @@ public static class Helpers } row.gameObject.SetActive(true); - column++; - if (column == columns) + if (groupsAsColumns && settings[i].Group is { } renderedGroup) { - column = 0; - rowIndex++; + groupRows[renderedGroup]++; + } + else + { + column++; + if (column == columns) + { + column = 0; + rowIndex++; + } } } From da7162d60d0d3d589e6064dfe5c989a22fe660b3 Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:03:48 -0400 Subject: [PATCH 28/29] resize option content with more than 2 columns --- MiraAPI/Utilities/Helpers.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 214e166..570bb04 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -941,6 +941,7 @@ public static class Helpers const float contentTop = 0.95f; const float horizontalOffset = -0.2f; const float headerContentGap = 0.2f; + const int contentSortingOrder = 151; columns = Mathf.Clamp(columns, 1, 6); var inner = parent.Inner; @@ -1008,7 +1009,7 @@ public static class Helpers groupsAsColumns = groupedSettings.Count > 1; if (groupsAsColumns) { - rowScale = columnWidth / prefabBounds.size.x; + rowScale = (columnWidth / prefabBounds.size.x) * (columns > 2 ? 1.15f : 1f); rowHeight = prefabBounds.size.y * rowScale; rowPitch = 0.45f * rowScale; } @@ -1157,6 +1158,7 @@ public static class Helpers { renderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; renderer.material.SetInt(PlayerMaterial.MaskLayer, 50); + renderer.sortingOrder = contentSortingOrder; if (renderer.enabled && renderer.sprite) { contentBottom = Mathf.Min( @@ -1169,6 +1171,7 @@ public static class Helpers { text.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); text.fontMaterial.SetFloat(ShaderID.Stencil, 50f); + text.renderer.sortingOrder = contentSortingOrder; if (text.enabled) { text.ForceMeshUpdate(); @@ -1227,6 +1230,7 @@ public static class Helpers text.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); text.fontMaterial.SetFloat(ShaderID.Stencil, 50f); + text.renderer.sortingOrder = contentSortingOrder; if (text.enabled) { text.ForceMeshUpdate(); @@ -1237,6 +1241,7 @@ public static class Helpers { renderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; renderer.material.SetInt(PlayerMaterial.MaskLayer, 50); + renderer.sortingOrder = contentSortingOrder; if (renderer.enabled && renderer.sprite && renderer.transform != row.PlusBtn.transform && From 5c51037503d17d82e7f815685d6b6b8cbcd48b0f Mon Sep 17 00:00:00 2001 From: neilmjain <69025896+neilmjain@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:09:27 -0400 Subject: [PATCH 29/29] track options better --- MiraAPI/Utilities/GuideOptionsPatches.cs | 72 +++++++++++++++++++++--- 1 file changed, 64 insertions(+), 8 deletions(-) diff --git a/MiraAPI/Utilities/GuideOptionsPatches.cs b/MiraAPI/Utilities/GuideOptionsPatches.cs index dceebc3..22703b5 100644 --- a/MiraAPI/Utilities/GuideOptionsPatches.cs +++ b/MiraAPI/Utilities/GuideOptionsPatches.cs @@ -23,29 +23,46 @@ internal sealed record GuideOptionEntry( internal sealed class GuideOptionPage( AbstractOptionGroup group, + string title, + int order, + int menuOrder, + uint groupPriority, + int columns, string modAbbreviation, IReadOnlyList modSearchTerms, - IReadOnlyList entries) + List entries) { public AbstractOptionGroup Group { get; } = group; - public string Title { get; } = group.RoleGuideTitle; + public List Groups { get; } = [group]; + + public string Title { get; } = title; public LoadableAsset Sprite { get; } = group.Sprite; + public int Order { get; set; } = order; + + public int MenuOrder { get; set; } = menuOrder; + + public uint GroupPriority { get; set; } = groupPriority; + + public int Columns { get; } = columns; + public string ModAbbreviation { get; } = modAbbreviation; - public IReadOnlyList ModSearchTerms { get; } = modSearchTerms; + public HashSet ModSearchTerms { get; } = new(modSearchTerms, StringComparer.OrdinalIgnoreCase); - public IReadOnlyList Entries { get; } = entries; + public List Entries { get; } = entries; } internal static class GuideOptionPages { private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); + public static List BuildPages(Func formatBool) { var pages = new List(); + var combinedPages = new Dictionary(StringComparer.OrdinalIgnoreCase); var mode = CustomGameModeManager.ActiveMode; var groups = MiraPluginManager.Instance.PluginsWithOptionsOrGameModes @@ -88,11 +105,49 @@ internal static class GuideOptionPages if (entries.Count > 0) { - pages.Add(new GuideOptionPage(item.Group, item.ModAbbreviation, modSearchTerms, entries)); + var pageKey = item.Group.MatchInfoGuidePageKey?.Trim(); + var combinedKey = string.IsNullOrEmpty(pageKey) ? null : $"{item.Plugin.PluginId}\0{pageKey}"; + if (combinedKey != null && combinedPages.TryGetValue(combinedKey, out var combinedPage)) + { + combinedPage.Groups.Add(item.Group); + combinedPage.Entries.AddRange(entries); + combinedPage.ModSearchTerms.UnionWith(modSearchTerms); + combinedPage.Order = Math.Min(combinedPage.Order, item.Group.MatchInfoGuidePageOrder); + combinedPage.MenuOrder = Math.Min( + combinedPage.MenuOrder, + GetMenuOrder(item.Group.ParentMenu)); + combinedPage.GroupPriority = Math.Min( + combinedPage.GroupPriority, + item.Group.GroupPriority); + } + else + { + var page = new GuideOptionPage( + item.Group, + item.Group.MatchInfoGuidePageTitle, + item.Group.MatchInfoGuidePageOrder, + GetMenuOrder(item.Group.ParentMenu), + item.Group.GroupPriority, + Mathf.Clamp(item.Group.MatchInfoGuidePageColumns, 1, 6), + item.ModAbbreviation, + modSearchTerms, + entries); + pages.Add(page); + if (combinedKey != null) + { + combinedPages.Add(combinedKey, page); + } + } } } - return pages; + return pages + .OrderBy(page => page.MenuOrder) + .ThenBy(page => page.GroupPriority) + .ThenBy(page => page.Order) + .ThenBy(page => page.Group.GroupName, StringComparer.OrdinalIgnoreCase) + .ThenBy(page => page.ModAbbreviation, StringComparer.OrdinalIgnoreCase) + .ToList(); } private static int GetMenuOrder(MenuCategory menu) @@ -138,7 +193,7 @@ internal static class GuideOptionPages .ToList(); } - private static IReadOnlyList GetModSearchTerms(MiraPluginInfo plugin, AbstractOptionGroup group) + private static string[] GetModSearchTerms(MiraPluginInfo plugin, AbstractOptionGroup group) { var terms = new HashSet(StringComparer.OrdinalIgnoreCase) { @@ -159,7 +214,7 @@ internal static class GuideOptionPages return terms.Where(term => !string.IsNullOrWhiteSpace(term)).ToArray(); } - private static void AddNamespace(ISet terms, Type type) + private static void AddNamespace(HashSet terms, Type type) { if (!string.IsNullOrWhiteSpace(type.Namespace)) { @@ -191,6 +246,7 @@ internal static class GuideOptionPages return entry.Value.Contains(query, cmp) ? 3 : int.MaxValue; } + private static bool IsSettingsGroup(AbstractOptionGroup group, AbstractGameMode? mode) { if (group.ParentMenu is not (MenuCategory.Game or MenuCategory.CustomOne or MenuCategory.CustomTwo))