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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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)) From 0b85c9fe965c908aaeaf275cf7a2779bd4dc6c69 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:57:56 -0500 Subject: [PATCH 30/30] Add new modifiers, settings, and page buttons --- MiraAPI/Patches/RoleGuidePatches.cs | 13 +++---- MiraAPI/Resources/Wiki/PageLeftHover.png | Bin 0 -> 3565 bytes MiraAPI/Resources/Wiki/PageLeftIdle.png | Bin 0 -> 3619 bytes MiraAPI/Resources/Wiki/PageRightHover.png | Bin 0 -> 3549 bytes MiraAPI/Resources/Wiki/PageRightIdle.png | Bin 0 -> 3644 bytes .../Wiki/SidebarModifiersButtonHover.png | Bin 0 -> 7937 bytes .../Wiki/SidebarModifiersButtonIdle.png | Bin 0 -> 7661 bytes .../Wiki/SidebarModifiersButtonOpen.png | Bin 0 -> 8122 bytes .../Wiki/SidebarSettingsButtonHover.png | Bin 8442 -> 7023 bytes .../Wiki/SidebarSettingsButtonIdle.png | Bin 8394 -> 6638 bytes .../Wiki/SidebarSettingsButtonOpen.png | Bin 8493 -> 7120 bytes MiraAPI/Utilities/Assets/MiraAssets.cs | 35 ++++++++++++++++++ 12 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 MiraAPI/Resources/Wiki/PageLeftHover.png create mode 100644 MiraAPI/Resources/Wiki/PageLeftIdle.png create mode 100644 MiraAPI/Resources/Wiki/PageRightHover.png create mode 100644 MiraAPI/Resources/Wiki/PageRightIdle.png create mode 100644 MiraAPI/Resources/Wiki/SidebarModifiersButtonHover.png create mode 100644 MiraAPI/Resources/Wiki/SidebarModifiersButtonIdle.png create mode 100644 MiraAPI/Resources/Wiki/SidebarModifiersButtonOpen.png diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index ab28562..b8196fc 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -661,9 +661,9 @@ public static class RoleGuidePatches instance.TabButtons[2].transform.parent); SetupTabButton( modifiersButton, - MiraAssets.WikiRolesButtonIdleSprite, - MiraAssets.WikiRolesButtonOpenSprite, - MiraAssets.WikiRolesButtonHoverSprite); + MiraAssets.WikiModifiersButtonIdleSprite, + MiraAssets.WikiModifiersButtonOpenSprite, + MiraAssets.WikiModifiersButtonHoverSprite); modifiersButton.OnClick = new Button.ButtonClickedEvent(); modifiersButton.OnClick.AddListener((Action)(() => OpenModifiersTab())); instance.TabButtons.Add(modifiersButton); @@ -2189,10 +2189,10 @@ public static class RoleGuidePatches SearchSortingGroupButton.gameObject.SetActive(paginationVisible); } - private static void SetPaginationSprites(SpriteRenderer idle, SpriteRenderer hover, bool flip) + private static void SetPaginationSprites(SpriteRenderer idle, SpriteRenderer hover, bool isLeft) { - var idleSprite = MiraAssets.NextButton.LoadAsset(); - var hoverSprite = MiraAssets.NextButtonActive.LoadAsset(); + var idleSprite = isLeft ? MiraAssets.PageLeftIdle.LoadAsset() : MiraAssets.PageRightIdle.LoadAsset(); + var hoverSprite = isLeft ? MiraAssets.PageLeftHover.LoadAsset() : MiraAssets.PageRightHover.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; @@ -2201,7 +2201,6 @@ public static class RoleGuidePatches idle.sprite = idleSprite; hover.sprite = hoverSprite; - idle.flipX = hover.flipX = flip; idle.transform.localScale = hover.transform.localScale = new Vector3(scale, scale, 1f); } diff --git a/MiraAPI/Resources/Wiki/PageLeftHover.png b/MiraAPI/Resources/Wiki/PageLeftHover.png new file mode 100644 index 0000000000000000000000000000000000000000..7fa3b1502958b9d2d206f1d42f4edfc0fda9b795 GIT binary patch literal 3565 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 nEi3=rg+i00000NkvXXu0mjfkSC)! literal 0 HcmV?d00001 diff --git a/MiraAPI/Resources/Wiki/PageLeftIdle.png b/MiraAPI/Resources/Wiki/PageLeftIdle.png new file mode 100644 index 0000000000000000000000000000000000000000..cbedc9307f5c35b5f7178d4fb87da6c6989d4e2b GIT binary patch literal 3619 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 literal 0 HcmV?d00001 diff --git a/MiraAPI/Resources/Wiki/PageRightHover.png b/MiraAPI/Resources/Wiki/PageRightHover.png new file mode 100644 index 0000000000000000000000000000000000000000..8ffb435aec0ecb9faff0f5e796da761e95eaa0b4 GIT binary patch literal 3549 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 XOzR!U$-RJ1uzJ2>)OI$*C{~;Co;}3svd74HA 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 bx2F6*6)Yjp0Q@tM00000NkvXXu0mjfS}4ry literal 0 HcmV?d00001 diff --git a/MiraAPI/Resources/Wiki/SidebarModifiersButtonOpen.png b/MiraAPI/Resources/Wiki/SidebarModifiersButtonOpen.png new file mode 100644 index 0000000000000000000000000000000000000000..11279563f53f4813ea4126c1c879e08cfa3fb081 GIT binary patch literal 8122 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�qH6(5O=XhF) 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 literal 0 HcmV?d00001 diff --git a/MiraAPI/Resources/Wiki/SidebarSettingsButtonHover.png b/MiraAPI/Resources/Wiki/SidebarSettingsButtonHover.png index 78139049cc46351f40561881e709b8bb8fd3ad55..b4e53a945786c6b77eb558173e182a2197434f67 100644 GIT binary patch literal 7023 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 literal 8442 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

GLFpW z7zqsG2Q0X7s_RUh^uRrn0k8@x7GhoqV-gVi4IPuPLpLowq6#`y0O`~D9;!%|O+tQt z4yGN8f^`t8Q`T%aQ`G~JGZ%r0?3d^q4H^bK@4ncmUJIQu<#SnQqXV_z^!=n2AP>Hg zaL>9g$d;!!;wx9J+FF~o|6YnV;D5Jq5fxt4!Bz`^xCo2Da22>A_F3Iv(4~#r#ra&e zh7ogY!Y_a5{TQDp2D7{Wv#_$`7B_?}zk>ecev zhwmbc_K<#ziQklSWU&8%$~lKI6`Y|eDL4->me{85{7B<90>wag^Zie%VbkL(4eUExc0%8`GZp z@lQ2yg#Ah9$+OBZx``ZFh#3GNc4GOIAgWC+-r{pS#U=8k?|(~ldft{;?wEooqW9aCVEuJ(%mM&T>6IU&XIfH8}QV&yQ#SIBStg$Y%!YYIM4-QH- zW;>f&FtrU8;(^L2UxLM0h-X-P^XDVqQ_uPT7aTu&NS^%Xho!~m(`kTQFkV+1M1W%o zAn-n@EOkc#tS|lGJ0ajr9{XPFhs9uJ_!a^`<6KiytsfaIz%1(BYt{-?_2r-;1BfrD z3vdy$P~a)l>1YrBu`ZV|CNj^$doSQijGR7a-_`cPAzkOg7OobcoqI4mIOEE`io~hv zMKk8ubHqGeyWn;aY#SbU00spmG%Mk_gP4UTabZXSGWFcBeuFt2(7p`u4TTsaHX@!{ z`xxBqpaH@D$9N!EBKoTKy_g9Fc=Z2OE?<#QBd}LSgFzAu2(jOn>f@xWxKz0_5`8JE zx@GIusF~b(@|?7xk755qb!OifG~#{f4F!jNTj_Lb3>EJ=sp>c4J!c@-lLE+E0R&Sw zf(Try%*=?Q5xt2;y)X_KR7h)3gX%dDrC|`+5uuO3^;fOc6}AG6cY%?g#S_Qdu3UMC zR~$fOF;`L-z459_5yZptdfR!=sS41_yYGqleK&jd;e32W0>uBL;P^p?e#M?+ombv^ zQ@>ho7|n0HmEXBYN@n&({g`W4+Wv|Jh)fQB`S7--hz0*r1mE!iKB&UK1iE4YvNM0E zJj?cH#yh4}mCI8Z|6NG5QbPhpML4OevlnO=+ri@g9(BHEo0fOHil8MdN zj=6Y5e&!=+p2U()6x&ZE1g&r^ zGHenbQWNW+{@5~)9Hy897LR**9;JAE(-p%h{5STnYY0yRt{0Go=)e(569*Wvc>#hD z7Dr4h>{B8}f{vL=I5B^3qzD-ih9kY};!UQJeX<;A4)Ru|hdj4Y2n(jUp!>ML==b~4 z`Cv)}A980z3`7h670`hYe+9@La5&sZ2%Yn0g%E6wt?V3E@cTL)RzN} z@Q;6jhn$~BG_7BUFW_-+fH(LZaQIt*hZXY`EX|dT&z*o8y1KgdAdq%33c#m1ERgU) zacdj|)FG0TA_!R+MR!JpW%+-6!p_7-kRE)-6+8{j@CGFZb{u#Pg6CmRW_D&cK*9SP zGBY!ecXf8+3qt-mkQggD5K#1Zf`U7M3YBZw0002BNklOCaeOG^+;-}aOIZ#%2NU2tv+z7;Z{s=&E7<9QWj{tPJ5s35r5rE<_=yG2k0qAlg c5a;>-2L>_uLm=YMm;e9(07*qoM6N<$g67&1UH||9 diff --git a/MiraAPI/Resources/Wiki/SidebarSettingsButtonIdle.png b/MiraAPI/Resources/Wiki/SidebarSettingsButtonIdle.png index fdf65df7bba4a39056e9ebbfa51ec6c9787482d1..2d040a6baea0b98b8c21ed64fb02132b09a56e3e 100644 GIT binary patch literal 6638 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 literal 8394 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 diff --git a/MiraAPI/Resources/Wiki/SidebarSettingsButtonOpen.png b/MiraAPI/Resources/Wiki/SidebarSettingsButtonOpen.png index ec5bb1430588c53ed7a358e8a84c17b6daddd997..e750bac451ab91d3f02c98cadd592a7968f44437 100644 GIT binary patch literal 7120 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 diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index 0feb75c..705bc32 100644 --- a/MiraAPI/Utilities/Assets/MiraAssets.cs +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -205,6 +205,21 @@ public static class MiraAssets /// public static LoadableResourceAsset WikiRolesButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonOpen.png"); + ///

+ /// Gets the sprite used for the modifiers button while hovering. + /// + public static LoadableResourceAsset WikiModifiersButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarModifiersButtonHover.png"); + + /// + /// Gets the sprite used for the modifiers button while hovering. + /// + public static LoadableResourceAsset WikiModifiersButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarModifiersButtonIdle.png"); + + /// + /// Gets the sprite used for the modifiers button while hovering. + /// + public static LoadableResourceAsset WikiModifiersButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarModifiersButtonOpen.png"); + /// /// Gets the sprite used for the roles button while hovering. /// @@ -335,5 +350,25 @@ public static class MiraAssets /// public static LoadableResourceAsset IconTerminology { get; } = new("MiraAPI.Resources.Wiki.IconTerminology.png"); + /// + /// Gets the Page switching sprite. + /// + public static LoadableResourceAsset PageLeftHover { get; } = new("MiraAPI.Resources.Wiki.PageLeftHover.png"); + + /// + /// Gets the Page switching sprite. + /// + public static LoadableResourceAsset PageLeftIdle { get; } = new("MiraAPI.Resources.Wiki.PageLeftIdle.png"); + + /// + /// Gets the Page switching sprite. + /// + public static LoadableResourceAsset PageRightHover { get; } = new("MiraAPI.Resources.Wiki.PageRightHover.png"); + + /// + /// Gets the Page switching sprite. + /// + public static LoadableResourceAsset PageRightIdle { get; } = new("MiraAPI.Resources.Wiki.PageRightIdle.png"); + internal static LoadableResourceAsset BlankSprite { get; } = new("MiraAPI.Resources.BlankSprite.png"); }