From 1ab662bcf122736a247df37a3d14389780262181 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:48:09 -0600 Subject: [PATCH 01/44] Adds a generic IModdedOptionList definition for easier typed iteration --- MiraAPI/GameOptions/IModdedOption.cs | 19 ++++++++++++++++--- .../OptionTypes/ModdedOptionList.cs | 13 +++++++++---- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index 099194e..7ecc57c 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -1,8 +1,8 @@ -using System; -using System.Collections.Generic; -using BepInEx.Configuration; +using BepInEx.Configuration; using MiraAPI.Networking; using MiraAPI.PluginLoading; +using System; +using System.Collections.Generic; using UnityEngine; namespace MiraAPI.GameOptions; @@ -122,3 +122,16 @@ public interface IModdedOption public interface IModdedOptionList : IReadOnlyList { } + +/// +/// Interface for list of options. +/// +/// The type of . +public interface IModdedOptionList : IModdedOptionList where T : IModdedOption +{ + /// + new T this[int index] { get; } + + /// + new IEnumerator GetEnumerator(); +} diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs index 5a764b2..88b32c5 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -9,7 +9,7 @@ namespace MiraAPI.GameOptions.OptionTypes; /// Represents a modded option list. /// /// The type of options. -public class ModdedOptionList : IModdedOptionList where T : IModdedOption +public class ModdedOptionList : IModdedOptionList where T : IModdedOption { /// public int Count { get; } @@ -40,10 +40,10 @@ public class ModdedOptionList : IModdedOptionList where T : IModdedOption Options = options.ToArray(); } - /// - public IEnumerator GetEnumerator() + /// + public IEnumerator GetEnumerator() { - return ((IEnumerable)Options).GetEnumerator(); + return Options.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() @@ -51,6 +51,11 @@ public class ModdedOptionList : IModdedOptionList where T : IModdedOption return Options.GetEnumerator(); } + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)Options).GetEnumerator(); + } + /// /// Indexes the option of type . /// From ffe9194d54a882913879623d50a410bc7f34f7d8 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:47:12 -0600 Subject: [PATCH 02/44] Resolved warnings --- MiraAPI/GameOptions/IModdedOption.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index 7ecc57c..5d424bb 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -1,8 +1,8 @@ -using BepInEx.Configuration; +using System; +using System.Collections.Generic; +using BepInEx.Configuration; using MiraAPI.Networking; using MiraAPI.PluginLoading; -using System; -using System.Collections.Generic; using UnityEngine; namespace MiraAPI.GameOptions; @@ -127,11 +127,12 @@ public interface IModdedOptionList : IReadOnlyList /// Interface for list of options. /// /// The type of . -public interface IModdedOptionList : IModdedOptionList where T : IModdedOption +public interface IModdedOptionList : IModdedOptionList + where T : IModdedOption { - /// + /// new T this[int index] { get; } - /// + /// new IEnumerator GetEnumerator(); } From 1b587547f770f8dfcbe8c5c21e4bc396724ec862 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 25 Sep 2026 12:43:14 -0600 Subject: [PATCH 03/44] Added ConfigName property to IModdedOptionList to optionally rename the individual options --- MiraAPI/GameOptions/IModdedOption.cs | 6 ++++++ MiraAPI/GameOptions/ModdedOptionsManager.cs | 4 +++- MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index 5d424bb..fdf71a6 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -121,6 +121,12 @@ public interface IModdedOption /// public interface IModdedOptionList : IReadOnlyList { + /// + /// Gets the function to name the keys of the individual options in the configuration file. + /// + /// Return to use the default of the property's name with its index appended. + /// + Func? ConfigName { get; init; } } /// diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 387a1f8..3518084 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -198,9 +198,11 @@ public static class ModdedOptionsManager return; } + Func configName = optionList.ConfigName ?? (i => property.Name + i); + for (int i = 0; i < optionList.Count; i++) { - RegisterOption(optionList[i], group, property.Name + i, pluginInfo); + RegisterOption(optionList[i], group, configName(i), pluginInfo); } } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs index a09b15b..11b54c3 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -20,6 +20,9 @@ public class ModdedOptionList : IModdedOptionList /// public IReadOnlyList Options { get; } + /// + public Func? ConfigName { get; init; } + /// /// Initializes a new instance of the class. /// From 2d4619f2cfe813848013726122d9d7c7a3818972 Mon Sep 17 00:00:00 2001 From: David Higueros <111460331+TheDavSmasher@users.noreply.github.com> Date: Fri, 25 Sep 2026 12:44:44 -0600 Subject: [PATCH 04/44] Used generic interface for ModdedOptionList --- MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs index 11b54c3..257ae6e 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -9,7 +9,7 @@ namespace MiraAPI.GameOptions.OptionTypes; /// Represents a modded option list. /// /// The type of options. -public class ModdedOptionList : IModdedOptionList +public class ModdedOptionList : IModdedOptionList where T : IModdedOption { /// From 5e6645ff3b11b9db8b2f9642ee39475d09284908 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 4 Oct 2026 11:49:19 -0500 Subject: [PATCH 05/44] Add wiki components to modifiers --- MiraAPI/Modifiers/BaseModifier.cs | 133 +++++++++++++++++++++++- MiraAPI/Modifiers/Types/GameModifier.cs | 3 + 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 0017fd9..0ca73e0 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -1,8 +1,16 @@ using System; +using System.Collections.Generic; +using Il2CppInterop.Runtime.Attributes; using MiraAPI.GameOptions; using MiraAPI.PluginLoading; +using MiraAPI.Roles; +using MiraAPI.Translation; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; +using TMPro; using UnityEngine; +using UnityEngine.UI; +using Object = UnityEngine.Object; namespace MiraAPI.Modifiers; @@ -44,16 +52,134 @@ public abstract class BaseModifier : IOptionable x => x.InternalModifiers.Exists(y => y.TypeId == TypeId)) ?? throw new InvalidOperationException("Modifier is not registered."); + /// + /// Gets the id part used to build the modifier's translation keys. It is recommended to simply call it what the modifier is, as other mods may utilize it. + /// + public virtual string IdPart => GetType().Name; + + /// + /// Gets the id part used to build the modifier's translation keys. It is recommended to call it ModGuid.Modifier.Type. + /// + public virtual string IdPrefix => GetType().Namespace!; + /// /// Gets the modifier name. /// - public abstract string ModifierName { get; } + public virtual string ModifierName => MiraLocaleManager.Get(ModifierNameLocale); + + /// + /// Gets the modifier's name id for localization. + /// + public virtual string ModifierNameLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart); + + /// + /// Gets the medium description of the modifier. Used in the modifier guide and options menu. + /// + public virtual string ModifierMedDescription => MiraLocaleManager.Get(ModifierMedDescriptionLocale, GetDescription()); + + /// + /// Gets the modifier's medium description id for localization. + /// + public virtual string ModifierMedDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "MedDescription"); + + /// + /// Gets the wiki description of the modifier. Used in the wiki and normally appends the options text as well. + /// + public virtual string ModifierWikiDescription => MiraLocaleManager.Get(ModifierWikiDescriptionLocale, GetDescription()) + + Helpers.GetOptionsText(GetType()); + + /// + /// Gets the modifier's wiki description id for localization. + /// + public virtual string ModifierWikiDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "WikiDescription"); + + /// + /// Gets the category of the modifier. Used in the wiki. + /// + public virtual string ModifierCategoryTitle => MiraLocaleManager.Get("Modifier"); + + /// + /// Gets whether the modifier is forcibly shown or disabled in the wiki screen. + /// + /// if the modifier is always displayed, otherwise if it is never displayable, or if it is dictated by amount and chance. + public virtual bool? ForceShowModifierOnWiki => false; + + /// + /// Gets the information to display in the wiki. + /// + /// The guide object. + /// The title text object. + /// The scroller parent. + /// The of the wiki. + public virtual GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, Scroller parent) + { + parent.ScrollToTop(); + var obj = Helpers.CreateAdvancedWikiTab( + guide, + 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; + } + + /// + /// Gets the list of abilities or other similar information to display below the text of a modifier's wiki page. + /// + [HideFromIl2Cpp] + public virtual List WikiAbilities => []; /// /// Gets the modifier icon. Useless if is . /// public virtual LoadableAsset? ModifierIcon => null; + /// + /// Gets the for the Modifier Icon. + /// + [HideFromIl2Cpp] + public virtual TMP_SpriteAsset IconTmp => null!; + /// /// Gets a value indicating whether the modifier is hidden on the UI. Will be hidden either way if no description is provided. /// @@ -69,6 +195,11 @@ public abstract class BaseModifier : IOptionable /// public virtual Color FreeplayFileColor => Color.gray; + /// + /// Gets the of the modifier, used in the wiki and other UI outside freeplay. + /// + public virtual Color GeneralColor => FreeplayFileColor; + /// /// Gets a value indicating whether the modifier is unique. If , the player can only have one instance of this modifier. /// diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index f574fd3..b432920 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -9,6 +9,9 @@ namespace MiraAPI.Modifiers.Types; [MiraIgnore] public abstract class GameModifier : BaseModifier { + /// + public override bool? ForceShowModifierOnWiki => null; + /// public override bool ShowInFreeplay => true; From 6d1557a48d41923aa5848600d7aa3fa74187898f Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 4 Oct 2026 17:17:42 -0500 Subject: [PATCH 06/44] Add modifier support for wiki --- MiraAPI/GameOptions/ISummarizedOptions.cs | 6 +- MiraAPI/Patches/RoleGuidePatches.cs | 332 ++++++++++++++++++---- 2 files changed, 284 insertions(+), 54 deletions(-) diff --git a/MiraAPI/GameOptions/ISummarizedOptions.cs b/MiraAPI/GameOptions/ISummarizedOptions.cs index 60b5ea6..7873902 100644 --- a/MiraAPI/GameOptions/ISummarizedOptions.cs +++ b/MiraAPI/GameOptions/ISummarizedOptions.cs @@ -9,13 +9,13 @@ namespace MiraAPI.GameOptions; public interface ISummarizedOptions { /// - /// Option string keys (StringNames) to omit from the wiki options list (e.g., a large set of related options). + /// Gets the option string keys (StringNames) to omit from the wiki options list (e.g., a large set of related options). /// IReadOnlySet WikiHiddenOptionKeys { get; } /// - /// Summary lines to insert when the first hidden option would have appeared. + /// Gets summary lines to insert when the first hidden option would have appeared. /// Lines should already be formatted as "Title: Value". /// IEnumerable GetWikiOptionSummaryLines(); -} \ No newline at end of file +} diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 5822bf8..1e24d05 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -5,6 +5,8 @@ using System.Linq; using AmongUs.GameOptions; using HarmonyLib; using Innersloth.Assets; +using MiraAPI.Modifiers; +using MiraAPI.Modifiers.Types; using MiraAPI.Roles; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; @@ -167,7 +169,7 @@ public static class RoleGuidePatches sortingFilterButton.transform.localPosition = new Vector3(1f, 0.765f, -0.1f); sortingFilterButton.transform.localScale = Vector3.one; - __instance.numOfTabs = 3; + __instance.numOfTabs = 4; __instance.TabButtons[2].SelectButton(true); __instance.MatchInfoRoleMaskArea.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); @@ -196,9 +198,20 @@ public static class RoleGuidePatches __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); + 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); + modifiersWikiTab = modifiersTab.GetComponent(); + __instance.settingsTabs.Add(modifiersTab); + var wikiTab = Object.Instantiate( __instance.settingsTabs[2], __instance.settingsTabs[2].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); @@ -269,20 +282,11 @@ public static class RoleGuidePatches _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; } - var newSorted = RolePanels - .OrderByDescending(child => - child.Value.GetTitle().Equals(text, StringComparison.OrdinalIgnoreCase)) - .ThenByDescending(child => child.Value.GetTitle().Contains( - text, - StringComparison.InvariantCultureIgnoreCase)) - .ThenBy(GetSortingOrder()); - - foreach (var pair in newSorted) - { - pair.Value.Panel.transform.SetAsLastSibling(); - } + SortAllModifiers(text); + SortAllRoles(text); _rolesScroller.ScrollToTop(); + modifiersWikiTab.ScrollToTop(); })); searchBoxTmp.transform.localPosition = new Vector3(-1.438f, 0.756f, -0.2f); @@ -334,13 +338,33 @@ public static class RoleGuidePatches rolesBtnHighlight.size = Vector2.one; rolesBtnHighlight.transform.GetChild(0).gameObject.SetActive(false); - playerCollider.size = settingsCollider.size = rolesCollider.size = new Vector2(0.8f, 0.74f); - playerCollider.offset = settingsCollider.offset = rolesCollider.offset = Vector2.zero; + var modifiersButton = Object.Instantiate(__instance.TabButtons[2], __instance.TabButtons[2].transform.parent); + modifiersButton.OnClick = new Button.ButtonClickedEvent(); + modifiersButton.OnClick.AddListener((Action)(() => OpenModifiersTab())); + var modifiersCollider = modifiersButton.GetComponent(); + modifiersButton.transform.GetChild(0).gameObject.SetActive(false); + var modifiersBtnInactive = modifiersButton.inactiveSprites.GetComponent(); + modifiersBtnInactive.sprite = MiraAssets.WikiRolesButtonIdleSprite; + modifiersBtnInactive.size = Vector2.one; + modifiersBtnInactive.transform.GetChild(0).gameObject.SetActive(false); + var modifiersBtnSelected = modifiersButton.selectedSprites.GetComponent(); + modifiersBtnSelected.sprite = MiraAssets.WikiRolesButtonOpenSprite; + modifiersBtnSelected.size = Vector2.one; + modifiersBtnSelected.transform.GetChild(0).gameObject.SetActive(false); + var modifiersBtnHighlight = modifiersButton.activeSprites.GetComponent(); + modifiersBtnHighlight.sprite = MiraAssets.WikiRolesButtonHoverSprite; + modifiersBtnHighlight.size = Vector2.one; + modifiersBtnHighlight.transform.GetChild(0).gameObject.SetActive(false); + __instance.TabButtons.Add(modifiersButton); + + playerCollider.size = settingsCollider.size = rolesCollider.size = modifiersCollider.size = new Vector2(0.8f, 0.74f); + playerCollider.offset = settingsCollider.offset = rolesCollider.offset = modifiersCollider.offset = Vector2.zero; playerButton.transform.localScale = settingButton.transform.localScale = - rolesButton.transform.localScale = new Vector3(0.7f, 0.7f, 1); + rolesButton.transform.localScale = modifiersButton.transform.localScale = new Vector3(0.7f, 0.7f, 1); 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 { @@ -368,7 +392,15 @@ public static class RoleGuidePatches return false; } + public static void OpenModifiersTab() + { + advancedWikiTab?.gameObject.SetActive(false); + MatchInfoGuide.Instance.SetActiveTab(3); + } + private static readonly Dictionary RolePanels = []; + private static readonly Dictionary ModifierPanels = []; + private static Scroller modifiersWikiTab; private static Scroller advancedWikiTab; private static TextBoxTMP searchBoxTmp; private static GameObject currentAdvancedTabObject; @@ -403,14 +435,18 @@ public static class RoleGuidePatches // ignored } - var newSorted = sortMethod is SortingMethod.Alphabetical - ? RolePanels.OrderBy(GetSortingOrder()) - : RolePanels.OrderByDescending(GetSortingOrder()); - - foreach (var pair in newSorted) + var text = string.Empty; + try { - pair.Value.Panel.transform.SetAsLastSibling(); + text = searchBoxTmp.outputText.text; } + catch + { + // ignored + } + + SortAllRoles(text); + SortAllModifiers(text); _rolesScroller.ScrollToTop(); } @@ -444,14 +480,7 @@ public static class RoleGuidePatches } ToggleRoleVisibility(); - var newSorted = sortMethod is SortingMethod.Alphabetical - ? RolePanels.OrderBy(GetSortingOrder()) - : RolePanels.OrderByDescending(GetSortingOrder()); - - foreach (var pair in newSorted) - { - pair.Value.Panel.transform.SetAsLastSibling(); - } + ToggleModifierVisibility(); } public static void SwitchMethod() @@ -473,14 +502,18 @@ public static class RoleGuidePatches ? MiraAssets.SortingAzHoverSprite : MiraAssets.SortingZaHoverSprite; - var newSorted = sortMethod is SortingMethod.Alphabetical - ? RolePanels.OrderBy(GetSortingOrder()) - : RolePanels.OrderByDescending(GetSortingOrder()); - - foreach (var pair in newSorted) + var text = string.Empty; + try { - pair.Value.Panel.transform.SetAsLastSibling(); + text = searchBoxTmp.outputText.text; } + catch + { + // ignored + } + + SortAllRoles(text); + SortAllModifiers(text); } public static void SwitchGrouping() @@ -511,14 +544,18 @@ public static class RoleGuidePatches break; } - var newSorted = sortMethod is SortingMethod.Alphabetical - ? RolePanels.OrderBy(GetSortingOrder()) - : RolePanels.OrderByDescending(GetSortingOrder()); - - foreach (var pair in newSorted) + var text = string.Empty; + try { - pair.Value.Panel.transform.SetAsLastSibling(); + text = searchBoxTmp.outputText.text; } + catch + { + // ignored + } + + SortAllRoles(text); + SortAllModifiers(text); } public static void DisplayNormalRoleSettings(MatchInfoGuide instance, bool reset) @@ -527,6 +564,7 @@ public static class RoleGuidePatches if (reset) { RolePanels.Clear(); + ModifierPanels.Clear(); instance.CreateSettingsEntry( StringNames.GameNumImpostors, GameManager.Instance.AllGameSettingData[StringNames.GameNumImpostors] @@ -553,6 +591,40 @@ public static class RoleGuidePatches GameManager.Instance.LogicOptions.GetTaskBarMode().ToString()); var hoverColor = new Color32(255, 255, 255, 150); + foreach (var modifier in ModifierManager.Modifiers.OfType()) + { + var panel = Object.Instantiate( + instance.MatchInfoRolePanelPrefab, + modifiersWikiTab.Inner); + var amount = + modifier.GetAmountPerGame(); + var chance = + modifier.GetAssignmentChance(); + panel.SetModifierPanel( + 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); })); + ModifierPanels.Add( + modifier, + new DetailedPanel( + panel, + modifier.ModifierName, + modifier.ModifierCategoryTitle, + modifier.ParentMod.MiraPlugin.GetAbbreviatedModName())); + } + foreach (var roleBehaviour in RoleManager.Instance.AllRoles.ToArray().OrderBy(x => x.GetRoleName())) { if (roleBehaviour.Role is not RoleTypes.Crewmate and not RoleTypes.Impostor and @@ -598,6 +670,7 @@ public static class RoleGuidePatches advancedWikiTab?.gameObject.SetActive(false); + ToggleModifierVisibility(); ToggleRoleVisibility(); if (reset) { @@ -605,6 +678,64 @@ public static class RoleGuidePatches } } + + public static void ToggleModifierVisibility() + { + var num = 0; + foreach (var (modifier, holder) in ModifierPanels) + { + var panel = holder.Panel; + var amount = + 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; + 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) + { + panel.gameObject.SetActive(false); + continue; + } + + panel.gameObject.SetActive(true); + num++; + } + + var text = string.Empty; + try + { + text = searchBoxTmp.outputText.text; + } + catch + { + // ignored + } + + SortAllModifiers(text); + + /*var instance = MatchInfoGuide.Instance; + instance.rolesEnabledMessage.SetActive(num == 0);*/ + + modifiersWikiTab.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); + modifiersWikiTab.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); + } + public static void ToggleRoleVisibility() { var num = 0; @@ -644,14 +775,17 @@ public static class RoleGuidePatches num++; } - var newSorted = sortMethod is SortingMethod.Alphabetical - ? RolePanels.OrderBy(GetSortingOrder()) - : RolePanels.OrderByDescending(GetSortingOrder()); - - foreach (var pair in newSorted) + var text = string.Empty; + try { - pair.Value.Panel.transform.SetAsLastSibling(); + text = searchBoxTmp.outputText.text; } + catch + { + // ignored + } + + SortAllRoles(text); var instance = MatchInfoGuide.Instance; instance.rolesEnabledMessage.SetActive(num == 0); @@ -660,6 +794,38 @@ public static class RoleGuidePatches instance.MatchInfoRoleScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); } + public static void SortAllRoles(string searchText) + { + var newSorted = RolePanels + .OrderByDescending(child => + child.Value.GetTitle().Equals(searchText, StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(child => child.Value.GetTitle().Contains( + searchText, + StringComparison.InvariantCultureIgnoreCase)) + .ThenBy(GetSortingOrder()); + + foreach (var pair in newSorted) + { + pair.Value.Panel.transform.SetAsLastSibling(); + } + } + + public static void SortAllModifiers(string searchText) + { + var newSorted = ModifierPanels + .OrderByDescending(child => + child.Value.GetTitle().Equals(searchText, StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(child => child.Value.GetTitle().Contains( + searchText, + StringComparison.InvariantCultureIgnoreCase)) + .ThenBy(GetModifierSortingOrder()); + + foreach (var pair in newSorted) + { + pair.Value.Panel.transform.SetAsLastSibling(); + } + } + public static Func, string> GetSortingOrder() { switch (sortOrder) @@ -676,6 +842,22 @@ public static class RoleGuidePatches } } + 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)}"; + } + } + public static string GetTitle(this DetailedPanel panel) { switch (sortOrder) @@ -741,10 +923,33 @@ public static class RoleGuidePatches advancedWikiTab?.gameObject.SetActive(false); } + public static void DisplayAdvancedWiki(MatchInfoGuide instance, BaseModifier modifier) + { + var name = modifier.ModifierName; + Warning($"Opening advanced tab for {name}."); + instance.settingsTabs[2].SetActive(false); + instance.settingsTabs[3].SetActive(false); + advancedWikiTab?.gameObject.SetActive(true); + if (currentAdvancedTabObject) + { + currentAdvancedTabObject.SetActive(false); + currentAdvancedTabObject.Destroy(); + } + + if (advancedWikiTab == null) + { + Warning($"Wiki tab is null."); + return; + } + + currentAdvancedTabObject = modifier.GetAdvancedWiki(instance, titleText, advancedWikiTab); + } + 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); advancedWikiTab?.gameObject.SetActive(true); if (currentAdvancedTabObject) { @@ -825,6 +1030,31 @@ public static class RoleGuidePatches return false; } + public static void SetModifierPanel( + this MatchInfoRolePanel instance, + GameModifier modifier, + 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()})"; + + instance.roleIcon.SetSizeLimit(0.13f); + instance.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50); + instance.roleName.fontMaterial.SetFloat(instance.STENCIL_NAME, 50f); + instance.roleDescription.fontMaterial.SetFloat(instance.STENCIL_NAME, 50f); + instance.roleCount.fontMaterial.SetFloat(instance.STENCIL_NAME, 50f); + instance.roleIcon.transform.localScale = new Vector3(4f, 4f, 1f); + } + public static string GetCategoryTitle(this RoleBehaviour role) { if (role is ICustomRole customRole) @@ -846,18 +1076,18 @@ public enum SortingGroups { Ungrouped, Faction, - AmountChance + AmountChance, } public enum SortingMethod { Alphabetical, - AlphabeticalDescending + AlphabeticalDescending, } public enum SortingFilter { EnabledOnly, DisabledOnly, - AllRoles + AllRoles, } From 3cf196341489eb24f3b8ec439c6e0a2c0dbd7fcb Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 4 Oct 2026 18:29:18 -0500 Subject: [PATCH 07/44] Optimize search and filter to be "per-menu" --- MiraAPI/Patches/RoleGuidePatches.cs | 243 ++++++++++++++++++++-------- 1 file changed, 179 insertions(+), 64 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 1e24d05..79f00c7 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -35,6 +35,10 @@ 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; [HarmonyPostfix] [HarmonyPriority(Priority.First)] @@ -67,7 +71,7 @@ public static class RoleGuidePatches 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); - advancedWikiTab = wikiTab.GetComponent(); + _advancedInfoTabScroller = wikiTab.GetComponent(); } [HarmonyPrefix] @@ -202,10 +206,11 @@ public static class RoleGuidePatches __instance.settingsTabs[2], __instance.settingsTabs[2].transform.parent); modifiersTab.name = "ModifiersPanel"; - modifiersTab.transform.FindChild("MaskArea")?.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); + 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); - modifiersWikiTab = modifiersTab.GetComponent(); + _modifiersScroller = modifiersTab.GetComponent(); __instance.settingsTabs.Add(modifiersTab); var wikiTab = Object.Instantiate( @@ -215,7 +220,7 @@ public static class RoleGuidePatches 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); - advancedWikiTab = wikiTab.GetComponent(); + _advancedInfoTabScroller = wikiTab.GetComponent(); DisplayNormalRoleSettings(__instance, true); @@ -282,11 +287,16 @@ public static class RoleGuidePatches _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; } - SortAllModifiers(text); - SortAllRoles(text); - - _rolesScroller.ScrollToTop(); - modifiersWikiTab.ScrollToTop(); + if (MatchInfoGuide.Instance.activeTabIndex != 3) + { + SortAllRoles(text); + _rolesScroller.ScrollToTop(); + } + else + { + SortAllModifiers(text); + _modifiersScroller.ScrollToTop(); + } })); searchBoxTmp.transform.localPosition = new Vector3(-1.438f, 0.756f, -0.2f); @@ -323,6 +333,8 @@ public static class RoleGuidePatches settingBtnHighlight.transform.GetChild(0).gameObject.SetActive(false); var rolesButton = __instance.TabButtons[2]; + rolesButton.OnClick = new Button.ButtonClickedEvent(); + rolesButton.OnClick.AddListener((Action)(() => OpenRolesTab())); var rolesCollider = rolesButton.GetComponent(); rolesButton.transform.GetChild(0).gameObject.SetActive(false); var rolesBtnInactive = rolesButton.inactiveSprites.GetComponent(); @@ -338,7 +350,9 @@ public static class RoleGuidePatches rolesBtnHighlight.size = Vector2.one; rolesBtnHighlight.transform.GetChild(0).gameObject.SetActive(false); - var modifiersButton = Object.Instantiate(__instance.TabButtons[2], __instance.TabButtons[2].transform.parent); + var modifiersButton = Object.Instantiate( + __instance.TabButtons[2], + __instance.TabButtons[2].transform.parent); modifiersButton.OnClick = new Button.ButtonClickedEvent(); modifiersButton.OnClick.AddListener((Action)(() => OpenModifiersTab())); var modifiersCollider = modifiersButton.GetComponent(); @@ -357,10 +371,13 @@ public static class RoleGuidePatches modifiersBtnHighlight.transform.GetChild(0).gameObject.SetActive(false); __instance.TabButtons.Add(modifiersButton); - playerCollider.size = settingsCollider.size = rolesCollider.size = modifiersCollider.size = new Vector2(0.8f, 0.74f); - playerCollider.offset = settingsCollider.offset = rolesCollider.offset = modifiersCollider.offset = Vector2.zero; + playerCollider.size = settingsCollider.size = + rolesCollider.size = modifiersCollider.size = new Vector2(0.8f, 0.74f); + playerCollider.offset = settingsCollider.offset = + rolesCollider.offset = modifiersCollider.offset = Vector2.zero; playerButton.transform.localScale = settingButton.transform.localScale = - rolesButton.transform.localScale = modifiersButton.transform.localScale = new Vector3(0.7f, 0.7f, 1); + rolesButton.transform.localScale = + modifiersButton.transform.localScale = new Vector3(0.7f, 0.7f, 1); 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); @@ -394,14 +411,64 @@ public static class RoleGuidePatches public static void OpenModifiersTab() { - advancedWikiTab?.gameObject.SetActive(false); + _advancedInfoTabScroller?.gameObject.SetActive(false); MatchInfoGuide.Instance.SetActiveTab(3); + if (_needsToggledModifierRefresh) + { + ToggleModifierVisibility(); + } + + 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) + { + ToggleRoleVisibility(); + } + + 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 modifiersWikiTab; - private static Scroller advancedWikiTab; + private static Scroller _modifiersScroller; + private static Scroller _advancedInfoTabScroller; private static TextBoxTMP searchBoxTmp; private static GameObject currentAdvancedTabObject; private static TextMeshPro titleText; @@ -436,19 +503,19 @@ public static class RoleGuidePatches } var text = string.Empty; - try - { - text = searchBoxTmp.outputText.text; - } - catch - { - // ignored - } - SortAllRoles(text); - SortAllModifiers(text); - - _rolesScroller.ScrollToTop(); + if (MatchInfoGuide.Instance.activeTabIndex != 3) + { + SortAllRoles(text); + _rolesScroller.ScrollToTop(); + _needsModifierRefresh = true; + } + else + { + SortAllModifiers(text); + _modifiersScroller.ScrollToTop(); + _needsRoleRefresh = true; + } } public static void SwitchFilter() @@ -479,8 +546,16 @@ public static class RoleGuidePatches break; } - ToggleRoleVisibility(); - ToggleModifierVisibility(); + if (MatchInfoGuide.Instance.activeTabIndex != 3) + { + ToggleRoleVisibility(); + _needsToggledModifierRefresh = _needsModifierRefresh = true; + } + else + { + ToggleModifierVisibility(); + _needsToggledRoleRefresh = _needsRoleRefresh = true; + } } public static void SwitchMethod() @@ -512,8 +587,16 @@ public static class RoleGuidePatches // ignored } - SortAllRoles(text); - SortAllModifiers(text); + if (MatchInfoGuide.Instance.activeTabIndex != 3) + { + SortAllRoles(text); + _needsModifierRefresh = true; + } + else + { + SortAllModifiers(text); + _needsRoleRefresh = true; + } } public static void SwitchGrouping() @@ -554,8 +637,16 @@ public static class RoleGuidePatches // ignored } - SortAllRoles(text); - SortAllModifiers(text); + if (MatchInfoGuide.Instance.activeTabIndex != 3) + { + SortAllRoles(text); + _needsModifierRefresh = true; + } + else + { + SortAllModifiers(text); + _needsRoleRefresh = true; + } } public static void DisplayNormalRoleSettings(MatchInfoGuide instance, bool reset) @@ -595,7 +686,7 @@ public static class RoleGuidePatches { var panel = Object.Instantiate( instance.MatchInfoRolePanelPrefab, - modifiersWikiTab.Inner); + _modifiersScroller.Inner); var amount = modifier.GetAmountPerGame(); var chance = @@ -668,10 +759,17 @@ public static class RoleGuidePatches } } - advancedWikiTab?.gameObject.SetActive(false); + _advancedInfoTabScroller?.gameObject.SetActive(false); + + if (MatchInfoGuide.Instance.activeTabIndex != 3) + { + ToggleRoleVisibility(); + } + else + { + ToggleModifierVisibility(); + } - ToggleModifierVisibility(); - ToggleRoleVisibility(); if (reset) { instance.CreatePlayerEntries(); @@ -728,12 +826,13 @@ public static class RoleGuidePatches } SortAllModifiers(text); + _needsRoleRefresh = true; /*var instance = MatchInfoGuide.Instance; instance.rolesEnabledMessage.SetActive(num == 0);*/ - modifiersWikiTab.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); - modifiersWikiTab.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); + _modifiersScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); + _modifiersScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); } public static void ToggleRoleVisibility() @@ -796,13 +895,21 @@ public static class RoleGuidePatches public static void SortAllRoles(string searchText) { - var newSorted = RolePanels - .OrderByDescending(child => - child.Value.GetTitle().Equals(searchText, StringComparison.OrdinalIgnoreCase)) - .ThenByDescending(child => child.Value.GetTitle().Contains( - searchText, - StringComparison.InvariantCultureIgnoreCase)) - .ThenBy(GetSortingOrder()); + 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) { @@ -812,13 +919,21 @@ public static class RoleGuidePatches public static void SortAllModifiers(string searchText) { - var newSorted = ModifierPanels - .OrderByDescending(child => - child.Value.GetTitle().Equals(searchText, StringComparison.OrdinalIgnoreCase)) - .ThenByDescending(child => child.Value.GetTitle().Contains( - searchText, - StringComparison.InvariantCultureIgnoreCase)) - .ThenBy(GetModifierSortingOrder()); + 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) { @@ -920,7 +1035,7 @@ public static class RoleGuidePatches titleText.text = TranslationController.Instance.GetString(StringNames.MatchInfoGuideTitle); } - advancedWikiTab?.gameObject.SetActive(false); + _advancedInfoTabScroller?.gameObject.SetActive(false); } public static void DisplayAdvancedWiki(MatchInfoGuide instance, BaseModifier modifier) @@ -929,20 +1044,20 @@ public static class RoleGuidePatches Warning($"Opening advanced tab for {name}."); instance.settingsTabs[2].SetActive(false); instance.settingsTabs[3].SetActive(false); - advancedWikiTab?.gameObject.SetActive(true); + _advancedInfoTabScroller?.gameObject.SetActive(true); if (currentAdvancedTabObject) { currentAdvancedTabObject.SetActive(false); currentAdvancedTabObject.Destroy(); } - if (advancedWikiTab == null) + if (_advancedInfoTabScroller == null) { Warning($"Wiki tab is null."); return; } - currentAdvancedTabObject = modifier.GetAdvancedWiki(instance, titleText, advancedWikiTab); + currentAdvancedTabObject = modifier.GetAdvancedWiki(instance, titleText, _advancedInfoTabScroller); } public static void DisplayAdvancedWiki(MatchInfoGuide instance, RoleBehaviour role) @@ -950,14 +1065,14 @@ public static class RoleGuidePatches Warning($"Opening advanced tab for {role.GetRoleName()}."); instance.settingsTabs[2].SetActive(false); instance.settingsTabs[3].SetActive(false); - advancedWikiTab?.gameObject.SetActive(true); + _advancedInfoTabScroller?.gameObject.SetActive(true); if (currentAdvancedTabObject) { currentAdvancedTabObject.SetActive(false); currentAdvancedTabObject.Destroy(); } - if (advancedWikiTab == null) + if (_advancedInfoTabScroller == null) { Warning($"Wiki tab is null."); return; @@ -965,11 +1080,11 @@ public static class RoleGuidePatches if (role is ICustomRole custom) { - currentAdvancedTabObject = custom.GetAdvancedWiki(instance, titleText, advancedWikiTab); + currentAdvancedTabObject = custom.GetAdvancedWiki(instance, titleText, _advancedInfoTabScroller); } else { - advancedWikiTab.ScrollToTop(); + _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); @@ -989,9 +1104,9 @@ public static class RoleGuidePatches description, titleText, out var desc); - currentAdvancedTabObject.transform.SetParent(advancedWikiTab.Inner.transform); + currentAdvancedTabObject.transform.SetParent(_advancedInfoTabScroller.Inner.transform); currentAdvancedTabObject.transform.localPosition = new Vector3(0f, 0f, 0f); - advancedWikiTab.SetYBoundsMax(Mathf.Clamp(desc.textBounds.size.y - 2, 0f, 999f)); + _advancedInfoTabScroller.SetYBoundsMax(Mathf.Clamp(desc.textBounds.size.y - 2, 0f, 999f)); } } From 0dfb65e9b617519262fe1482d28cb5101f73ab58 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 4 Oct 2026 18:29:34 -0500 Subject: [PATCH 08/44] clear redundant code --- MiraAPI/Patches/RoleGuidePatches.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 79f00c7..978cb57 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -832,7 +832,6 @@ public static class RoleGuidePatches instance.rolesEnabledMessage.SetActive(num == 0);*/ _modifiersScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); - _modifiersScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); } public static void ToggleRoleVisibility() @@ -890,7 +889,6 @@ public static class RoleGuidePatches instance.rolesEnabledMessage.SetActive(num == 0); instance.MatchInfoRoleScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); - instance.MatchInfoRoleScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); } public static void SortAllRoles(string searchText) From aec2cfcad3b66bec8b88c815b2ec0990c7d9ac05 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:09:03 -0500 Subject: [PATCH 09/44] Fix presets failing to save --- MiraAPI/Presets/SavePresetPopup.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MiraAPI/Presets/SavePresetPopup.cs b/MiraAPI/Presets/SavePresetPopup.cs index 8df354c..b9932ff 100644 --- a/MiraAPI/Presets/SavePresetPopup.cs +++ b/MiraAPI/Presets/SavePresetPopup.cs @@ -9,7 +9,10 @@ using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; +// TODO: for the love of fucking god please add a namespace on the asset bundle AND here - Atony. +/* namespace MiraAPI.Presets; +*/ [RegisterInIl2Cpp] [SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity convention.")] From 206b00c11ff27d49a9c1a7a2329e4adc09fda566 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:18:39 -0500 Subject: [PATCH 10/44] De-duplicate some code --- MiraAPI/Patches/RoleGuidePatches.cs | 306 +++++++++++++++++----------- MiraAPI/Resources/Locale/en_US.xml | 2 + MiraAPI/Roles/CustomRoleManager.cs | 6 + 3 files changed, 194 insertions(+), 120 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 978cb57..1a18950 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -8,6 +8,7 @@ using Innersloth.Assets; using MiraAPI.Modifiers; using MiraAPI.Modifiers.Types; using MiraAPI.Roles; +using MiraAPI.Translation; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; @@ -134,44 +135,43 @@ public static class RoleGuidePatches HudManager.Instance.SettingsButton, __instance.TabButtons[2].transform.parent); SearchSortingOrderButton = sortingOrderButton.GetComponent(); - SearchSortingOrderButton.name = "SearchSortingOrderButton"; - _searchSortingOrderIdle = SearchSortingOrderButton.inactiveSprites.GetComponent(); - _searchSortingOrderIdle.sprite = MiraAssets.SortingAzIdleSprite; - _searchSortingOrderHover = SearchSortingOrderButton.activeSprites.GetComponent(); - _searchSortingOrderHover.sprite = MiraAssets.SortingAzHoverSprite; - SearchSortingOrderButton.OnClick = new Button.ButtonClickedEvent(); - SearchSortingOrderButton.OnClick.AddListener(new Action(SwitchMethod)); - sortingOrderButton.transform.localPosition = new Vector3(2.4f, 0.765f, -0.1f); - sortingOrderButton.transform.localScale = Vector3.one; - sortingOrderButton.transform.GetChild(2).gameObject.SetActive(false); + FixSearchButton( + 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(); - SearchSortingGroupButton.name = "SearchSortingGroupButton"; - _searchSortingGroupIdle = SearchSortingGroupButton.inactiveSprites.GetComponent(); - _searchSortingGroupIdle.sprite = MiraAssets.GroupNoneIdleSprite; - _searchSortingGroupHover = SearchSortingGroupButton.activeSprites.GetComponent(); - _searchSortingGroupHover.sprite = MiraAssets.GroupNoneHoverSprite; - SearchSortingGroupButton.OnClick = new Button.ButtonClickedEvent(); - SearchSortingGroupButton.OnClick.AddListener(new Action(SwitchGrouping)); - sortingGroupButton.transform.localPosition = new Vector3(1.8f, 0.765f, -0.1f); - sortingGroupButton.transform.localScale = Vector3.one; + FixSearchButton( + 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(); - SearchSortingFilterButton.name = "SearchSortingFilterButton"; - _searchSortingFilterIdle = SearchSortingFilterButton.inactiveSprites.GetComponent(); - _searchSortingFilterIdle.sprite = MiraAssets.VisibleOnIdleSprite; - _searchSortingFilterHover = SearchSortingFilterButton.activeSprites.GetComponent(); - _searchSortingFilterHover.sprite = MiraAssets.VisibleOnHoverSprite; - SearchSortingFilterButton.OnClick = new Button.ButtonClickedEvent(); - SearchSortingFilterButton.OnClick.AddListener(new Action(SwitchFilter)); - sortingFilterButton.transform.localPosition = new Vector3(1f, 0.765f, -0.1f); - sortingFilterButton.transform.localScale = Vector3.one; + FixSearchButton( + 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); @@ -228,15 +228,15 @@ public static class RoleGuidePatches sortingOrderButton, sortingOrderButton.transform.parent); SearchIconButton = searchIconButton.GetComponent(); - SearchIconButton.name = "SearchIconButton"; - _searchIconIdle = SearchIconButton.inactiveSprites.GetComponent(); - _searchIconIdle.sprite = MiraAssets.SearchIconIdleSprite; - _searchIconHover = SearchIconButton.activeSprites.GetComponent(); - _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; - SearchIconButton.OnClick = new Button.ButtonClickedEvent(); - SearchIconButton.OnClick.AddListener(new Action(ClearSearchText)); - searchIconButton.transform.localPosition = new Vector3(-2.79f, 0.765f, -0.1f); - searchIconButton.transform.localScale = Vector3.one; + FixSearchButton( + 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, @@ -301,83 +301,40 @@ public static class RoleGuidePatches searchBoxTmp.transform.localPosition = new Vector3(-1.438f, 0.756f, -0.2f); var playerButton = __instance.TabButtons[0]; - var playerCollider = playerButton.GetComponent(); - playerButton.transform.GetChild(0).gameObject.SetActive(false); - var playerBtnInactive = playerButton.inactiveSprites.GetComponent(); - playerBtnInactive.sprite = MiraAssets.WikiPlayersButtonIdleSprite; - playerBtnInactive.size = Vector2.one; - playerBtnInactive.transform.GetChild(0).gameObject.SetActive(false); - var playerBtnSelected = playerButton.selectedSprites.GetComponent(); - playerBtnSelected.sprite = MiraAssets.WikiPlayersButtonOpenSprite; - playerBtnSelected.size = Vector2.one; - playerBtnSelected.transform.GetChild(0).gameObject.SetActive(false); - var playerBtnHighlight = playerButton.activeSprites.GetComponent(); - playerBtnHighlight.sprite = MiraAssets.WikiPlayersButtonHoverSprite; - playerBtnHighlight.size = Vector2.one; - playerBtnHighlight.transform.GetChild(0).gameObject.SetActive(false); + FixTabButton( + playerButton, + MiraAssets.WikiPlayersButtonIdleSprite, + MiraAssets.WikiPlayersButtonOpenSprite, + MiraAssets.WikiPlayersButtonHoverSprite); var settingButton = __instance.TabButtons[1]; - var settingsCollider = settingButton.GetComponent(); - settingButton.transform.GetChild(0).gameObject.SetActive(false); - var settingBtnInactive = settingButton.inactiveSprites.GetComponent(); - settingBtnInactive.sprite = MiraAssets.WikiSettingsButtonIdleSprite; - settingBtnInactive.size = Vector2.one; - settingBtnInactive.transform.GetChild(0).gameObject.SetActive(false); - var settingBtnSelected = settingButton.selectedSprites.GetComponent(); - settingBtnSelected.sprite = MiraAssets.WikiSettingsButtonOpenSprite; - settingBtnSelected.size = Vector2.one; - settingBtnSelected.transform.GetChild(0).gameObject.SetActive(false); - var settingBtnHighlight = settingButton.activeSprites.GetComponent(); - settingBtnHighlight.sprite = MiraAssets.WikiSettingsButtonHoverSprite; - settingBtnHighlight.size = Vector2.one; - settingBtnHighlight.transform.GetChild(0).gameObject.SetActive(false); + FixTabButton( + settingButton, + MiraAssets.WikiSettingsButtonIdleSprite, + MiraAssets.WikiSettingsButtonOpenSprite, + MiraAssets.WikiSettingsButtonHoverSprite); var rolesButton = __instance.TabButtons[2]; + FixTabButton( + rolesButton, + MiraAssets.WikiRolesButtonIdleSprite, + MiraAssets.WikiRolesButtonOpenSprite, + MiraAssets.WikiRolesButtonHoverSprite); rolesButton.OnClick = new Button.ButtonClickedEvent(); rolesButton.OnClick.AddListener((Action)(() => OpenRolesTab())); - var rolesCollider = rolesButton.GetComponent(); - rolesButton.transform.GetChild(0).gameObject.SetActive(false); - var rolesBtnInactive = rolesButton.inactiveSprites.GetComponent(); - rolesBtnInactive.sprite = MiraAssets.WikiRolesButtonIdleSprite; - rolesBtnInactive.size = Vector2.one; - rolesBtnInactive.transform.GetChild(0).gameObject.SetActive(false); - var rolesBtnSelected = rolesButton.selectedSprites.GetComponent(); - rolesBtnSelected.sprite = MiraAssets.WikiRolesButtonOpenSprite; - rolesBtnSelected.size = Vector2.one; - rolesBtnSelected.transform.GetChild(0).gameObject.SetActive(false); - var rolesBtnHighlight = rolesButton.activeSprites.GetComponent(); - rolesBtnHighlight.sprite = MiraAssets.WikiRolesButtonHoverSprite; - rolesBtnHighlight.size = Vector2.one; - rolesBtnHighlight.transform.GetChild(0).gameObject.SetActive(false); var modifiersButton = Object.Instantiate( __instance.TabButtons[2], __instance.TabButtons[2].transform.parent); + FixTabButton( + modifiersButton, + MiraAssets.WikiRolesButtonIdleSprite, + MiraAssets.WikiRolesButtonOpenSprite, + MiraAssets.WikiRolesButtonHoverSprite); modifiersButton.OnClick = new Button.ButtonClickedEvent(); modifiersButton.OnClick.AddListener((Action)(() => OpenModifiersTab())); - var modifiersCollider = modifiersButton.GetComponent(); - modifiersButton.transform.GetChild(0).gameObject.SetActive(false); - var modifiersBtnInactive = modifiersButton.inactiveSprites.GetComponent(); - modifiersBtnInactive.sprite = MiraAssets.WikiRolesButtonIdleSprite; - modifiersBtnInactive.size = Vector2.one; - modifiersBtnInactive.transform.GetChild(0).gameObject.SetActive(false); - var modifiersBtnSelected = modifiersButton.selectedSprites.GetComponent(); - modifiersBtnSelected.sprite = MiraAssets.WikiRolesButtonOpenSprite; - modifiersBtnSelected.size = Vector2.one; - modifiersBtnSelected.transform.GetChild(0).gameObject.SetActive(false); - var modifiersBtnHighlight = modifiersButton.activeSprites.GetComponent(); - modifiersBtnHighlight.sprite = MiraAssets.WikiRolesButtonHoverSprite; - modifiersBtnHighlight.size = Vector2.one; - modifiersBtnHighlight.transform.GetChild(0).gameObject.SetActive(false); __instance.TabButtons.Add(modifiersButton); - playerCollider.size = settingsCollider.size = - rolesCollider.size = modifiersCollider.size = new Vector2(0.8f, 0.74f); - playerCollider.offset = settingsCollider.offset = - rolesCollider.offset = modifiersCollider.offset = Vector2.zero; - playerButton.transform.localScale = settingButton.transform.localScale = - rolesButton.transform.localScale = - modifiersButton.transform.localScale = new Vector3(0.7f, 0.7f, 1); 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); @@ -409,6 +366,40 @@ public static class RoleGuidePatches return false; } + public static void FixTabButton(MatchInfoGuideTabButton button, Sprite inactive, Sprite selected, Sprite hover) + { + var buttonCollider = button.GetComponent(); + button.transform.GetChild(0).gameObject.SetActive(false); + var settingBtnInactive = button.inactiveSprites.GetComponent(); + settingBtnInactive.sprite = inactive; + settingBtnInactive.transform.GetChild(0).gameObject.SetActive(false); + var settingBtnSelected = button.selectedSprites.GetComponent(); + settingBtnSelected.sprite = selected; + settingBtnSelected.transform.GetChild(0).gameObject.SetActive(false); + var settingBtnHighlight = button.activeSprites.GetComponent(); + settingBtnHighlight.sprite = hover; + settingBtnInactive.size = settingBtnSelected.size = settingBtnHighlight.size = Vector2.one; + settingBtnHighlight.transform.GetChild(0).gameObject.SetActive(false); + buttonCollider.size = new Vector2(0.8f, 0.74f); + buttonCollider.offset = Vector2.zero; + 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) + { + button.name = objName; + inactiveSprite = button.inactiveSprites.GetComponent(); + inactiveSprite.sprite = inactive; + hoverSprite = button.activeSprites.GetComponent(); + hoverSprite.sprite = hover; + button.OnClick = new Button.ButtonClickedEvent(); + button.OnClick.AddListener(onClick); + + button.transform.localPosition = pos; + button.transform.localScale = Vector3.one; + button.transform.GetChild(2).gameObject.SetActive(false); + } + public static void OpenModifiersTab() { _advancedInfoTabScroller?.gameObject.SetActive(false); @@ -716,7 +707,7 @@ public static class RoleGuidePatches modifier.ParentMod.MiraPlugin.GetAbbreviatedModName())); } - foreach (var roleBehaviour in RoleManager.Instance.AllRoles.ToArray().OrderBy(x => x.GetRoleName())) + foreach (var roleBehaviour in CustomRoleManager.AllStoredRoleBehaviours) { if (roleBehaviour.Role is not RoleTypes.Crewmate and not RoleTypes.Impostor and not RoleTypes.CrewmateGhost and @@ -725,11 +716,25 @@ public static class RoleGuidePatches var panel = Object.Instantiate( instance.MatchInfoRolePanelPrefab, inner); - var amount = - GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleBehaviour.Role); - var chance = - GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(roleBehaviour.Role); - panel.SetPanel( + 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); @@ -752,9 +757,7 @@ public static class RoleGuidePatches panel, roleBehaviour.GetRoleName(), roleBehaviour.GetCategoryTitle(), - roleBehaviour is ICustomRole custom - ? custom.ParentMod.MiraPlugin.GetAbbreviatedModName() - : "AU")); + abbreviation)); } } } @@ -840,16 +843,27 @@ public static class RoleGuidePatches foreach (var (roleData, holder) in RolePanels) { var panel = holder.Panel; - var amount = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleData.Role); - var chance = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(roleData.Role); - var showVal = roleData is ICustomRole custom ? custom.ForceShowRoleOnWiki : null; - var forciblyShow = showVal switch + int amount; + int chance; + int forciblyShow = 0; + if (roleData is ICustomRole custom) { - true => 2, - false => 1, - _ => 0, - }; - panel.SetPanel( + amount = custom.GetCount().GetValueOrDefault(0); + chance = custom.GetChance().GetValueOrDefault(0); + if (custom.ForceShowRoleOnWiki.HasValue) + { + forciblyShow = custom.ForceShowRoleOnWiki.Value ? 2 : 1; + } + } + else + { + amount = + GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleData.Role); + chance = + GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(roleData.Role); + } + + panel.SetRolePanel( roleData, amount, chance); @@ -1026,11 +1040,28 @@ public static class RoleGuidePatches [HarmonyPostfix] [HarmonyPriority(Priority.First)] [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.SetActiveTab))] - private static void SetActiveTab() + private static void SetActiveTab(int tabIndex) { if (titleText) { - titleText.text = TranslationController.Instance.GetString(StringNames.MatchInfoGuideTitle); + switch (tabIndex) + { + case 0: + titleText.text = MiraLocaleManager.Get("MiraApi.Wiki.PlayersTab"); + break; + case 1: + titleText.text = MiraLocaleManager.Get("MiraApi.Wiki.SettingsTab"); + break; + case 2: + titleText.text = MiraLocaleManager.Get("Roles"); + break; + case 3: + titleText.text = MiraLocaleManager.Get("Modifiers"); + break; + default: + titleText.text = TranslationController.Instance.GetString(StringNames.MatchInfoGuideTitle); + break; + } } _advancedInfoTabScroller?.gameObject.SetActive(false); @@ -1143,6 +1174,41 @@ public static class RoleGuidePatches return false; } + 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()})"; + } + else + { + instance.roleName.text = role.NiceName; + instance.roleDescription.text = $"{role.GetCategoryTitle()}\n" + role.BlurbMed; + instance.roleIcon.sprite = role.RoleIconColor; + instance.roleCount.text += " (AU)"; + } + + instance.roleIcon.SetSizeLimit(0.13f); + instance.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50); + instance.roleName.fontMaterial.SetFloat(instance.STENCIL_NAME, 50f); + instance.roleDescription.fontMaterial.SetFloat(instance.STENCIL_NAME, 50f); + instance.roleCount.fontMaterial.SetFloat(instance.STENCIL_NAME, 50f); + instance.roleIcon.transform.localScale = new Vector3(4f, 4f, 1f); + } + public static void SetModifierPanel( this MatchInfoRolePanel instance, GameModifier modifier, diff --git a/MiraAPI/Resources/Locale/en_US.xml b/MiraAPI/Resources/Locale/en_US.xml index 028119a..245b7ae 100644 --- a/MiraAPI/Resources/Locale/en_US.xml +++ b/MiraAPI/Resources/Locale/en_US.xml @@ -53,4 +53,6 @@ Outlive the timer or eliminate the crew. Selective Meeting Ability Meeting Menu Ability Options + Player List + Game Settings diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index 060b1a5..25ab64e 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -45,6 +45,11 @@ public static class CustomRoleManager /// public static IReadOnlyList CustomRoleBehaviours { get; private set; } = []; + /// + /// Gets the of all roles as objects. + /// + public static IReadOnlyList AllStoredRoleBehaviours { get; private set; } = []; + /// /// Gets the of custom roles as objects. /// @@ -77,6 +82,7 @@ public static class CustomRoleManager CustomRoleBehaviours = [.. CustomRoles.Values]; CustomMiraRoles = [.. CustomRoles.Values.OfType()]; + AllStoredRoleBehaviours = [.. RoleManager.Instance.AllRoles]; } internal static void RegisterRoleTypes(List roles, MiraPluginInfo pluginInfo) From 7ee4d7da969a0e5ccb085dca6240dcb66aaf7cb0 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Mon, 5 Oct 2026 01:00:01 -0500 Subject: [PATCH 11/44] Bump appended number for option lists --- MiraAPI/GameOptions/ModdedOptionsManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 3518084..b893040 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -198,7 +198,7 @@ public static class ModdedOptionsManager return; } - Func configName = optionList.ConfigName ?? (i => property.Name + i); + Func configName = optionList.ConfigName ?? (i => property.Name + (i + 1)); for (int i = 0; i < optionList.Count; i++) { @@ -279,7 +279,7 @@ public static class ModdedOptionsManager option.Visible = () => visibilityFunc(i); } - RegisterOption(option, group, property.Name + i, pluginInfo); + RegisterOption(option, group, property.Name + (i + 1), pluginInfo); } } From eac86ca4ebf5972f75fc3459b2acc360fb2c025b Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:02:05 -0500 Subject: [PATCH 12/44] Remove clear garbage collector functionality Crashes instantly on x64 --- MiraAPI/Utilities/Extensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 57cd64b..1a8f962 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -372,8 +372,8 @@ public static class Extensions public static void ClearGarbageCollector() { Resources.UnloadUnusedAssets(); - Il2CppSystem.GC.Collect(); - GC.Collect(); + /*Il2CppSystem.GC.Collect(); + GC.Collect();*/ } /// 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 13/44] 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 14/44] 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 15/44] 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 16/44] 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 17/44] 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 18/44] 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 19/44] 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 20/44] 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 21/44] 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 22/44] 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 23/44] 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 24/44] 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 25/44] 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 26/44] 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 27/44] 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 28/44] 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 29/44] 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 30/44] 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 31/44] 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 32/44] 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 33/44] 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 34/44] 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 35/44] 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 36/44] 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 37/44] 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 38/44] 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 39/44] 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 40/44] 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); From 67e3a6a7bcfa4e54cf5ed6ef9eaf1ee53dfbce94 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:33:15 -0500 Subject: [PATCH 41/44] Add labels to filter, grouping, and order buttons --- MiraAPI/Patches/RoleGuidePatches.cs | 27 ++++++++++++++++++++++++++- MiraAPI/Resources/Locale/en_US.xml | 3 +++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index e3b134a..59163be 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -33,15 +33,19 @@ public static class RoleGuidePatches private static readonly List RoleEntries = []; private static readonly List ModifierEntries = []; + public static GameObject SearchComponentContainer; public static PassiveButton SearchIconButton; private static SpriteRenderer _searchIconIdle; private static SpriteRenderer _searchIconHover; + public static TextMeshPro OrderSortingTmp; public static PassiveButton SearchSortingOrderButton; private static SpriteRenderer _searchSortingOrderIdle; private static SpriteRenderer _searchSortingOrderHover; + public static TextMeshPro GroupSortingTmp; public static PassiveButton SearchSortingGroupButton; private static SpriteRenderer _searchSortingGroupIdle; private static SpriteRenderer _searchSortingGroupHover; + public static TextMeshPro FilterSortingTmp; public static PassiveButton SearchSortingFilterButton; private static SpriteRenderer _searchSortingFilterIdle; private static SpriteRenderer _searchSortingFilterHover; @@ -589,6 +593,27 @@ public static class RoleGuidePatches modifiersButton.OnClick.AddListener((Action)(() => OpenModifiersTab())); instance.TabButtons.Add(modifiersButton); + OrderSortingTmp = Object.Instantiate( + _titleText, + sortingOrderButton.transform); + OrderSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.OrderLabel"); + OrderSortingTmp.fontSize = OrderSortingTmp.fontSizeMax = 3f; + OrderSortingTmp.alignment = TextAlignmentOptions.Center; + OrderSortingTmp.horizontalAlignment = HorizontalAlignmentOptions.Center; + OrderSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0); + + GroupSortingTmp = Object.Instantiate( + OrderSortingTmp, + sortingGroupButton.transform); + OrderSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.GroupingLabel"); + GroupSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0); + + FilterSortingTmp = Object.Instantiate( + OrderSortingTmp, + sortingFilterButton.transform); + FilterSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.FilterLabel"); + FilterSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0); + 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); @@ -822,7 +847,7 @@ public static class RoleGuidePatches return; } - var panel = Object.Instantiate(_guide!.MatchInfoRolePanelPrefab, parent); + var panel = Object.Instantiate(_guide.MatchInfoRolePanelPrefab, parent); if (entry.Modifier != null) { panel.SetModifierPanel(entry.Modifier, entry.Amount, entry.Chance); diff --git a/MiraAPI/Resources/Locale/en_US.xml b/MiraAPI/Resources/Locale/en_US.xml index be120fa..ba60484 100644 --- a/MiraAPI/Resources/Locale/en_US.xml +++ b/MiraAPI/Resources/Locale/en_US.xml @@ -56,4 +56,7 @@ Outlive the timer or eliminate the crew. Player List Game Settings Search... + Filter + Grouping + Order From a6dab42df3367ea534d5ec05dee3f5fe8f41cfbc Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:31:10 -0500 Subject: [PATCH 42/44] Hide searchbox in advanced menu pane --- MiraAPI/Patches/RoleGuidePatches.cs | 29 ++++++++++++++++++++++++----- MiraAPI/Utilities/Helpers.cs | 6 +++--- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 59163be..193bd54 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -354,9 +354,14 @@ public static class RoleGuidePatches _guide = instance; _loading = false; + SearchComponentContainer = new GameObject(); + SearchComponentContainer.transform.SetParent(instance.MatchInfoParent.transform); + SearchComponentContainer.transform.localPosition = Vector3.zero; + instance.MatchInfoParent.transform.GetChild(1).transform.SetParent(SearchComponentContainer.transform); + var sortingOrderButton = Object.Instantiate( HudManager.Instance.SettingsButton, - instance.TabButtons[2].transform.parent); + SearchComponentContainer.transform); sortingOrderButton.gameObject.SetActive(true); SearchSortingOrderButton = sortingOrderButton.GetComponent(); SetupSearchButton( @@ -371,7 +376,7 @@ public static class RoleGuidePatches var sortingGroupButton = Object.Instantiate( sortingOrderButton, - sortingOrderButton.transform.parent); + SearchComponentContainer.transform); sortingGroupButton.gameObject.SetActive(true); SearchSortingGroupButton = sortingGroupButton.GetComponent(); SetupSearchButton( @@ -386,7 +391,7 @@ public static class RoleGuidePatches var sortingFilterButton = Object.Instantiate( sortingOrderButton, - sortingOrderButton.transform.parent); + SearchComponentContainer.transform); sortingFilterButton.gameObject.SetActive(true); SearchSortingFilterButton = sortingFilterButton.GetComponent(); SetupSearchButton( @@ -420,10 +425,18 @@ public static class RoleGuidePatches 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); + var wikiMask = wikiTab.transform.FindChild("MaskArea"); + wikiMask.transform.localPosition = new Vector3(-0.0184f, 0.425f, -0.1f); + wikiMask.transform.localScale = new Vector3(1, 1.3f, 1); wikiTab.transform.GetAllChildren() .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5); _advancedInfoTabScroller = wikiTab.GetComponent(); + var wikiScrollHitbox = wikiTab.transform.FindChild("Hitbox"); + _advancedInfoTabScroller.ScrollbarYBounds.max = 1.2f; + wikiScrollHitbox.GetComponent().size = new Vector2(5.75f, 2.4f); + wikiScrollHitbox.transform.localPosition += new Vector3(0, 0.3f); + _advancedInfoTabScroller.ScrollbarY.trackGraphic.transform.localPosition = new Vector3(2.8364f, 0.3649f, -0.2f); + _advancedInfoTabScroller.ScrollbarY.trackGraphic.transform.localScale = new Vector3(0.0234f, 2.068f, 0.5598f); var spinnerParent = instance.settingsTabs[RolesTabIndex].transform.parent; var spinnerPos = spinnerParent.InverseTransformPoint(instance.MatchInfoRoleMaskArea.transform.position); @@ -448,7 +461,7 @@ public static class RoleGuidePatches var searchBox = Object.Instantiate( instance.TabButtons[2].gameObject, - instance.TabButtons[2].transform.parent); + SearchComponentContainer.transform); var searchButton = searchBox.GetComponent(); var tmpText = searchButton.transform.GetChild(0).GetComponent(); tmpText.GetComponent().Destroy(); @@ -1211,6 +1224,7 @@ public static class RoleGuidePatches } _advancedInfoTabScroller?.gameObject.SetActive(false); + SearchComponentContainer?.SetActive(true); UpdateSpinner(); } @@ -1224,6 +1238,11 @@ public static class RoleGuidePatches _currentAdvancedTabObject.Destroy(); } + if (SearchComponentContainer) + { + SearchComponentContainer.SetActive(false); + } + if (_advancedInfoTabScroller == null) { Warning("Wiki tab is null."); diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 8472e6b..4587f36 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -824,10 +824,10 @@ public static class Helpers } obj.transform.SetParent(parent.Inner.transform); - obj.transform.localPosition = new Vector3(0f, 0f, 0f); + obj.transform.localPosition = new Vector3(0f, 0.5f, 0f); var scrollHeight = variableTextSizing - ? (desc.textBounds.size.y - 2) + maxTextSize + 0.475f - : desc.textBounds.size.y - 2 + Mathf.Ceil(num / 2f) * 1.45f; + ? (desc.textBounds.size.y - 2.5f) + maxTextSize + 0.475f + : desc.textBounds.size.y - 2.5f + Mathf.Ceil(num / 2f) * 1.45f; parent.SetYBoundsMax(Mathf.Clamp(scrollHeight, 0f, 999f)); return obj; } From 5d354eff5e62b81f248fe1750c47b9b2f1d4e47e Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:52:29 -0500 Subject: [PATCH 43/44] Save wiki preferences in config file --- MiraAPI/MiraApiPlugin.cs | 22 +++++ MiraAPI/Patches/RoleGuidePatches.cs | 130 +++++++++++++++++++--------- 2 files changed, 110 insertions(+), 42 deletions(-) diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index d68a87d..4da8968 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -4,6 +4,7 @@ using BepInEx; using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; +using MiraAPI.Patches; using MiraAPI.Patches.Roles; using MiraAPI.PluginLoading; using MiraAPI.Translation; @@ -63,9 +64,30 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin private static MiraPluginManager? PluginManager { get; set; } internal Harmony Harmony { get; } = new(Id); + public static ConfigEntry WikiSortingGroup { get; private set; } + + public static ConfigEntry WikiSortingMethod { get; private set; } + + public static ConfigEntry WikiSortingFilter { get; private set; } + /// public override void Load() { + WikiSortingGroup = Config.Bind( + "Wiki", + "WikiSortingGroup", + SortingGroups.Ungrouped, + "Determines how clusters of roles / modifiers are grouped together, if at all."); + WikiSortingMethod = Config.Bind( + "Wiki", + "WikiSortingMethod", + SortingMethod.Alphabetical, + "Determines if the list of roles / modifiers are organized in ascending or descending order."); + WikiSortingFilter = Config.Bind( + "Wiki", + "WikiSortingFilter", + SortingFilter.AllRoles, + "Determines what roles / modifiers are displayed at once."); Harmony.PatchAll(); RoleOptionsCollectionPatch.PatchRoleMethods(Harmony); UiResetEvents.Initialize(); diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 193bd54..77f9cf4 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -346,6 +346,10 @@ public static class RoleGuidePatches return; } + _sortFilter = MiraApiPlugin.WikiSortingFilter.Value; + _sortMethod = MiraApiPlugin.WikiSortingMethod.Value; + _sortGrouping = MiraApiPlugin.WikiSortingGroup.Value; + if (_populate != null) { Coroutines.Stop(_populate); @@ -368,8 +372,8 @@ public static class RoleGuidePatches SearchSortingOrderButton, "SearchSortingOrderButton", SwitchMethod, - MiraAssets.SortingAzIdleSprite, - MiraAssets.SortingAzHoverSprite, + GetSortMethodSprite(_sortMethod, false), + GetSortMethodSprite(_sortMethod, true), new Vector3(2.4f, 0.765f, -0.1f), out _searchSortingOrderIdle, out _searchSortingOrderHover); @@ -383,8 +387,8 @@ public static class RoleGuidePatches SearchSortingGroupButton, "SearchSortingGroupButton", SwitchGrouping, - MiraAssets.GroupNoneIdleSprite, - MiraAssets.GroupNoneHoverSprite, + GetSortGroupSprite(_sortGrouping, false), + GetSortGroupSprite(_sortGrouping, true), new Vector3(1.7f, 0.765f, -0.1f), out _searchSortingGroupIdle, out _searchSortingGroupHover); @@ -398,8 +402,8 @@ public static class RoleGuidePatches SearchSortingFilterButton, "SearchSortingFilterButton", SwitchFilter, - MiraAssets.VisibleOnIdleSprite, - MiraAssets.VisibleOnHoverSprite, + GetSortFilterSprite(_sortFilter, false), + GetSortFilterSprite(_sortFilter, true), new Vector3(1f, 0.765f, -0.1f), out _searchSortingFilterIdle, out _searchSortingFilterHover); @@ -1042,25 +1046,40 @@ public static class RoleGuidePatches _sortFilter = SortingFilter.EnabledOnly; } - switch (_sortFilter) - { - case SortingFilter.AllRoles: - _searchSortingFilterIdle.sprite = MiraAssets.VisibleAllIdleSprite; - _searchSortingFilterHover.sprite = MiraAssets.VisibleAllHoverSprite; - break; - case SortingFilter.DisabledOnly: - _searchSortingFilterIdle.sprite = MiraAssets.VisibleOffIdleSprite; - _searchSortingFilterHover.sprite = MiraAssets.VisibleOffHoverSprite; - break; - default: - _searchSortingFilterIdle.sprite = MiraAssets.VisibleOnIdleSprite; - _searchSortingFilterHover.sprite = MiraAssets.VisibleOnHoverSprite; - break; - } + MiraApiPlugin.WikiSortingFilter.Value = _sortFilter; + + _searchSortingFilterIdle.sprite = GetSortFilterSprite(_sortFilter, false); + _searchSortingFilterHover.sprite = GetSortFilterSprite(_sortFilter, true); RefreshActiveTab(false); } + private static Sprite GetSortFilterSprite(SortingFilter value, bool hover) + { + if (hover) + { + switch (value) + { + case SortingFilter.AllRoles: + return MiraAssets.VisibleAllHoverSprite; + case SortingFilter.DisabledOnly: + return MiraAssets.VisibleOffHoverSprite; + default: + return MiraAssets.VisibleOnHoverSprite; + } + } + + switch (value) + { + case SortingFilter.AllRoles: + return MiraAssets.VisibleAllIdleSprite; + case SortingFilter.DisabledOnly: + return MiraAssets.VisibleOffIdleSprite; + default: + return MiraAssets.VisibleOnIdleSprite; + } + } + public static void SwitchMethod() { var stepUp = (SortingMethod)((int)_sortMethod + 1); @@ -1073,16 +1092,28 @@ public static class RoleGuidePatches _sortMethod = SortingMethod.Alphabetical; } - _searchSortingOrderIdle.sprite = _sortMethod is SortingMethod.Alphabetical - ? MiraAssets.SortingAzIdleSprite - : MiraAssets.SortingZaIdleSprite; - _searchSortingOrderHover.sprite = _sortMethod is SortingMethod.Alphabetical - ? MiraAssets.SortingAzHoverSprite - : MiraAssets.SortingZaHoverSprite; + MiraApiPlugin.WikiSortingMethod.Value = _sortMethod; + + _searchSortingOrderIdle.sprite = GetSortMethodSprite(_sortMethod, false); + _searchSortingOrderHover.sprite = GetSortMethodSprite(_sortMethod, true); RefreshActiveTab(false); } + private static Sprite GetSortMethodSprite(SortingMethod value, bool hover) + { + if (hover) + { + return value is SortingMethod.Alphabetical + ? MiraAssets.SortingAzHoverSprite + : MiraAssets.SortingZaHoverSprite; + } + + return _sortMethod is SortingMethod.Alphabetical + ? MiraAssets.SortingAzIdleSprite + : MiraAssets.SortingZaIdleSprite; + } + public static void SwitchGrouping() { var stepUp = (SortingGroups)((int)_sortGrouping + 1); @@ -1095,25 +1126,40 @@ public static class RoleGuidePatches _sortGrouping = SortingGroups.Ungrouped; } - switch (_sortGrouping) - { - case SortingGroups.Faction: - _searchSortingGroupIdle.sprite = MiraAssets.GroupFactionIdleSprite; - _searchSortingGroupHover.sprite = MiraAssets.GroupFactionHoverSprite; - break; - case SortingGroups.AmountChance: - _searchSortingGroupIdle.sprite = MiraAssets.GroupCountIdleSprite; - _searchSortingGroupHover.sprite = MiraAssets.GroupCountHoverSprite; - break; - default: - _searchSortingGroupIdle.sprite = MiraAssets.GroupNoneIdleSprite; - _searchSortingGroupHover.sprite = MiraAssets.GroupNoneHoverSprite; - break; - } + MiraApiPlugin.WikiSortingGroup.Value = _sortGrouping; + + _searchSortingGroupIdle.sprite = GetSortGroupSprite(_sortGrouping, false); + _searchSortingGroupHover.sprite = GetSortGroupSprite(_sortGrouping, true); RefreshActiveTab(false); } + private static Sprite GetSortGroupSprite(SortingGroups value, bool hover) + { + if (hover) + { + switch (value) + { + case SortingGroups.Faction: + return MiraAssets.GroupFactionHoverSprite; + case SortingGroups.AmountChance: + return MiraAssets.GroupCountHoverSprite; + default: + return MiraAssets.GroupNoneHoverSprite; + } + } + + switch (value) + { + case SortingGroups.Faction: + return MiraAssets.GroupFactionIdleSprite; + case SortingGroups.AmountChance: + return MiraAssets.GroupCountIdleSprite; + default: + return MiraAssets.GroupNoneIdleSprite; + } + } + private static void CopyTextLayout(TextMeshPro source, TextMeshPro target) { target.font = source.font; From 7349a259a19cba19079e0583ab9bd2ffaeb48de5 Mon Sep 17 00:00:00 2001 From: Atony <75191600+AtonyGit@users.noreply.github.com> Date: Mon, 12 Oct 2026 03:07:57 -0500 Subject: [PATCH 44/44] Add icons to all menus --- MiraAPI/Modifiers/BaseModifier.cs | 7 ++- MiraAPI/Patches/RoleGuidePatches.cs | 67 +++++++++++++++++++-- MiraAPI/Resources/Wiki/IconCrewmate.png | Bin 0 -> 22171 bytes MiraAPI/Resources/Wiki/IconImpostor.png | Bin 0 -> 21582 bytes MiraAPI/Resources/Wiki/IconNeutral.png | Bin 0 -> 16790 bytes MiraAPI/Resources/Wiki/IconPlayerList.png | Bin 0 -> 19700 bytes MiraAPI/Resources/Wiki/IconSettings.png | Bin 0 -> 24987 bytes MiraAPI/Resources/Wiki/IconTerminology.png | Bin 0 -> 26151 bytes MiraAPI/Roles/ICustomRole.cs | 7 ++- MiraAPI/Utilities/Assets/MiraAssets.cs | 30 +++++++++ MiraAPI/Utilities/Helpers.cs | 12 +++- 11 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 MiraAPI/Resources/Wiki/IconCrewmate.png create mode 100644 MiraAPI/Resources/Wiki/IconImpostor.png create mode 100644 MiraAPI/Resources/Wiki/IconNeutral.png create mode 100644 MiraAPI/Resources/Wiki/IconPlayerList.png create mode 100644 MiraAPI/Resources/Wiki/IconSettings.png create mode 100644 MiraAPI/Resources/Wiki/IconTerminology.png diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index cf108b3..3daeb56 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -107,17 +107,20 @@ public abstract class BaseModifier : IOptionable /// /// The guide object. /// The title text object. + /// The title's icon. /// The scroller parent. /// The of the wiki. - public virtual GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, Scroller parent) + public virtual GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, SpriteRenderer titleIcon, Scroller parent) { var abilities = WikiAbilities; return Helpers.CreateAdvancedWikiPage( guide, titleText, + titleIcon, parent, ModifierNameLocale, - $"{this.GetTmpIcon()} {ModifierName} ({ModifierCategoryTitle})".TrimStart(), + $"{ModifierName} ({ModifierCategoryTitle})".TrimStart(), + ModifierIcon ?? MiraAssets.IconNeutral.LoadAsset(), ModifierWikiDescription, abilities); } diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 77f9cf4..92080e8 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -55,6 +55,7 @@ public static class RoleGuidePatches private static TextBoxTMP _searchBoxTmp; private static GameObject _currentAdvancedTabObject; private static TextMeshPro _titleText; + private static SpriteRenderer _titleIcon; private static bool _suppressSearchRefresh; private static IEnumerator? _searchDebounce; private static string _appliedSearch = string.Empty; @@ -145,6 +146,7 @@ public static class RoleGuidePatches _appliedSearch = string.Empty; _suppressSearchRefresh = false; _titleText = null!; + _titleIcon = null!; _searchBoxTmp = null!; _rolesScroller = null!; _modifiersScroller = null!; @@ -254,9 +256,18 @@ public static class RoleGuidePatches _titleText = __instance.transitionOpen.transform.FindChild("Text_Title")?.GetComponent()!; if (_titleText) { + _titleText.transform.localPosition += new Vector3(0.5f, 0); _titleText.transform.GetComponent().Destroy(); } } + if (!_titleIcon) + { + var titleIconObj = new GameObject("TitleIcon"); + _titleIcon = titleIconObj.AddComponent(); + _titleIcon.transform.SetParent(_titleText.transform.parent); + _titleIcon.transform.localPosition = new Vector3(-2.75f, 1.4f, 0); + titleIconObj.layer = _titleText.gameObject.layer; + } if (_titleText) { @@ -1247,26 +1258,33 @@ public static class RoleGuidePatches [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.SetActiveTab))] private static void SetActiveTab(int tabIndex) { - if (_titleText) + if (_titleText && _titleIcon) { switch (tabIndex) { case 0: _titleText.text = MiraLocaleManager.Get("MiraApi.Wiki.PlayersTab"); + _titleIcon.sprite = MiraAssets.IconPlayerList.LoadAsset(); break; case 1: _titleText.text = MiraLocaleManager.Get("MiraApi.Wiki.SettingsTab"); + _titleIcon.sprite = MiraAssets.IconSettings.LoadAsset(); break; case RolesTabIndex: _titleText.text = MiraLocaleManager.Get("Roles"); + _titleIcon.sprite = GetRandomRoleIcon(); break; case ModifiersTabIndex: _titleText.text = MiraLocaleManager.Get("Modifiers"); + _titleIcon.sprite = GetRandomModifierIcon(); break; default: _titleText.text = TranslationController.Instance.GetString(StringNames.MatchInfoGuideTitle); + _titleIcon.sprite = MiraAssets.IconPlayerList.LoadAsset(); break; } + + _titleIcon.SetSizeLimit(0.55f); } _advancedInfoTabScroller?.gameObject.SetActive(false); @@ -1274,6 +1292,45 @@ public static class RoleGuidePatches UpdateSpinner(); } + public static Sprite GetRandomModifierIcon() + { + var activeMods = PlayerControl.LocalPlayer.GetModifiers() + .Where(x => !x.HideOnUi && x.ModifierIcon != null).ToList(); + if (activeMods.Count > 0) + { + return activeMods.Random()!.ModifierIcon!.LoadAsset(); + } + + return GetRandomFactionIcon(); + } + + public static Sprite GetRandomRoleIcon() + { + var aliveRole = PlayerControl.LocalPlayer.Data?.Role; + if (aliveRole != null && aliveRole.RoleIconSolid != null) + { + return aliveRole.RoleIconSolid; + } + + return GetRandomFactionIcon(); + } + + public static Sprite GetRandomFactionIcon() + { + var random = UnityEngine.Random.Range(1, 4); + if (random == 1) + { + return MiraAssets.IconCrewmate.LoadAsset(); + } + + if (random == 2) + { + return MiraAssets.IconImpostor.LoadAsset(); + } + + return MiraAssets.IconNeutral.LoadAsset(); + } + private static bool TryPrepareAdvancedWiki(MatchInfoGuide instance) { instance.settingsTabs[RolesTabIndex].SetActive(false); @@ -1325,7 +1382,7 @@ public static class RoleGuidePatches return; } - _currentAdvancedTabObject = modifier.GetAdvancedWiki(instance, _titleText, _advancedInfoTabScroller); + _currentAdvancedTabObject = modifier.GetAdvancedWiki(instance, _titleText, _titleIcon, _advancedInfoTabScroller); } public static void DisplayAdvancedWiki(MatchInfoGuide instance, RoleBehaviour role) @@ -1345,11 +1402,11 @@ public static class RoleGuidePatches if (role is ICustomRole custom) { - _currentAdvancedTabObject = custom.GetAdvancedWiki(instance, _titleText, _advancedInfoTabScroller); + _currentAdvancedTabObject = custom.GetAdvancedWiki(instance, _titleText, _titleIcon, _advancedInfoTabScroller); } else { - var titleTxt = $"{role.GetTmpIcon()} {role.GetRoleName()}" + + var titleTxt = $"{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")) @@ -1364,9 +1421,11 @@ public static class RoleGuidePatches _currentAdvancedTabObject = Helpers.CreateAdvancedWikiPage( instance, _titleText, + _titleIcon, _advancedInfoTabScroller, role.Role.ToString(), titleTxt, + role.RoleIconColor, description, []); } diff --git a/MiraAPI/Resources/Wiki/IconCrewmate.png b/MiraAPI/Resources/Wiki/IconCrewmate.png new file mode 100644 index 0000000000000000000000000000000000000000..61f706f6df8c96d765ef81a34f051e34cac79feb GIT binary patch literal 22171 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 PS%9h{RH5RPWyJpgZz9XI literal 0 HcmV?d00001 diff --git a/MiraAPI/Resources/Wiki/IconImpostor.png b/MiraAPI/Resources/Wiki/IconImpostor.png new file mode 100644 index 0000000000000000000000000000000000000000..6100cd121620881555c85da964a7f6e271b112ba GIT binary patch literal 21582 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 literal 0 HcmV?d00001 diff --git a/MiraAPI/Resources/Wiki/IconPlayerList.png b/MiraAPI/Resources/Wiki/IconPlayerList.png new file mode 100644 index 0000000000000000000000000000000000000000..234baa0a01b0b4c5ea9ea46f9a9310b5103bb4dd GIT binary patch literal 19700 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 literal 0 HcmV?d00001 diff --git a/MiraAPI/Resources/Wiki/IconTerminology.png b/MiraAPI/Resources/Wiki/IconTerminology.png new file mode 100644 index 0000000000000000000000000000000000000000..8b95d6247154142ce82bdf298445a764d9cda912 GIT binary patch literal 26151 zcmbq)Wm8>E)Ahk2xCM6$?(RVk&cQvnyE_DThv4q+9!PKq?(TMQcX+w(s^=HHAEx$~ zuA1)YnV#Kit%*=lkV5`~{{;X5AOoevK>z>*>3;(r761TX1=R$6eh?g_wVVL}&bj{v zgudxMJODrn0E&yMdO)0YBP0`nY*4?5h>rwtOM?pOK z?^44owkJK;c@FOuiDlxSq9YNEqp`B2vZy$hC>I$6FyPdiPu9JZKU-lYTj723@T_K{ zyY*y4=T3dT+qLq9xAb|VM#p|$M^Vl9WH}c&6i1@rH&X;mdR^1&s{IAU3P=7gumC<+ z$H$5l&Ilzq3yKvG@(MXH8^w!eiqa3j+OH(Y)%h0~?@S7t7jKZ~;l&D9@GlTIpuop< z*ZI@P=|7`uL@Y(-1CeB2G$^IyT00**R=DpIpGM#P0(XuF2*cUni~y0>^D&c*2O`7& z84WWl33UI0Qa%tt5>fu$an(LR2+#Xz^yLXAvJ}Uebd)e0;@hYOw@~XZD6+#(BeEWm zt6L)!@$^ssB%Z*7hhLpZ_x>~5!}*q@|1a>{fBtrpnz_--cFGoLlv%x%oNYO#s z6Q>p;uGC6XQ7>Rqy=J0}=Y$zX4HSg*!!X8b(qC7-onq1 zv|E7ysn*dMN?E!865h5Yg2ZziIKX%WDc(AQ0hJI%z_Q_DJ`CdiO1bK{%NTHhV2F7Di~noB>&WNy=d-+oPZ; zC~eRSerNLaW@ilZ!Yz8zgWQ1>-Ngt<^C6OiaB7T}0k60R%_9W~5M~_bq7e@yC`^Wg?3Q|ts^E(xf#Q9V0cSvZKE&hyrKca|e z8ll2JbMwu-ryszPGmjfrJ#2hk(xyc}XwOx@FejgKTfbZQx4(#7O;xwbYL&XOAQ7Yn z(kGK`hs`cX#OOvr!^h_-3q>IT?Q^8JP^@(w@62W;N|cwI!zMB3H^5w{Zxnc?P90FL zQ|TZSIE`wW!;L%1NqmF|SMWt-57JjOb#fSViGfmqJ|mM3kd@Hut|+TkL1yPuRa1$o z$QF=K8T$ypScPB{1=9asbF_0K4{(uF>I_=eosV@ zW*nXFK73{=vkn`un`<;JT)BH5vs$dsm{V5X6aWdS)%SaI)z5vIq(X${Ji(JN8{iqF zk);GvXwlFds>COIKp^6{^#2Hg4x$VSR-xD^=1Z^vmyWA)RzvdaNMwWuB%Q|!aQUd- zU7V}x82Cuq`kj}fqp>~Y?e{Up1;6{7v@3t@(N6}<=rmxd@~(b|&XW#6v9qaGM>tYl z;4A|#aH$EqmXxVCs;W6x(lKI6twAR_>}@;B4M_-|Y_67Xcz>33g9?QpK9*s;@`1i6icjZlxY`^l#2n2dO{m@PpJdl^@und#9Z ze&BqK@S6VyYb>CW62n-aKCTqgcu;1=N}ARWiaoUUw6Tz`_PW$>Hr_|%E1T{^32J~z zcuqi!7&y)zkMg@OiMN<1Q2e~B&319+f6Sjl| z)Ld0cDobKLR|5WZkducPdn-u3dpyzXWT)ZM|Vg_|Ca#_*EK)0E8mCMrEac zK04hfd_BrD!w#m2dL%gIWk@j^EB~s94N}m1Q3Yz1%});|L=KI-FeZ9JLhC5#_@%ZT zXjIh1Sj%G?TuCOR?uJn+S!_WCCCs5oJJHqet3cA>a#nsfat!arLx8qXYCcO+>y{S4 zcj#{s`)>A)BC5Tb8a~0V!M;xiyr26f^7Ct_yy!40XBFBXnk%L!oF^J{(qv}9mr6qg zDaCE)2Q_c6`)QHN`Wvb$)h&(9~Y%LSW`%~PN|zLd`Ps|DvMWVKJu)jq8y{EV-Bwhf`;QwV9)H#0 z?|po9CaV5`Wob?^AJz8`jTvqvcxM2u0Ch26{87E0WFP~=w@Z!`U0IPQI1__r3({s4bx}t21Z$%$6e%F1}srT^h zJ~P*JIiL&4po!C`3e%16M9`Z0tE6`}GEFNfsbW{CAp;N*@f+=6)8_l>&5E~GuFQ$` zv!JlsM)|KjGjwu?XAf#)0JMyU&R{svHctk^s|23CG-wP-km2Tdf z^UT9$TNuwIX&%S9r2U#CQjGP}+TLD6HkCnnwnTo`(Ujl)T(wfY8kC)#4W7yu10PKn z5-UtzDa}X>YaueoQ)vMDVZQJ^NH}9>Dp;B}r#e{;Fvfa!=|{S3|JiB%rQiUVL{C9@ zakE{7`BP@2Nz28u^9!C;#EL<@qRbjgabMMOq?VmiGAaE$*`wWW@OUu2g+8sJ1rdiy zmE`?0Y(4}>zf4<4M=b;^nm#d&Dg_Wq3HE-uKeO5&O#we0<}T)Tx?iaK`uaY%%DvoQ zuEwx^%|wIs+N8yC*W9YWnL*-Jk6}Nw*s{!N6#3%P45kZshBk(Bnu#fF9C>h&!8oks zO9O`|i&FiH`-V1J3&tRI#;LeZQdCYsmWQEbJlFXs@p=o)E#JttkgOSrAsS9PgNtWQ znQ5`LwFN_wJO(^J4hh#BOk`JDSzBYzDA65%Ndz^x{rDjQAv7uUQsuRmAl0O*N+AJW zB015h)vtCtTiH)5+Eb^J&vsk+2!bWuK(Qae2)`B6ng*wG&AZKjJT+6n!^TCqJl8yD zcMHYz_ch*Pw!YhVwEi7H%DfH7kZ$S7YMXdI@f!sY`=npzUc>mppOoVV{NDy;MQ!(} zJ<2D!KK*mJXM6d5)}h(`(r@1z$}1{BY5HE}&vT_h#S^@TglZZZhnm;W(XkO&`~qL3 z13>=%Jzy|+reYZ+ov>cFEuE^Sa(KwF1+Y zo;()Ikp@TvA1|f97uz5%Elsnc>wfDEIg@mcNAp&gY#FzDAGnwq)Zj;h?BZMO0^|Br?OLIE zS;ttRP0q*xo5rb4wRZnrYN@NytHDP@gvZjBX&y({zDk}KSVJOB@SQN+MJW1^qg?{C z&?HZ175L>VJx&&DHEO%>Gt`$cS{*OfU!1cJNJSygaE5fBrwdFYVDar%|&1IMf+=0YWS+0G)zl;^1N->=y` z(3-*AQxv-}ELTilzmOpma2c`B=?gtzPMW~6fqLo|v%J9N;7IzKx;v-#+=51Qd3kwo zK@<~c^JU{@rRRA&;;kYNFnqMb!klq{&eFC;bb&r^ZS}a>c72J8SVnlh zTKg=44aVk2M85ZOx0#Q*15k+2IwX1`MY@OqBWYz73E)J^K-w#8S>ePN-B2Oh)8k<& zvYyEcPc%`%kC7t^Gs}U|^UOBF+-nX&RyH;q>2X)M0ky@&5{!BsbI<{ULr}Prl%7A7 z0LkZnsYnF(g&|=G_K<}he!3sc6dQ7}Pz>ZP063NmI*QHb1NHb_L`VWm$SsTN`tEw# zOe`Iv>z5Tk)0~mI7tM8WQ+WyGj-kmDC?*Wsg>HHdfzoA6g8HF1iujUw6rq+-qo?|u zOZl9X@PM7O-VkSbae764rwWvBNbHYWC4;eI3wN;^`dDZyTt+-@zJo%jBKVjT+@Z&+ z1hO(p!_}?Rfq*yI+#q`agL#-#wK!*Dv3jT(>0>U%Ey7!qT7!4h)-SG#s|g z;M9n2H?<2p@b|vT0C6qubxG00RSKfBMpOCh_)0epr%^hIf9ICA;LN+S=LpJPLPmfL zh~7Zy%7-tT9q2hV!~f|TKl}+UCdCWt;z8HVgma3OA<_##mhY(=6Ea6RO6p= zvjD#Qg+E()7bC`;o^VNqXSdXN*;wChc#392;zt(9@_Sx&p+)8w`U(_~8(V|n>-0T| za;>oL(sK|y zsx-`qq@!?mx3?Ama>x0AXz(f@uL2ri?^%K97=MNEIof*dJO~bzXWLq#^$(W{w`)g6 z&yExF_w?THvcSasSUrwL@D*in2UZI`Z=Nly*g$-55ZjQh92>%?-W zBIA%<{047Pv;eJhk64?%ZN0i5flN%Ps4uAC#SYh#Ir`eJomA(A`_(FDKYv&Vk(zY| ztN@xHNT@0$VM7;5?w!I1X-RqC!!Uz1mVCm$UU;mAbgWP)_bkH$;rW$gu+GR;Uc<$u zF)gG8Nde*gDthr%0mV70#B(k3)118#%WO)y3E->qg2=YC%aAyF^(rN4(I?VTm)8UC z9_97*l5wGARwq;bO;P~7JKK{ga@-HkCqAltUrK1w@@sR87NlCc5nBBsOQH|gsENuyj&P-P% z^aJy)&}&2zSfcM0z<_<5-(*5)KEduy|4OJ6A@G*{cu>yW$@9|1Y7lmBQ2igpnr&CP zk{CQnqC~Eu@(zfg0c17VJ8pmX6hnaB^_jx!%2x|#o{bCND%b3CjZY;1z#hXYk=d0> zf|r$MVKb4e(6=+mE+jjwfepMSSvO$yD2uluXMo(fPj&TCexvmEZ?2i(w z0SRQv(;Rw0j6S^_+^_#l<$KysmgRf0(xd=)?-{P?>PCvHb2U|Xza?1E5{uJ2ml#tf zQsz>O0i{NFzPz$P3xfI4VwLP?IzMhpey1ukl@T87>EZ14L&YM@>Ft{Z++-MS&`zxG zs^5|zufbui|8FLyi*tvKR#+Fmt0;q`M-o(T#DDL7Sw8`!{mmD{J2Km6I1gNiRtmq6 zr`tNrvm9e5TxiheS>a=3cs#1&?dp~-?318jK{nMbwodP7vTNw*)FLE5q+B^q@ec5V zSp-z@IZ4jlXP8)oysJJif4zn2Fv0`CX>U7~ZZIMp`t@^-e}1Ez(ZY*y{PEk;+k-DbMX3g=r1~P{n8BVvFAbc(w#7{`l+rfRpUSy)<6zvhvWGEc}qCFD# zjfsw4v-0Qwhy!^ZJt3ze2oRs@X66~O z^W?N%|F@4RI&=o-G~GOh6`!9k+)b?2V>vLXUKC<#*Xi;kTaMI`z-l?(a)U)oJ2^Q_ z;KpmNM9c@lib7TnrWYb2@&HDezNJg3u_Po`F|o{i?V_DYDD+u^fOyAWU`n2CfyZh3 z*io-ko@edqtsy&M>{`h_PWmS4eA?aNyt{TseirU&mQzhqYzk2gY;&s-$ zUWnbM{9aq(uO~tO1?*iF)b>h6O*7P*F#K7)N!~Plbl!cQ%P5XJC z-=zOs+YEvx?Z^GwrkCWV)=7+w8qT|w1=Vy%%}F6m4#$v&P}Zd9w-Z4lcOs-y1WV{&4!9;_+Qf+ z5fV@r*xd8&_R6fu(oZS-a3@SRx;&q=*lZ3?6wCCni1_Kuki^{W82UmBghMQN4ySxEK`IcDl91g=l6V3*7%t2p(I}Dh4Q$NT7%3A7ly<|OO zumjgxOiZ>X9~cnPE0JvTq?``;mGt}E5NHs@);h6;g#-IZVaXSS`cUycPF*XJ*9rAb zYcAXqC#!Dd{O^zE^X;1qCPBv5{PBBxFc2cHuC7+kF0`ifLnAQTP9JX<{FO z;6^*@2cuSlxO?xVp+>Bm=yJ?w#oLIw6QN=Q*WzmBBdq1{|`y?Cnp^ zh5|rYAqPNe0!U*ZQRc}b>T2XDlf58*m1M}wgLDcjsPbF1$;S!y$Y8}szbNh7VqI$S zG|N{nw|mPKj82sAF}g@VvP!z!>YjLO4{Dibr!-;^mg&X`J(sO?c{*Psbi6Q^4w_qA zwF5{`G-HLs;!wag&L`t19zVpn=LVOQT}w5E#$;q9AZceY02uIeO4-ts@|^P(dPQG* zL++YEkXS$nko_)1wNszZ)KU}RH71{diD2__v=>a#*4js<`G=AT;GG^TMaG^5%* zrT6>R9yQ{~*!{If8@*ax0Gh9eE>J{ON*vi4^kbbbq0C47N06!!1Olp7w=|s!*t%JJ z4y04<-)KYC3^Ia5upmbdSkX5q6kWm!P`awju;A4@w7wObp6mytjJhIbg!@omx%7o1 z>!YMN?>2HS>jL7fxnbT~VT)lr!$ z9$!2)GzX`D$p6hOS)psLHt)IOIL?S93hz)T7{j}uaU?~bO8zk}fr5LM$|^MbFcyDp z^m*7a5CILb`;Rxjk`s%XV6dSIzo)X|X39x;MpUG_0HV7ZGYIzO^()n=D2XYFh#6Fo zZ1`D=4*~a;RI{aJYODM)>sO|r`nr;0`7<4wkj~JU^+jM(7N>Qp!gqOXyeVO<%#U4? zk0Xh<&r08ZwQMBC1b`2Ar5fGJAUgW$UeIre#b(&|J*5Kuhp4<&d8NAZYR3FuT3f4F zp9UeTmULZxW*dXqOt6F(gNAQdG+|B0fO28-!1~j(jH@Ocs|Qt4PLcv2u(`R}b6suW ziZb)}{ff0${da-}6qyY`uJ452MTE*Q3;<6}>J%s8*H2}`8r1Az>E9Ifq}y}h59L~O zG|@#-rSj^HFg7eGooa<`LI2HlW$^ zaZ`JUMzu^NJY$72)J+AjvlbCvA|6Hp5IzHJXPjJt3)ku^BtOG zCUhCnjrnDi>%O{mW5!gY(Kc<{y-~FsiQnZ#g$WS;`1534;dB_n5=VfeY}9EB=)lem z2szp?vn*sUE>{@0v{k183XG~kknC8+2e6^nfC55Vai%Ljk;o2v?)eigSwc8~xS+uT z<~%q*)3n6$XcLfIQbN7X6L*fx0KhaA7k)cdG-sf)0_tj^VqC&P%jB>bSCF$kAJtj= z55bdESoISuRAfTqHk|9atG&< zu=|HH!vy$btbj=xt-;;v8H|C|mcnKrV=kxss>6zSwUIBi3Tq+)2r;0+@6BsK=cG7I zA7s5yHBH6-^87qFSE8Fs1H!s70v1_8EV|I)^@^3k6wwrw_%vk#v*@?s*!xk$rN2*# zvhz<-UWw1t7dg+%M9?~|Guznkm9;jSPpW@{vIIIfb7|e)58#S0V(s%i#(SCsn$Kc& z{zYK{8(~ees0ISeI3~NGwjIs!rNelZxC=GCH$=Hx}Ty^k!k;13qt*gx+_9lG7}Q* zrZ(ZXjSIi>4A!4yd;$VBDJiKffo-ZwaAhrcpy3Jc-xCU+ap)2(oGdpb4xqoD)vaAJ zv9B9S?(*{gv;Z$^Z*zFZ2gJi(0bv}la~14||5T&)Fy||frV=(!!wDdPa8sf&iqIUn;gW4Z`-n1hBX~tzZJxc) z*v4h3>4J*R_;8pqDitn%)tGU}qPl@_XOI)`1mf3_aKn@j;>J1vT9tK*2dPZ33ke4l z>Gj`?VsG13V3gWn#-eWLE`k8%JIxUju7^_l!(}Qy2GWiv63pkypk!n*=3n6!TH2rD zY`1GO(JkOX3iO|D369*7YV`&|JpBidK9Ae`73%xHxrVT|ps7{K`E+glL~PR8up>~F zpUCwn9*Dx}tZ;?OWxHBB_pn<`xKkWKFVZ&Vk1u-v;kXOkA4W|G ztRK8JKK|5a^t<)k)NM5v{9T*$e|L7B6nyxmo$bN$xJ*RtauR;Xd-IRoc5|!jm!i;9 zQH$W#)8E;oi$@ZE*d3lSEC3PPKSbkPb+T zFJvq}&}Jhg?c2KP*QwqkJ^hQ_Ja*lz<-}>EGl_DfRRUzxwHJbp$YFZr)a-snFlwpF zVTt3>^V9B6&@5DEA>5akt7X10UC7S{YAQ|(&h42OBc6veWcnlc=5P`f zD?nP>Z#x91tIQyuO-KiaIt|dDG`>Se?)3@fgE!g~IOVVvf3?YUV=($#?9b5aS4m0a zz8@?4SLApe;AKDJx$-OWZA^bZ$iDC?J%bXTVX!j@=YF*;%}!lVvTgk!Er#seP2gfz ziR|hYsvA2umpX|YD+=%*5|D5Xwh>oYYcL&aQvGGCGrI}lIO91?!J%q-qyIp&-hPM? z@GD4JMsl)6ex~^FS7HU%@kHO1mu^!wd4d0xb!)}u`h#<`>DX6hCi$Tu#bnbH2fFD!&|AZS!mj~OM6*gxem8$8xiQMAG;s#MV)VmUpx#Osy z=}+cDSC>zLpP>&m+FZQH`L*_^2VGG!Z)j6qAU0iKPruOB}qt7_!;*yZa<$w29LXBWQWrQo)(nPsx5-Ok~5im;yf{uNb8>CF+x(lQ` zY2>$sAl1I)*^d~>lI|9PWJ~trDVlaRXVYjkcKU35)E~t@StsUC&R4?;Xd84se7q`z z=pt2w()D&`-|8DsCUx77sTiG&=lWAyyK7%I=8Peaxo9e$NKqyRN9h}F+8PV3YH9?_ z3g$-;^0#l0F5lKMQs$ymh4%3S;33=@M+sAROg=jxh3w9&kEBRh7yeYEr>vtUqk%7a zZ!9UO9$lC4+GHR7AG%H>RIw_^*JN*+hXJe^9jnPItW?_H=j9VRgdi=B!|PzcA8o9o zrK=hpcS}?woVQDN3)Q{o$pN`#dU7nh5K_l0-8QMB z;N9Gkt2B2S(#b{q<|))B8`9{!VmmD9#F#Y6`qPTR+?3(Rqcn5bXvZm^Uk#JEP^{R! zcgKowQJ&J`F!`x)nYDuGKy~-eGXzMgg#lFiy>w@@BewRvP`retK)Stmi|pLrU9bt0 z(4>5ysSzln8E9sqH1hwh{J9ZcZy-dVDLYCwh*n+nf1Lzc!X_`yo%}l_wa?)QLm9UM z<6tgljY9Qq1;P>IvJ0^rLY_G+mfB^Wn`GIU#|-t|e4Y&{-(y*W8AEhGRopy!U_w}I zy|{#V#V`=oCw`xM3qs;d0E-w=4;oERg#UD%WFh6!24u=z8<)!%jF1>k=w z0?m2tR!A}@PKi`X@-!@EQ(SX*|1 zP)_I=Pn{x%PUY`=P<36KiBFun$9DzYcvDMS9>v>69lzKd=1DD!&&!^31Pyq&QOn1_OY&!V-RxmOD@%_es|e?LLMbWfEV!SnqPy&$m4hN8RzSbeBJPd3`xHZ${BdvpQNm@vaq5x=Tpdq-7E$fKH40~{ zO-WN7_dqzxa-MMfmsAAgeGS_&_J6<0O(KLq3DeHRv&382()B$+4B!!_2;O)-O#(TL zE`3Yu*AU=L`IL-Rbx>RS&VA(Du`=zcDq<(X-lxrJW+5_nO*ukZfrZ$w5@nz_-_G5! zbKRgxXWq6~H{V;%OU+v4TU@I_RP~??sSHp=R}sGXggR~!X2h>=GnzliPmPjA1L0s* z(e6nm=yL(LZ4k!uTYvnT{a?(I@>`6-)QXu70GDOcwB-teC4E4iIQbub#S9)q5HJHdL-Glcq`#&}`F?#*DrTFVa>+d^8y>CqGT*nKG8fsS)Ca8XudW?_ajC@Q8>WMcO5FGNZpVSCO zN3f{tnKU_SV_gcY&{^@J_ux7Qkm*3=Z9&MwV9nL!zV6WgsYsSW#np&;gb{!TVZyJ5 ziIu{u@0M`EbO-8mB9H~$Zr~>~e*K*bH4qWHPP_zwO*CB;{TQeJOClZ2|*9ZCCHYE3LXCwJm$~q91pi zma~<$iI3{%)#b!=KIOb*iY19lYq4u%TK`<)^_LOEq`d`Y;g%!$ zjUek|{0-%5CI+E?QJAV91abdEo_ae~^gIAfdG_BjLV~K-#48FB=AManr>zHYUNN)y zOeB~oK6QtKZq3d=eE~v5+Pmq6i*klpS6C%<=vshb69BDEU^?^(qgA3joIm^nxaA2N zCO%-g7W{g}GtXj0Q95ez{CInaOOoc}<8vt%7lwf67Ou!C_Y(a}N5sd+PsKPyrlqGs zWM|i@)fbJTMs_8l4mX!ao`=v?1gnxf%Z!1SWe+f7l7wx17tFTWeCC#4PmU0X_o@3* z57kUl9knBT;rGxvbXT(zBMTn*6s_=_C)Z;eHr+=7vOgZ>LhaB|NOG~v2nYN-^r0`O$_`q z5~NKXGEySro99$-nGSMvEU!p`G^JtGuL?&({mGN&da{65URroVi~GdSRK$yFY)d}=4rxFH3)rEwPZ)5mwt$`CD)z5!Y$aVSrO>VJhmuH;#(Vr! z>)2NMp~odz$%=yi%(>%+TQYhK__39+sYtDUX;5k#X|LQ?g5C5BC zV)zm=yBYs_S6+))BH25Qa>h0_&HYf#8jTu;MwRAv=37|H#F8}`Lh<3moyD)6`!)Nj|~Nb zIhHdkYig(pFF)-BZ-ZyIFq+Dy>xcY*@r;Wr^;rY zRkFM}8UrrRPq_r!b(K*~){)&t`sb)!nAtip!LoI2_d)Tqq3DYL54m4}@F(1egj=MO z)n+jwelVk0PW($Ko-<_o2v55Ynr#}eT$(ZC{%-EL z_>Dv@8kb^DhS-81QkQ0Nu>wTPx|1qX)ESCm=E6Vq=f2D!x%e*64_ozFf?W1f4m)VH z7n{QT2x9K=1G|*x9lpcLn^^t&{c&gTamT}we+nyU$X_rXO;Q~S0ih}o-QP|s5_?@N zE^WWj#d}7!3KK92W3mpqDk2}DL8MlQCgi4^kpmm2KH&>$>8$g#<5oxmUA|HSjJaYL$EHN zpurKT1b+l11>2xWYY5m}I+mD(-aSQBRO}Je+ftCs9kiQ;p7CP2f)j`T6Q3&TE3B8 zg?@_i$D1MwH!F&jd>t)Wo_`l2Jk}{&TUVazfDtRTZ~dLH2A@A(o+$VO-u3@$TV z*Wrr9cl&o)N6|N04>fMEXG!uz?DMG}i&g1y=v<}BVbc%^QyJ@R*X{I3ZqnYZ_BO@)Q9}L+`TQB(wlvTh~`~3;3gN(0d^*TXRjN%t^7H6Qz ziIAbMP)$kRVle5r8YD=f#2b6GDNS|AQGJ98=t|z)3hcO<;Mc=OM2sAg7V0NawxZ;0 zPqH|oy_>M+(~!oCGji;Ek%tcY{dzsNdvLhJBgt%q^MdN5^3e{SOlFk~IyJ2_MMPwNO(3V^tF=&Z9K9mZYVVj^<{ChlA89uxo%^rzAw%Uu&9yd5vU({6LE zJ?JW@Oy*SN4kiO975^nD5K~c>?-))p!7m|$`5-ht z^3Wk>?xXn3YWA;RwSIS%cyYd012}Pwc&3E$*A&H+|A`}(@O@Rd#*cf%aWKi=jtb+b ziQJ2XlssoTTs~Y#2$izC;fl_)TX7rv-wJC$9vfm;tc5r(JA{8P1^)p|szT3Fw5R*T zCUkx;>A`DXsdI5)>#E+SXy~V_vSA&E5vzPQzsc*CVAtEoQcd52F`=_TE@9HQ%lOs# zl$K?I(n=oxzmR^ng(Or!QOe>8EZ{H#G9jL-{pnhDeDEvzoLH9uAOS++``b>}XORCc z9N*ihPDO&CHmoF)aMbxdD{c|#tL*Up>+=%7#6h66uDj_BG9fNS#b;%$h=Xe3DxhcL zR!aiQUl73l#7-i=);OX*rBGAp2q?KaSC$!)m+*VYX8fDJU~N)Xu=zC<6MIOn5=YA? zpu{}P@v=@A1A{cwGDg?*7; z^52qJf(YlW*^AQwuQ$shlMp%aGg!W09OEpoT~L0a_!&(?RUd0UH{(Dz!d^0!J@&(T zi)r#z{VRougs_Y`l(|e6S>c8DAGf}b+hIeeO#M*fSngdaf4}zcq&W1blt6R-3m`ZC z*j_{Y-``-E#2xOn1zZ8P?E0Q%2UYE#n8aDK{&!LcEig_%z&0-@EVV1>Q(ZN+E~K)^ z|C9@N6w7};{LPSeS+S-Cf(1BDs!c%t*Hf~QogZzr@CS-_az%Qdz5k1jo?qV-U|f~l z@MBy23q*j!P^THs3@QS$3ZdjqUA>O6LO=4 zpg*hk1e=eN>c?S^x#ES*v(CmJ0&-1SOse1C3UsTAL&p=1j5}lsf*cUxGA%5_>Y%&q zl11{l?hl1OJQ*Q8%Y5KFDfS_DgJm970Kw>9K@76%6L$fWB`u7)Y-S{m{5fZ|3S4)| zv)CaQ$6{ZppL^ki;wcA6v!`e|2pILdG%IbtI+(6*JQFRxzddZ$o-Apee7QgKALSCI_#BfgK$+dZ1pl=51uJ&O5f3Q}bfg+wwBk3O)}mMzDH)+%vXh?pg06bFZDXyfvW}Aa(qfAjl1GR5-xIP?-Pt8O}KY9*NM2xI<|1IY{ z>>i?IWMm}DlwF6umHI!IR)uMPN-h7s?=HNAK%^B-)E)Sk3SI>x*BL;Rs3-Ht!+juIlV*K63J!;JA`0bmd5i~dHjdpS%Z$>i zCSlPgb&lQqeqH&~^E%cO?OG?om*1*tR3X~2o8@_s-tn%;>Yh5GW15>QYu z$J#1@k97>^-nHmK+OUft!%$o9${!-qQhK=f32slbu6{+ua^zixpCWr0vpqAX3|Y1B zS?BO2U2VF)e)*J=yXJGzlY1)YBxR6=jIvEDWt?|*!=aW7*RB`Qoe-$40%h6G{+`+6 z=GZ&(mnpR{Qv!aL>LO{le7;GPI62tu+W?C@PmOlK&huEuR_?+zU;PJ)sHv#1x4e8x z=#Q=ojp=86^1<7mY;1;V1I2iWab<#s1<_3}6l!#J)L)!AmdIP399r^190kj9+~lQs z``n(pEqd!YH&&xGwZ)@eH4-XQq{#%8?y8s4DZuczFSk?gcCLF{5s6s~Bb{#y=!Ldp zSs`{|Fb%#&lgt}i3O2%W!GDcEU)&fcCMAua|9SbOTqpIt()fAhCRzjSJ<6-{#2a?Y zvGuf+Q{9+WJBJ9$QIS!~hZd@4D=d|j>-%GxpXL{66GIQh55D4ma%#@<|Gjbjd^Xid zoW_?|)+WyB_V2$3qvgW;w;v-(G}gO~u1b*Wb>lqwV@#b4+HrWX!+;6#{_}M+|Gy8io+=sy(UVOY?uz$^Jjg`M7 zTfS(^&c?W6C_>e=ikx)}X&`E0q@&VSUXPgxc5stb(dInmEJsMT_~|d!rQhSbA5-+* z6x#?c!)w%1U+WTkS(FlaG9sFm1JGeWebEtdZ+QGmM*Q|N?<7B>Yi3iHzEV{-M~B;3 zyhT7$d~M^C$lE=zY&BYLFs&#oG`6W)=ZZLq_xpCBV56KI(Lkl{w;)G5DO!Ex(r)OD zN~nrkK&#ZEU6fz@{;b@r^JResgLt-s7PPY<@i5- z2s9*p%fF_HpqC<HGR#hr4hx2YK_odK~ZjWRtAUB{5!nM!HlB6_8> zsfIYsm&g?wuBQ1)jkx_#PMNf&W8`C79)BHh`lBo*7O;-R%;I$xZ8 z^2uZ1Wvx&5iEZQ`|M4Fq=lQZbU(!lmc;SUHJ6eO-foNW60lnRrZ0pK#$4{G;ZL@U0 zBRXA`?#C#tFGK(`vI7y2c(7})8aiYj$KmFp7D;j{QV^ncF6owAZW)Vw#%TJ*FFH39 zko%PwHk$Vj}Rp5CMo_ zM579cc*6#|z1uT*Gd9hhC(Hbt-XoV>a)~}306|$%r#V@8zuy1;_bWb~5Qgo0?zv}7 zC2=yQ(6=FDom3M`-E66?R!ud=HXJQI;f^npikgOWs^PeT8dit^1cS}ufy53(P@*4H z-P|r=_YHpR%F#ZqyY4#G-rlaarQPto_ulJh92=p!5&i5$r9k3g-;^!29!AFpD&P&V zr(p-8oC!q1`q`44>V=2}YMsBxbQc(qB5wyOQ~-M1^oLf;PHCD?NN_U-yq z=C!wIJ5bi__zWjrR$Z&R>k9H`*edN$6T%Kur~rf=$aP_+qel*FJJ3-ddVC{IG>ZyG z@?p?4XU-fofBuNNhwTSXm{X$m53YMB>K76ZF+XJ0TM3#@j}wLMHf-3SqoS>I!4^c8 zyeSol%3CcjC=Y$*R+t{UP{tK10AU9*iw6=ZXrRZt9f%7|?|9-pJOrFMx=-*%Ra8{y zZ3&v&ym_;uaco5Ai{;CgJ5s-hdj85+zEbeT#vC&zWkX7H5D#P(TWgTqFzN)E5-%yD zqCJ%7h`b%BPyvX0!YCdHL$n`->X|@kd8Mdf)4c?ov74-f{RR~Q+s|h{^O>yMCvJe@ znhf}Cgv5jNLy3xs;`vJD3DPBoXzW2yePzXnJKlpMu(Gkqic9j+6duEtK%7cq zzLJw;5&et}C=(%BQIg{O-~WErV)mK#6P4cppRTU1F(W)Rf?A|dnK6x)=kSoa6KOk8 zAp;Qi#49E@#z0S(8uTF|nqy;tZtj?;%KfSTl5{$s``qUgh36?YPavTiDl02fbj+}f z*XhQx?{9wdo4Qx|M2h>##1HIXR{4?ostTzQgeGK8H4+g`P@*A4+749c0Ayqb8X7pL z$c{fQZf8FTL*uk%XZW$Jq7_avneGj4r5|cOUwP$~W3fX?J0AT9>F3Qi-&A+r zd8Z#etLzR@kn2{iT$xZGs7ePV7MqvWkUHXZQb$Tky_)R<^t-ws8R=lsg$_W(1G(0v zXfD{lw^JQEI^=*w**8JIr$!K^I{cy~OO~i}&plVG18(AetmD(L&??(utxNqR;q+%e z``MV+C-wJ<`aeC~;SE$JaX_baO!sUDYU-G)%BreOS$eVQz_5=ibO6GVY!nZ)f7=#C z%N{?7Hyce0s~Y`_ivHB6KBd(s(gRklTIG70*I$2q#^6JwAM(inARjt*x}IN{2jFR> z1`<4iP{F+9g-Nq99Wfu^+gyM4n+Bd_LG+S(hcFZ+iugw=ezE@ z%Sie;=bUpg8lN#XQ%i_q^E_-ZjpqP{Xe%9sfW*bpJKmceNZk1ijize4H!Jw~(+ka- zC;$-gK(6gTyxr*Y?niwft|qSXh8Z2cWXKbIh=lStfAcp+H_G4r-QQ(2KI3No_kaKQ zSoO-lUE+bPl#H^HVjYlJU0$AHMCPBc4>ru0Eq%j`8e2x;Ikja_6i&?84zy$C3sN7b z%a$=FD=TX2r7PxAlc8f{P`N0(;k44>*uc>*1Ro*+0f=_QTb*ooylU1P-fp{zzK&>U zt276^XsfFgHOx4eP_wg2yrsmA84wrKXrRHdayldm00hH~>_CUa72msKtCZ&$_J!%X zZ`jNZZICwIA$5N`J3EbRKOB?s=?c#y^-;tO+Ygb|F{S4K0Axi(vsH`Ofk@5pLbP<^ z9x)(9uSk|?jS2u6*@1=!kEoul8znVkB>B+b#z-C_-i{^-O5{yvVvNT4OyD2@VdD8> zGN@naq#sm@Z+zn$I;uK(;_-i}9Kbklx#Ib)M!KaE7469mBocCoxf-`3b&gT^KPmuZ zWC!8`QmCFHWqx_X2uh?(N3p@14&yVPuUl`u)#%9n_{>>ntgDZEhb4;J@tVMDUz;uBZbtEfk6)=E#rEVgLH3RuMSD~ z)as^IK@fZ6t(*NeOrNF5{~_nhDPer(ZEGt1aGZ4cV(gdn0;m1soz6LG+YwM&T2ZNA z9t~1v<&RtTsE2p|L3chqEWkrIROR9Jm^7(KXU3xL~$-J#Ai=^Rn-aO#Lr z2^#?f#BY%zf!a)oUAo@Zw6^OabL$R>p}y~kI_}3!A;$&a=qX3)jzJW@PqRp1KnT7@ z34l<;z9q}&-0~GK*q%36smUiVlQ<0zr58pIObr;iA#8g3F*PWOuh|IW^Ft3kWNdh5 z{{WDk@tL4QTI|r}bfTwRz@K7y_$%cFQ7s@t+*aeP*#bC|^>r^6FajhjK57}o=f}QO z!X&U04(-~J?Y2n$Ju2psr1Odr0C8epmE}5Fnq_fLA`b6c|E5;M$|@yc(5o6I$* zmIT#ZufLpi9$)yv7t~k3%GAw>!t-aIdFHsmi1|Bq?9jF!0vjhH0Vqfu#Ar3oU7%kK z01vH`vDZyP=Jk5LT`-n;6v+$9ka#;v{!nx%Y5;`MWveXi07wQ9X%-Zjr`+mBmpE#txXW{5wI3 z*PG0tf8TB$8I2LrD{;eDH8#uJu0!qHkhx-r7wLzxt1PL6+a0wI6)Y-*%0x*E47r3k zl#9KNwn(*;r)dd?PNXIBfNQ|Xv1;X%$CIRVZz*cN85zA;%?_2DQBjnD2ul2pEUvv_ zgVL54(RlLWWxiy!msUv~87V}S4uCK|uV263_(~)F+<4=S3L8)&Vf!J|{fQ@@NL*cT z-?87|6rh}GZTl3ltF`I6I#tar+Q!Cj`LN@7Vx&l49?JJdE*2bH0$jE&9n7OLVW|DcA`RA2NW{m?BH*L{+DA(P&+WsCay*S}skl@77n z2-u*WP_m+4Y$S~m%t6mC%AwK`%|gBI8YJ$+`CqUPKNu}zV4+21`C`psYO7TwND#uQOx{?a6IXx$W1?3t_FxSYZ9|sozEvv$r zRoFo42P@b|Q~`*_P7lg*JtyggC|19J$5u6E$#U&6aWmF#+tR99)wbYdwaEs6?6!Bh zZsEd(>Y8h=8CUmb&6+jpzWeS|y}i9ocNOS%T8$Fv1)B{4ny4T2ORtvLn|c8q+~LL2 z!6QtnRR5k%9Vm!IOd?pdsC*c?k!X?BG$?uCTB-0u@7TjWqB1n90L1lqTo%^=MBw0# zm!DT1Z@a*k)Qn&wf_b*ViZAhXfkla?34AWh9a| z2>2hs2FZndr}}9#wNx`w1E*Sy$2F=-Y&pFB@eti4{+I15P*0*cBIXLL1$P4%|9ss-g>Z_Ba9}Lgrdj0BGzhaZ%NZV_QPk5umBgF#>(9k88 zl!gkCc1GOIBG~{ob`NWaG!VdN*Q%9j>+?@*;GtY^63IFDY}u%`zx13=u{r6o_i3Xz zQjrOjii)Y#I+MW>8xv&-2xIiUvRvo(nvt5ZTZ-0CIps7RMeBBa>h@Kla-eGXK}bJ0 z-E@=qfkCzd;E|5^ytP_))t!0v+f~Oo7mnMdlNF?V0MxxZw>jB7aT{~7 zD{(I&`W|Hfg3ZJPUB?$!BsUv1f_O8Ql!}YnqvxfhymBPC(K2YdnoRP>lv{o8d*7QR z{gChVzyl8$mvP0Wf$Bu`cBvSK`A+%*AVUN(Ktm1_AjCHORwS9-wtNYZWRuLf@&6r@ z0&V-&y&)?13nMvua^15L%`B-Ujv8eOK$D6~HJ`H`%jS2u8>w`Rr~{A{6XaS}raVWl zc*Fe2kmp%#l9Yf($w8w2kQbtCM`FoU-ppjV|JQ&0m-9kns8JFfTrPP@1n-fbgpH=M zp;@aI+Wjr6l~$v~@H#G%*vkp{Jyy;i*cj|TpdV_gT;fGLdgQPw7WEIqvG$|^{wajS zgX(p#TY7*Aus9`*(>>cZ>#;a1+!FT#KyN@iuUE-(m%-Rj4v9Jdq0IkWmg`+}p9g#W z8>(ggNje6__4uR#leEWY#QTx-gR6h0OqrrCy6B>j2WoUAD%ah2->q9LC+-Y&2pa`a z!$>Yf2;+q!U16k073D!I^n<|ffKAv{DEhc@9^G3u=xZ`0=iakzlN#vSD~jX76IG`cJCCs+P;2V&26s8q54N{yr=F>%EjvTvj7|g+Gfww~6R@(i`(!zt zJkt^<7A*hSC;$RLd{_x`=RHOL&=-z_${10%pA>eR3R#zeaW1T=zlLLorj2ZVrb`n{p zW1O17gR%96r^SUnM}t6?<7EC>B<|)pbJdPlUQmfWICv1S!Jr^W(cCl8j~{6oXX)Q}iaC!coC$Z-n*XeS%t(U9TR?<5E2$I2U8&7B@xCH8<1@0jmV){vAPrF+Da0#PHn!>vc>o5$A^iZgtWvV(0UWaD$$PR=5E9j- z-XVEVhQQJU!mz5voP>2D{EE6m@L{ibj|lSX-`lD4pfDU$fnaE0K)_~LySjszz6K$Y zW;yomHLt1mlTQ&(UztcR;`tNx>5z-s-PZh2ZCj+?lL=uIM>-?W(SQ;KJ%i!l8Zr?i zAUIBH23<3*=N~sT(9^BsHK?NJ!EmS#iF=Uk|hERlFDfJ3T_4)hZ#4q(U?Ax-J9iiFOj^`Z{fO2VsK0OCGiECz0qTs6)c z)lbJAxo3cPvC#c70~;|>00;&e8KZfai2Il-ModI*uT&?sYZMNU+&z0(a+XKHlM^o zO+W2y{Sv`g?v{oJht%M~1IG3zx5o?f_7e$!%#6`;;~tQ*2|QqQP4GX(~w-}Zx|jYC&MNZ0KrhBdePVj@Yoy`n?^~w_k#5~xv~xpLdJK`<_#jzNVtJW zFu1@`IeA#I*AQ!is-<1vBI#&K^Kd#iTwXV!u-VjQXNt#B04QN51ScZd;&I$u|D@^` zeh`0zWCYk^tfurUDJe~}=c!p|pRa+;)E)>w;Wf4}L*gEe3{5luGO`14wXq!>>e;80 z2n+q{^AKRy>RkPb4vfUhhpk4dTbx{hf=s&W0*JJpv{;wgC5LP5JVysmj|g?_q<6hf z!T>tOhP6lYhwV_=kP5LCnR>kv=XFFXEnssxW&wKQkIuex4@ZU)Uj~smsSGDVPS`$K zT)X4(dd$D%ay9+bv-75gA)R2mI40#?>FBA8V{x)BQ~^|sy<+oN_t@{WTIs~@!66Eg zgf9eTwn&r6C0Be%mw3&19s;~5U+@>uAqNgy7hw{95Vog%!u!{L@e?ftrBn4X91Hso zRX$VVF*1w@`A-u~0^-8mA!vns!borpg=lD2i;A~pT=vvt;fVzTA9eGB;DYThE?vqz$Wk%S^i z!XGltTjnf~3Wkf-wB={{5=82SSI3WuCske~o1c41c)wZ=izH-7kupHTgNX=01cKZv z%g11V8#>&#UuRzX5f7BsH?LRgpLj%nrpBls5dlP0ZNz4QF?qgtawu}8-`;6< zj#mpKD{(ndBUDp7T-b7^13F?<27vNSM1k+$xm60DJ+3;X^v4jqH@C4x$^gx^nTP{qmHXwkdgujrX&M;JX1 zNxTlFE9{-jcB8_*4z;sXN{#?JRB9wxv|UuLFc7g&7@T*#_OiZ~M1~uxU5Td?DFZYo zHcBN-aH^zkzM{owJo9^gs$1c!r0E^nC%~ zQ1c1(80pjpsmGeix8zHt4A7j|L$DHVb@}d! zM5-A9uMM^Uger%}Oh*BWdJtB^dj;r}N#G#W0kG?!RslQ|R3jKL6eOTCB)Mg%v^Wd! zl5SrqwWS4gbSjF!lSKNt#ot|qV{iUGLE$(jr?bGICr)*DL@dYd)vv0lOHb1ehr7|4 zx5hY3G_q^(8Qm?>%RIbxRr;)A^OP*bl^+&jzNCE+AV^6P%ANp}#9blb(2vVaoKukJ z1+WYqk~}U+qp+$p5;aX>Gtw)F91n;H9XXLj8_+@WvD=9ZKp6C{Ghur7){PP=yIHl@ zE|Uk|zo;N80sZj!@AQu=|7`Mc7& zy@+;4L$5}({hZy8+18Q7*(G81(1hN z{p6)O7pZg2tG=lW*li$E7l_IR2uq~!7u{f)6Gm|WNB0;Lce##(86Bg?C4i6KQ7-== z0oz4M8L`pvGI+jVpenM)5*bEh=0lVxFjC z3*UK#R`I>G=PLWs7N$4`qM?&#Xwv)b-H{n);-ec@ z5ACO#AvK!?@Z?xXGZ4!K1?xqeYw+#^Xc#dn7P$pW619XEg}1O^C^|M`99)tijoi|1kiK zaD{+GYLAY%k?c5?W*t%x_KxM(UN7oTt8WHrBp)j0kwLAi^hxXvfWU)BR5Pj$EqxlL zXBmNd#LQ$$L`^gO!dyLSI7W4{;&66K&K1eF7?EixsZ&l0)OK3M`qp~!COUP zwUDb-={SwLh2vwwN*YH;l`*vI`mvPF=cOEq>oMs-n8iH&yJ!^d`VJk#YcA z0EgB!2fOxZZ`D5W)a{o#%1AyTH6CZXIDpPQvOFdW0kDDPY{TYp2mtn*K1u4N zY)Jp!J-UEbfdYNmbh(QR}XtGqQV@GNABetBrK5aNAw`%8WFKK&Dp_L1<&Mo27l6D@Kbx1q>A5$6* z!~is&F`lF#4MI{nV)ByZx`Srp^bTJQ)8nTbk~K&(+)biG9oG#DhesqEF&bmDIw~CjnQ~eY&dyH<_v;8iBit}a)8*TW60$gfb1FPNeW8bCB|tYhg;^%*OA4fB|SUsE8y(y(03YaDPME3x(@j zynuM7B22NA6VSzt0mz=QNI^VhnG>t*$Bths4=4ePfDy@?%^hX+IBQ)QnB9G@#B=&kV=7)Z-sh;o-^{>sAQtojUE=aJ%A#SzsE&d8AdXZ zMth`PSdxi0`bv3Lq!Wz4`^2?vkxs;%7@%UEvfU>V4Tu`6Rd(F*aa{~R>6vL97nFXN zBfsd3a*=+>yTWKq1an10gKnNj8U*D?t)lk;0#XPGp#&jfL($a@DT>ekVqYOE9chXT zZ@>nCA<+^-O0hgi9G5tnm~1(Klta5F%Z|hUF~uCnD+f@;5HoVJNZys>;|Q2I-ZoeB zFR^1pmWjn66n-AWYE@XDpwW5hU{o9>qf5xA8>;EW zAQVmjvZS2YjuWcKWn?lI(4_l}Z9wiW8X)w4-3^Ji_L&2*Eyrhu`n@~A#wo=B%xYog}TqpaMriMqvaMH!O+)$PKs5woL`38L}G-kgU&4dq!=+uQ`$$pJ~fvl_v(3 z%)BbtUke>{5*Z%@P$D;S`yF7qT9%7tK~l1SDN&WNY$Wn?l2m24ttFwv#j)U4geeA~ z2w&Xz)M5^VodZzS}F$coR0b2lKvM~sS^^(Q&ia8J+4x|MjPB8|d=v?UdOkxg%kpme4hzZ9a z6voRI&nf0WG&qnIfH=(GH*Mi8)Z%9B>UloNf$4g?-WE*~c6R3kQqr2Pi /// The guide object. /// The title text object. + /// The title's icon. /// The scroller parent. /// The of the wiki. - GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, Scroller parent) + GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, SpriteRenderer titleIcon, Scroller parent) { var abilities = WikiAbilities; return Helpers.CreateAdvancedWikiPage( guide, titleText, + titleIcon, parent, RoleNameLocale, - $"{this.GetTmpIcon()} {RoleName} ({RoleFactionTitle})", + $"{RoleName} ({RoleFactionTitle})", + Configuration.Icon ?? MiraAssets.IconNeutral.LoadAsset(), RoleWikiDescription, abilities); } diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index a57332b..0feb75c 100644 --- a/MiraAPI/Utilities/Assets/MiraAssets.cs +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -305,5 +305,35 @@ public static class MiraAssets /// public static LoadableResourceAsset VisibleOnIdleSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOnIdle.png"); + /// + /// Gets the sprite used for crewmates. + /// + public static LoadableResourceAsset IconCrewmate { get; } = new("MiraAPI.Resources.Wiki.IconCrewmate.png"); + + /// + /// Gets the sprite used for impostors. + /// + public static LoadableResourceAsset IconImpostor { get; } = new("MiraAPI.Resources.Wiki.IconImpostor.png"); + + /// + /// Gets the sprite used for neutrals. + /// + public static LoadableResourceAsset IconNeutral { get; } = new("MiraAPI.Resources.Wiki.IconNeutral.png"); + + /// + /// Gets the sprite used for the player list. + /// + public static LoadableResourceAsset IconPlayerList { get; } = new("MiraAPI.Resources.Wiki.IconPlayerList.png"); + + /// + /// Gets the sprite used for settings. + /// + public static LoadableResourceAsset IconSettings { get; } = new("MiraAPI.Resources.Wiki.IconSettings.png"); + + /// + /// Gets the sprite used for terminology. + /// + public static LoadableResourceAsset IconTerminology { get; } = new("MiraAPI.Resources.Wiki.IconTerminology.png"); + internal static LoadableResourceAsset BlankSprite { get; } = new("MiraAPI.Resources.BlankSprite.png"); } diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 4587f36..773a4ae 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -741,15 +741,19 @@ public static class Helpers /// The guide panel to attach the tab to. /// The name of the tab. /// The title of the tab. + /// The icon of the page. /// The description of the tab. /// The instance of the title. + /// The instance for the title. /// The instance of the description. /// The of the tab. - public static GameObject CreateAdvancedWikiTab(MatchInfoGuide guide, string objName, string title, string description, TextMeshPro titleTmp, out TextMeshPro descriptionTmp) + public static GameObject CreateAdvancedWikiTab(MatchInfoGuide guide, string objName, string title, Sprite icon, string description, TextMeshPro titleTmp, SpriteRenderer titleIcon, out TextMeshPro descriptionTmp) { var obj = new GameObject(objName); titleTmp.richText = true; titleTmp.text = title; + titleIcon.sprite = icon; + titleIcon.SetSizeLimit(0.55f); descriptionTmp = Object.Instantiate(guide.MatchInfoRolePanelPrefab.roleCount, obj.transform); descriptionTmp.fontSizeMin = descriptionTmp.fontSizeMax = descriptionTmp.fontSize = 2f; var sourceTmp = HudManager.Instance.TaskPanel.taskText; @@ -769,9 +773,11 @@ public static class Helpers /// /// The guide object. /// The instance of the title. + /// The instance for the title. /// The the page should be parented to. /// The name of the page object. /// The title of the page. + /// The icon 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). @@ -779,9 +785,11 @@ public static class Helpers public static GameObject CreateAdvancedWikiPage( MatchInfoGuide guide, TextMeshPro titleTmp, + SpriteRenderer titleIcon, Scroller parent, string objName, string title, + Sprite icon, string description, IReadOnlyCollection abilities, bool variableTextSizing = false) @@ -791,8 +799,10 @@ public static class Helpers guide, objName, title, + icon, description, titleTmp, + titleIcon, out var desc); var num = 0; var maxTextSize = 0f;