From a1d22b2facc9436b91c6643687d5a7125d8ab5c7 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 01:29:13 -0700 Subject: [PATCH 01/28] Fix preset lookup racing mod switches during option creation --- .../Patches/Options/GameOptionsMenuPatch.cs | 114 +++++++----------- MiraAPI/Presets/OptionPreset.cs | 11 +- 2 files changed, 52 insertions(+), 73 deletions(-) diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 7f0920d..e65f4c3 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -618,6 +618,8 @@ internal static class GameOptionsMenuPatch { menu.MapPicker.gameObject.SetActive(false); var mod = MenuState.Instance.CurrentMod; + var container = MenuState.Instance.CurrentContainer.transform; + PresetManager.DefaultPresets.TryGetValue(mod, out var defaultPreset); var filteredGroups = MenuState.Instance.CurrentMenu switch { @@ -643,15 +645,13 @@ internal static class GameOptionsMenuPatch { foreach (var group in filteredGroups) { - yield return CoCreateGroup(menu, group); + yield return CoCreateGroup(menu, group, container, defaultPreset); } } } - private static IEnumerator CoCreateGroup(GameOptionsMenu menu, AbstractOptionGroup group) + private static IEnumerator CoCreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, OptionPreset? defaultPreset) { - var container = MenuState.Instance.CurrentContainer.transform; - group.Ready = false; if (group.Header == null || !group.Header) { @@ -713,12 +713,6 @@ internal static class GameOptionsMenuPatch menu.playerOptionOrigin, container)); - OptionPreset? defaultPreset = null; - if (PresetManager.DefaultPresets.TryGetValue(MenuState.Instance.CurrentMod, out var preset)) - { - defaultPreset = preset; - } - yield return options.CoLoopWithBudget(newOpt => { newOpt.SetClickMask(menu.ButtonClickMask); @@ -765,73 +759,53 @@ internal static class GameOptionsMenuPatch } menu.Children.Add(newOpt); - var resetBtn = new GameObject("ResetOption") + if (defaultPreset != null && defaultPreset.IsOptionInPreset(newOpt) && + newOpt is ToggleOption or NumberOption or StringOption) { - transform = + var resetBtn = new GameObject("ResetOption") { - parent = newOpt.transform, - localScale = new(.5f, .5f, 1), - localPosition = new Vector3(-3.1f, 0f, -2f), - }, - layer = LayerMask.NameToLayer("UI"), - }; - var resetRend = resetBtn.AddComponent(); - resetRend.sprite = MiraAssets.ResetButton.LoadAsset(); - resetRend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; - resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) - ? Color.white - : group.GroupColor.FindAlternateColor(); - var resetBoxCol = resetBtn.gameObject.AddComponent(); - resetBoxCol.size = new Vector2(1f, 1f); - resetBoxCol.offset = new Vector2(0, 0); - var passiveButton = resetBtn.AddComponent(); - passiveButton.OnClick = new Button.ButtonClickedEvent(); - passiveButton.ClickSound = menu.BackButton.GetComponent().ClickSound; - passiveButton.OnMouseOver = new UnityEvent(); - passiveButton.OnMouseOver.AddListener( - (UnityAction)(() => - { - resetRend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor - ? group.GroupColor - : MiraAssets.AcceptedTeal; - })); - passiveButton.OnMouseOut = new UnityEvent(); - passiveButton.OnMouseOut.AddListener( - (UnityAction)(() => - { - resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) - ? Color.white - : group.GroupColor.FindAlternateColor(); - })); - if (newOpt is ToggleOption toggleOpt) - { + transform = + { + parent = newOpt.transform, + localScale = new(.5f, .5f, 1), + localPosition = new Vector3(-3.1f, 0f, -2f), + }, + layer = LayerMask.NameToLayer("UI"), + }; + var resetRend = resetBtn.AddComponent(); + resetRend.sprite = MiraAssets.ResetButton.LoadAsset(); + resetRend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) + ? Color.white + : group.GroupColor.FindAlternateColor(); + var resetBoxCol = resetBtn.gameObject.AddComponent(); + resetBoxCol.size = new Vector2(1f, 1f); + resetBoxCol.offset = new Vector2(0, 0); + var passiveButton = resetBtn.AddComponent(); + passiveButton.OnClick = new Button.ButtonClickedEvent(); + passiveButton.ClickSound = menu.BackButton.GetComponent().ClickSound; + passiveButton.OnMouseOver = new UnityEvent(); + passiveButton.OnMouseOver.AddListener( + (UnityAction)(() => + { + resetRend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor + ? group.GroupColor + : MiraAssets.AcceptedTeal; + })); + passiveButton.OnMouseOut = new UnityEvent(); + passiveButton.OnMouseOut.AddListener( + (UnityAction)(() => + { + resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) + ? Color.white + : group.GroupColor.FindAlternateColor(); + })); passiveButton.OnClick.AddListener( (UnityAction)(() => { - defaultPreset!.ResetOption(toggleOpt); + defaultPreset.ResetOption(newOpt); })); } - else if (newOpt is NumberOption numOpt) - { - passiveButton.OnClick.AddListener( - (UnityAction)(() => - { - defaultPreset!.ResetOption(numOpt); - })); - } - else if (newOpt is StringOption strOpt) - { - passiveButton.OnClick.AddListener( - (UnityAction)(() => - { - defaultPreset!.ResetOption(strOpt); - })); - } - - if (!defaultPreset!.IsOptionInPreset(newOpt)) - { - resetBtn.Destroy(); - } newOpt.Initialize(); newOpt.gameObject.SetActive(false); diff --git a/MiraAPI/Presets/OptionPreset.cs b/MiraAPI/Presets/OptionPreset.cs index 01758c5..077afc9 100644 --- a/MiraAPI/Presets/OptionPreset.cs +++ b/MiraAPI/Presets/OptionPreset.cs @@ -70,7 +70,12 @@ public class OptionPreset(string name, MiraPluginInfo plugin, ConfigFile presetC /// The option to reset. public void ResetOption(OptionBehaviour baseOption) { - var selectedOpt = Plugin.InternalOptions.First(x => x.OptionBehaviour == baseOption); + var selectedOpt = Plugin.InternalOptions.FirstOrDefault(x => x.OptionBehaviour == baseOption); + if (selectedOpt == null) + { + return; + } + selectedOpt.LoadFromPreset(PresetConfig); /*ModdedOptionsManager.SyncAllOptions(); @@ -84,7 +89,7 @@ public class OptionPreset(string name, MiraPluginInfo plugin, ConfigFile presetC /// The value of whether the option is in the preset. public bool IsOptionInPreset(OptionBehaviour baseOption) { - var selectedOpt = Plugin.InternalOptions.First(x => x.OptionBehaviour == baseOption); - return selectedOpt.ConfigDefinition != null && PresetConfig.ContainsKey(selectedOpt.ConfigDefinition); + var selectedOpt = Plugin.InternalOptions.FirstOrDefault(x => x.OptionBehaviour == baseOption); + return selectedOpt != null && selectedOpt.ConfigDefinition != null && PresetConfig.ContainsKey(selectedOpt.ConfigDefinition); } } From 83722a81c8a9c819cb8112516124cc8a40fe7272 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 01:31:06 -0700 Subject: [PATCH 02/28] Share advanced wiki page creation --- MiraAPI/Modifiers/BaseModifier.cs | 51 +++------------------ MiraAPI/Roles/ICustomRole.cs | 51 +++------------------ MiraAPI/Utilities/Helpers.cs | 74 +++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 92 deletions(-) diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 0ca73e0..11fac1d 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -9,8 +9,6 @@ using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using TMPro; using UnityEngine; -using UnityEngine.UI; -using Object = UnityEngine.Object; namespace MiraAPI.Modifiers; @@ -113,54 +111,15 @@ public abstract class BaseModifier : IOptionable /// The of the wiki. public virtual GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, Scroller parent) { - parent.ScrollToTop(); - var obj = Helpers.CreateAdvancedWikiTab( + var abilities = WikiAbilities; + return Helpers.CreateAdvancedWikiPage( guide, + titleText, + parent, ModifierNameLocale, ModifierName + $" ({ModifierCategoryTitle})", ModifierWikiDescription, - titleText, - out var desc); - var num = 0; - if (WikiAbilities.Count != 0) - { - var grid = Object.Instantiate(parent.Inner, obj.transform); - var layoutGroup = grid.GetComponent(); - layoutGroup.startAxis = GridLayoutGroup.Axis.Vertical; - layoutGroup.constraint = GridLayoutGroup.Constraint.FixedColumnCount; - layoutGroup.spacing = new Vector2(0.75f, 0.4f); - grid.DestroyChildren(); - foreach (var ability in WikiAbilities) - { - num++; - var panel = Object.Instantiate( - guide.MatchInfoRolePanelPrefab, - grid); - panel.roleCount.text = ability.AbilityType; - panel.roleCount.transform.localPosition += new Vector3(-0.04f, 0); - panel.roleName.text = ability.Name; - panel.roleName.transform.localPosition += new Vector3(-0.04f, 0); - panel.roleDescription.text = ability.Description; - panel.roleDescription.rectTransform.sizeDelta = new Vector2(2.601f, 0.8f); - panel.roleDescription.transform.localPosition += new Vector3(0, -0.1f); - panel.roleIcon.sprite = ability.Icon.LoadAsset(); - panel.roleIcon.SetSizeLimit(0.13f); - - panel.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50); - panel.roleName.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f); - panel.roleDescription.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f); - panel.roleCount.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f); - panel.roleIcon.transform.localScale = new Vector3(3.5f, 3.5f, 1f); - } - - grid.localPosition = new Vector3(-3.9f, 1.1f - desc.textBounds.size.y, 0f); - grid.localScale = new Vector3(1.3f, 1.3f, 1); - } - - obj.transform.SetParent(parent.Inner.transform); - obj.transform.localPosition = new Vector3(0f, 0f, 0f); - parent.SetYBoundsMax(Mathf.Clamp(desc.textBounds.size.y - 2 + Mathf.Ceil(num / 2f) * 1.45f, 0f, 999f)); - return obj; + abilities); } /// diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index dc38423..79f6570 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -12,8 +12,6 @@ using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using TMPro; using UnityEngine; -using UnityEngine.UI; -using Object = UnityEngine.Object; namespace MiraAPI.Roles; @@ -324,54 +322,15 @@ public interface ICustomRole : IOptionable /// The of the wiki. GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, Scroller parent) { - parent.ScrollToTop(); - var obj = Helpers.CreateAdvancedWikiTab( + var abilities = WikiAbilities; + return Helpers.CreateAdvancedWikiPage( guide, + titleText, + parent, RoleNameLocale, RoleName + $" ({RoleFactionTitle})", RoleWikiDescription, - titleText, - out var desc); - var num = 0; - if (WikiAbilities.Count != 0) - { - var grid = Object.Instantiate(parent.Inner, obj.transform); - var layoutGroup = grid.GetComponent(); - layoutGroup.startAxis = GridLayoutGroup.Axis.Vertical; - layoutGroup.constraint = GridLayoutGroup.Constraint.FixedColumnCount; - layoutGroup.spacing = new Vector2(0.75f, 0.4f); - grid.DestroyChildren(); - foreach (var ability in WikiAbilities) - { - num++; - var panel = Object.Instantiate( - guide.MatchInfoRolePanelPrefab, - grid); - panel.roleCount.text = ability.AbilityType; - panel.roleCount.transform.localPosition += new Vector3(-0.04f, 0); - panel.roleName.text = ability.Name; - panel.roleName.transform.localPosition += new Vector3(-0.04f, 0); - panel.roleDescription.text = ability.Description; - panel.roleDescription.rectTransform.sizeDelta = new Vector2(2.601f, 0.8f); - panel.roleDescription.transform.localPosition += new Vector3(0, -0.1f); - panel.roleIcon.sprite = ability.Icon.LoadAsset(); - panel.roleIcon.SetSizeLimit(0.13f); - - panel.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50); - panel.roleName.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f); - panel.roleDescription.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f); - panel.roleCount.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f); - panel.roleIcon.transform.localScale = new Vector3(3.5f, 3.5f, 1f); - } - - grid.localPosition = new Vector3(-3.9f, 1.1f - desc.textBounds.size.y, 0f); - grid.localScale = new Vector3(1.3f, 1.3f, 1); - } - - obj.transform.SetParent(parent.Inner.transform); - obj.transform.localPosition = new Vector3(0f, 0f, 0f); - parent.SetYBoundsMax(Mathf.Clamp((desc.textBounds.size.y - 2) + (Mathf.Ceil(num / 2f) * 1.45f), 0f, 999f)); - return obj; + abilities); } /// diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 3584588..c8b85b1 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -13,9 +13,11 @@ using MiraAPI.GameOptions.OptionTypes; using MiraAPI.Roles; using MiraAPI.Translation; using MiraAPI.Utilities.Assets; +using Reactor.Utilities.Extensions; using Rewired; using TMPro; using UnityEngine; +using UnityEngine.UI; using MethodBase = System.Reflection.MethodBase; using Object = UnityEngine.Object; using Random = UnityEngine.Random; @@ -757,4 +759,76 @@ public static class Helpers descriptionTmp.ForceMeshUpdate(); return obj; } + + /// + /// Creates a full advanced wiki page with a title, a description, and an optional grid of ability panels. + /// + /// The guide object. + /// The instance of the title. + /// The the page should be parented to. + /// The name of the page object. + /// The title of the page. + /// The description of the page. + /// The abilities to display in a grid below the description. + /// The of the wiki page. + public static GameObject CreateAdvancedWikiPage( + MatchInfoGuide guide, + TextMeshPro titleTmp, + Scroller parent, + string objName, + string title, + string description, + IReadOnlyCollection abilities) + { + parent.ScrollToTop(); + var obj = CreateAdvancedWikiTab( + guide, + objName, + title, + description, + titleTmp, + out var desc); + var num = 0; + if (abilities.Count != 0) + { + var grid = Object.Instantiate(parent.Inner, obj.transform); + var layoutGroup = grid.GetComponent(); + layoutGroup.startAxis = GridLayoutGroup.Axis.Vertical; + layoutGroup.constraint = GridLayoutGroup.Constraint.FixedColumnCount; + layoutGroup.spacing = new Vector2(0.75f, 0.4f); + grid.DestroyChildren(); + foreach (var ability in abilities) + { + num++; + SetupAbilityPanel(Object.Instantiate(guide.MatchInfoRolePanelPrefab, grid), ability); + } + + grid.localPosition = new Vector3(-3.9f, 1.1f - desc.textBounds.size.y, 0f); + grid.localScale = new Vector3(1.3f, 1.3f, 1); + } + + obj.transform.SetParent(parent.Inner.transform); + obj.transform.localPosition = new Vector3(0f, 0f, 0f); + parent.SetYBoundsMax(Mathf.Clamp(desc.textBounds.size.y - 2 + Mathf.Ceil(num / 2f) * 1.45f, 0f, 999f)); + return obj; + } + + private static void SetupAbilityPanel(MatchInfoRolePanel panel, AdvancedWikiAbilityDescription ability) + { + panel.roleCount.text = ability.AbilityType; + panel.roleCount.transform.localPosition += new Vector3(-0.04f, 0); + panel.roleName.text = ability.Name; + panel.roleName.transform.localPosition += new Vector3(-0.04f, 0); + panel.roleDescription.text = ability.Description; + panel.roleDescription.rectTransform.sizeDelta = new Vector2(2.601f, 0.8f); + panel.roleDescription.transform.localPosition += new Vector3(0, -0.1f); + panel.roleIcon.sprite = ability.Icon.LoadAsset(); + panel.roleIcon.SetSizeLimit(0.13f); + + panel.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50); + panel.roleName.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f); + panel.roleDescription.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f); + panel.roleCount.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f); + panel.roleIcon.transform.localScale = new Vector3(3.5f, 3.5f, 1f); + } } From b00775e868d87fdc2f596c8a6eabbcc762c3c3b6 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 01:31:44 -0700 Subject: [PATCH 03/28] Clean up wiki options text generation --- MiraAPI/Utilities/Helpers.cs | 151 ++++++++++++++--------------------- 1 file changed, 60 insertions(+), 91 deletions(-) diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index c8b85b1..dd10b75 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.GameOptions; @@ -29,83 +30,67 @@ namespace MiraAPI.Utilities; /// public static class Helpers { + private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled); + public static ReadOnlyCollection? GetModdedOptionsForType(Type classType) { - var optionGroups = - AccessTools.Field(typeof(ModdedOptionsManager), "Groups").GetValue(null) as List; + var group = ModdedOptionsManager.Groups.FirstOrDefault(x => x.OptionableType == classType); - return optionGroups?.FirstOrDefault(x => x.OptionableType == classType)?.Children; + return group?.Children; } public static string GetOptionsText(Type classType) { - var options = GetModdedOptionsForType(classType); + var group = ModdedOptionsManager.Groups.FirstOrDefault(x => x.OptionableType == classType); + var options = group?.Children; + var summaryProvider = group as ISummarizedOptions; + var hiddenKeys = summaryProvider?.WikiHiddenOptionKeys; + if (options == null) { return string.Empty; } - ISummarizedOptions? summaryProvider; - IReadOnlySet? hiddenKeys; - try - { - var optionGroups = - AccessTools.Field(typeof(ModdedOptionsManager), "Groups").GetValue(null) as List; - summaryProvider = - optionGroups?.FirstOrDefault(x => x.OptionableType == classType) as ISummarizedOptions; - hiddenKeys = summaryProvider?.WikiHiddenOptionKeys; - } - catch - { - summaryProvider = null; - hiddenKeys = null; - } - - var builder = new StringBuilder(); - builder.AppendLine( - MiraApiPlugin.Culture, - $"\n \n{MiraLocaleManager.Get("Options")}"); - + var lines = new List(); var insertedSummary = false; foreach (var option in options) { - if (!insertedSummary && summaryProvider != null && hiddenKeys != null) + StringNames? key = option switch { - StringNames? key = option switch - { - ModdedToggleOption t => t.StringName, - ModdedEnumOption e => e.StringName, - ModdedNumberOption n => n.StringName, - _ => null, - }; - if (key.HasValue && hiddenKeys.Contains(key.Value)) - { - foreach (var line in summaryProvider.GetWikiOptionSummaryLines()) - { - if (!string.IsNullOrWhiteSpace(line)) - { - builder.AppendLine(line); - } - } + ModdedToggleOption t => t.StringName, + ModdedEnumOption e => e.StringName, + ModdedNumberOption n => n.StringName, + _ => null, + }; - insertedSummary = true; + if (!insertedSummary && summaryProvider != null && hiddenKeys != null && + key.HasValue && hiddenKeys.Contains(key.Value)) + { + foreach (var line in summaryProvider.GetWikiOptionSummaryLines()) + { + if (!string.IsNullOrWhiteSpace(line)) + { + lines.Add(line); + } } + + insertedSummary = true; + } + + if (!option.Visible()) + { + continue; + } + + if (key.HasValue && hiddenKeys?.Contains(key.Value) == true) + { + continue; } switch (option) { case ModdedToggleOption toggleOption: - if (!toggleOption.Visible()) - { - continue; - } - - if (hiddenKeys != null && hiddenKeys.Contains(toggleOption.StringName)) - { - continue; - } - - builder.AppendLine( + lines.Add( TranslationController.Instance.GetString(toggleOption.StringName) + ": " + toggleOption.Value); break; @@ -118,65 +103,49 @@ public static class Helpers builder.AppendLine(enumOption.Title + ": " + enumOption.Values[enumOption.Value]); break;*/ case ModdedEnumOption enumOption: - if (!enumOption.Visible()) - { - continue; - } - - if (hiddenKeys != null && hiddenKeys.Contains(enumOption.StringName)) - { - continue; - } - - builder.AppendLine( + lines.Add( TranslationController.Instance.GetString(enumOption.StringName) + ": " + MiraLocaleManager.Get( enumOption.Values[enumOption.Value], enumOption.Values[enumOption.Value])); break; case ModdedNumberOption numberOption: - if (!numberOption.Visible()) - { - continue; - } - - if (hiddenKeys != null && hiddenKeys.Contains(numberOption.StringName)) - { - continue; - } - - var optionStr = numberOption.Data.GetValueString(numberOption.Value); - if (optionStr.Contains(".000")) - { - optionStr = optionStr.Replace(".000", string.Empty); - } - else if (optionStr.Contains(".00")) - { - optionStr = optionStr.Replace(".00", string.Empty); - } - else if (optionStr.Contains(".0")) - { - optionStr = optionStr.Replace(".0", string.Empty); - } + var optionStr = TrailingZeroRegex.Replace( + numberOption.Data.GetValueString(numberOption.Value), + string.Empty); var title = TranslationController.Instance.GetString(numberOption.StringName); if (numberOption is { NegativeWordValue: not "#", Value: -1 }) { - builder.AppendLine(title + $": {numberOption.NegativeWordValue}"); + lines.Add(title + $": {numberOption.NegativeWordValue}"); } else if (numberOption is { ZeroWordValue: not "#", Value: 0 }) { - builder.AppendLine(title + $": {numberOption.ZeroWordValue}"); + lines.Add(title + $": {numberOption.ZeroWordValue}"); } else { - builder.AppendLine(title + ": " + optionStr); + lines.Add(title + ": " + optionStr); } break; } } + if (lines.Count == 0) + { + return string.Empty; + } + + var builder = new StringBuilder(); + builder.AppendLine( + MiraApiPlugin.Culture, + $"\n \n{MiraLocaleManager.Get("Options")}"); + foreach (var line in lines) + { + builder.AppendLine(line); + } + return builder.ToString(); } From d827e5d1f996d476d1813ee5b8d735cb7e01c9d9 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 01:40:35 -0700 Subject: [PATCH 04/28] Tidy role guide sorting, refresh and layout code --- MiraAPI/Patches/Options/OptionsPatches.cs | 1 - MiraAPI/Patches/RoleGuidePatches.cs | 665 ++++++++-------------- MiraAPI/Utilities/Helpers.cs | 3 +- 3 files changed, 248 insertions(+), 421 deletions(-) diff --git a/MiraAPI/Patches/Options/OptionsPatches.cs b/MiraAPI/Patches/Options/OptionsPatches.cs index 8ed0f7d..0b7f5b9 100644 --- a/MiraAPI/Patches/Options/OptionsPatches.cs +++ b/MiraAPI/Patches/Options/OptionsPatches.cs @@ -4,7 +4,6 @@ using MiraAPI.Roles; using MiraAPI.Translation; using MiraAPI.Utilities; using UnityEngine; -using Object = Il2CppSystem.Object; namespace MiraAPI.Patches.Options; diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 1a18950..da284a7 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -23,6 +23,12 @@ namespace MiraAPI.Patches; [HarmonyPatch] public static class RoleGuidePatches { + private const int RolesTabIndex = 2; + private const int ModifiersTabIndex = 3; + + private static readonly Dictionary RolePanels = []; + private static readonly Dictionary ModifierPanels = []; + public static PassiveButton SearchIconButton; private static SpriteRenderer _searchIconIdle; private static SpriteRenderer _searchIconHover; @@ -36,10 +42,18 @@ public static class RoleGuidePatches private static SpriteRenderer _searchSortingFilterIdle; private static SpriteRenderer _searchSortingFilterHover; private static Scroller _rolesScroller; - private static bool _needsRoleRefresh; - private static bool _needsModifierRefresh; - private static bool _needsToggledRoleRefresh; - private static bool _needsToggledModifierRefresh; + private static Scroller _modifiersScroller; + private static Scroller _advancedInfoTabScroller; + private static TextBoxTMP _searchBoxTmp; + private static GameObject _currentAdvancedTabObject; + private static TextMeshPro _titleText; + private static bool _rolesTabDirty; + private static bool _modifiersTabDirty; + private static SortingFilter _sortFilter = SortingFilter.EnabledOnly; + private static SortingMethod _sortMethod = SortingMethod.Alphabetical; + private static SortingGroups _sortGrouping = SortingGroups.Ungrouped; + + private static bool IsModifiersTabActive => MatchInfoGuide.Instance.activeTabIndex == ModifiersTabIndex; [HarmonyPostfix] [HarmonyPriority(Priority.First)] @@ -47,20 +61,25 @@ public static class RoleGuidePatches [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.CreateHnSModeSettings))] public static void CreateNormalModeSettings(MatchInfoGuide __instance) { - __instance.MatchInfoRoleMaskArea.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); + ApplyPanelLayout(__instance); + } - __instance.matchInfoPlayersMaskArea.transform.localPosition = - __instance.matchInfoSettingsMaskArea.transform.localPosition = new Vector3(1.22f, -0.335f, -0.1f); + private static void ApplyPanelLayout(MatchInfoGuide instance) + { + instance.MatchInfoRoleMaskArea.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); - __instance.matchInfoPlayersMaskArea.size = - __instance.matchInfoSettingsMaskArea.size = - __instance.MatchInfoRoleMaskArea.size = new Vector2(-6, 1.8f); + instance.matchInfoPlayersMaskArea.transform.localPosition = + instance.matchInfoSettingsMaskArea.transform.localPosition = new Vector3(1.22f, -0.335f, -0.1f); - var playerMenuGradient = __instance.matchInfoPlayersMaskArea.transform.parent.GetAllChildren() + instance.matchInfoPlayersMaskArea.size = + instance.matchInfoSettingsMaskArea.size = + instance.MatchInfoRoleMaskArea.size = new Vector2(-6, 1.8f); + + var playerMenuGradient = instance.matchInfoPlayersMaskArea.transform.parent.GetAllChildren() .First(x => x.name.Contains("BG_Gradient")); - var settingsenuGradient = __instance.matchInfoSettingsMaskArea.transform.parent.GetAllChildren() + var settingsenuGradient = instance.matchInfoSettingsMaskArea.transform.parent.GetAllChildren() .First(x => x.name.Contains("BG_Gradient")); - var roleMenuGradient = __instance.MatchInfoRoleMaskArea.transform.parent.GetAllChildren() + var roleMenuGradient = instance.MatchInfoRoleMaskArea.transform.parent.GetAllChildren() .First(x => x.name.Contains("BG_Gradient")); playerMenuGradient .GetComponent() @@ -71,8 +90,6 @@ public static class RoleGuidePatches .maskInteraction = SpriteMaskInteraction.VisibleInsideMask; roleMenuGradient.transform.localScale = new Vector3(0.5297f, 0.1565f, 1); roleMenuGradient.transform.localPosition = new Vector3(0, -0.62f, -5); - var wikiTab = Object.Instantiate(__instance.settingsTabs[2], __instance.settingsTabs[2].transform.parent); - _advancedInfoTabScroller = wikiTab.GetComponent(); } [HarmonyPrefix] @@ -112,18 +129,18 @@ public static class RoleGuidePatches var enabled = ActiveInputManager.currentControlType == ActiveInputManager.InputType.Joystick; __instance.glyphL.enabled = enabled; __instance.glyphR.enabled = enabled; - if (!titleText) + if (!_titleText) { - titleText = __instance.transitionOpen.transform.FindChild("Text_Title")?.GetComponent()!; - if (titleText) + _titleText = __instance.transitionOpen.transform.FindChild("Text_Title")?.GetComponent()!; + if (_titleText) { - titleText.transform.GetComponent().Destroy(); + _titleText.transform.GetComponent().Destroy(); } } - if (titleText) + if (_titleText) { - titleText.text = TranslationController.Instance.GetString(StringNames.MatchInfoGuideTitle); + _titleText.text = TranslationController.Instance.GetString(StringNames.MatchInfoGuideTitle); } var regGame = GameManager.Instance.TryCast() != null; @@ -135,7 +152,7 @@ public static class RoleGuidePatches HudManager.Instance.SettingsButton, __instance.TabButtons[2].transform.parent); SearchSortingOrderButton = sortingOrderButton.GetComponent(); - FixSearchButton( + SetupSearchButton( SearchSortingOrderButton, "SearchSortingOrderButton", SwitchMethod, @@ -149,7 +166,7 @@ public static class RoleGuidePatches sortingOrderButton, sortingOrderButton.transform.parent); SearchSortingGroupButton = sortingGroupButton.GetComponent(); - FixSearchButton( + SetupSearchButton( SearchSortingGroupButton, "SearchSortingGroupButton", SwitchGrouping, @@ -163,7 +180,7 @@ public static class RoleGuidePatches sortingOrderButton, sortingOrderButton.transform.parent); SearchSortingFilterButton = sortingFilterButton.GetComponent(); - FixSearchButton( + SetupSearchButton( SearchSortingFilterButton, "SearchSortingFilterButton", SwitchFilter, @@ -175,36 +192,13 @@ public static class RoleGuidePatches __instance.numOfTabs = 4; __instance.TabButtons[2].SelectButton(true); - __instance.MatchInfoRoleMaskArea.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); - - __instance.matchInfoPlayersMaskArea.transform.localPosition = - __instance.matchInfoSettingsMaskArea.transform.localPosition = new Vector3(1.22f, -0.335f, -0.1f); - - __instance.matchInfoPlayersMaskArea.size = - __instance.matchInfoSettingsMaskArea.size = - __instance.MatchInfoRoleMaskArea.size = new Vector2(-6, 1.8f); - - var playerMenuGradient = __instance.matchInfoPlayersMaskArea.transform.parent.GetAllChildren() - .First(x => x.name.Contains("BG_Gradient")); - var settingsenuGradient = __instance.matchInfoSettingsMaskArea.transform.parent.GetAllChildren() - .First(x => x.name.Contains("BG_Gradient")); - var roleMenuGradient = __instance.MatchInfoRoleMaskArea.transform.parent.GetAllChildren() - .First(x => x.name.Contains("BG_Gradient")); - playerMenuGradient - .GetComponent() - .maskInteraction = - settingsenuGradient.GetComponent() - .maskInteraction = - roleMenuGradient.GetComponent() - .maskInteraction = SpriteMaskInteraction.VisibleInsideMask; - roleMenuGradient.transform.localScale = new Vector3(0.5297f, 0.1565f, 1); - roleMenuGradient.transform.localPosition = new Vector3(0, -0.62f, -5); + ApplyPanelLayout(__instance); __instance.MatchInfoRoleMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); __instance.matchInfoSettingsMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); var modifiersTab = Object.Instantiate( - __instance.settingsTabs[2], - __instance.settingsTabs[2].transform.parent); + __instance.settingsTabs[RolesTabIndex], + __instance.settingsTabs[RolesTabIndex].transform.parent); modifiersTab.name = "ModifiersPanel"; modifiersTab.transform.FindChild("MaskArea")?.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); @@ -214,8 +208,8 @@ public static class RoleGuidePatches __instance.settingsTabs.Add(modifiersTab); var wikiTab = Object.Instantiate( - __instance.settingsTabs[2], - __instance.settingsTabs[2].transform.parent); + __instance.settingsTabs[RolesTabIndex], + __instance.settingsTabs[RolesTabIndex].transform.parent); wikiTab.name = "AdvancedWikiPanels"; wikiTab.transform.FindChild("MaskArea")?.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); wikiTab.transform.GetAllChildren() @@ -228,7 +222,7 @@ public static class RoleGuidePatches sortingOrderButton, sortingOrderButton.transform.parent); SearchIconButton = searchIconButton.GetComponent(); - FixSearchButton( + SetupSearchButton( SearchIconButton, "SearchIconButton", ClearSearchText, @@ -265,18 +259,17 @@ public static class RoleGuidePatches disabledSprite.gameObject.SetActive(true); var button = searchBox.AddComponent(); button.OnUp = true; - searchBoxTmp = searchBox.AddComponent(); - searchBoxTmp.outputText = tmpText; - button.OnClick.AddListener((UnityAction)(() => { searchBoxTmp.GiveFocus(); })); + _searchBoxTmp = searchBox.AddComponent(); + _searchBoxTmp.outputText = tmpText; + button.OnClick.AddListener((UnityAction)(() => { _searchBoxTmp.GiveFocus(); })); button.OnMouseOver = new UnityEvent(); button.OnMouseOut = new UnityEvent(); - _rolesScroller = __instance.settingsTabs[2].GetComponent(); - searchBoxTmp.OnChange = new Button.ButtonClickedEvent(); - searchBoxTmp.OnChange.AddListener( + _rolesScroller = __instance.settingsTabs[RolesTabIndex].GetComponent(); + _searchBoxTmp.OnChange = new Button.ButtonClickedEvent(); + _searchBoxTmp.OnChange.AddListener( (UnityAction)(() => { - var text = searchBoxTmp.outputText.text; - if (text != string.Empty) + if (GetSearchText() != string.Empty) { _searchIconIdle.sprite = MiraAssets.SearchClearIdleSprite; _searchIconHover.sprite = MiraAssets.SearchClearHoverSprite; @@ -287,35 +280,26 @@ public static class RoleGuidePatches _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; } - if (MatchInfoGuide.Instance.activeTabIndex != 3) - { - SortAllRoles(text); - _rolesScroller.ScrollToTop(); - } - else - { - SortAllModifiers(text); - _modifiersScroller.ScrollToTop(); - } + RefreshActiveTab(true); })); - searchBoxTmp.transform.localPosition = new Vector3(-1.438f, 0.756f, -0.2f); + _searchBoxTmp.transform.localPosition = new Vector3(-1.438f, 0.756f, -0.2f); var playerButton = __instance.TabButtons[0]; - FixTabButton( + SetupTabButton( playerButton, MiraAssets.WikiPlayersButtonIdleSprite, MiraAssets.WikiPlayersButtonOpenSprite, MiraAssets.WikiPlayersButtonHoverSprite); var settingButton = __instance.TabButtons[1]; - FixTabButton( + SetupTabButton( settingButton, MiraAssets.WikiSettingsButtonIdleSprite, MiraAssets.WikiSettingsButtonOpenSprite, MiraAssets.WikiSettingsButtonHoverSprite); var rolesButton = __instance.TabButtons[2]; - FixTabButton( + SetupTabButton( rolesButton, MiraAssets.WikiRolesButtonIdleSprite, MiraAssets.WikiRolesButtonOpenSprite, @@ -326,7 +310,7 @@ public static class RoleGuidePatches var modifiersButton = Object.Instantiate( __instance.TabButtons[2], __instance.TabButtons[2].transform.parent); - FixTabButton( + SetupTabButton( modifiersButton, MiraAssets.WikiRolesButtonIdleSprite, MiraAssets.WikiRolesButtonOpenSprite, @@ -362,11 +346,11 @@ public static class RoleGuidePatches var instance2 = ControllerManager.Instance; var controllerSelectable2 = __instance.ControllerSelectable; instance2.SetCurrentSelected(controllerSelectable2[^1]); - __instance.SetActiveTab(regGame ? 2 : 0); + __instance.SetActiveTab(regGame ? RolesTabIndex : 0); return false; } - public static void FixTabButton(MatchInfoGuideTabButton button, Sprite inactive, Sprite selected, Sprite hover) + private static void SetupTabButton(MatchInfoGuideTabButton button, Sprite inactive, Sprite selected, Sprite hover) { var buttonCollider = button.GetComponent(); button.transform.GetChild(0).gameObject.SetActive(false); @@ -385,7 +369,7 @@ public static class RoleGuidePatches button.transform.localScale = new Vector3(0.7f, 0.7f, 1); } - public static void FixSearchButton(PassiveButton button, string objName, Action onClick, Sprite inactive, Sprite hover, Vector3 pos, out SpriteRenderer inactiveSprite, out SpriteRenderer hoverSprite) + private static void SetupSearchButton(PassiveButton button, string objName, Action onClick, Sprite inactive, Sprite hover, Vector3 pos, out SpriteRenderer inactiveSprite, out SpriteRenderer hoverSprite) { button.name = objName; inactiveSprite = button.inactiveSprites.GetComponent(); @@ -403,125 +387,118 @@ public static class RoleGuidePatches public static void OpenModifiersTab() { _advancedInfoTabScroller?.gameObject.SetActive(false); - MatchInfoGuide.Instance.SetActiveTab(3); - if (_needsToggledModifierRefresh) + MatchInfoGuide.Instance.SetActiveTab(ModifiersTabIndex); + if (_modifiersTabDirty) { ToggleModifierVisibility(); + _modifiersTabDirty = false; } - - if (_needsModifierRefresh) - { - var text = string.Empty; - try - { - text = searchBoxTmp.outputText.text; - } - catch - { - // ignored - } - - SortAllModifiers(text); - } - - _needsModifierRefresh = false; - _needsToggledModifierRefresh = false; } public static void OpenRolesTab() { _advancedInfoTabScroller?.gameObject.SetActive(false); - MatchInfoGuide.Instance.SetActiveTab(2); - if (_needsToggledRoleRefresh) + MatchInfoGuide.Instance.SetActiveTab(RolesTabIndex); + if (_rolesTabDirty) { ToggleRoleVisibility(); + _rolesTabDirty = false; } - - if (_needsRoleRefresh) - { - var text = string.Empty; - try - { - text = searchBoxTmp.outputText.text; - } - catch - { - // ignored - } - - SortAllRoles(text); - } - - _needsRoleRefresh = false; - _needsToggledRoleRefresh = false; } - private static readonly Dictionary RolePanels = []; - private static readonly Dictionary ModifierPanels = []; - private static Scroller _modifiersScroller; - private static Scroller _advancedInfoTabScroller; - private static TextBoxTMP searchBoxTmp; - private static GameObject currentAdvancedTabObject; - private static TextMeshPro titleText; - public sealed class DetailedPanel(MatchInfoRolePanel panel, string title, string category, string modId) { - public MatchInfoRolePanel Panel = panel; - public string Title = title; - public string Category = category; - public string ModId = modId; + public MatchInfoRolePanel Panel { get; } = panel; + public string Title { get; } = title; + public string Category { get; } = category; + public string ModId { get; } = modId; public int Amount { get; set; } public int Chance { get; set; } - public float LikelyhoodOfRole { get; set; } + public float Likelihood { get; set; } + + public string GetSortKey(SortingGroups grouping) + { + return grouping switch + { + SortingGroups.Faction => + $"{Category} ({Title}) {Likelihood.ToString("0000.0", CultureInfo.InvariantCulture)}", + SortingGroups.AmountChance => + $"{Likelihood.ToString("0000.0", CultureInfo.InvariantCulture)} {Title} ({Category})", + _ => + $"{Title} ({Category}) {Likelihood.ToString("0000.0", CultureInfo.InvariantCulture)}", + }; + } } - private static SortingFilter sortFilter = SortingFilter.EnabledOnly; - private static SortingMethod sortMethod = SortingMethod.Alphabetical; - private static SortingGroups sortOrder = SortingGroups.Ungrouped; + private static string GetSearchText() + { + return _searchBoxTmp && _searchBoxTmp.outputText ? _searchBoxTmp.outputText.text : string.Empty; + } + + private static bool ShouldShowPanel(bool? forceShow, bool isDisabled, bool isHidden) + { + return forceShow ?? + !(isHidden || (isDisabled && _sortFilter is SortingFilter.EnabledOnly) || + (!isDisabled && _sortFilter is SortingFilter.DisabledOnly)); + } + + private static float GetGridScrollBounds(int count) + { + return Mathf.Clamp(Mathf.Ceil(count / 2f) * 1.3f - 1.5f, 0f, 999f); + } + + private static void RefreshActiveTab(bool scrollToTop) + { + if (IsModifiersTabActive) + { + ToggleModifierVisibility(); + if (scrollToTop) + { + _modifiersScroller.ScrollToTop(); + } + + _modifiersTabDirty = false; + _rolesTabDirty = true; + } + else + { + ToggleRoleVisibility(); + if (scrollToTop) + { + _rolesScroller.ScrollToTop(); + } + + _rolesTabDirty = false; + _modifiersTabDirty = true; + } + } public static void ClearSearchText() { _searchIconIdle.sprite = MiraAssets.SearchIconIdleSprite; _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; - try + if (_searchBoxTmp) { - searchBoxTmp.outputText.SetText(string.Empty); - searchBoxTmp.SetText(string.Empty); - } - catch - { - // ignored + _searchBoxTmp.outputText.SetText(string.Empty); + _searchBoxTmp.SetText(string.Empty); } - var text = string.Empty; - - if (MatchInfoGuide.Instance.activeTabIndex != 3) - { - SortAllRoles(text); - _rolesScroller.ScrollToTop(); - _needsModifierRefresh = true; - } - else - { - SortAllModifiers(text); - _modifiersScroller.ScrollToTop(); - _needsRoleRefresh = true; - } + RefreshActiveTab(true); } public static void SwitchFilter() { - var stepUp = (SortingFilter)((int)sortFilter + 1); + var stepUp = (SortingFilter)((int)_sortFilter + 1); if (Enum.IsDefined(stepUp)) { - sortFilter = stepUp; + _sortFilter = stepUp; } else { - sortFilter = SortingFilter.EnabledOnly; + _sortFilter = SortingFilter.EnabledOnly; } - switch (sortFilter) + switch (_sortFilter) { case SortingFilter.AllRoles: _searchSortingFilterIdle.sprite = MiraAssets.VisibleAllIdleSprite; @@ -537,72 +514,44 @@ public static class RoleGuidePatches break; } - if (MatchInfoGuide.Instance.activeTabIndex != 3) - { - ToggleRoleVisibility(); - _needsToggledModifierRefresh = _needsModifierRefresh = true; - } - else - { - ToggleModifierVisibility(); - _needsToggledRoleRefresh = _needsRoleRefresh = true; - } + RefreshActiveTab(false); } public static void SwitchMethod() { - var stepUp = (SortingMethod)((int)sortMethod + 1); + var stepUp = (SortingMethod)((int)_sortMethod + 1); if (Enum.IsDefined(stepUp)) { - sortMethod = stepUp; + _sortMethod = stepUp; } else { - sortMethod = SortingMethod.Alphabetical; + _sortMethod = SortingMethod.Alphabetical; } - _searchSortingOrderIdle.sprite = sortMethod is SortingMethod.Alphabetical + _searchSortingOrderIdle.sprite = _sortMethod is SortingMethod.Alphabetical ? MiraAssets.SortingAzIdleSprite : MiraAssets.SortingZaIdleSprite; - _searchSortingOrderHover.sprite = sortMethod is SortingMethod.Alphabetical + _searchSortingOrderHover.sprite = _sortMethod is SortingMethod.Alphabetical ? MiraAssets.SortingAzHoverSprite : MiraAssets.SortingZaHoverSprite; - var text = string.Empty; - try - { - text = searchBoxTmp.outputText.text; - } - catch - { - // ignored - } - - if (MatchInfoGuide.Instance.activeTabIndex != 3) - { - SortAllRoles(text); - _needsModifierRefresh = true; - } - else - { - SortAllModifiers(text); - _needsRoleRefresh = true; - } + RefreshActiveTab(false); } public static void SwitchGrouping() { - var stepUp = (SortingGroups)((int)sortOrder + 1); + var stepUp = (SortingGroups)((int)_sortGrouping + 1); if (Enum.IsDefined(stepUp)) { - sortOrder = stepUp; + _sortGrouping = stepUp; } else { - sortOrder = SortingGroups.Ungrouped; + _sortGrouping = SortingGroups.Ungrouped; } - switch (sortOrder) + switch (_sortGrouping) { case SortingGroups.Faction: _searchSortingGroupIdle.sprite = MiraAssets.GroupFactionIdleSprite; @@ -618,31 +567,29 @@ public static class RoleGuidePatches break; } - var text = string.Empty; - try - { - text = searchBoxTmp.outputText.text; - } - catch - { - // ignored - } + RefreshActiveTab(false); + } - if (MatchInfoGuide.Instance.activeTabIndex != 3) - { - SortAllRoles(text); - _needsModifierRefresh = true; - } - else - { - SortAllModifiers(text); - _needsRoleRefresh = true; - } + private static void SetupPanelButton(MatchInfoRolePanel panel, Action onClick) + { + var 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(); + passiveButton.ClickSound = HudManager.Instance.MapButton.ClickSound; + passiveButton.OnMouseOver = new UnityEvent(); + passiveButton.OnMouseOver.AddListener( + (UnityAction)(() => { panel.roleIcon.color = new Color32(255, 255, 255, 150); })); + passiveButton.OnMouseOut = new UnityEvent(); + passiveButton.OnMouseOut.AddListener((UnityAction)(() => { panel.roleIcon.color = Color.white; })); + + passiveButton.OnClick = new Button.ButtonClickedEvent(); + passiveButton.OnClick.AddListener(onClick); } public static void DisplayNormalRoleSettings(MatchInfoGuide instance, bool reset) { - var inner = instance.settingsTabs[2].GetComponent().Inner; + var inner = instance.settingsTabs[RolesTabIndex].GetComponent().Inner; if (reset) { RolePanels.Clear(); @@ -672,7 +619,6 @@ public static class RoleGuidePatches StringNames.GameTaskBarMode, GameManager.Instance.LogicOptions.GetTaskBarMode().ToString()); - var hoverColor = new Color32(255, 255, 255, 150); foreach (var modifier in ModifierManager.Modifiers.OfType()) { var panel = Object.Instantiate( @@ -686,18 +632,7 @@ public static class RoleGuidePatches modifier, amount, chance); - var 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(); - passiveButton.ClickSound = HudManager.Instance.MapButton.ClickSound; - passiveButton.OnMouseOver = new UnityEvent(); - passiveButton.OnMouseOver.AddListener((UnityAction)(() => { panel.roleIcon.color = hoverColor; })); - passiveButton.OnMouseOut = new UnityEvent(); - passiveButton.OnMouseOut.AddListener((UnityAction)(() => { panel.roleIcon.color = Color.white; })); - - passiveButton.OnClick = new Button.ButtonClickedEvent(); - passiveButton.OnClick.AddListener((Action)(() => { DisplayAdvancedWiki(instance, modifier); })); + SetupPanelButton(panel, () => { DisplayAdvancedWiki(instance, modifier); }); ModifierPanels.Add( modifier, new DetailedPanel( @@ -738,19 +673,7 @@ public static class RoleGuidePatches roleBehaviour, amount, chance); - var 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(); - passiveButton.ClickSound = HudManager.Instance.MapButton.ClickSound; - passiveButton.OnMouseOver = new UnityEvent(); - passiveButton.OnMouseOver.AddListener((UnityAction)(() => { panel.roleIcon.color = hoverColor; })); - passiveButton.OnMouseOut = new UnityEvent(); - passiveButton.OnMouseOut.AddListener((UnityAction)(() => { panel.roleIcon.color = Color.white; })); - - passiveButton.OnClick = new Button.ButtonClickedEvent(); - passiveButton.OnClick.AddListener( - (Action)(() => { DisplayAdvancedWiki(instance, roleBehaviour); })); + SetupPanelButton(panel, () => { DisplayAdvancedWiki(instance, roleBehaviour); }); RolePanels.Add( roleBehaviour, new DetailedPanel( @@ -764,13 +687,22 @@ public static class RoleGuidePatches _advancedInfoTabScroller?.gameObject.SetActive(false); - if (MatchInfoGuide.Instance.activeTabIndex != 3) + if (reset) { - ToggleRoleVisibility(); + if (IsModifiersTabActive) + { + ToggleModifierVisibility(); + } + else + { + ToggleRoleVisibility(); + } } else { - ToggleModifierVisibility(); + _rolesTabDirty = true; + _modifiersTabDirty = true; + RefreshActiveTab(false); } if (reset) @@ -790,25 +722,16 @@ public static class RoleGuidePatches modifier.GetAmountPerGame(); var chance = modifier.GetAssignmentChance(); - var forciblyShow = modifier.ForceShowModifierOnWiki switch - { - true => 2, - false => 1, - _ => 0, - }; panel.SetModifierPanel( modifier, amount, chance); holder.Amount = amount; holder.Chance = chance; - holder.LikelyhoodOfRole = amount * chance; + holder.Likelihood = amount * chance; var isRoleDisabled = amount == 0 || chance == 0; var isRoleNotVisible = !modifier.CanSpawnOnCurrentMode() || modifier.GetDescription() == string.Empty; - if ((forciblyShow == 0 && ((isRoleDisabled && sortFilter is SortingFilter.EnabledOnly) || - (!isRoleDisabled && sortFilter is SortingFilter.DisabledOnly) || - isRoleNotVisible)) - || forciblyShow == 1) + if (!ShouldShowPanel(modifier.ForceShowModifierOnWiki, isRoleDisabled, isRoleNotVisible)) { panel.gameObject.SetActive(false); continue; @@ -818,23 +741,12 @@ public static class RoleGuidePatches num++; } - var text = string.Empty; - try - { - text = searchBoxTmp.outputText.text; - } - catch - { - // ignored - } - - SortAllModifiers(text); - _needsRoleRefresh = true; + SortAllModifiers(GetSearchText()); /*var instance = MatchInfoGuide.Instance; instance.rolesEnabledMessage.SetActive(num == 0);*/ - _modifiersScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); + _modifiersScroller.SetYBoundsMax(GetGridScrollBounds(num)); } public static void ToggleRoleVisibility() @@ -845,15 +757,12 @@ public static class RoleGuidePatches var panel = holder.Panel; int amount; int chance; - int forciblyShow = 0; + bool? forceShow = null; if (roleData is ICustomRole custom) { amount = custom.GetCount().GetValueOrDefault(0); chance = custom.GetChance().GetValueOrDefault(0); - if (custom.ForceShowRoleOnWiki.HasValue) - { - forciblyShow = custom.ForceShowRoleOnWiki.Value ? 2 : 1; - } + forceShow = custom.ForceShowRoleOnWiki; } else { @@ -869,15 +778,12 @@ public static class RoleGuidePatches chance); holder.Amount = amount; holder.Chance = chance; - holder.LikelyhoodOfRole = amount * chance; + holder.Likelihood = amount * chance; var isRoleDisabled = amount == 0 || chance == 0; var isRoleNotVisible = (roleData is ICustomRole custom2 && !custom2.CanSpawnOnCurrentMode()) || (Enum.IsDefined(roleData.Role) && roleData.IsRoleBlacklisted()); - if ((forciblyShow == 0 && ((isRoleDisabled && sortFilter is SortingFilter.EnabledOnly) || - (!isRoleDisabled && sortFilter is SortingFilter.DisabledOnly) || - isRoleNotVisible)) - || forciblyShow == 1) + if (!ShouldShowPanel(forceShow, isRoleDisabled, isRoleNotVisible)) { panel.gameObject.SetActive(false); continue; @@ -887,117 +793,40 @@ public static class RoleGuidePatches num++; } - var text = string.Empty; - try - { - text = searchBoxTmp.outputText.text; - } - catch - { - // ignored - } - - SortAllRoles(text); + SortAllRoles(GetSearchText()); var instance = MatchInfoGuide.Instance; instance.rolesEnabledMessage.SetActive(num == 0); - instance.MatchInfoRoleScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); + instance.MatchInfoRoleScroller.SetYBoundsMax(GetGridScrollBounds(num)); } public static void SortAllRoles(string searchText) { - var newSorted = sortMethod is SortingMethod.Alphabetical - ? RolePanels - .OrderByDescending(child => - child.Value.GetTitle().Equals(searchText, StringComparison.OrdinalIgnoreCase)) - .ThenByDescending(child => child.Value.GetTitle().Contains( - searchText, - StringComparison.InvariantCultureIgnoreCase)) - .ThenBy(GetSortingOrder()) - : RolePanels - .OrderByDescending(child => - child.Value.GetTitle().Equals(searchText, StringComparison.OrdinalIgnoreCase)) - .ThenByDescending(child => child.Value.GetTitle().Contains( - searchText, - StringComparison.InvariantCultureIgnoreCase)) - .ThenByDescending(GetSortingOrder()); - - foreach (var pair in newSorted) - { - pair.Value.Panel.transform.SetAsLastSibling(); - } + SortPanels(RolePanels, searchText); } public static void SortAllModifiers(string searchText) { - var newSorted = sortMethod is SortingMethod.Alphabetical - ? ModifierPanels - .OrderByDescending(child => - child.Value.GetTitle().Equals(searchText, StringComparison.OrdinalIgnoreCase)) - .ThenByDescending(child => child.Value.GetTitle().Contains( - searchText, - StringComparison.InvariantCultureIgnoreCase)) - .ThenBy(GetModifierSortingOrder()) - : ModifierPanels - .OrderByDescending(child => - child.Value.GetTitle().Equals(searchText, StringComparison.OrdinalIgnoreCase)) - .ThenByDescending(child => child.Value.GetTitle().Contains( - searchText, - StringComparison.InvariantCultureIgnoreCase)) - .ThenByDescending(GetModifierSortingOrder()); - - foreach (var pair in newSorted) - { - pair.Value.Panel.transform.SetAsLastSibling(); - } + SortPanels(ModifierPanels, searchText); } - public static Func, string> GetSortingOrder() + private static void SortPanels(Dictionary panels, string searchText) + where T : notnull { - switch (sortOrder) - { - case SortingGroups.Faction: - return x => - $"{x.Value.Category} ({x.Value.Title}) {x.Value.LikelyhoodOfRole.ToString("0000.0", CultureInfo.InvariantCulture)}"; - case SortingGroups.AmountChance: - return x => - $"{x.Value.LikelyhoodOfRole.ToString("0000.0", CultureInfo.InvariantCulture)} {x.Value.Title} ({x.Value.Category})"; - default: - return x => - $"{x.Value.Title} ({x.Value.Category}) {x.Value.LikelyhoodOfRole.ToString("0000.0", CultureInfo.InvariantCulture)}"; - } - } + var sorted = panels.Values + .OrderByDescending(p => p.Title.Equals(searchText, StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(p => + p.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase) || + p.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase)); - public static Func, string> GetModifierSortingOrder() - { - switch (sortOrder) - { - case SortingGroups.Faction: - return x => - $"{x.Value.Category} ({x.Value.Title}) {x.Value.LikelyhoodOfRole.ToString("0000.0", CultureInfo.InvariantCulture)}"; - case SortingGroups.AmountChance: - return x => - $"{x.Value.LikelyhoodOfRole.ToString("0000.0", CultureInfo.InvariantCulture)} {x.Value.Title} ({x.Value.Category})"; - default: - return x => - $"{x.Value.Title} ({x.Value.Category}) {x.Value.LikelyhoodOfRole.ToString("0000.0", CultureInfo.InvariantCulture)}"; - } - } + var ordered = _sortMethod is SortingMethod.Alphabetical + ? sorted.ThenBy(p => p.GetSortKey(_sortGrouping)) + : sorted.ThenByDescending(p => p.GetSortKey(_sortGrouping)); - public static string GetTitle(this DetailedPanel panel) - { - switch (sortOrder) + foreach (var panel in ordered) { - case SortingGroups.Faction: - return - $"{panel.Category} ({panel.Title}) {panel.LikelyhoodOfRole.ToString("0000.0", CultureInfo.InvariantCulture)}"; - case SortingGroups.AmountChance: - return - $"{panel.LikelyhoodOfRole.ToString("0000.0", CultureInfo.InvariantCulture)} {panel.Title} ({panel.Category})"; - default: - return - $"{panel.Title} ({panel.Category}) {panel.LikelyhoodOfRole.ToString("0000.0", CultureInfo.InvariantCulture)}"; + panel.Panel.transform.SetAsLastSibling(); } } @@ -1021,10 +850,13 @@ public static class RoleGuidePatches component.NameText.text += $"\n{networkedPlayerInfo.GetPlayerColorString()}"; var namePlate = HatManager.Instance.GetNamePlateById(networkedPlayerInfo.DefaultOutfit.NamePlateId); - __instance.StartCoroutine( - __instance.CoLoadAssetAsync( - namePlate.GetAssetReference(), - (Action?)LoadNameplate)); + if (namePlate != null) + { + __instance.StartCoroutine( + __instance.CoLoadAssetAsync( + namePlate.GetAssetReference(), + (Action?)LoadNameplate)); + } void LoadNameplate(NamePlateViewData viewData) { @@ -1042,24 +874,24 @@ public static class RoleGuidePatches [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.SetActiveTab))] private static void SetActiveTab(int tabIndex) { - if (titleText) + if (_titleText) { switch (tabIndex) { case 0: - titleText.text = MiraLocaleManager.Get("MiraApi.Wiki.PlayersTab"); + _titleText.text = MiraLocaleManager.Get("MiraApi.Wiki.PlayersTab"); break; case 1: - titleText.text = MiraLocaleManager.Get("MiraApi.Wiki.SettingsTab"); + _titleText.text = MiraLocaleManager.Get("MiraApi.Wiki.SettingsTab"); break; - case 2: - titleText.text = MiraLocaleManager.Get("Roles"); + case RolesTabIndex: + _titleText.text = MiraLocaleManager.Get("Roles"); break; - case 3: - titleText.text = MiraLocaleManager.Get("Modifiers"); + case ModifiersTabIndex: + _titleText.text = MiraLocaleManager.Get("Modifiers"); break; default: - titleText.text = TranslationController.Instance.GetString(StringNames.MatchInfoGuideTitle); + _titleText.text = TranslationController.Instance.GetString(StringNames.MatchInfoGuideTitle); break; } } @@ -1067,53 +899,52 @@ public static class RoleGuidePatches _advancedInfoTabScroller?.gameObject.SetActive(false); } - public static void DisplayAdvancedWiki(MatchInfoGuide instance, BaseModifier modifier) + private static bool TryPrepareAdvancedWiki(MatchInfoGuide instance) { - var name = modifier.ModifierName; - Warning($"Opening advanced tab for {name}."); - instance.settingsTabs[2].SetActive(false); - instance.settingsTabs[3].SetActive(false); - _advancedInfoTabScroller?.gameObject.SetActive(true); - if (currentAdvancedTabObject) + instance.settingsTabs[RolesTabIndex].SetActive(false); + _modifiersScroller?.gameObject.SetActive(false); + if (_currentAdvancedTabObject) { - currentAdvancedTabObject.SetActive(false); - currentAdvancedTabObject.Destroy(); + _currentAdvancedTabObject.SetActive(false); + _currentAdvancedTabObject.Destroy(); } if (_advancedInfoTabScroller == null) { - Warning($"Wiki tab is null."); + Warning("Wiki tab is null."); + return false; + } + + _advancedInfoTabScroller.gameObject.SetActive(true); + return true; + } + + public static void DisplayAdvancedWiki(MatchInfoGuide instance, BaseModifier modifier) + { + var name = modifier.ModifierName; + Info($"Opening advanced tab for {name}."); + if (!TryPrepareAdvancedWiki(instance)) + { return; } - currentAdvancedTabObject = modifier.GetAdvancedWiki(instance, titleText, _advancedInfoTabScroller); + _currentAdvancedTabObject = modifier.GetAdvancedWiki(instance, _titleText, _advancedInfoTabScroller); } public static void DisplayAdvancedWiki(MatchInfoGuide instance, RoleBehaviour role) { - Warning($"Opening advanced tab for {role.GetRoleName()}."); - instance.settingsTabs[2].SetActive(false); - instance.settingsTabs[3].SetActive(false); - _advancedInfoTabScroller?.gameObject.SetActive(true); - if (currentAdvancedTabObject) + Info($"Opening advanced tab for {role.GetRoleName()}."); + if (!TryPrepareAdvancedWiki(instance)) { - currentAdvancedTabObject.SetActive(false); - currentAdvancedTabObject.Destroy(); - } - - if (_advancedInfoTabScroller == null) - { - Warning($"Wiki tab is null."); return; } if (role is ICustomRole custom) { - currentAdvancedTabObject = custom.GetAdvancedWiki(instance, titleText, _advancedInfoTabScroller); + _currentAdvancedTabObject = custom.GetAdvancedWiki(instance, _titleText, _advancedInfoTabScroller); } else { - _advancedInfoTabScroller.ScrollToTop(); var titleTxt = role.GetRoleName() + $" ({TranslationController.Instance.GetString(role.TeamType is RoleTeamTypes.Crewmate ? StringNames.Crewmate : StringNames.Impostor)})"; var description = TranslationController.Instance.GetString(role.BlurbNameLong); @@ -1126,16 +957,14 @@ public static class RoleGuidePatches } } - currentAdvancedTabObject = Helpers.CreateAdvancedWikiTab( + _currentAdvancedTabObject = Helpers.CreateAdvancedWikiPage( instance, + _titleText, + _advancedInfoTabScroller, role.Role.ToString(), titleTxt, description, - titleText, - out var desc); - currentAdvancedTabObject.transform.SetParent(_advancedInfoTabScroller.Inner.transform); - currentAdvancedTabObject.transform.localPosition = new Vector3(0f, 0f, 0f); - _advancedInfoTabScroller.SetYBoundsMax(Mathf.Clamp(desc.textBounds.size.y - 2, 0f, 999f)); + []); } } diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index dd10b75..52fd46a 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -14,7 +14,6 @@ using MiraAPI.GameOptions.OptionTypes; using MiraAPI.Roles; using MiraAPI.Translation; using MiraAPI.Utilities.Assets; -using Reactor.Utilities.Extensions; using Rewired; using TMPro; using UnityEngine; @@ -30,7 +29,7 @@ namespace MiraAPI.Utilities; /// public static class Helpers { - private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled); + private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); public static ReadOnlyCollection? GetModdedOptionsForType(Type classType) { From 9e5b470ae6fb267a9ba25c8a097e95d1af42760e Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 01:53:09 -0700 Subject: [PATCH 05/28] Fix CoLoopWithBudget default budget being zero --- MiraAPI/Utilities/Extensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 57cd64b..24d6c95 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -242,7 +242,7 @@ public static class Extensions public static IEnumerator CoLoopWithBudget(this IEnumerable collection, Action action) { var fps = Application.targetFrameRate > 0 ? Application.targetFrameRate : 60; - long budget = 1 / (fps * 2); // default is half of current frame time. + var budget = 1000L / (fps * 2); // default is half of current frame time. yield return collection.CoLoopWithBudget(budget, action); } From c5c55745fd312e75f2576a20c7a5e9eb50780c02 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 01:56:58 -0700 Subject: [PATCH 06/28] Build wiki panels across frames and stop re-initialising them on refresh --- MiraAPI/Patches/RoleGuidePatches.cs | 210 +++++++++++++++------------- 1 file changed, 112 insertions(+), 98 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index da284a7..11b3743 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -11,6 +12,7 @@ using MiraAPI.Roles; using MiraAPI.Translation; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; +using Reactor.Utilities; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; @@ -49,6 +51,8 @@ public static class RoleGuidePatches private static TextMeshPro _titleText; private static bool _rolesTabDirty; private static bool _modifiersTabDirty; + private static IEnumerator? _panelCreation; + private static bool _panelsReady; private static SortingFilter _sortFilter = SortingFilter.EnabledOnly; private static SortingMethod _sortMethod = SortingMethod.Alphabetical; private static SortingGroups _sortGrouping = SortingGroups.Ungrouped; @@ -388,6 +392,11 @@ public static class RoleGuidePatches { _advancedInfoTabScroller?.gameObject.SetActive(false); MatchInfoGuide.Instance.SetActiveTab(ModifiersTabIndex); + if (!_panelsReady) + { + return; + } + if (_modifiersTabDirty) { ToggleModifierVisibility(); @@ -399,6 +408,11 @@ public static class RoleGuidePatches { _advancedInfoTabScroller?.gameObject.SetActive(false); MatchInfoGuide.Instance.SetActiveTab(RolesTabIndex); + if (!_panelsReady) + { + return; + } + if (_rolesTabDirty) { ToggleRoleVisibility(); @@ -449,6 +463,11 @@ public static class RoleGuidePatches private static void RefreshActiveTab(bool scrollToTop) { + if (!_panelsReady) + { + return; + } + if (IsModifiersTabActive) { ToggleModifierVisibility(); @@ -589,7 +608,6 @@ public static class RoleGuidePatches public static void DisplayNormalRoleSettings(MatchInfoGuide instance, bool reset) { - var inner = instance.settingsTabs[RolesTabIndex].GetComponent().Inner; if (reset) { RolePanels.Clear(); @@ -619,96 +637,98 @@ public static class RoleGuidePatches StringNames.GameTaskBarMode, GameManager.Instance.LogicOptions.GetTaskBarMode().ToString()); - foreach (var modifier in ModifierManager.Modifiers.OfType()) + _panelsReady = false; + if (_panelCreation != null) { - var panel = Object.Instantiate( - instance.MatchInfoRolePanelPrefab, - _modifiersScroller.Inner); - var amount = - modifier.GetAmountPerGame(); - var chance = - modifier.GetAssignmentChance(); - panel.SetModifierPanel( - modifier, - amount, - chance); - SetupPanelButton(panel, () => { DisplayAdvancedWiki(instance, modifier); }); - ModifierPanels.Add( - modifier, - new DetailedPanel( - panel, - modifier.ModifierName, - modifier.ModifierCategoryTitle, - modifier.ParentMod.MiraPlugin.GetAbbreviatedModName())); + Coroutines.Stop(_panelCreation); } - foreach (var roleBehaviour in CustomRoleManager.AllStoredRoleBehaviours) - { - if (roleBehaviour.Role is not RoleTypes.Crewmate and not RoleTypes.Impostor and - not RoleTypes.CrewmateGhost and - not RoleTypes.ImpostorGhost) - { - var panel = Object.Instantiate( - instance.MatchInfoRolePanelPrefab, - inner); - string abbreviation; - int amount; - int chance; - if (roleBehaviour is ICustomRole custom) - { - amount = custom.GetCount().GetValueOrDefault(0); - chance = custom.GetChance().GetValueOrDefault(0); - abbreviation = custom.ParentMod.MiraPlugin.GetAbbreviatedModName(); - } - else - { - abbreviation = "AU"; - amount = - GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleBehaviour.Role); - chance = - GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(roleBehaviour.Role); - } - - panel.SetRolePanel( - roleBehaviour, - amount, - chance); - SetupPanelButton(panel, () => { DisplayAdvancedWiki(instance, roleBehaviour); }); - RolePanels.Add( - roleBehaviour, - new DetailedPanel( - panel, - roleBehaviour.GetRoleName(), - roleBehaviour.GetCategoryTitle(), - abbreviation)); - } - } + _panelCreation = Coroutines.Start(CoCreatePanels(instance)); } _advancedInfoTabScroller?.gameObject.SetActive(false); - if (reset) + if (!_panelsReady) { - if (IsModifiersTabActive) + return; + } + + _rolesTabDirty = true; + _modifiersTabDirty = true; + RefreshActiveTab(false); + } + + private static IEnumerator CoCreatePanels(MatchInfoGuide instance) + { + var inner = instance.settingsTabs[RolesTabIndex].GetComponent().Inner; + yield return ModifierManager.Modifiers.OfType().CoLoopWithBudget(modifier => + { + var panel = Object.Instantiate( + instance.MatchInfoRolePanelPrefab, + _modifiersScroller.Inner); + var amount = + modifier.GetAmountPerGame(); + var chance = + modifier.GetAssignmentChance(); + panel.SetModifierPanel( + modifier, + amount, + chance); + SetupPanelButton(panel, () => { DisplayAdvancedWiki(instance, modifier); }); + ModifierPanels.Add( + modifier, + new DetailedPanel( + panel, + modifier.ModifierName, + modifier.ModifierCategoryTitle, + modifier.ParentMod.MiraPlugin.GetAbbreviatedModName())); + panel.gameObject.SetActive(false); + }); + yield return CustomRoleManager.AllStoredRoleBehaviours.Where(roleBehaviour => + roleBehaviour.Role is not RoleTypes.Crewmate and not RoleTypes.Impostor and + not RoleTypes.CrewmateGhost and + not RoleTypes.ImpostorGhost).CoLoopWithBudget(roleBehaviour => + { + var panel = Object.Instantiate( + instance.MatchInfoRolePanelPrefab, + inner); + string abbreviation; + int amount; + int chance; + if (roleBehaviour is ICustomRole custom) { - ToggleModifierVisibility(); + amount = custom.GetCount().GetValueOrDefault(0); + chance = custom.GetChance().GetValueOrDefault(0); + abbreviation = custom.ParentMod.MiraPlugin.GetAbbreviatedModName(); } else { - ToggleRoleVisibility(); + abbreviation = "AU"; + amount = + GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleBehaviour.Role); + chance = + GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(roleBehaviour.Role); } - } - else - { - _rolesTabDirty = true; - _modifiersTabDirty = true; - RefreshActiveTab(false); - } - if (reset) - { - instance.CreatePlayerEntries(); - } + panel.SetRolePanel( + roleBehaviour, + amount, + chance); + SetupPanelButton(panel, () => { DisplayAdvancedWiki(instance, roleBehaviour); }); + RolePanels.Add( + roleBehaviour, + new DetailedPanel( + panel, + roleBehaviour.GetRoleName(), + roleBehaviour.GetCategoryTitle(), + abbreviation)); + panel.gameObject.SetActive(false); + }); + + _panelsReady = true; + _panelCreation = null; + RefreshActiveTab(false); + instance.CreatePlayerEntries(); } @@ -722,10 +742,7 @@ public static class RoleGuidePatches modifier.GetAmountPerGame(); var chance = modifier.GetAssignmentChance(); - panel.SetModifierPanel( - modifier, - amount, - chance); + SetPanelCount(panel, amount, chance, holder.ModId); holder.Amount = amount; holder.Chance = chance; holder.Likelihood = amount * chance; @@ -772,10 +789,7 @@ public static class RoleGuidePatches GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(roleData.Role); } - panel.SetRolePanel( - roleData, - amount, - chance); + SetPanelCount(panel, amount, chance, holder.ModId); holder.Amount = amount; holder.Chance = chance; holder.Likelihood = amount * chance; @@ -1003,31 +1017,36 @@ public static class RoleGuidePatches return false; } + private static void SetPanelCount(MatchInfoRolePanel panel, int amount, int chance, string modId) + { + panel.roleCount.text = string.Format( + CultureInfo.InvariantCulture, + "{0} at {1}% ({2})", + amount.ToString(CultureInfo.InvariantCulture), + chance, + modId); + } + public static void SetRolePanel( this MatchInfoRolePanel instance, RoleBehaviour role, int numPerGame, int chancePerGame) { - instance.roleCount.text = string.Format( - CultureInfo.InvariantCulture, - "{0} at {1}%", - numPerGame.ToString(CultureInfo.InvariantCulture), - chancePerGame); if (role is ICustomRole customRole) { instance.roleName.text = customRole.RoleName; instance.roleDescription.text = $"{role.GetCategoryTitle()}\n" + customRole.RoleMedDescription; instance.roleIcon.sprite = customRole.Configuration.Icon?.LoadAsset(); - instance.roleCount.text += $" ({customRole.ParentMod.MiraPlugin.GetAbbreviatedModName()})"; + SetPanelCount(instance, numPerGame, chancePerGame, customRole.ParentMod.MiraPlugin.GetAbbreviatedModName()); } else { instance.roleName.text = role.NiceName; instance.roleDescription.text = $"{role.GetCategoryTitle()}\n" + role.BlurbMed; instance.roleIcon.sprite = role.RoleIconColor; - instance.roleCount.text += " (AU)"; + SetPanelCount(instance, numPerGame, chancePerGame, "AU"); } instance.roleIcon.SetSizeLimit(0.13f); @@ -1044,16 +1063,11 @@ public static class RoleGuidePatches int numPerGame, int chancePerGame) { - instance.roleCount.text = string.Format( - CultureInfo.InvariantCulture, - "{0} at {1}%", - numPerGame.ToString(CultureInfo.InvariantCulture), - chancePerGame); instance.roleName.text = modifier.ModifierName; instance.roleDescription.text = $"{modifier.ModifierCategoryTitle}\n" + modifier.ModifierMedDescription; instance.roleIcon.sprite = modifier.ModifierIcon?.LoadAsset(); - instance.roleCount.text += $" ({modifier.ParentMod.MiraPlugin.GetAbbreviatedModName()})"; + SetPanelCount(instance, numPerGame, chancePerGame, modifier.ParentMod.MiraPlugin.GetAbbreviatedModName()); instance.roleIcon.SetSizeLimit(0.13f); instance.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50); From a297898e6ed62334de92641f0a518b64134837e2 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:03:03 -0700 Subject: [PATCH 07/28] Fix wiki failing to open after clearing the search box --- 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 11b3743..8efe152 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -498,8 +498,8 @@ public static class RoleGuidePatches _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; if (_searchBoxTmp) { - _searchBoxTmp.outputText.SetText(string.Empty); - _searchBoxTmp.SetText(string.Empty); + _searchBoxTmp.outputText.text = string.Empty; + _searchBoxTmp.text = string.Empty; } RefreshActiveTab(true); From 09035037829dfb1e3e3365147d35b51c7a876a74 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:06:07 -0700 Subject: [PATCH 08/28] Show a loading spinner while wiki panels are built --- MiraAPI/Patches/RoleGuidePatches.cs | 18 +++++++ MiraAPI/Utilities/LoadingRing.cs | 81 +++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 MiraAPI/Utilities/LoadingRing.cs diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 8efe152..f6baacb 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -53,6 +53,7 @@ public static class RoleGuidePatches private static bool _modifiersTabDirty; private static IEnumerator? _panelCreation; private static bool _panelsReady; + private static LoadingRing? _loadingSpinner; private static SortingFilter _sortFilter = SortingFilter.EnabledOnly; private static SortingMethod _sortMethod = SortingMethod.Alphabetical; private static SortingGroups _sortGrouping = SortingGroups.Ungrouped; @@ -220,6 +221,12 @@ public static class RoleGuidePatches .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5); _advancedInfoTabScroller = wikiTab.GetComponent(); + var spinnerParent = __instance.settingsTabs[RolesTabIndex].transform.parent; + var spinnerPos = spinnerParent.InverseTransformPoint(__instance.MatchInfoRoleMaskArea.transform.position); + spinnerPos.z = -5f; + _loadingSpinner = LoadingRing.Create(spinnerParent, spinnerPos, 0.6f); + _loadingSpinner.GetComponent().color = MiraAssets.AcceptedTeal; + DisplayNormalRoleSettings(__instance, true); var searchIconButton = Object.Instantiate( @@ -456,6 +463,12 @@ public static class RoleGuidePatches (!isDisabled && _sortFilter is SortingFilter.DisabledOnly)); } + private static void UpdateSpinner() + { + _loadingSpinner?.gameObject.SetActive( + !_panelsReady && MatchInfoGuide.Instance.activeTabIndex is RolesTabIndex or ModifiersTabIndex); + } + private static float GetGridScrollBounds(int count) { return Mathf.Clamp(Mathf.Ceil(count / 2f) * 1.3f - 1.5f, 0f, 999f); @@ -638,6 +651,7 @@ public static class RoleGuidePatches GameManager.Instance.LogicOptions.GetTaskBarMode().ToString()); _panelsReady = false; + instance.rolesEnabledMessage.SetActive(false); if (_panelCreation != null) { Coroutines.Stop(_panelCreation); @@ -647,6 +661,7 @@ public static class RoleGuidePatches } _advancedInfoTabScroller?.gameObject.SetActive(false); + UpdateSpinner(); if (!_panelsReady) { @@ -728,6 +743,7 @@ public static class RoleGuidePatches _panelsReady = true; _panelCreation = null; RefreshActiveTab(false); + UpdateSpinner(); instance.CreatePlayerEntries(); } @@ -911,12 +927,14 @@ public static class RoleGuidePatches } _advancedInfoTabScroller?.gameObject.SetActive(false); + UpdateSpinner(); } private static bool TryPrepareAdvancedWiki(MatchInfoGuide instance) { instance.settingsTabs[RolesTabIndex].SetActive(false); _modifiersScroller?.gameObject.SetActive(false); + UpdateSpinner(); if (_currentAdvancedTabObject) { _currentAdvancedTabObject.SetActive(false); diff --git a/MiraAPI/Utilities/LoadingRing.cs b/MiraAPI/Utilities/LoadingRing.cs new file mode 100644 index 0000000..34ce27f --- /dev/null +++ b/MiraAPI/Utilities/LoadingRing.cs @@ -0,0 +1,81 @@ +using System; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI.Utilities; + +/// +/// A spinning ring used to indicate that content is loading. +/// +[RegisterInIl2Cpp] +public class LoadingRing(IntPtr cppPtr) : MonoBehaviour(cppPtr) +{ + private static Sprite? _ringSprite; + + /// + /// Gets or sets the rotation speed in degrees per second. + /// + public float Speed { get; set; } = 270f; + + /// + /// Creates a on the UI layer. + /// + /// The transform to parent the spinner to. + /// The local position of the spinner. + /// The uniform scale of the spinner. + /// The created . + public static LoadingRing Create(Transform parent, Vector3 localPosition, float scale) + { + _ringSprite ??= CreateRingSprite(); + var obj = new GameObject("LoadingRing") + { + transform = + { + parent = parent, + localPosition = localPosition, + localScale = Vector3.one * scale, + }, + layer = LayerMask.NameToLayer("UI"), + }; + var renderer = obj.AddComponent(); + renderer.sprite = _ringSprite; + return obj.AddComponent(); + } + + /// + /// Rotates the ring. + /// + public void Update() + { + transform.Rotate(0f, 0f, -Speed * Time.deltaTime); + } + + private static Sprite CreateRingSprite() + { + const int size = 128; + const float innerRadius = 46f; + const float outerRadius = 60f; + var texture = new Texture2D(size, size, TextureFormat.RGBA32, false) + { + filterMode = FilterMode.Bilinear, + wrapMode = TextureWrapMode.Clamp, + }; + var pixels = new Color[size * size]; + for (var y = 0; y < size; y++) + { + for (var x = 0; x < size; x++) + { + var dx = x + 0.5f - (size / 2f); + var dy = y + 0.5f - (size / 2f); + var radius = Mathf.Sqrt((dx * dx) + (dy * dy)); + var ring = Mathf.Clamp01(radius - innerRadius) * Mathf.Clamp01(outerRadius - radius); + var angle = (Mathf.Atan2(dy, dx) + Mathf.PI) / (2f * Mathf.PI); + pixels[(y * size) + x] = new Color(1f, 1f, 1f, ring * Mathf.Lerp(1f, 0.1f, angle)); + } + } + + texture.SetPixels(pixels); + texture.Apply(); + return Sprite.Create(texture, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 100f); + } +} From 77366593f109be48250966c906e689eb9018884b Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:16:11 -0700 Subject: [PATCH 09/28] Fix wiki open aborting before the player list is created --- MiraAPI/Patches/RoleGuidePatches.cs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index f6baacb..0ae0507 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -350,13 +350,16 @@ public static class RoleGuidePatches PlayerControl.LocalPlayer.NetTransform.Halt(); __instance.MatchInfoParent.SetActive(true); - var instance = ControllerManager.Instance; - var currentUiState = ControllerManager.Instance.CurrentUiState; - var controllerSelectable = __instance.ControllerSelectable; - instance.SetUpSelectables(currentUiState, controllerSelectable[^1], __instance.ControllerSelectable); - var instance2 = ControllerManager.Instance; - var controllerSelectable2 = __instance.ControllerSelectable; - instance2.SetCurrentSelected(controllerSelectable2[^1]); + if (__instance.ControllerSelectable.Count > 0) + { + var controllerManager = ControllerManager.Instance; + controllerManager.SetUpSelectables( + controllerManager.CurrentUiState, + __instance.ControllerSelectable[^1], + __instance.ControllerSelectable); + controllerManager.SetCurrentSelected(__instance.ControllerSelectable[^1]); + } + __instance.SetActiveTab(regGame ? RolesTabIndex : 0); return false; } @@ -373,6 +376,7 @@ public static class RoleGuidePatches settingBtnSelected.transform.GetChild(0).gameObject.SetActive(false); var settingBtnHighlight = button.activeSprites.GetComponent(); settingBtnHighlight.sprite = hover; + settingBtnInactive.drawMode = settingBtnSelected.drawMode = settingBtnHighlight.drawMode = SpriteDrawMode.Sliced; settingBtnInactive.size = settingBtnSelected.size = settingBtnHighlight.size = Vector2.one; settingBtnHighlight.transform.GetChild(0).gameObject.SetActive(false); buttonCollider.size = new Vector2(0.8f, 0.74f); @@ -649,6 +653,7 @@ public static class RoleGuidePatches instance.CreateSettingsEntry( StringNames.GameTaskBarMode, GameManager.Instance.LogicOptions.GetTaskBarMode().ToString()); + instance.CreatePlayerEntries(); _panelsReady = false; instance.rolesEnabledMessage.SetActive(false); @@ -744,7 +749,6 @@ public static class RoleGuidePatches _panelCreation = null; RefreshActiveTab(false); UpdateSpinner(); - instance.CreatePlayerEntries(); } From 034521b7b6e0fe61631a736d0fad94164585b8be Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:16:11 -0700 Subject: [PATCH 10/28] Create resource sprites as full rect to stop tiling warnings --- MiraAPI/Utilities/Assets/MiraAssets.cs | 2 +- MiraAPI/Utilities/Assets/SpriteTools.cs | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index 249e942..a57332b 100644 --- a/MiraAPI/Utilities/Assets/MiraAssets.cs +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -19,7 +19,7 @@ public static class MiraAssets new Vector2(0.5f, 0.5f), 100f, 0U, - SpriteMeshType.Tight, + SpriteMeshType.FullRect, new Vector4(20, 20, 20, 20)); RoundedBox = new LoadableAssetWrapper(boxSprite); diff --git a/MiraAPI/Utilities/Assets/SpriteTools.cs b/MiraAPI/Utilities/Assets/SpriteTools.cs index b0d4a1e..05ebcfc 100644 --- a/MiraAPI/Utilities/Assets/SpriteTools.cs +++ b/MiraAPI/Utilities/Assets/SpriteTools.cs @@ -48,7 +48,13 @@ public static class SpriteTools public static Sprite LoadSpriteFromPath(string resourcePath, Assembly assembly, float pixelsPerUnit) { var tex = LoadTextureFromResourcePath(resourcePath, assembly); - var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), pixelsPerUnit); + var sprite = Sprite.Create( + tex, + new Rect(0, 0, tex.width, tex.height), + new Vector2(0.5f, 0.5f), + pixelsPerUnit, + 0, + SpriteMeshType.FullRect); sprite.name = resourcePath; return sprite; } From a88f9b66119bf353d8499ab753192ee0606d33d8 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:24:27 -0700 Subject: [PATCH 11/28] Give the wiki search box a caret so clearing it does not corrupt input --- MiraAPI/Patches/RoleGuidePatches.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 0ae0507..3398d5f 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -272,6 +272,9 @@ public static class RoleGuidePatches button.OnUp = true; _searchBoxTmp = searchBox.AddComponent(); _searchBoxTmp.outputText = tmpText; + var pipe = new GameObject("Pipe") { layer = searchBox.layer }; + pipe.transform.SetParent(_searchBoxTmp.transform, false); + _searchBoxTmp.Pipe = pipe.AddComponent(); button.OnClick.AddListener((UnityAction)(() => { _searchBoxTmp.GiveFocus(); })); button.OnMouseOver = new UnityEvent(); button.OnMouseOut = new UnityEvent(); @@ -469,8 +472,11 @@ public static class RoleGuidePatches private static void UpdateSpinner() { - _loadingSpinner?.gameObject.SetActive( - !_panelsReady && MatchInfoGuide.Instance.activeTabIndex is RolesTabIndex or ModifiersTabIndex); + if (_loadingSpinner != null) + { + _loadingSpinner.gameObject.SetActive( + !_panelsReady && MatchInfoGuide.Instance.activeTabIndex is RolesTabIndex or ModifiersTabIndex); + } } private static float GetGridScrollBounds(int count) @@ -515,8 +521,7 @@ public static class RoleGuidePatches _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; if (_searchBoxTmp) { - _searchBoxTmp.outputText.text = string.Empty; - _searchBoxTmp.text = string.Empty; + _searchBoxTmp.Clear(); } RefreshActiveTab(true); From 9cefeebe2b9797d7407e8b20803274e3bcc9a797 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:24:27 -0700 Subject: [PATCH 12/28] Keep the loading ring sprite alive across scene loads --- MiraAPI/Utilities/LoadingRing.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Utilities/LoadingRing.cs b/MiraAPI/Utilities/LoadingRing.cs index 34ce27f..3f8af87 100644 --- a/MiraAPI/Utilities/LoadingRing.cs +++ b/MiraAPI/Utilities/LoadingRing.cs @@ -26,7 +26,11 @@ public class LoadingRing(IntPtr cppPtr) : MonoBehaviour(cppPtr) /// The created . public static LoadingRing Create(Transform parent, Vector3 localPosition, float scale) { - _ringSprite ??= CreateRingSprite(); + if (!_ringSprite) + { + _ringSprite = CreateRingSprite(); + } + var obj = new GameObject("LoadingRing") { transform = @@ -76,6 +80,9 @@ public class LoadingRing(IntPtr cppPtr) : MonoBehaviour(cppPtr) texture.SetPixels(pixels); texture.Apply(); - return Sprite.Create(texture, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 100f); + texture.hideFlags = HideFlags.HideAndDontSave; + var sprite = Sprite.Create(texture, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 100f); + sprite.hideFlags = HideFlags.HideAndDontSave; + return sprite; } } From 5d9c38a503623c4f876c4f94c9a33035a752b666 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:32:42 -0700 Subject: [PATCH 13/28] Wire a placeholder into the wiki search box so clearing an empty box works --- MiraAPI/Patches/RoleGuidePatches.cs | 7 +++++++ MiraAPI/Resources/Locale/en_US.xml | 1 + 2 files changed, 8 insertions(+) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 3398d5f..5994daf 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -296,7 +296,14 @@ public static class RoleGuidePatches RefreshActiveTab(true); })); + var placeholder = Object.Instantiate(tmpText, tmpText.transform.parent); + placeholder.name = "Placeholder"; + placeholder.color = new Color(0.55f, 0.55f, 0.55f); + placeholder.text = MiraLocaleManager.Get("MiraApi.Wiki.SearchPlaceholder"); + placeholder.GetComponent()?.Destroy(); + _searchBoxTmp.placeholderText = placeholder; _searchBoxTmp.transform.localPosition = new Vector3(-1.438f, 0.756f, -0.2f); + _searchBoxTmp.Clear(); var playerButton = __instance.TabButtons[0]; SetupTabButton( diff --git a/MiraAPI/Resources/Locale/en_US.xml b/MiraAPI/Resources/Locale/en_US.xml index 245b7ae..be120fa 100644 --- a/MiraAPI/Resources/Locale/en_US.xml +++ b/MiraAPI/Resources/Locale/en_US.xml @@ -55,4 +55,5 @@ Outlive the timer or eliminate the crew. Options Player List Game Settings + Search... From b94f0aa63b65ad722ffc96748e2358cc22f7adee Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:37:48 -0700 Subject: [PATCH 14/28] Build the wiki search box from the chat text field instead of a bare TextBoxTMP --- MiraAPI/Patches/RoleGuidePatches.cs | 84 +++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 5994daf..534f8d1 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -249,15 +249,6 @@ public static class RoleGuidePatches var searchButton = searchBox.GetComponent(); var tmpText = searchButton.transform.GetChild(0).GetComponent(); tmpText.GetComponent().Destroy(); - tmpText.color = new Color(0.75f, 0.75f, 0.75f); - tmpText.text = string.Empty; - tmpText.fontSizeMax = 4; - tmpText.overflowMode = TextOverflowModes.Ellipsis; - tmpText.alignment = TextAlignmentOptions.Left; - tmpText.horizontalAlignment = HorizontalAlignmentOptions.Left; - tmpText.rectTransform.offsetMax = new Vector2(-0.1531f, 0.2972f); - tmpText.rectTransform.sizeDelta = new Vector2(3, 1); - tmpText.transform.localPosition = new Vector3(0, 0.0343f, -0.2f); var inactive = searchButton.inactiveSprites; var selected = searchButton.selectedSprites; var highlight = searchButton.activeSprites; @@ -270,16 +261,70 @@ public static class RoleGuidePatches disabledSprite.gameObject.SetActive(true); var button = searchBox.AddComponent(); button.OnUp = true; - _searchBoxTmp = searchBox.AddComponent(); - _searchBoxTmp.outputText = tmpText; - var pipe = new GameObject("Pipe") { layer = searchBox.layer }; - pipe.transform.SetParent(_searchBoxTmp.transform, false); - _searchBoxTmp.Pipe = pipe.AddComponent(); + var template = HudManager.Instance.Chat.freeChatField.textArea; + _searchBoxTmp = Object.Instantiate(template, searchBox.transform); + _searchBoxTmp.name = "SearchField"; + _searchBoxTmp.transform.localPosition = Vector3.zero; + _searchBoxTmp.OnEnter = new Button.ButtonClickedEvent(); + _searchBoxTmp.OnChange = new Button.ButtonClickedEvent(); + _searchBoxTmp.OnFocus = new Button.ButtonClickedEvent(); + _searchBoxTmp.OnFocusLost = new Button.ButtonClickedEvent(); + _searchBoxTmp.sendButtonGlyph = null; + _searchBoxTmp.SendOnFullChars = false; + _searchBoxTmp.ClearOnFocus = false; + _searchBoxTmp.characterLimit = 40; + _searchBoxTmp.AllowSymbols = true; + _searchBoxTmp.allowAllCharacters = true; + if (_searchBoxTmp.Background) + { + _searchBoxTmp.Background.enabled = false; + } + + var boxCollider = _searchBoxTmp.GetComponent(); + if (boxCollider) + { + boxCollider.size = new Vector2(3f, 0.6f); + boxCollider.offset = Vector2.zero; + } + + var outputText = _searchBoxTmp.outputText; + outputText.GetComponent()?.Destroy(); + outputText.font = tmpText.font; + outputText.fontSharedMaterial = tmpText.fontSharedMaterial; + outputText.color = new Color(0.75f, 0.75f, 0.75f); + outputText.text = string.Empty; + outputText.fontSizeMax = 4; + outputText.overflowMode = TextOverflowModes.Ellipsis; + outputText.alignment = TextAlignmentOptions.Left; + outputText.horizontalAlignment = HorizontalAlignmentOptions.Left; + outputText.rectTransform.offsetMax = new Vector2(-0.1531f, 0.2972f); + outputText.rectTransform.sizeDelta = new Vector2(3, 1); + outputText.transform.localPosition = new Vector3(0, 0.0343f, -0.2f); + + var placeholderText = _searchBoxTmp.placeholderText; + if (placeholderText) + { + placeholderText.GetComponent()?.Destroy(); + placeholderText.font = tmpText.font; + placeholderText.fontSharedMaterial = tmpText.fontSharedMaterial; + placeholderText.color = new Color(0.55f, 0.55f, 0.55f); + placeholderText.text = MiraLocaleManager.Get("MiraApi.Wiki.SearchPlaceholder"); + placeholderText.fontSizeMax = 4; + placeholderText.overflowMode = TextOverflowModes.Ellipsis; + placeholderText.alignment = TextAlignmentOptions.Left; + placeholderText.horizontalAlignment = HorizontalAlignmentOptions.Left; + placeholderText.rectTransform.offsetMax = new Vector2(-0.1531f, 0.2972f); + placeholderText.rectTransform.sizeDelta = new Vector2(3, 1); + placeholderText.transform.localPosition = new Vector3(0, 0.0343f, -0.2f); + placeholderText.gameObject.SetActive(true); + } + + tmpText.gameObject.Destroy(); + button.OnClick.AddListener((UnityAction)(() => { _searchBoxTmp.GiveFocus(); })); button.OnMouseOver = new UnityEvent(); button.OnMouseOut = new UnityEvent(); _rolesScroller = __instance.settingsTabs[RolesTabIndex].GetComponent(); - _searchBoxTmp.OnChange = new Button.ButtonClickedEvent(); _searchBoxTmp.OnChange.AddListener( (UnityAction)(() => { @@ -296,14 +341,7 @@ public static class RoleGuidePatches RefreshActiveTab(true); })); - var placeholder = Object.Instantiate(tmpText, tmpText.transform.parent); - placeholder.name = "Placeholder"; - placeholder.color = new Color(0.55f, 0.55f, 0.55f); - placeholder.text = MiraLocaleManager.Get("MiraApi.Wiki.SearchPlaceholder"); - placeholder.GetComponent()?.Destroy(); - _searchBoxTmp.placeholderText = placeholder; - _searchBoxTmp.transform.localPosition = new Vector3(-1.438f, 0.756f, -0.2f); - _searchBoxTmp.Clear(); + searchBox.transform.localPosition = new Vector3(-1.438f, 0.756f, -0.2f); var playerButton = __instance.TabButtons[0]; SetupTabButton( From ffd42a4a913f7d5de614bb9bb7aa505b1c352774 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:44:04 -0700 Subject: [PATCH 15/28] Copy the original label layout onto the cloned wiki search field --- MiraAPI/Patches/RoleGuidePatches.cs | 68 +++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 534f8d1..ce0a589 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -249,6 +249,15 @@ public static class RoleGuidePatches var searchButton = searchBox.GetComponent(); var tmpText = searchButton.transform.GetChild(0).GetComponent(); tmpText.GetComponent().Destroy(); + tmpText.color = new Color(0.75f, 0.75f, 0.75f); + tmpText.text = string.Empty; + tmpText.fontSizeMax = 4; + tmpText.overflowMode = TextOverflowModes.Ellipsis; + tmpText.alignment = TextAlignmentOptions.Left; + tmpText.horizontalAlignment = HorizontalAlignmentOptions.Left; + tmpText.rectTransform.offsetMax = new Vector2(-0.1531f, 0.2972f); + tmpText.rectTransform.sizeDelta = new Vector2(3, 1); + tmpText.transform.localPosition = new Vector3(0, 0.0343f, -0.2f); var inactive = searchButton.inactiveSprites; var selected = searchButton.selectedSprites; var highlight = searchButton.activeSprites; @@ -288,37 +297,34 @@ public static class RoleGuidePatches } var outputText = _searchBoxTmp.outputText; + var textParent = tmpText.transform.parent; + outputText.transform.SetParent(textParent, false); outputText.GetComponent()?.Destroy(); - outputText.font = tmpText.font; - outputText.fontSharedMaterial = tmpText.fontSharedMaterial; + CopyTextLayout(tmpText, outputText); outputText.color = new Color(0.75f, 0.75f, 0.75f); outputText.text = string.Empty; - outputText.fontSizeMax = 4; - outputText.overflowMode = TextOverflowModes.Ellipsis; - outputText.alignment = TextAlignmentOptions.Left; - outputText.horizontalAlignment = HorizontalAlignmentOptions.Left; - outputText.rectTransform.offsetMax = new Vector2(-0.1531f, 0.2972f); - outputText.rectTransform.sizeDelta = new Vector2(3, 1); - outputText.transform.localPosition = new Vector3(0, 0.0343f, -0.2f); var placeholderText = _searchBoxTmp.placeholderText; if (placeholderText) { + placeholderText.transform.SetParent(textParent, false); placeholderText.GetComponent()?.Destroy(); - placeholderText.font = tmpText.font; - placeholderText.fontSharedMaterial = tmpText.fontSharedMaterial; + CopyTextLayout(tmpText, placeholderText); placeholderText.color = new Color(0.55f, 0.55f, 0.55f); placeholderText.text = MiraLocaleManager.Get("MiraApi.Wiki.SearchPlaceholder"); - placeholderText.fontSizeMax = 4; - placeholderText.overflowMode = TextOverflowModes.Ellipsis; - placeholderText.alignment = TextAlignmentOptions.Left; - placeholderText.horizontalAlignment = HorizontalAlignmentOptions.Left; - placeholderText.rectTransform.offsetMax = new Vector2(-0.1531f, 0.2972f); - placeholderText.rectTransform.sizeDelta = new Vector2(3, 1); - placeholderText.transform.localPosition = new Vector3(0, 0.0343f, -0.2f); placeholderText.gameObject.SetActive(true); } + if (_searchBoxTmp.Pipe) + { + _searchBoxTmp.Pipe.transform.SetParent(textParent, false); + _searchBoxTmp.Pipe.transform.localPosition = new Vector3( + outputText.transform.localPosition.x - + (outputText.rectTransform.sizeDelta.x * outputText.rectTransform.pivot.x), + outputText.transform.localPosition.y + _searchBoxTmp.caretYOffset, + -0.3f); + } + tmpText.gameObject.Destroy(); button.OnClick.AddListener((UnityAction)(() => { _searchBoxTmp.GiveFocus(); })); @@ -656,6 +662,32 @@ public static class RoleGuidePatches RefreshActiveTab(false); } + private static void CopyTextLayout(TextMeshPro source, TextMeshPro target) + { + target.font = source.font; + target.fontSharedMaterial = source.fontSharedMaterial; + target.fontSize = source.fontSize; + target.fontSizeMin = source.fontSizeMin; + target.fontSizeMax = source.fontSizeMax; + target.enableAutoSizing = source.enableAutoSizing; + target.overflowMode = source.overflowMode; + target.alignment = source.alignment; + target.horizontalAlignment = source.horizontalAlignment; + target.verticalAlignment = source.verticalAlignment; + target.margin = source.margin; + target.enableWordWrapping = source.enableWordWrapping; + var sourceRect = source.rectTransform; + var targetRect = target.rectTransform; + targetRect.anchorMin = sourceRect.anchorMin; + targetRect.anchorMax = sourceRect.anchorMax; + targetRect.pivot = sourceRect.pivot; + targetRect.sizeDelta = sourceRect.sizeDelta; + targetRect.anchoredPosition = sourceRect.anchoredPosition; + targetRect.localScale = sourceRect.localScale; + targetRect.localRotation = sourceRect.localRotation; + targetRect.localPosition = sourceRect.localPosition; + } + private static void SetupPanelButton(MatchInfoRolePanel panel, Action onClick) { var collider = panel.roleIcon.gameObject.AddComponent(); From b84c5b064c7b58f50a88d96c06d3159254b917f0 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:53:04 -0700 Subject: [PATCH 16/28] Avoid redundant panel refreshes when reopening the wiki --- MiraAPI/Patches/RoleGuidePatches.cs | 61 +++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index ce0a589..bbe7456 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -30,6 +30,7 @@ public static class RoleGuidePatches private static readonly Dictionary RolePanels = []; private static readonly Dictionary ModifierPanels = []; + private static readonly Dictionary> LastOrders = []; public static PassiveButton SearchIconButton; private static SpriteRenderer _searchIconIdle; @@ -53,6 +54,7 @@ public static class RoleGuidePatches private static bool _modifiersTabDirty; private static IEnumerator? _panelCreation; private static bool _panelsReady; + private static bool _suppressSearchRefresh; private static LoadingRing? _loadingSpinner; private static SortingFilter _sortFilter = SortingFilter.EnabledOnly; private static SortingMethod _sortMethod = SortingMethod.Alphabetical; @@ -149,6 +151,8 @@ public static class RoleGuidePatches } var regGame = GameManager.Instance.TryCast() != null; + System.Diagnostics.Stopwatch? reopenWatch = null; + long reopenRefreshMs = 0; if (regGame) { if (__instance.NormalModeSettings.Count == 0) @@ -345,6 +349,11 @@ public static class RoleGuidePatches _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; } + if (_suppressSearchRefresh) + { + return; + } + RefreshActiveTab(true); })); searchBox.transform.localPosition = new Vector3(-1.438f, 0.756f, -0.2f); @@ -391,8 +400,11 @@ public static class RoleGuidePatches } else { + reopenWatch = System.Diagnostics.Stopwatch.StartNew(); DisplayNormalRoleSettings(__instance, false); ClearSearchText(); + reopenRefreshMs = reopenWatch.ElapsedMilliseconds; + reopenWatch.Restart(); } } else if (__instance.HnSModeSettings.Count == 0) @@ -415,6 +427,11 @@ public static class RoleGuidePatches } __instance.SetActiveTab(regGame ? RolesTabIndex : 0); + if (reopenWatch != null) + { + Debug($"Wiki reopen: refresh {reopenRefreshMs} ms, show {reopenWatch.ElapsedMilliseconds} ms"); + } + return false; } @@ -570,9 +587,11 @@ public static class RoleGuidePatches { _searchIconIdle.sprite = MiraAssets.SearchIconIdleSprite; _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; - if (_searchBoxTmp) + if (_searchBoxTmp && GetSearchText().Length > 0) { + _suppressSearchRefresh = true; _searchBoxTmp.Clear(); + _suppressSearchRefresh = false; } RefreshActiveTab(true); @@ -745,19 +764,13 @@ public static class RoleGuidePatches } _panelCreation = Coroutines.Start(CoCreatePanels(instance)); + LastOrders.Clear(); } _advancedInfoTabScroller?.gameObject.SetActive(false); - UpdateSpinner(); - - if (!_panelsReady) - { - return; - } - _rolesTabDirty = true; _modifiersTabDirty = true; - RefreshActiveTab(false); + UpdateSpinner(); } private static IEnumerator CoCreatePanels(MatchInfoGuide instance) @@ -850,13 +863,17 @@ public static class RoleGuidePatches holder.Likelihood = amount * chance; var isRoleDisabled = amount == 0 || chance == 0; var isRoleNotVisible = !modifier.CanSpawnOnCurrentMode() || modifier.GetDescription() == string.Empty; - if (!ShouldShowPanel(modifier.ForceShowModifierOnWiki, isRoleDisabled, isRoleNotVisible)) + var show = ShouldShowPanel(modifier.ForceShowModifierOnWiki, isRoleDisabled, isRoleNotVisible); + if (panel.gameObject.activeSelf != show) + { + panel.gameObject.SetActive(show); + } + + if (!show) { - panel.gameObject.SetActive(false); continue; } - panel.gameObject.SetActive(true); num++; } @@ -899,13 +916,17 @@ public static class RoleGuidePatches var isRoleNotVisible = (roleData is ICustomRole custom2 && !custom2.CanSpawnOnCurrentMode()) || (Enum.IsDefined(roleData.Role) && roleData.IsRoleBlacklisted()); - if (!ShouldShowPanel(forceShow, isRoleDisabled, isRoleNotVisible)) + var show = ShouldShowPanel(forceShow, isRoleDisabled, isRoleNotVisible); + if (panel.gameObject.activeSelf != show) + { + panel.gameObject.SetActive(show); + } + + if (!show) { - panel.gameObject.SetActive(false); continue; } - panel.gameObject.SetActive(true); num++; } @@ -940,10 +961,18 @@ public static class RoleGuidePatches ? sorted.ThenBy(p => p.GetSortKey(_sortGrouping)) : sorted.ThenByDescending(p => p.GetSortKey(_sortGrouping)); - foreach (var panel in ordered) + var orderedList = ordered.ToList(); + if (LastOrders.TryGetValue(panels, out var lastOrder) && lastOrder.SequenceEqual(orderedList)) + { + return; + } + + foreach (var panel in orderedList) { panel.Panel.transform.SetAsLastSibling(); } + + LastOrders[panels] = orderedList; } [HarmonyPrefix] From 5c0e5473a5c72395477b54412dc4cf23a3527d27 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:59:10 -0700 Subject: [PATCH 17/28] Reveal wiki panels across frames on every open --- MiraAPI/Patches/RoleGuidePatches.cs | 152 +++++++++++++++++++++++----- 1 file changed, 125 insertions(+), 27 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index bbe7456..b0a9d5f 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -55,6 +55,8 @@ public static class RoleGuidePatches private static IEnumerator? _panelCreation; private static bool _panelsReady; private static bool _suppressSearchRefresh; + private static IEnumerator? _reveal; + private static bool _pendingRefresh; private static LoadingRing? _loadingSpinner; private static SortingFilter _sortFilter = SortingFilter.EnabledOnly; private static SortingMethod _sortMethod = SortingMethod.Alphabetical; @@ -151,8 +153,6 @@ public static class RoleGuidePatches } var regGame = GameManager.Instance.TryCast() != null; - System.Diagnostics.Stopwatch? reopenWatch = null; - long reopenRefreshMs = 0; if (regGame) { if (__instance.NormalModeSettings.Count == 0) @@ -400,11 +400,8 @@ public static class RoleGuidePatches } else { - reopenWatch = System.Diagnostics.Stopwatch.StartNew(); DisplayNormalRoleSettings(__instance, false); ClearSearchText(); - reopenRefreshMs = reopenWatch.ElapsedMilliseconds; - reopenWatch.Restart(); } } else if (__instance.HnSModeSettings.Count == 0) @@ -427,11 +424,6 @@ public static class RoleGuidePatches } __instance.SetActiveTab(regGame ? RolesTabIndex : 0); - if (reopenWatch != null) - { - Debug($"Wiki reopen: refresh {reopenRefreshMs} ms, show {reopenWatch.ElapsedMilliseconds} ms"); - } - return false; } @@ -476,13 +468,25 @@ public static class RoleGuidePatches MatchInfoGuide.Instance.SetActiveTab(ModifiersTabIndex); if (!_panelsReady) { + if (_reveal != null) + { + StartReveal(); + } + return; } if (_modifiersTabDirty) { - ToggleModifierVisibility(); - _modifiersTabDirty = false; + foreach (var holder in ModifierPanels.Values) + { + if (holder.Panel.gameObject.activeSelf) + { + holder.Panel.gameObject.SetActive(false); + } + } + + StartReveal(); } } @@ -492,13 +496,25 @@ public static class RoleGuidePatches MatchInfoGuide.Instance.SetActiveTab(RolesTabIndex); if (!_panelsReady) { + if (_reveal != null) + { + StartReveal(); + } + return; } if (_rolesTabDirty) { - ToggleRoleVisibility(); - _rolesTabDirty = false; + foreach (var holder in RolePanels.Values) + { + if (holder.Panel.gameObject.activeSelf) + { + holder.Panel.gameObject.SetActive(false); + } + } + + StartReveal(); } } @@ -552,10 +568,50 @@ public static class RoleGuidePatches return Mathf.Clamp(Mathf.Ceil(count / 2f) * 1.3f - 1.5f, 0f, 999f); } + private static void StartReveal() + { + if (_reveal != null) + { + Coroutines.Stop(_reveal); + } + + _reveal = Coroutines.Start(CoRevealActiveTab()); + } + + private static IEnumerator CoRevealActiveTab() + { + _panelsReady = false; + UpdateSpinner(); + var modifiers = IsModifiersTabActive; + var toShow = modifiers ? UpdateModifierPanels(out _) : UpdateRolePanels(out _); + yield return toShow.Where(p => !p.Panel.gameObject.activeSelf) + .CoLoopWithBudget(p => p.Panel.gameObject.SetActive(true)); + if (modifiers) + { + _modifiersTabDirty = false; + _rolesTabDirty = true; + } + else + { + _rolesTabDirty = false; + _modifiersTabDirty = true; + } + + _reveal = null; + _panelsReady = true; + UpdateSpinner(); + if (_pendingRefresh) + { + _pendingRefresh = false; + RefreshActiveTab(true); + } + } + private static void RefreshActiveTab(bool scrollToTop) { if (!_panelsReady) { + _pendingRefresh = true; return; } @@ -766,6 +822,18 @@ public static class RoleGuidePatches _panelCreation = Coroutines.Start(CoCreatePanels(instance)); LastOrders.Clear(); } + else + { + foreach (var holder in RolePanels.Values.Concat(ModifierPanels.Values)) + { + if (holder.Panel.gameObject.activeSelf) + { + holder.Panel.gameObject.SetActive(false); + } + } + + StartReveal(); + } _advancedInfoTabScroller?.gameObject.SetActive(false); _rolesTabDirty = true; @@ -840,15 +908,26 @@ public static class RoleGuidePatches panel.gameObject.SetActive(false); }); - _panelsReady = true; _panelCreation = null; - RefreshActiveTab(false); - UpdateSpinner(); + yield return CoRevealActiveTab(); } public static void ToggleModifierVisibility() { + var toShow = UpdateModifierPanels(out _); + foreach (var holder in toShow) + { + if (!holder.Panel.gameObject.activeSelf) + { + holder.Panel.gameObject.SetActive(true); + } + } + } + + private static List UpdateModifierPanels(out int shownCount) + { + var toShow = new List(); var num = 0; foreach (var (modifier, holder) in ModifierPanels) { @@ -864,16 +943,17 @@ public static class RoleGuidePatches var isRoleDisabled = amount == 0 || chance == 0; var isRoleNotVisible = !modifier.CanSpawnOnCurrentMode() || modifier.GetDescription() == string.Empty; var show = ShouldShowPanel(modifier.ForceShowModifierOnWiki, isRoleDisabled, isRoleNotVisible); - if (panel.gameObject.activeSelf != show) - { - panel.gameObject.SetActive(show); - } - if (!show) { + if (panel.gameObject.activeSelf) + { + panel.gameObject.SetActive(false); + } + continue; } + toShow.Add(holder); num++; } @@ -883,10 +963,25 @@ public static class RoleGuidePatches instance.rolesEnabledMessage.SetActive(num == 0);*/ _modifiersScroller.SetYBoundsMax(GetGridScrollBounds(num)); + shownCount = num; + return toShow; } public static void ToggleRoleVisibility() { + var toShow = UpdateRolePanels(out _); + foreach (var holder in toShow) + { + if (!holder.Panel.gameObject.activeSelf) + { + holder.Panel.gameObject.SetActive(true); + } + } + } + + private static List UpdateRolePanels(out int shownCount) + { + var toShow = new List(); var num = 0; foreach (var (roleData, holder) in RolePanels) { @@ -917,16 +1012,17 @@ public static class RoleGuidePatches !custom2.CanSpawnOnCurrentMode()) || (Enum.IsDefined(roleData.Role) && roleData.IsRoleBlacklisted()); var show = ShouldShowPanel(forceShow, isRoleDisabled, isRoleNotVisible); - if (panel.gameObject.activeSelf != show) - { - panel.gameObject.SetActive(show); - } - if (!show) { + if (panel.gameObject.activeSelf) + { + panel.gameObject.SetActive(false); + } + continue; } + toShow.Add(holder); num++; } @@ -936,6 +1032,8 @@ public static class RoleGuidePatches instance.rolesEnabledMessage.SetActive(num == 0); instance.MatchInfoRoleScroller.SetYBoundsMax(GetGridScrollBounds(num)); + shownCount = num; + return toShow; } public static void SortAllRoles(string searchText) From ff17c946a4783048bb792777ff34c297a92112f0 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 03:03:51 -0700 Subject: [PATCH 18/28] Spread wiki panel reveal over frames and show the ring immediately --- MiraAPI/Patches/RoleGuidePatches.cs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index b0a9d5f..2dcd269 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -27,6 +27,7 @@ public static class RoleGuidePatches { private const int RolesTabIndex = 2; private const int ModifiersTabIndex = 3; + private const int RevealPanelsPerFrame = 6; private static readonly Dictionary RolePanels = []; private static readonly Dictionary ModifierPanels = []; @@ -57,6 +58,7 @@ public static class RoleGuidePatches private static bool _suppressSearchRefresh; private static IEnumerator? _reveal; private static bool _pendingRefresh; + private static int _revealedThisFrame; private static LoadingRing? _loadingSpinner; private static SortingFilter _sortFilter = SortingFilter.EnabledOnly; private static SortingMethod _sortMethod = SortingMethod.Alphabetical; @@ -575,17 +577,35 @@ public static class RoleGuidePatches Coroutines.Stop(_reveal); } + _panelsReady = false; + UpdateSpinner(); _reveal = Coroutines.Start(CoRevealActiveTab()); } private static IEnumerator CoRevealActiveTab() { - _panelsReady = false; - UpdateSpinner(); + yield return null; var modifiers = IsModifiersTabActive; var toShow = modifiers ? UpdateModifierPanels(out _) : UpdateRolePanels(out _); - yield return toShow.Where(p => !p.Panel.gameObject.activeSelf) - .CoLoopWithBudget(p => p.Panel.gameObject.SetActive(true)); + var batch = 0; + foreach (var holder in toShow) + { + var panel = holder.Panel.gameObject; + if (panel.activeSelf) + { + continue; + } + + panel.SetActive(true); + _revealedThisFrame++; + if (++batch >= RevealPanelsPerFrame) + { + batch = 0; + yield return null; + _revealedThisFrame = 0; + } + } + if (modifiers) { _modifiersTabDirty = false; From cb292308c4ffc098312b7388596ec7cdb91dbb3a Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 03:04:16 -0700 Subject: [PATCH 19/28] Add temporary wiki open frame timing diagnostics --- MiraAPI/Patches/RoleGuidePatches.cs | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 2dcd269..cd62a03 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -59,6 +59,7 @@ public static class RoleGuidePatches private static IEnumerator? _reveal; private static bool _pendingRefresh; private static int _revealedThisFrame; + private static IEnumerator? _perfLogger; private static LoadingRing? _loadingSpinner; private static SortingFilter _sortFilter = SortingFilter.EnabledOnly; private static SortingMethod _sortMethod = SortingMethod.Alphabetical; @@ -116,6 +117,13 @@ public static class RoleGuidePatches [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Open))] public static bool Open(MatchInfoGuide __instance) { + if (_perfLogger != null) + { + Coroutines.Stop(_perfLogger); + } + + _perfLogger = Coroutines.Start(CoLogOpenFrames(__instance.NormalModeSettings.Count == 0 ? "first-open" : "reopen")); + var perfWatch = System.Diagnostics.Stopwatch.StartNew(); if (HudManager.Instance.GameMenu.IsOpen || HudManager.Instance.Chat.IsOpenOrOpening) { return false; @@ -413,6 +421,8 @@ public static class RoleGuidePatches __instance.CreateHnSModeSettings(); } + var prepMs = perfWatch.ElapsedMilliseconds; + perfWatch.Restart(); PlayerControl.LocalPlayer.NetTransform.Halt(); __instance.MatchInfoParent.SetActive(true); if (__instance.ControllerSelectable.Count > 0) @@ -425,7 +435,16 @@ public static class RoleGuidePatches controllerManager.SetCurrentSelected(__instance.ControllerSelectable[^1]); } + var showMs = perfWatch.ElapsedMilliseconds; + perfWatch.Restart(); __instance.SetActiveTab(regGame ? RolesTabIndex : 0); + Debug( + $"[WikiPerf] open: prep {prepMs}ms, show {showMs}ms, tab {perfWatch.ElapsedMilliseconds}ms"); + Debug( + $"[WikiPerf] panels: roles {RolePanels.Count} " + + $"({RolePanels.Values.Count(h => h.Panel.gameObject.activeSelf)}), " + + $"modifiers {ModifierPanels.Count} " + + $"({ModifierPanels.Values.Count(h => h.Panel.gameObject.activeSelf)})"); return false; } @@ -627,6 +646,34 @@ public static class RoleGuidePatches } } + private static IEnumerator CoLogOpenFrames(string label) + { + var frames = 0; + var worst = 0f; + var worstIdx = 0; + while (frames < 90) + { + yield return null; + frames++; + var ms = Time.unscaledDeltaTime * 1000f; + if (ms > worst) + { + worst = ms; + worstIdx = frames; + } + + if (ms > 33f) + { + Debug( + $"[WikiPerf] frame +{frames}: {ms:F0}ms (reveal={_reveal != null}, " + + $"panelsReady={_panelsReady}, revealedThisFrame={_revealedThisFrame})"); + } + } + + Debug($"[WikiPerf] done ({label}): worst {worst:F0}ms at +{worstIdx}"); + _perfLogger = null; + } + private static void RefreshActiveTab(bool scrollToTop) { if (!_panelsReady) From 5379f2236475a44eb5f70f8ef07169b6c9e3b8ed Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Wed, 7 Oct 2026 03:28:30 -0700 Subject: [PATCH 20/28] Remove wiki open timing diagnostics --- MiraAPI/Patches/RoleGuidePatches.cs | 50 ----------------------------- 1 file changed, 50 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index cd62a03..91da4f5 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -58,8 +58,6 @@ public static class RoleGuidePatches private static bool _suppressSearchRefresh; private static IEnumerator? _reveal; private static bool _pendingRefresh; - private static int _revealedThisFrame; - private static IEnumerator? _perfLogger; private static LoadingRing? _loadingSpinner; private static SortingFilter _sortFilter = SortingFilter.EnabledOnly; private static SortingMethod _sortMethod = SortingMethod.Alphabetical; @@ -117,13 +115,6 @@ public static class RoleGuidePatches [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Open))] public static bool Open(MatchInfoGuide __instance) { - if (_perfLogger != null) - { - Coroutines.Stop(_perfLogger); - } - - _perfLogger = Coroutines.Start(CoLogOpenFrames(__instance.NormalModeSettings.Count == 0 ? "first-open" : "reopen")); - var perfWatch = System.Diagnostics.Stopwatch.StartNew(); if (HudManager.Instance.GameMenu.IsOpen || HudManager.Instance.Chat.IsOpenOrOpening) { return false; @@ -421,8 +412,6 @@ public static class RoleGuidePatches __instance.CreateHnSModeSettings(); } - var prepMs = perfWatch.ElapsedMilliseconds; - perfWatch.Restart(); PlayerControl.LocalPlayer.NetTransform.Halt(); __instance.MatchInfoParent.SetActive(true); if (__instance.ControllerSelectable.Count > 0) @@ -435,16 +424,7 @@ public static class RoleGuidePatches controllerManager.SetCurrentSelected(__instance.ControllerSelectable[^1]); } - var showMs = perfWatch.ElapsedMilliseconds; - perfWatch.Restart(); __instance.SetActiveTab(regGame ? RolesTabIndex : 0); - Debug( - $"[WikiPerf] open: prep {prepMs}ms, show {showMs}ms, tab {perfWatch.ElapsedMilliseconds}ms"); - Debug( - $"[WikiPerf] panels: roles {RolePanels.Count} " + - $"({RolePanels.Values.Count(h => h.Panel.gameObject.activeSelf)}), " + - $"modifiers {ModifierPanels.Count} " + - $"({ModifierPanels.Values.Count(h => h.Panel.gameObject.activeSelf)})"); return false; } @@ -616,12 +596,10 @@ public static class RoleGuidePatches } panel.SetActive(true); - _revealedThisFrame++; if (++batch >= RevealPanelsPerFrame) { batch = 0; yield return null; - _revealedThisFrame = 0; } } @@ -646,34 +624,6 @@ public static class RoleGuidePatches } } - private static IEnumerator CoLogOpenFrames(string label) - { - var frames = 0; - var worst = 0f; - var worstIdx = 0; - while (frames < 90) - { - yield return null; - frames++; - var ms = Time.unscaledDeltaTime * 1000f; - if (ms > worst) - { - worst = ms; - worstIdx = frames; - } - - if (ms > 33f) - { - Debug( - $"[WikiPerf] frame +{frames}: {ms:F0}ms (reveal={_reveal != null}, " + - $"panelsReady={_panelsReady}, revealedThisFrame={_revealedThisFrame})"); - } - } - - Debug($"[WikiPerf] done ({label}): worst {worst:F0}ms at +{worstIdx}"); - _perfLogger = null; - } - private static void RefreshActiveTab(bool scrollToTop) { if (!_panelsReady) From f0650a9841423ce64699a0e82642bdaa48bd85fb Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:16:13 -0700 Subject: [PATCH 21/28] Look up role parent mods by type instead of scanning IL2CPP objects --- MiraAPI/Roles/CustomRoleManager.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index 25ab64e..0548eaa 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -57,6 +57,7 @@ public static class CustomRoleManager internal static readonly Dictionary CustomRoles = []; internal static readonly Dictionary RoleIds = []; + internal static readonly Dictionary RoleParentMods = []; private static CppCollections.List? _emptySettings; private static Il2CppReferenceArray? _emptyKillAnimations; @@ -105,6 +106,7 @@ public static class CustomRoleManager } pluginInfo.InternalRoles.Add((ushort)role.Role, role); + RoleParentMods[roleType] = pluginInfo; } catch (Exception ex) { @@ -211,7 +213,9 @@ public static class CustomRoleManager /// A object representing the parent mod of the role. public static MiraPluginInfo FindParentMod(ICustomRole role) { - return MiraPluginManager.Instance.RegisteredPlugins.First(plugin => plugin.InternalRoles.ContainsValue(role as RoleBehaviour ?? throw new InvalidOperationException())); + return RoleParentMods.TryGetValue(role.GetType(), out var mod) + ? mod + : MiraPluginManager.Instance.RegisteredPlugins.First(plugin => plugin.InternalRoles.ContainsValue(role as RoleBehaviour ?? throw new InvalidOperationException())); } /// From abfbd8ee731969f3a7f2c22b5876f75e411dc300 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:16:13 -0700 Subject: [PATCH 22/28] Reveal wiki panels progressively and preload them before first open --- MiraAPI/Patches/RoleGuidePatches.cs | 1179 ++++++++++++--------------- 1 file changed, 526 insertions(+), 653 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 91da4f5..66f9509 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using AmongUs.GameOptions; @@ -27,11 +28,9 @@ public static class RoleGuidePatches { private const int RolesTabIndex = 2; private const int ModifiersTabIndex = 3; - private const int RevealPanelsPerFrame = 6; - private static readonly Dictionary RolePanels = []; - private static readonly Dictionary ModifierPanels = []; - private static readonly Dictionary> LastOrders = []; + private static readonly List RoleEntries = []; + private static readonly List ModifierEntries = []; public static PassiveButton SearchIconButton; private static SpriteRenderer _searchIconIdle; @@ -51,13 +50,10 @@ public static class RoleGuidePatches private static TextBoxTMP _searchBoxTmp; private static GameObject _currentAdvancedTabObject; private static TextMeshPro _titleText; - private static bool _rolesTabDirty; - private static bool _modifiersTabDirty; - private static IEnumerator? _panelCreation; - private static bool _panelsReady; private static bool _suppressSearchRefresh; - private static IEnumerator? _reveal; - private static bool _pendingRefresh; + private static MatchInfoGuide? _guide; + private static IEnumerator? _populate; + private static bool _loading; private static LoadingRing? _loadingSpinner; private static SortingFilter _sortFilter = SortingFilter.EnabledOnly; private static SortingMethod _sortMethod = SortingMethod.Alphabetical; @@ -110,6 +106,41 @@ public static class RoleGuidePatches __instance.transitionOpen.targetSize = 1.3f; } + [HarmonyPostfix] + [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Awake))] + private static void AwakePostfix(MatchInfoGuide __instance) + { + Coroutines.Start(CoPreload(__instance)); + } + + private static IEnumerator CoPreload(MatchInfoGuide instance) + { + while (instance && (!HudManager.InstanceExists || !GameManager.Instance)) + { + yield return null; + } + + if (!instance || GameManager.Instance.TryCast() == null) + { + yield break; + } + + Initialize(instance); + if (_populate != null) + { + yield break; + } + + _populate = Coroutines.Start(CoPreloadPanels()); + } + + private static IEnumerator CoPreloadPanels() + { + yield return CoForEachBudgeted(RoleEntries, PreloadPanel); + yield return CoForEachBudgeted(ModifierEntries, PreloadPanel); + _populate = null; + } + [HarmonyPrefix] [HarmonyPriority(Priority.First)] [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Open))] @@ -156,254 +187,38 @@ public static class RoleGuidePatches var regGame = GameManager.Instance.TryCast() != null; if (regGame) { + Initialize(__instance); if (__instance.NormalModeSettings.Count == 0) { - var sortingOrderButton = Object.Instantiate( - HudManager.Instance.SettingsButton, - __instance.TabButtons[2].transform.parent); - SearchSortingOrderButton = sortingOrderButton.GetComponent(); - SetupSearchButton( - SearchSortingOrderButton, - "SearchSortingOrderButton", - SwitchMethod, - MiraAssets.SortingAzIdleSprite, - MiraAssets.SortingAzHoverSprite, - new Vector3(2.4f, 0.765f, -0.1f), - out _searchSortingOrderIdle, - out _searchSortingOrderHover); - - var sortingGroupButton = Object.Instantiate( - sortingOrderButton, - sortingOrderButton.transform.parent); - SearchSortingGroupButton = sortingGroupButton.GetComponent(); - SetupSearchButton( - SearchSortingGroupButton, - "SearchSortingGroupButton", - SwitchGrouping, - MiraAssets.GroupNoneIdleSprite, - MiraAssets.GroupNoneHoverSprite, - new Vector3(1.8f, 0.765f, -0.1f), - out _searchSortingGroupIdle, - out _searchSortingGroupHover); - - var sortingFilterButton = Object.Instantiate( - sortingOrderButton, - sortingOrderButton.transform.parent); - SearchSortingFilterButton = sortingFilterButton.GetComponent(); - SetupSearchButton( - SearchSortingFilterButton, - "SearchSortingFilterButton", - SwitchFilter, - MiraAssets.VisibleOnIdleSprite, - MiraAssets.VisibleOnHoverSprite, - new Vector3(1f, 0.765f, -0.1f), - out _searchSortingFilterIdle, - out _searchSortingFilterHover); - - __instance.numOfTabs = 4; - __instance.TabButtons[2].SelectButton(true); - ApplyPanelLayout(__instance); - __instance.MatchInfoRoleMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); - __instance.matchInfoSettingsMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); - - var modifiersTab = Object.Instantiate( - __instance.settingsTabs[RolesTabIndex], - __instance.settingsTabs[RolesTabIndex].transform.parent); - modifiersTab.name = "ModifiersPanel"; - modifiersTab.transform.FindChild("MaskArea")?.transform.localPosition = - new Vector3(-0.0184f, 0.15f, -0.1f); - modifiersTab.transform.GetAllChildren() - .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5); - _modifiersScroller = modifiersTab.GetComponent(); - __instance.settingsTabs.Add(modifiersTab); - - var wikiTab = Object.Instantiate( - __instance.settingsTabs[RolesTabIndex], - __instance.settingsTabs[RolesTabIndex].transform.parent); - wikiTab.name = "AdvancedWikiPanels"; - wikiTab.transform.FindChild("MaskArea")?.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); - wikiTab.transform.GetAllChildren() - .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5); - _advancedInfoTabScroller = wikiTab.GetComponent(); - - var spinnerParent = __instance.settingsTabs[RolesTabIndex].transform.parent; - var spinnerPos = spinnerParent.InverseTransformPoint(__instance.MatchInfoRoleMaskArea.transform.position); - spinnerPos.z = -5f; - _loadingSpinner = LoadingRing.Create(spinnerParent, spinnerPos, 0.6f); - _loadingSpinner.GetComponent().color = MiraAssets.AcceptedTeal; - - DisplayNormalRoleSettings(__instance, true); - - var searchIconButton = Object.Instantiate( - sortingOrderButton, - sortingOrderButton.transform.parent); - SearchIconButton = searchIconButton.GetComponent(); - SetupSearchButton( - SearchIconButton, - "SearchIconButton", - ClearSearchText, - MiraAssets.SearchIconIdleSprite, - MiraAssets.SearchIconHoverSprite, - new Vector3(-2.79f, 0.765f, -0.1f), - out _searchIconIdle, - out _searchIconHover); - - var searchBox = Object.Instantiate( - __instance.TabButtons[2].gameObject, - __instance.TabButtons[2].transform.parent); - var searchButton = searchBox.GetComponent(); - var tmpText = searchButton.transform.GetChild(0).GetComponent(); - tmpText.GetComponent().Destroy(); - tmpText.color = new Color(0.75f, 0.75f, 0.75f); - tmpText.text = string.Empty; - tmpText.fontSizeMax = 4; - tmpText.overflowMode = TextOverflowModes.Ellipsis; - tmpText.alignment = TextAlignmentOptions.Left; - tmpText.horizontalAlignment = HorizontalAlignmentOptions.Left; - tmpText.rectTransform.offsetMax = new Vector2(-0.1531f, 0.2972f); - tmpText.rectTransform.sizeDelta = new Vector2(3, 1); - tmpText.transform.localPosition = new Vector3(0, 0.0343f, -0.2f); - var inactive = searchButton.inactiveSprites; - var selected = searchButton.selectedSprites; - var highlight = searchButton.activeSprites; - var disabledSprite = searchButton.disabledSprites; - disabledSprite.GetComponent().size = new Vector2(3, 0.6f); - searchButton.Destroy(); - inactive.gameObject.SetActive(false); - selected.gameObject.SetActive(false); - highlight.gameObject.SetActive(false); - disabledSprite.gameObject.SetActive(true); - var button = searchBox.AddComponent(); - button.OnUp = true; - var template = HudManager.Instance.Chat.freeChatField.textArea; - _searchBoxTmp = Object.Instantiate(template, searchBox.transform); - _searchBoxTmp.name = "SearchField"; - _searchBoxTmp.transform.localPosition = Vector3.zero; - _searchBoxTmp.OnEnter = new Button.ButtonClickedEvent(); - _searchBoxTmp.OnChange = new Button.ButtonClickedEvent(); - _searchBoxTmp.OnFocus = new Button.ButtonClickedEvent(); - _searchBoxTmp.OnFocusLost = new Button.ButtonClickedEvent(); - _searchBoxTmp.sendButtonGlyph = null; - _searchBoxTmp.SendOnFullChars = false; - _searchBoxTmp.ClearOnFocus = false; - _searchBoxTmp.characterLimit = 40; - _searchBoxTmp.AllowSymbols = true; - _searchBoxTmp.allowAllCharacters = true; - if (_searchBoxTmp.Background) - { - _searchBoxTmp.Background.enabled = false; - } - - var boxCollider = _searchBoxTmp.GetComponent(); - if (boxCollider) - { - boxCollider.size = new Vector2(3f, 0.6f); - boxCollider.offset = Vector2.zero; - } - - var outputText = _searchBoxTmp.outputText; - var textParent = tmpText.transform.parent; - outputText.transform.SetParent(textParent, false); - outputText.GetComponent()?.Destroy(); - CopyTextLayout(tmpText, outputText); - outputText.color = new Color(0.75f, 0.75f, 0.75f); - outputText.text = string.Empty; - - var placeholderText = _searchBoxTmp.placeholderText; - if (placeholderText) - { - placeholderText.transform.SetParent(textParent, false); - placeholderText.GetComponent()?.Destroy(); - CopyTextLayout(tmpText, placeholderText); - placeholderText.color = new Color(0.55f, 0.55f, 0.55f); - placeholderText.text = MiraLocaleManager.Get("MiraApi.Wiki.SearchPlaceholder"); - placeholderText.gameObject.SetActive(true); - } - - if (_searchBoxTmp.Pipe) - { - _searchBoxTmp.Pipe.transform.SetParent(textParent, false); - _searchBoxTmp.Pipe.transform.localPosition = new Vector3( - outputText.transform.localPosition.x - - (outputText.rectTransform.sizeDelta.x * outputText.rectTransform.pivot.x), - outputText.transform.localPosition.y + _searchBoxTmp.caretYOffset, - -0.3f); - } - - tmpText.gameObject.Destroy(); - - button.OnClick.AddListener((UnityAction)(() => { _searchBoxTmp.GiveFocus(); })); - button.OnMouseOver = new UnityEvent(); - button.OnMouseOut = new UnityEvent(); - _rolesScroller = __instance.settingsTabs[RolesTabIndex].GetComponent(); - _searchBoxTmp.OnChange.AddListener( - (UnityAction)(() => - { - if (GetSearchText() != string.Empty) - { - _searchIconIdle.sprite = MiraAssets.SearchClearIdleSprite; - _searchIconHover.sprite = MiraAssets.SearchClearHoverSprite; - } - else - { - _searchIconIdle.sprite = MiraAssets.SearchIconIdleSprite; - _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; - } - - if (_suppressSearchRefresh) - { - return; - } - - RefreshActiveTab(true); - })); - searchBox.transform.localPosition = new Vector3(-1.438f, 0.756f, -0.2f); - - var playerButton = __instance.TabButtons[0]; - SetupTabButton( - playerButton, - MiraAssets.WikiPlayersButtonIdleSprite, - MiraAssets.WikiPlayersButtonOpenSprite, - MiraAssets.WikiPlayersButtonHoverSprite); - - var settingButton = __instance.TabButtons[1]; - SetupTabButton( - settingButton, - MiraAssets.WikiSettingsButtonIdleSprite, - MiraAssets.WikiSettingsButtonOpenSprite, - MiraAssets.WikiSettingsButtonHoverSprite); - - var rolesButton = __instance.TabButtons[2]; - SetupTabButton( - rolesButton, - MiraAssets.WikiRolesButtonIdleSprite, - MiraAssets.WikiRolesButtonOpenSprite, - MiraAssets.WikiRolesButtonHoverSprite); - rolesButton.OnClick = new Button.ButtonClickedEvent(); - rolesButton.OnClick.AddListener((Action)(() => OpenRolesTab())); - - var modifiersButton = Object.Instantiate( - __instance.TabButtons[2], - __instance.TabButtons[2].transform.parent); - SetupTabButton( - modifiersButton, - MiraAssets.WikiRolesButtonIdleSprite, - MiraAssets.WikiRolesButtonOpenSprite, - MiraAssets.WikiRolesButtonHoverSprite); - modifiersButton.OnClick = new Button.ButtonClickedEvent(); - modifiersButton.OnClick.AddListener((Action)(() => OpenModifiersTab())); - __instance.TabButtons.Add(modifiersButton); - - playerButton.transform.localPosition = new Vector3(-3.6f, 0.656f, -0.2f); - settingButton.transform.localPosition = new Vector3(-3.6f, 0.056f, -0.2f); - rolesButton.transform.localPosition = new Vector3(-3.6f, -0.544f, -0.2f); - modifiersButton.transform.localPosition = new Vector3(-3.6f, -1.144f, -0.2f); - } - else - { - DisplayNormalRoleSettings(__instance, false); - ClearSearchText(); + __instance.CreateSettingsEntry( + StringNames.GameNumImpostors, + GameManager.Instance.AllGameSettingData[StringNames.GameNumImpostors] + .GetValueString(GameManager.Instance.LogicOptions.NumImpostors)); + __instance.CreateSettingsEntry( + StringNames.GameKillCooldown, + GameManager.Instance.AllGameSettingData[StringNames.GameKillCooldown] + .GetValueString(GameManager.Instance.LogicOptions.GetKillCooldown())); + __instance.CreateSettingsEntry( + StringNames.GameEmergencyCooldown, + GameManager.Instance.AllGameSettingData[StringNames.GameEmergencyCooldown] + .GetValueString(GameManager.Instance.LogicOptions.GetEmergencyCooldown())); + __instance.CreateSettingsEntry( + StringNames.GameVisualTasks, + __instance.GetBoolString(GameManager.Instance.LogicOptions.GetVisualTasks())); + __instance.CreateSettingsEntry( + StringNames.GameAnonymousVotes, + __instance.GetBoolString(GameManager.Instance.LogicOptions.GetAnonymousVotes())); + __instance.CreateSettingsEntry( + StringNames.GameConfirmImpostor, + __instance.GetBoolString(GameManager.Instance.LogicOptions.GetConfirmImpostor())); + __instance.CreateSettingsEntry( + StringNames.GameTaskBarMode, + GameManager.Instance.LogicOptions.GetTaskBarMode().ToString()); + __instance.CreatePlayerEntries(); + __instance.rolesEnabledMessage.SetActive(false); } + + ResetSearchBox(); } else if (__instance.HnSModeSettings.Count == 0) { @@ -425,9 +240,300 @@ public static class RoleGuidePatches } __instance.SetActiveTab(regGame ? RolesTabIndex : 0); + if (regGame) + { + OpenTab(RolesTabIndex); + } + return false; } + private static void Initialize(MatchInfoGuide instance) + { + if (_guide == instance) + { + return; + } + + if (_populate != null) + { + Coroutines.Stop(_populate); + _populate = null; + } + + _guide = instance; + _loading = false; + RoleEntries.Clear(); + ModifierEntries.Clear(); + var sortingOrderButton = Object.Instantiate( + HudManager.Instance.SettingsButton, + instance.TabButtons[2].transform.parent); + SearchSortingOrderButton = sortingOrderButton.GetComponent(); + SetupSearchButton( + SearchSortingOrderButton, + "SearchSortingOrderButton", + SwitchMethod, + MiraAssets.SortingAzIdleSprite, + MiraAssets.SortingAzHoverSprite, + new Vector3(2.4f, 0.765f, -0.1f), + out _searchSortingOrderIdle, + out _searchSortingOrderHover); + + var sortingGroupButton = Object.Instantiate( + sortingOrderButton, + sortingOrderButton.transform.parent); + SearchSortingGroupButton = sortingGroupButton.GetComponent(); + SetupSearchButton( + SearchSortingGroupButton, + "SearchSortingGroupButton", + SwitchGrouping, + MiraAssets.GroupNoneIdleSprite, + MiraAssets.GroupNoneHoverSprite, + new Vector3(1.8f, 0.765f, -0.1f), + out _searchSortingGroupIdle, + out _searchSortingGroupHover); + + var sortingFilterButton = Object.Instantiate( + sortingOrderButton, + sortingOrderButton.transform.parent); + SearchSortingFilterButton = sortingFilterButton.GetComponent(); + SetupSearchButton( + SearchSortingFilterButton, + "SearchSortingFilterButton", + SwitchFilter, + MiraAssets.VisibleOnIdleSprite, + MiraAssets.VisibleOnHoverSprite, + new Vector3(1f, 0.765f, -0.1f), + out _searchSortingFilterIdle, + out _searchSortingFilterHover); + + instance.numOfTabs = 4; + instance.TabButtons[2].SelectButton(true); + ApplyPanelLayout(instance); + instance.MatchInfoRoleMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); + instance.matchInfoSettingsMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); + + var modifiersTab = Object.Instantiate( + instance.settingsTabs[RolesTabIndex], + instance.settingsTabs[RolesTabIndex].transform.parent); + modifiersTab.name = "ModifiersPanel"; + modifiersTab.transform.FindChild("MaskArea")?.transform.localPosition = + new Vector3(-0.0184f, 0.15f, -0.1f); + modifiersTab.transform.GetAllChildren() + .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5); + _modifiersScroller = modifiersTab.GetComponent(); + instance.settingsTabs.Add(modifiersTab); + + var wikiTab = Object.Instantiate( + instance.settingsTabs[RolesTabIndex], + instance.settingsTabs[RolesTabIndex].transform.parent); + wikiTab.name = "AdvancedWikiPanels"; + wikiTab.transform.FindChild("MaskArea")?.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); + wikiTab.transform.GetAllChildren() + .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5); + _advancedInfoTabScroller = wikiTab.GetComponent(); + + var spinnerParent = instance.settingsTabs[RolesTabIndex].transform.parent; + var spinnerPos = spinnerParent.InverseTransformPoint(instance.MatchInfoRoleMaskArea.transform.position); + spinnerPos.z = -5f; + _loadingSpinner = LoadingRing.Create(spinnerParent, spinnerPos, 0.6f); + _loadingSpinner.GetComponent().color = MiraAssets.AcceptedTeal; + + var searchIconButton = Object.Instantiate( + sortingOrderButton, + sortingOrderButton.transform.parent); + SearchIconButton = searchIconButton.GetComponent(); + SetupSearchButton( + SearchIconButton, + "SearchIconButton", + ClearSearchText, + MiraAssets.SearchIconIdleSprite, + MiraAssets.SearchIconHoverSprite, + new Vector3(-2.79f, 0.765f, -0.1f), + out _searchIconIdle, + out _searchIconHover); + + var searchBox = Object.Instantiate( + instance.TabButtons[2].gameObject, + instance.TabButtons[2].transform.parent); + var searchButton = searchBox.GetComponent(); + var tmpText = searchButton.transform.GetChild(0).GetComponent(); + tmpText.GetComponent().Destroy(); + tmpText.color = new Color(0.75f, 0.75f, 0.75f); + tmpText.text = string.Empty; + tmpText.fontSizeMax = 4; + tmpText.overflowMode = TextOverflowModes.Ellipsis; + tmpText.alignment = TextAlignmentOptions.Left; + tmpText.horizontalAlignment = HorizontalAlignmentOptions.Left; + tmpText.rectTransform.offsetMax = new Vector2(-0.1531f, 0.2972f); + tmpText.rectTransform.sizeDelta = new Vector2(3, 1); + tmpText.transform.localPosition = new Vector3(0, 0.0343f, -0.2f); + var inactive = searchButton.inactiveSprites; + var selected = searchButton.selectedSprites; + var highlight = searchButton.activeSprites; + var disabledSprite = searchButton.disabledSprites; + disabledSprite.GetComponent().size = new Vector2(3, 0.6f); + searchButton.Destroy(); + inactive.gameObject.SetActive(false); + selected.gameObject.SetActive(false); + highlight.gameObject.SetActive(false); + disabledSprite.gameObject.SetActive(true); + var button = searchBox.AddComponent(); + button.OnUp = true; + var template = HudManager.Instance.Chat.freeChatField.textArea; + _searchBoxTmp = Object.Instantiate(template, searchBox.transform); + _searchBoxTmp.name = "SearchField"; + _searchBoxTmp.transform.localPosition = Vector3.zero; + _searchBoxTmp.OnEnter = new Button.ButtonClickedEvent(); + _searchBoxTmp.OnChange = new Button.ButtonClickedEvent(); + _searchBoxTmp.OnFocus = new Button.ButtonClickedEvent(); + _searchBoxTmp.OnFocusLost = new Button.ButtonClickedEvent(); + _searchBoxTmp.sendButtonGlyph = null; + _searchBoxTmp.SendOnFullChars = false; + _searchBoxTmp.ClearOnFocus = false; + _searchBoxTmp.characterLimit = 40; + _searchBoxTmp.AllowSymbols = true; + _searchBoxTmp.allowAllCharacters = true; + if (_searchBoxTmp.Background) + { + _searchBoxTmp.Background.enabled = false; + } + + var boxCollider = _searchBoxTmp.GetComponent(); + if (boxCollider) + { + boxCollider.size = new Vector2(3f, 0.6f); + boxCollider.offset = Vector2.zero; + } + + var outputText = _searchBoxTmp.outputText; + var textParent = tmpText.transform.parent; + outputText.transform.SetParent(textParent, false); + outputText.GetComponent()?.Destroy(); + CopyTextLayout(tmpText, outputText); + outputText.color = new Color(0.75f, 0.75f, 0.75f); + outputText.text = string.Empty; + + var placeholderText = _searchBoxTmp.placeholderText; + if (placeholderText) + { + placeholderText.transform.SetParent(textParent, false); + placeholderText.GetComponent()?.Destroy(); + CopyTextLayout(tmpText, placeholderText); + placeholderText.color = new Color(0.55f, 0.55f, 0.55f); + placeholderText.text = MiraLocaleManager.Get("MiraApi.Wiki.SearchPlaceholder"); + placeholderText.gameObject.SetActive(true); + } + + if (_searchBoxTmp.Pipe) + { + _searchBoxTmp.Pipe.transform.SetParent(textParent, false); + _searchBoxTmp.Pipe.transform.localPosition = new Vector3( + outputText.transform.localPosition.x - + (outputText.rectTransform.sizeDelta.x * outputText.rectTransform.pivot.x), + outputText.transform.localPosition.y + _searchBoxTmp.caretYOffset, + -0.3f); + } + + tmpText.gameObject.Destroy(); + + button.OnClick.AddListener((UnityAction)(() => { _searchBoxTmp.GiveFocus(); })); + button.OnMouseOver = new UnityEvent(); + button.OnMouseOut = new UnityEvent(); + _rolesScroller = instance.settingsTabs[RolesTabIndex].GetComponent(); + _searchBoxTmp.OnChange.AddListener( + (UnityAction)(() => + { + if (GetSearchText() != string.Empty) + { + _searchIconIdle.sprite = MiraAssets.SearchClearIdleSprite; + _searchIconHover.sprite = MiraAssets.SearchClearHoverSprite; + } + else + { + _searchIconIdle.sprite = MiraAssets.SearchIconIdleSprite; + _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; + } + + if (_suppressSearchRefresh) + { + return; + } + + RefreshActiveTab(true); + })); + searchBox.transform.localPosition = new Vector3(-1.438f, 0.756f, -0.2f); + + var playerButton = instance.TabButtons[0]; + SetupTabButton( + playerButton, + MiraAssets.WikiPlayersButtonIdleSprite, + MiraAssets.WikiPlayersButtonOpenSprite, + MiraAssets.WikiPlayersButtonHoverSprite); + + var settingButton = instance.TabButtons[1]; + SetupTabButton( + settingButton, + MiraAssets.WikiSettingsButtonIdleSprite, + MiraAssets.WikiSettingsButtonOpenSprite, + MiraAssets.WikiSettingsButtonHoverSprite); + + var rolesButton = instance.TabButtons[2]; + SetupTabButton( + rolesButton, + MiraAssets.WikiRolesButtonIdleSprite, + MiraAssets.WikiRolesButtonOpenSprite, + MiraAssets.WikiRolesButtonHoverSprite); + rolesButton.OnClick = new Button.ButtonClickedEvent(); + rolesButton.OnClick.AddListener((Action)(() => OpenRolesTab())); + + var modifiersButton = Object.Instantiate( + instance.TabButtons[2], + instance.TabButtons[2].transform.parent); + SetupTabButton( + modifiersButton, + MiraAssets.WikiRolesButtonIdleSprite, + MiraAssets.WikiRolesButtonOpenSprite, + MiraAssets.WikiRolesButtonHoverSprite); + modifiersButton.OnClick = new Button.ButtonClickedEvent(); + modifiersButton.OnClick.AddListener((Action)(() => OpenModifiersTab())); + instance.TabButtons.Add(modifiersButton); + + playerButton.transform.localPosition = new Vector3(-3.6f, 0.656f, -0.2f); + settingButton.transform.localPosition = new Vector3(-3.6f, 0.056f, -0.2f); + rolesButton.transform.localPosition = new Vector3(-3.6f, -0.544f, -0.2f); + modifiersButton.transform.localPosition = new Vector3(-3.6f, -1.144f, -0.2f); + BuildEntries(); + } + + private static void BuildEntries() + { + foreach (var modifier in ModifierManager.Modifiers.OfType()) + { + ModifierEntries.Add(new DetailedPanel( + modifier.ModifierName, + modifier.ModifierCategoryTitle, + modifier.ParentMod.MiraPlugin.GetAbbreviatedModName(), + null, + modifier)); + } + + 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)) + { + var modId = roleBehaviour is ICustomRole custom + ? custom.ParentMod.MiraPlugin.GetAbbreviatedModName() + : "AU"; + RoleEntries.Add(new DetailedPanel( + roleBehaviour.GetRoleName(), + roleBehaviour.GetCategoryTitle(), + modId, + roleBehaviour, + null)); + } + } + private static void SetupTabButton(MatchInfoGuideTabButton button, Sprite inactive, Sprite selected, Sprite hover) { var buttonCollider = button.GetComponent(); @@ -463,71 +569,37 @@ public static class RoleGuidePatches button.transform.GetChild(2).gameObject.SetActive(false); } - public static void OpenModifiersTab() + private static void OpenTab(int tabIndex) { _advancedInfoTabScroller?.gameObject.SetActive(false); - MatchInfoGuide.Instance.SetActiveTab(ModifiersTabIndex); - if (!_panelsReady) + MatchInfoGuide.Instance.SetActiveTab(tabIndex); + foreach (var entry in tabIndex == ModifiersTabIndex ? ModifierEntries : RoleEntries) { - if (_reveal != null) + if (entry.Panel && entry.Panel!.gameObject.activeSelf) { - StartReveal(); + entry.Panel.gameObject.SetActive(false); } - - return; } - if (_modifiersTabDirty) - { - foreach (var holder in ModifierPanels.Values) - { - if (holder.Panel.gameObject.activeSelf) - { - holder.Panel.gameObject.SetActive(false); - } - } - - StartReveal(); - } + StartPopulate(); } - public static void OpenRolesTab() + public static void OpenModifiersTab() => OpenTab(ModifiersTabIndex); + + public static void OpenRolesTab() => OpenTab(RolesTabIndex); + + public sealed class DetailedPanel(string title, string category, string modId, RoleBehaviour? role, GameModifier? modifier) { - _advancedInfoTabScroller?.gameObject.SetActive(false); - MatchInfoGuide.Instance.SetActiveTab(RolesTabIndex); - if (!_panelsReady) - { - if (_reveal != null) - { - StartReveal(); - } - - return; - } - - if (_rolesTabDirty) - { - foreach (var holder in RolePanels.Values) - { - if (holder.Panel.gameObject.activeSelf) - { - holder.Panel.gameObject.SetActive(false); - } - } - - StartReveal(); - } - } - - public sealed class DetailedPanel(MatchInfoRolePanel panel, string title, string category, string modId) - { - public MatchInfoRolePanel Panel { get; } = panel; + public MatchInfoRolePanel? Panel { get; set; } public string Title { get; } = title; public string Category { get; } = category; public string ModId { get; } = modId; + public RoleBehaviour? Role { get; } = role; + public GameModifier? Modifier { get; } = modifier; public int Amount { get; set; } public int Chance { get; set; } public float Likelihood { get; set; } + public bool Visible { get; set; } public string GetSortKey(SortingGroups grouping) { @@ -560,7 +632,8 @@ public static class RoleGuidePatches if (_loadingSpinner != null) { _loadingSpinner.gameObject.SetActive( - !_panelsReady && MatchInfoGuide.Instance.activeTabIndex is RolesTabIndex or ModifiersTabIndex); + _loading && MatchInfoGuide.Instance.activeTabIndex is RolesTabIndex or ModifiersTabIndex && + !(_advancedInfoTabScroller && _advancedInfoTabScroller.gameObject.activeSelf)); } } @@ -569,94 +642,183 @@ public static class RoleGuidePatches return Mathf.Clamp(Mathf.Ceil(count / 2f) * 1.3f - 1.5f, 0f, 999f); } - private static void StartReveal() + private static void StartPopulate() { - if (_reveal != null) + if (_populate != null) { - Coroutines.Stop(_reveal); + Coroutines.Stop(_populate); } - _panelsReady = false; + _loading = true; UpdateSpinner(); - _reveal = Coroutines.Start(CoRevealActiveTab()); + _populate = Coroutines.Start(CoPopulate()); } - private static IEnumerator CoRevealActiveTab() + private static IEnumerator CoPopulate() { - yield return null; var modifiers = IsModifiersTabActive; - var toShow = modifiers ? UpdateModifierPanels(out _) : UpdateRolePanels(out _); - var batch = 0; - foreach (var holder in toShow) + var entries = modifiers ? ModifierEntries : RoleEntries; + var shown = UpdateEntries(entries, modifiers); + yield return CoForEachBudgeted(shown, entry => { - var panel = holder.Panel.gameObject; - if (panel.activeSelf) + EnsurePanel(entry); + entry.Panel!.transform.SetAsLastSibling(); + if (!entry.Panel.gameObject.activeSelf) { - continue; + entry.Panel.gameObject.SetActive(true); } + }); + _loading = false; + UpdateSpinner(); + yield return CoForEachBudgeted(entries, EnsurePanel); + yield return CoForEachBudgeted(modifiers ? RoleEntries : ModifierEntries, PreloadPanel); + _populate = null; + } - panel.SetActive(true); - if (++batch >= RevealPanelsPerFrame) + private static IEnumerator CoForEachBudgeted(IEnumerable collection, Action action) + { + var fps = Application.targetFrameRate > 0 ? Application.targetFrameRate : 60; + var budget = 1000L / (fps * 2); + var timer = new Stopwatch(); + timer.Start(); + foreach (var item in collection) + { + action(item); + if (timer.ElapsedMilliseconds > budget) { - batch = 0; + timer.Restart(); yield return null; + if (!_guide) + { + yield break; + } } } + } - if (modifiers) + private static void EnsurePanel(DetailedPanel entry) + { + if (entry.Panel) { - _modifiersTabDirty = false; - _rolesTabDirty = true; + return; + } + + var panel = Object.Instantiate( + _guide!.MatchInfoRolePanelPrefab, + entry.Modifier != null ? _modifiersScroller.Inner : _rolesScroller.Inner); + if (entry.Modifier != null) + { + panel.SetModifierPanel(entry.Modifier, entry.Amount, entry.Chance); + SetupPanelButton(panel, () => DisplayAdvancedWiki(MatchInfoGuide.Instance, entry.Modifier)); } else { - _rolesTabDirty = false; - _modifiersTabDirty = true; + panel.SetRolePanel(entry.Role!, entry.Amount, entry.Chance); + SetupPanelButton(panel, () => DisplayAdvancedWiki(MatchInfoGuide.Instance, entry.Role!)); } - _reveal = null; - _panelsReady = true; - UpdateSpinner(); - if (_pendingRefresh) + panel.gameObject.SetActive(false); + entry.Panel = panel; + } + + private static void PreloadPanel(DetailedPanel entry) + { + if (!entry.Panel) { - _pendingRefresh = false; - RefreshActiveTab(true); + ComputeCounts(entry); + EnsurePanel(entry); } } + private static void ComputeCounts(DetailedPanel entry) + { + int amount; + int chance; + bool? forceShow = null; + bool isHidden; + if (entry.Modifier is { } modifier) + { + amount = modifier.GetAmountPerGame(); + chance = modifier.GetAssignmentChance(); + forceShow = modifier.ForceShowModifierOnWiki; + isHidden = !modifier.CanSpawnOnCurrentMode() || modifier.GetDescription() == string.Empty; + } + else + { + var role = entry.Role!; + if (role is ICustomRole custom) + { + amount = custom.GetCount().GetValueOrDefault(0); + chance = custom.GetChance().GetValueOrDefault(0); + forceShow = custom.ForceShowRoleOnWiki; + } + else + { + amount = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(role.Role); + chance = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(role.Role); + } + + isHidden = (role is ICustomRole custom2 && !custom2.CanSpawnOnCurrentMode()) || + (Enum.IsDefined(role.Role) && role.IsRoleBlacklisted()); + } + + entry.Amount = amount; + entry.Chance = chance; + entry.Likelihood = amount * chance; + entry.Visible = ShouldShowPanel(forceShow, amount == 0 || chance == 0, isHidden); + } + + private static List UpdateEntries(List entries, bool modifiers) + { + foreach (var entry in entries) + { + ComputeCounts(entry); + if (entry.Panel) + { + SetPanelCount(entry.Panel!, entry.Amount, entry.Chance, entry.ModId); + if (!entry.Visible && entry.Panel!.gameObject.activeSelf) + { + entry.Panel.gameObject.SetActive(false); + } + } + } + + 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 instance = MatchInfoGuide.Instance; + if (modifiers) + { + _modifiersScroller.SetYBoundsMax(GetGridScrollBounds(shown.Count)); + /*instance.rolesEnabledMessage.SetActive(shown.Count == 0);*/ + } + else + { + instance.MatchInfoRoleScroller.SetYBoundsMax(GetGridScrollBounds(shown.Count)); + instance.rolesEnabledMessage.SetActive(shown.Count == 0); + } + + return shown; + } + private static void RefreshActiveTab(bool scrollToTop) { - if (!_panelsReady) + StartPopulate(); + if (scrollToTop) { - _pendingRefresh = true; - return; - } - - if (IsModifiersTabActive) - { - ToggleModifierVisibility(); - if (scrollToTop) - { - _modifiersScroller.ScrollToTop(); - } - - _modifiersTabDirty = false; - _rolesTabDirty = true; - } - else - { - ToggleRoleVisibility(); - if (scrollToTop) - { - _rolesScroller.ScrollToTop(); - } - - _rolesTabDirty = false; - _modifiersTabDirty = true; + (IsModifiersTabActive ? _modifiersScroller : _rolesScroller).ScrollToTop(); } } - public static void ClearSearchText() + private static void ResetSearchBox() { _searchIconIdle.sprite = MiraAssets.SearchIconIdleSprite; _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; @@ -666,7 +828,11 @@ public static class RoleGuidePatches _searchBoxTmp.Clear(); _suppressSearchRefresh = false; } + } + public static void ClearSearchText() + { + ResetSearchBox(); RefreshActiveTab(true); } @@ -797,299 +963,6 @@ public static class RoleGuidePatches passiveButton.OnClick.AddListener(onClick); } - public static void DisplayNormalRoleSettings(MatchInfoGuide instance, bool reset) - { - if (reset) - { - RolePanels.Clear(); - ModifierPanels.Clear(); - instance.CreateSettingsEntry( - StringNames.GameNumImpostors, - GameManager.Instance.AllGameSettingData[StringNames.GameNumImpostors] - .GetValueString(GameManager.Instance.LogicOptions.NumImpostors)); - instance.CreateSettingsEntry( - StringNames.GameKillCooldown, - GameManager.Instance.AllGameSettingData[StringNames.GameKillCooldown] - .GetValueString(GameManager.Instance.LogicOptions.GetKillCooldown())); - instance.CreateSettingsEntry( - StringNames.GameEmergencyCooldown, - GameManager.Instance.AllGameSettingData[StringNames.GameEmergencyCooldown] - .GetValueString(GameManager.Instance.LogicOptions.GetEmergencyCooldown())); - instance.CreateSettingsEntry( - StringNames.GameVisualTasks, - instance.GetBoolString(GameManager.Instance.LogicOptions.GetVisualTasks())); - instance.CreateSettingsEntry( - StringNames.GameAnonymousVotes, - instance.GetBoolString(GameManager.Instance.LogicOptions.GetAnonymousVotes())); - instance.CreateSettingsEntry( - StringNames.GameConfirmImpostor, - instance.GetBoolString(GameManager.Instance.LogicOptions.GetConfirmImpostor())); - instance.CreateSettingsEntry( - StringNames.GameTaskBarMode, - GameManager.Instance.LogicOptions.GetTaskBarMode().ToString()); - instance.CreatePlayerEntries(); - - _panelsReady = false; - instance.rolesEnabledMessage.SetActive(false); - if (_panelCreation != null) - { - Coroutines.Stop(_panelCreation); - } - - _panelCreation = Coroutines.Start(CoCreatePanels(instance)); - LastOrders.Clear(); - } - else - { - foreach (var holder in RolePanels.Values.Concat(ModifierPanels.Values)) - { - if (holder.Panel.gameObject.activeSelf) - { - holder.Panel.gameObject.SetActive(false); - } - } - - StartReveal(); - } - - _advancedInfoTabScroller?.gameObject.SetActive(false); - _rolesTabDirty = true; - _modifiersTabDirty = true; - UpdateSpinner(); - } - - private static IEnumerator CoCreatePanels(MatchInfoGuide instance) - { - var inner = instance.settingsTabs[RolesTabIndex].GetComponent().Inner; - yield return ModifierManager.Modifiers.OfType().CoLoopWithBudget(modifier => - { - var panel = Object.Instantiate( - instance.MatchInfoRolePanelPrefab, - _modifiersScroller.Inner); - var amount = - modifier.GetAmountPerGame(); - var chance = - modifier.GetAssignmentChance(); - panel.SetModifierPanel( - modifier, - amount, - chance); - SetupPanelButton(panel, () => { DisplayAdvancedWiki(instance, modifier); }); - ModifierPanels.Add( - modifier, - new DetailedPanel( - panel, - modifier.ModifierName, - modifier.ModifierCategoryTitle, - modifier.ParentMod.MiraPlugin.GetAbbreviatedModName())); - panel.gameObject.SetActive(false); - }); - yield return CustomRoleManager.AllStoredRoleBehaviours.Where(roleBehaviour => - roleBehaviour.Role is not RoleTypes.Crewmate and not RoleTypes.Impostor and - not RoleTypes.CrewmateGhost and - not RoleTypes.ImpostorGhost).CoLoopWithBudget(roleBehaviour => - { - var panel = Object.Instantiate( - instance.MatchInfoRolePanelPrefab, - inner); - string abbreviation; - int amount; - int chance; - if (roleBehaviour is ICustomRole custom) - { - amount = custom.GetCount().GetValueOrDefault(0); - chance = custom.GetChance().GetValueOrDefault(0); - abbreviation = custom.ParentMod.MiraPlugin.GetAbbreviatedModName(); - } - else - { - abbreviation = "AU"; - amount = - GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleBehaviour.Role); - chance = - GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(roleBehaviour.Role); - } - - panel.SetRolePanel( - roleBehaviour, - amount, - chance); - SetupPanelButton(panel, () => { DisplayAdvancedWiki(instance, roleBehaviour); }); - RolePanels.Add( - roleBehaviour, - new DetailedPanel( - panel, - roleBehaviour.GetRoleName(), - roleBehaviour.GetCategoryTitle(), - abbreviation)); - panel.gameObject.SetActive(false); - }); - - _panelCreation = null; - yield return CoRevealActiveTab(); - } - - - public static void ToggleModifierVisibility() - { - var toShow = UpdateModifierPanels(out _); - foreach (var holder in toShow) - { - if (!holder.Panel.gameObject.activeSelf) - { - holder.Panel.gameObject.SetActive(true); - } - } - } - - private static List UpdateModifierPanels(out int shownCount) - { - var toShow = new List(); - var num = 0; - foreach (var (modifier, holder) in ModifierPanels) - { - var panel = holder.Panel; - var amount = - modifier.GetAmountPerGame(); - var chance = - modifier.GetAssignmentChance(); - SetPanelCount(panel, amount, chance, holder.ModId); - holder.Amount = amount; - holder.Chance = chance; - holder.Likelihood = amount * chance; - var isRoleDisabled = amount == 0 || chance == 0; - var isRoleNotVisible = !modifier.CanSpawnOnCurrentMode() || modifier.GetDescription() == string.Empty; - var show = ShouldShowPanel(modifier.ForceShowModifierOnWiki, isRoleDisabled, isRoleNotVisible); - if (!show) - { - if (panel.gameObject.activeSelf) - { - panel.gameObject.SetActive(false); - } - - continue; - } - - toShow.Add(holder); - num++; - } - - SortAllModifiers(GetSearchText()); - - /*var instance = MatchInfoGuide.Instance; - instance.rolesEnabledMessage.SetActive(num == 0);*/ - - _modifiersScroller.SetYBoundsMax(GetGridScrollBounds(num)); - shownCount = num; - return toShow; - } - - public static void ToggleRoleVisibility() - { - var toShow = UpdateRolePanels(out _); - foreach (var holder in toShow) - { - if (!holder.Panel.gameObject.activeSelf) - { - holder.Panel.gameObject.SetActive(true); - } - } - } - - private static List UpdateRolePanels(out int shownCount) - { - var toShow = new List(); - var num = 0; - foreach (var (roleData, holder) in RolePanels) - { - var panel = holder.Panel; - int amount; - int chance; - bool? forceShow = null; - if (roleData is ICustomRole custom) - { - amount = custom.GetCount().GetValueOrDefault(0); - chance = custom.GetChance().GetValueOrDefault(0); - forceShow = custom.ForceShowRoleOnWiki; - } - else - { - amount = - GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleData.Role); - chance = - GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(roleData.Role); - } - - SetPanelCount(panel, amount, chance, holder.ModId); - holder.Amount = amount; - holder.Chance = chance; - holder.Likelihood = amount * chance; - var isRoleDisabled = amount == 0 || chance == 0; - var isRoleNotVisible = (roleData is ICustomRole custom2 && - !custom2.CanSpawnOnCurrentMode()) || - (Enum.IsDefined(roleData.Role) && roleData.IsRoleBlacklisted()); - var show = ShouldShowPanel(forceShow, isRoleDisabled, isRoleNotVisible); - if (!show) - { - if (panel.gameObject.activeSelf) - { - panel.gameObject.SetActive(false); - } - - continue; - } - - toShow.Add(holder); - num++; - } - - SortAllRoles(GetSearchText()); - - var instance = MatchInfoGuide.Instance; - instance.rolesEnabledMessage.SetActive(num == 0); - - instance.MatchInfoRoleScroller.SetYBoundsMax(GetGridScrollBounds(num)); - shownCount = num; - return toShow; - } - - public static void SortAllRoles(string searchText) - { - SortPanels(RolePanels, searchText); - } - - public static void SortAllModifiers(string searchText) - { - SortPanels(ModifierPanels, searchText); - } - - private static void SortPanels(Dictionary panels, string searchText) - where T : notnull - { - var sorted = panels.Values - .OrderByDescending(p => p.Title.Equals(searchText, StringComparison.OrdinalIgnoreCase)) - .ThenByDescending(p => - p.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase) || - p.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase)); - - var ordered = _sortMethod is SortingMethod.Alphabetical - ? sorted.ThenBy(p => p.GetSortKey(_sortGrouping)) - : sorted.ThenByDescending(p => p.GetSortKey(_sortGrouping)); - - var orderedList = ordered.ToList(); - if (LastOrders.TryGetValue(panels, out var lastOrder) && lastOrder.SequenceEqual(orderedList)) - { - return; - } - - foreach (var panel in orderedList) - { - panel.Panel.transform.SetAsLastSibling(); - } - - LastOrders[panels] = orderedList; - } - [HarmonyPrefix] [HarmonyPriority(Priority.First)] [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.CreatePlayerEntries))] @@ -1164,7 +1037,6 @@ public static class RoleGuidePatches { instance.settingsTabs[RolesTabIndex].SetActive(false); _modifiersScroller?.gameObject.SetActive(false); - UpdateSpinner(); if (_currentAdvancedTabObject) { _currentAdvancedTabObject.SetActive(false); @@ -1178,6 +1050,7 @@ public static class RoleGuidePatches } _advancedInfoTabScroller.gameObject.SetActive(true); + UpdateSpinner(); return true; } From ec4a5f2baedc13d6d0cc52f780bde39879e4e16e Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:27:14 -0700 Subject: [PATCH 23/28] Keep the wiki open animation smooth by deferring panel work --- MiraAPI/Patches/RoleGuidePatches.cs | 38 ++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 66f9509..95edf22 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -228,6 +228,8 @@ public static class RoleGuidePatches } PlayerControl.LocalPlayer.NetTransform.Halt(); + HidePanels(RoleEntries); + HidePanels(ModifierEntries); __instance.MatchInfoParent.SetActive(true); if (__instance.ControllerSelectable.Count > 0) { @@ -242,7 +244,7 @@ public static class RoleGuidePatches __instance.SetActiveTab(regGame ? RolesTabIndex : 0); if (regGame) { - OpenTab(RolesTabIndex); + OpenTab(RolesTabIndex, __instance.transitionOpen.duration); } return false; @@ -569,19 +571,23 @@ public static class RoleGuidePatches button.transform.GetChild(2).gameObject.SetActive(false); } - private static void OpenTab(int tabIndex) + private static void OpenTab(int tabIndex, float delay = 0f) { _advancedInfoTabScroller?.gameObject.SetActive(false); MatchInfoGuide.Instance.SetActiveTab(tabIndex); - foreach (var entry in tabIndex == ModifiersTabIndex ? ModifierEntries : RoleEntries) + HidePanels(tabIndex == ModifiersTabIndex ? ModifierEntries : RoleEntries); + StartPopulate(delay); + } + + private static void HidePanels(List entries) + { + foreach (var entry in entries) { if (entry.Panel && entry.Panel!.gameObject.activeSelf) { entry.Panel.gameObject.SetActive(false); } } - - StartPopulate(); } public static void OpenModifiersTab() => OpenTab(ModifiersTabIndex); @@ -642,7 +648,7 @@ public static class RoleGuidePatches return Mathf.Clamp(Mathf.Ceil(count / 2f) * 1.3f - 1.5f, 0f, 999f); } - private static void StartPopulate() + private static void StartPopulate(float delay = 0f) { if (_populate != null) { @@ -651,11 +657,25 @@ public static class RoleGuidePatches _loading = true; UpdateSpinner(); - _populate = Coroutines.Start(CoPopulate()); + _populate = Coroutines.Start(CoPopulate(delay)); } - private static IEnumerator CoPopulate() + private static IEnumerator CoPopulate(float delay) { + if (delay > 0f) + { + yield return new WaitForSeconds(delay); + } + else + { + yield return null; + } + + if (!_guide) + { + yield break; + } + var modifiers = IsModifiersTabActive; var entries = modifiers ? ModifierEntries : RoleEntries; var shown = UpdateEntries(entries, modifiers); @@ -678,7 +698,7 @@ public static class RoleGuidePatches private static IEnumerator CoForEachBudgeted(IEnumerable collection, Action action) { var fps = Application.targetFrameRate > 0 ? Application.targetFrameRate : 60; - var budget = 1000L / (fps * 2); + var budget = Math.Max(1L, 1000L / (fps * 4)); var timer = new Stopwatch(); timer.Start(); foreach (var item in collection) From c5431f0666762c2c4db5e0d78b8b7c6968a35599 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:46:44 -0700 Subject: [PATCH 24/28] Keep advanced wiki pages at local scale when parenting --- MiraAPI/Utilities/Helpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 52fd46a..37ddfd6 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -775,7 +775,7 @@ 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.localPosition = new Vector3(0f, 0f, 0f); parent.SetYBoundsMax(Mathf.Clamp(desc.textBounds.size.y - 2 + Mathf.Ceil(num / 2f) * 1.45f, 0f, 999f)); return obj; From edadd4f350836f022884a68c1220d13891312ffb Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:15:04 -0700 Subject: [PATCH 25/28] Defer UI init to first open, add search debounce, fix advanced wiki scale --- MiraAPI/Patches/RoleGuidePatches.cs | 85 +++++++++++++++++++++++++++-- MiraAPI/Utilities/Helpers.cs | 2 +- 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 95edf22..9327d7c 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -28,6 +28,7 @@ public static class RoleGuidePatches { private const int RolesTabIndex = 2; private const int ModifiersTabIndex = 3; + private const float SearchDebounceSeconds = 0.3f; private static readonly List RoleEntries = []; private static readonly List ModifierEntries = []; @@ -51,6 +52,10 @@ public static class RoleGuidePatches private static GameObject _currentAdvancedTabObject; private static TextMeshPro _titleText; private static bool _suppressSearchRefresh; + private static IEnumerator? _searchDebounce; + private static string _appliedSearch = string.Empty; + private static float _openedAt; + private static bool _uiInitialized; private static MatchInfoGuide? _guide; private static IEnumerator? _populate; private static bool _loading; @@ -125,7 +130,12 @@ public static class RoleGuidePatches yield break; } - Initialize(instance); + _guide = instance; + if (RoleEntries.Count == 0) + { + BuildEntries(); + } + if (_populate != null) { yield break; @@ -187,7 +197,11 @@ public static class RoleGuidePatches var regGame = GameManager.Instance.TryCast() != null; if (regGame) { - Initialize(__instance); + if (!_uiInitialized) + { + Initialize(__instance); + } + if (__instance.NormalModeSettings.Count == 0) { __instance.CreateSettingsEntry( @@ -231,6 +245,7 @@ public static class RoleGuidePatches HidePanels(RoleEntries); HidePanels(ModifierEntries); __instance.MatchInfoParent.SetActive(true); + _openedAt = Time.time; if (__instance.ControllerSelectable.Count > 0) { var controllerManager = ControllerManager.Instance; @@ -252,7 +267,7 @@ public static class RoleGuidePatches private static void Initialize(MatchInfoGuide instance) { - if (_guide == instance) + if (_uiInitialized) { return; } @@ -265,11 +280,10 @@ public static class RoleGuidePatches _guide = instance; _loading = false; - RoleEntries.Clear(); - ModifierEntries.Clear(); var sortingOrderButton = Object.Instantiate( HudManager.Instance.SettingsButton, instance.TabButtons[2].transform.parent); + sortingOrderButton.gameObject.SetActive(true); SearchSortingOrderButton = sortingOrderButton.GetComponent(); SetupSearchButton( SearchSortingOrderButton, @@ -284,6 +298,7 @@ public static class RoleGuidePatches var sortingGroupButton = Object.Instantiate( sortingOrderButton, sortingOrderButton.transform.parent); + sortingGroupButton.gameObject.SetActive(true); SearchSortingGroupButton = sortingGroupButton.GetComponent(); SetupSearchButton( SearchSortingGroupButton, @@ -298,6 +313,7 @@ public static class RoleGuidePatches var sortingFilterButton = Object.Instantiate( sortingOrderButton, sortingOrderButton.transform.parent); + sortingFilterButton.gameObject.SetActive(true); SearchSortingFilterButton = sortingFilterButton.GetComponent(); SetupSearchButton( SearchSortingFilterButton, @@ -344,6 +360,7 @@ public static class RoleGuidePatches var searchIconButton = Object.Instantiate( sortingOrderButton, sortingOrderButton.transform.parent); + searchIconButton.gameObject.SetActive(true); SearchIconButton = searchIconButton.GetComponent(); SetupSearchButton( SearchIconButton, @@ -384,6 +401,7 @@ public static class RoleGuidePatches button.OnUp = true; var template = HudManager.Instance.Chat.freeChatField.textArea; _searchBoxTmp = Object.Instantiate(template, searchBox.transform); + _searchBoxTmp.gameObject.SetActive(true); _searchBoxTmp.name = "SearchField"; _searchBoxTmp.transform.localPosition = Vector3.zero; _searchBoxTmp.OnEnter = new Button.ButtonClickedEvent(); @@ -462,7 +480,7 @@ public static class RoleGuidePatches return; } - RefreshActiveTab(true); + QueueSearchRefresh(); })); searchBox.transform.localPosition = new Vector3(-1.438f, 0.756f, -0.2f); @@ -506,6 +524,7 @@ public static class RoleGuidePatches rolesButton.transform.localPosition = new Vector3(-3.6f, -0.544f, -0.2f); modifiersButton.transform.localPosition = new Vector3(-3.6f, -1.144f, -0.2f); BuildEntries(); + _uiInitialized = true; } private static void BuildEntries() @@ -838,8 +857,39 @@ public static class RoleGuidePatches } } + private static void QueueSearchRefresh() + { + if (_searchDebounce != null) + { + Coroutines.Stop(_searchDebounce); + } + + _searchDebounce = Coroutines.Start(CoSearchRefresh()); + } + + private static IEnumerator CoSearchRefresh() + { + yield return new WaitForSeconds(SearchDebounceSeconds); + _searchDebounce = null; + var text = GetSearchText(); + if (text == _appliedSearch) + { + yield break; + } + + _appliedSearch = text; + RefreshActiveTab(true); + } + private static void ResetSearchBox() { + if (_searchDebounce != null) + { + Coroutines.Stop(_searchDebounce); + _searchDebounce = null; + } + + _appliedSearch = string.Empty; _searchIconIdle.sprite = MiraAssets.SearchIconIdleSprite; _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; if (_searchBoxTmp && GetSearchText().Length > 0) @@ -1074,8 +1124,24 @@ public static class RoleGuidePatches return true; } + private static IEnumerator CoDisplayAdvancedWikiLater(float delay, Action display) + { + yield return new WaitForSeconds(delay); + if (_guide && MatchInfoGuide.Instance && MatchInfoGuide.Instance.MatchInfoParent.activeSelf) + { + display(); + } + } + public static void DisplayAdvancedWiki(MatchInfoGuide instance, BaseModifier modifier) { + var remaining = _openedAt + instance.transitionOpen.duration - Time.time; + if (remaining > 0f) + { + Coroutines.Start(CoDisplayAdvancedWikiLater(remaining, () => DisplayAdvancedWiki(instance, modifier))); + return; + } + var name = modifier.ModifierName; Info($"Opening advanced tab for {name}."); if (!TryPrepareAdvancedWiki(instance)) @@ -1088,6 +1154,13 @@ public static class RoleGuidePatches public static void DisplayAdvancedWiki(MatchInfoGuide instance, RoleBehaviour role) { + var remaining = _openedAt + instance.transitionOpen.duration - Time.time; + if (remaining > 0f) + { + Coroutines.Start(CoDisplayAdvancedWikiLater(remaining, () => DisplayAdvancedWiki(instance, role))); + return; + } + Info($"Opening advanced tab for {role.GetRoleName()}."); if (!TryPrepareAdvancedWiki(instance)) { diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 37ddfd6..52fd46a 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -775,7 +775,7 @@ public static class Helpers grid.localScale = new Vector3(1.3f, 1.3f, 1); } - obj.transform.SetParent(parent.Inner.transform, false); + obj.transform.SetParent(parent.Inner.transform); obj.transform.localPosition = new Vector3(0f, 0f, 0f); parent.SetYBoundsMax(Mathf.Clamp(desc.textBounds.size.y - 2 + Mathf.Ceil(num / 2f) * 1.45f, 0f, 999f)); return obj; From b6bddbd0dffff6c17848e5bba69d2a116092d354 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:16:37 -0700 Subject: [PATCH 26/28] Fix duplicate entries by building entries once on first open --- MiraAPI/Patches/RoleGuidePatches.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 9327d7c..509df6a 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -202,6 +202,11 @@ public static class RoleGuidePatches Initialize(__instance); } + if (RoleEntries.Count == 0) + { + BuildEntries(); + } + if (__instance.NormalModeSettings.Count == 0) { __instance.CreateSettingsEntry( @@ -523,7 +528,6 @@ public static class RoleGuidePatches settingButton.transform.localPosition = new Vector3(-3.6f, 0.056f, -0.2f); rolesButton.transform.localPosition = new Vector3(-3.6f, -0.544f, -0.2f); modifiersButton.transform.localPosition = new Vector3(-3.6f, -1.144f, -0.2f); - BuildEntries(); _uiInitialized = true; } From 317fd243b1e7f9147f22647c2316b2e234514043 Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:15:16 -0700 Subject: [PATCH 27/28] Show role/modifier icons on wiki pages, fix wiki softlock after rejoining, even out sort buttons --- MiraAPI/Modifiers/BaseModifier.cs | 2 +- MiraAPI/Patches/RoleGuidePatches.cs | 71 +++++++++++++++++++++++++++-- MiraAPI/Roles/ICustomRole.cs | 2 +- MiraAPI/Utilities/Helpers.cs | 65 ++++++++++++++++++++++++-- 4 files changed, 130 insertions(+), 10 deletions(-) diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 11fac1d..cf108b3 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -117,7 +117,7 @@ public abstract class BaseModifier : IOptionable titleText, parent, ModifierNameLocale, - ModifierName + $" ({ModifierCategoryTitle})", + $"{this.GetTmpIcon()} {ModifierName} ({ModifierCategoryTitle})".TrimStart(), ModifierWikiDescription, abilities); } diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 509df6a..3e520a5 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -109,6 +109,56 @@ public static class RoleGuidePatches public static void Awake(MatchInfoGuide __instance) { __instance.transitionOpen.targetSize = 1.3f; + if (_guide != __instance) + { + ResetState(); + } + } + + /// + /// 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. + /// + private static void ResetState() + { + if (_populate != null) + { + Coroutines.Stop(_populate); + _populate = null; + } + + if (_searchDebounce != null) + { + Coroutines.Stop(_searchDebounce); + _searchDebounce = null; + } + + RoleEntries.Clear(); + ModifierEntries.Clear(); + _guide = null; + _uiInitialized = false; + _loading = false; + _appliedSearch = string.Empty; + _suppressSearchRefresh = false; + _titleText = null!; + _searchBoxTmp = null!; + _rolesScroller = null!; + _modifiersScroller = null!; + _advancedInfoTabScroller = null!; + _currentAdvancedTabObject = null!; + _loadingSpinner = null; + SearchIconButton = null!; + SearchSortingOrderButton = null!; + SearchSortingGroupButton = null!; + SearchSortingFilterButton = null!; + _searchIconIdle = null!; + _searchIconHover = null!; + _searchSortingOrderIdle = null!; + _searchSortingOrderHover = null!; + _searchSortingGroupIdle = null!; + _searchSortingGroupHover = null!; + _searchSortingFilterIdle = null!; + _searchSortingFilterHover = null!; } [HarmonyPostfix] @@ -156,6 +206,11 @@ public static class RoleGuidePatches [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Open))] public static bool Open(MatchInfoGuide __instance) { + if (!HudManager.InstanceExists || !GameManager.Instance || !PlayerControl.LocalPlayer) + { + return false; + } + if (HudManager.Instance.GameMenu.IsOpen || HudManager.Instance.Chat.IsOpenOrOpening) { return false; @@ -311,7 +366,7 @@ public static class RoleGuidePatches SwitchGrouping, MiraAssets.GroupNoneIdleSprite, MiraAssets.GroupNoneHoverSprite, - new Vector3(1.8f, 0.765f, -0.1f), + new Vector3(1.7f, 0.765f, -0.1f), out _searchSortingGroupIdle, out _searchSortingGroupHover); @@ -894,8 +949,16 @@ public static class RoleGuidePatches } _appliedSearch = string.Empty; - _searchIconIdle.sprite = MiraAssets.SearchIconIdleSprite; - _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; + if (_searchIconIdle) + { + _searchIconIdle.sprite = MiraAssets.SearchIconIdleSprite; + } + + if (_searchIconHover) + { + _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; + } + if (_searchBoxTmp && GetSearchText().Length > 0) { _suppressSearchRefresh = true; @@ -1177,7 +1240,7 @@ public static class RoleGuidePatches } else { - var titleTxt = role.GetRoleName() + + var titleTxt = $"{role.GetTmpIcon()} {role.GetRoleName()}" + $" ({TranslationController.Instance.GetString(role.TeamType is RoleTeamTypes.Crewmate ? StringNames.Crewmate : StringNames.Impostor)})"; var description = TranslationController.Instance.GetString(role.BlurbNameLong); if (description.Contains("STRMISS")) diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 79f6570..307b6ca 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -328,7 +328,7 @@ public interface ICustomRole : IOptionable titleText, parent, RoleNameLocale, - RoleName + $" ({RoleFactionTitle})", + $"{this.GetTmpIcon()} {RoleName} ({RoleFactionTitle})", RoleWikiDescription, abilities); } diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 52fd46a..8472e6b 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -11,6 +11,7 @@ using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; using MiraAPI.GameOptions; using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Modifiers; using MiraAPI.Roles; using MiraAPI.Translation; using MiraAPI.Utilities.Assets; @@ -688,6 +689,40 @@ public static class Helpers return role is ICustomRole custom ? custom.RoleLongDescription : role.BlurbLong; } + /// + /// Returns the TextMeshPro sprite tag for a role, matching how role names are shown in-game. + /// Uses the role's if set, otherwise falls back to the faction icon. + /// + /// The . + /// A <sprite> rich text tag. + public static string GetTmpIcon(this RoleBehaviour role) + { + return role is ICustomRole custom ? custom.GetTmpIcon() : $""; + } + + /// + /// Returns the TextMeshPro sprite tag for a custom role, matching how role names are shown in-game. + /// Uses the role's if set, otherwise falls back to the faction icon. + /// + /// The . + /// A <sprite> rich text tag. + public static string GetTmpIcon(this ICustomRole role) + { + return role.Configuration.IconTmp + ? $"" + : $""; + } + + /// + /// Returns the TextMeshPro sprite tag for a modifier, or an empty string if it has no . + /// + /// The . + /// A <sprite> rich text tag, or . + public static string GetTmpIcon(this BaseModifier modifier) + { + return modifier.IconTmp ? $"" : string.Empty; + } + /// /// Returns whether the is blacklisted from appearing and spawning. (Only applicable to vanilla roles). /// @@ -713,6 +748,7 @@ public static class Helpers public static GameObject CreateAdvancedWikiTab(MatchInfoGuide guide, string objName, string title, string description, TextMeshPro titleTmp, out TextMeshPro descriptionTmp) { var obj = new GameObject(objName); + titleTmp.richText = true; titleTmp.text = title; descriptionTmp = Object.Instantiate(guide.MatchInfoRolePanelPrefab.roleCount, obj.transform); descriptionTmp.fontSizeMin = descriptionTmp.fontSizeMax = descriptionTmp.fontSize = 2f; @@ -738,6 +774,7 @@ public static class Helpers /// The title of the page. /// The description of the page. /// The abilities to display in a grid below the description. + /// Whether to use variable text sizing for ability descriptions (allows different heights). /// The of the wiki page. public static GameObject CreateAdvancedWikiPage( MatchInfoGuide guide, @@ -746,7 +783,8 @@ public static class Helpers string objName, string title, string description, - IReadOnlyCollection abilities) + IReadOnlyCollection abilities, + bool variableTextSizing = false) { parent.ScrollToTop(); var obj = CreateAdvancedWikiTab( @@ -757,6 +795,7 @@ public static class Helpers titleTmp, out var desc); var num = 0; + var maxTextSize = 0f; if (abilities.Count != 0) { var grid = Object.Instantiate(parent.Inner, obj.transform); @@ -768,7 +807,16 @@ public static class Helpers foreach (var ability in abilities) { num++; - SetupAbilityPanel(Object.Instantiate(guide.MatchInfoRolePanelPrefab, grid), ability); + var panel = Object.Instantiate(guide.MatchInfoRolePanelPrefab, grid); + SetupAbilityPanel(panel, ability, variableTextSizing); + if (variableTextSizing) + { + var descSize = panel.roleDescription.textBounds.size.y; + if (maxTextSize < descSize) + { + maxTextSize = descSize; + } + } } grid.localPosition = new Vector3(-3.9f, 1.1f - desc.textBounds.size.y, 0f); @@ -777,11 +825,14 @@ public static class Helpers obj.transform.SetParent(parent.Inner.transform); obj.transform.localPosition = new Vector3(0f, 0f, 0f); - parent.SetYBoundsMax(Mathf.Clamp(desc.textBounds.size.y - 2 + Mathf.Ceil(num / 2f) * 1.45f, 0f, 999f)); + var scrollHeight = variableTextSizing + ? (desc.textBounds.size.y - 2) + maxTextSize + 0.475f + : desc.textBounds.size.y - 2 + Mathf.Ceil(num / 2f) * 1.45f; + parent.SetYBoundsMax(Mathf.Clamp(scrollHeight, 0f, 999f)); return obj; } - private static void SetupAbilityPanel(MatchInfoRolePanel panel, AdvancedWikiAbilityDescription ability) + private static void SetupAbilityPanel(MatchInfoRolePanel panel, AdvancedWikiAbilityDescription ability, bool variableTextSizing = false) { panel.roleCount.text = ability.AbilityType; panel.roleCount.transform.localPosition += new Vector3(-0.04f, 0); @@ -790,6 +841,12 @@ public static class Helpers panel.roleDescription.text = ability.Description; panel.roleDescription.rectTransform.sizeDelta = new Vector2(2.601f, 0.8f); panel.roleDescription.transform.localPosition += new Vector3(0, -0.1f); + if (variableTextSizing) + { + panel.roleDescription.alignment = TextAlignmentOptions.Top; + panel.roleDescription.fontSizeMin = 1.5f; + panel.roleDescription.ForceMeshUpdate(); + } panel.roleIcon.sprite = ability.Icon.LoadAsset(); panel.roleIcon.SetSizeLimit(0.13f); From d3bce25ef6f1d3776c9bfcbbc0be4b849e68167d Mon Sep 17 00:00:00 2001 From: ReAdmin <170283204+rewalo@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:49:11 -0700 Subject: [PATCH 28/28] Guard EnsurePanel and expose TriggerPreload --- MiraAPI/Patches/RoleGuidePatches.cs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 3e520a5..e3b134a 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -168,6 +168,16 @@ public static class RoleGuidePatches Coroutines.Start(CoPreload(__instance)); } + public static void TriggerPreload(MatchInfoGuide instance) + { + if (!instance || _populate != null) + { + return; + } + + Coroutines.Start(CoPreload(instance)); + } + private static IEnumerator CoPreload(MatchInfoGuide instance) { while (instance && (!HudManager.InstanceExists || !GameManager.Instance)) @@ -801,9 +811,18 @@ public static class RoleGuidePatches return; } - var panel = Object.Instantiate( - _guide!.MatchInfoRolePanelPrefab, - entry.Modifier != null ? _modifiersScroller.Inner : _rolesScroller.Inner); + if (!_guide || !_guide!.MatchInfoRolePanelPrefab) + { + return; + } + + var parent = entry.Modifier != null ? _modifiersScroller?.Inner : _rolesScroller?.Inner; + if (parent == null) + { + return; + } + + var panel = Object.Instantiate(_guide!.MatchInfoRolePanelPrefab, parent); if (entry.Modifier != null) { panel.SetModifierPanel(entry.Modifier, entry.Amount, entry.Chance);