diff --git a/MiraAPI.Example/Options/ExampleOptions.cs b/MiraAPI.Example/Options/ExampleOptions.cs
index 6f33856..b5745e5 100644
--- a/MiraAPI.Example/Options/ExampleOptions.cs
+++ b/MiraAPI.Example/Options/ExampleOptions.cs
@@ -14,6 +14,7 @@ public class ExampleOptions : AbstractOptionGroup
{
public override string GroupName => "options.exampleOptions1";
public override Color GroupColor => Color.green;
+ public override bool ShowInMatchInfoGuide => true;
[ModdedToggleOption("options.exampleOptions1.toggleOpt1")]
[ModdedOptionVisibility] // Automatically resolves to using ToggleOpt1Visibility, can also target toggle options
diff --git a/MiraAPI.Example/Options/ExampleOptions2.cs b/MiraAPI.Example/Options/ExampleOptions2.cs
index bdc5a1b..e0d4aaa 100644
--- a/MiraAPI.Example/Options/ExampleOptions2.cs
+++ b/MiraAPI.Example/Options/ExampleOptions2.cs
@@ -9,6 +9,7 @@ public class ExampleOptions2 : AbstractOptionGroup
public override string GroupName => "options.exampleOptions2";
public override uint GroupPriority => 0; // This group will be displayed first. The default value is uint.MaxValue.
public override MenuCategory ParentMenu => MenuCategory.CustomTwo;
+ public override bool ShowInMatchInfoGuide => true;
public ModdedPlayerOption PlayerOption2 { get; } = new("options.exampleOptions2.youMustChoseAPlayer", false);
public ModdedPlayerOption PlayerOption { get; } = new("options.exampleOptions2.ehWhatever");
diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs
index c58030a..2d1cc88 100644
--- a/MiraAPI/GameModes/GameModeOption.cs
+++ b/MiraAPI/GameModes/GameModeOption.cs
@@ -4,6 +4,7 @@ using System.Linq;
using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.Networking;
+using MiraAPI.Patches;
using MiraAPI.Patches.GameModes;
using MiraAPI.Patches.Options;
using MiraAPI.Translation;
@@ -128,6 +129,9 @@ public static class GameModeOption
MenuState.Instance.QueuedRoleMenuRefresh[roleMenu.Key] = true;
}
}
+
+ GameOptionsMenuPatch.RefreshForGameMode();
+ RoleGuidePatches.RefreshSettingsForGameMode();
}
// TODO: could make Values a dict of AbstractGameMode too
@@ -143,12 +147,6 @@ public static class GameModeOption
Info($"Game mode changed to {option.GetInt()}");
RpcSyncGamemode(PlayerControl.LocalPlayer, option.GetInt());
- if (GameSettingMenu.Instance && CustomGameModeManager.ActiveMode != null)
- {
- GameOptionsMenuPatch.ToggleGamemodeOptions(CustomGameModeManager.ActiveMode, __instance);
- GameSettingMenu.Instance.RoleSettingsButton.gameObject.SetActive(CustomGameModeManager.ActiveMode.ShowNormalRoleSettings);
- }
-
return false;
}
diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs
index c2977c2..52d8fdf 100644
--- a/MiraAPI/GameOptions/AbstractOptionGroup.cs
+++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs
@@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
+using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.GameOptions;
@@ -26,6 +28,12 @@ public abstract class AbstractOptionGroup
///
public abstract string GroupName { get; }
+ ///
+ /// Gets the sprite of the group that is displayed in the match info guide.
+ ///
+ [HideFromIl2Cpp]
+ public virtual LoadableAsset Sprite => MiraAssets.IconSettings;
+
///
/// Gets the Optionable type of the group.
///
@@ -49,6 +57,42 @@ public abstract class AbstractOptionGroup
///
public virtual Func GroupVisible => () => true;
+ ///
+ /// Gets a value indicating whether this group's options are shown in the Match Info Guide.
+ ///
+ public virtual bool ShowInMatchInfoGuide => ParentMenu is MenuCategory.Game;
+
+ ///
+ /// Gets the name title of the group when it is in the match info guide.
+ ///
+ public virtual string RoleGuideTitle => GroupName;
+
+ ///
+ /// Gets the key used to combine this group with other option groups on one Match Info Guide page.
+ /// Groups are combined only with groups from the same plugin that use the same non-empty key.
+ ///
+ public virtual string? MatchInfoGuidePageKey => null;
+
+ ///
+ /// Gets the title of the Match Info Guide page. For groups sharing a page key, the first
+ /// group in menu order supplies the page title.
+ ///
+ public virtual string MatchInfoGuidePageTitle => RoleGuideTitle;
+
+ ///
+ /// Gets the ordering priority of this Match Info Guide page. Lower values appear first
+ /// among pages with the same menu category and group priority. Groups sharing a page key
+ /// use the lowest page-order value among those groups.
+ ///
+ public virtual int MatchInfoGuidePageOrder => int.MaxValue;
+
+ ///
+ /// Gets the number of columns used to lay out options on this Match Info Guide page.
+ /// For a combined page, groups are placed into these columns. Values are limited to 1-6;
+ /// for groups sharing a page key, the first group in menu order supplies this value.
+ ///
+ public virtual int MatchInfoGuidePageColumns => 2;
+
///
/// Gets the group . This is used to color the group in the options menu.
///
diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs
index bb6ed51..1047fcd 100644
--- a/MiraAPI/MiraApiSettings.cs
+++ b/MiraAPI/MiraApiSettings.cs
@@ -69,6 +69,10 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
OldUiButtonScaleFactor = TopRightButtonsFactorSlider.Value;
}
+ else if (configEntry == MatchInfoGuideScaleFactor && MatchInfoGuide.Instance)
+ {
+ RoleGuidePatches.ApplyGuideScale(MatchInfoGuide.Instance, MatchInfoGuideScaleFactor.Value);
+ }
else if (configEntry == WikiOnBottomRow)
{
SetUpButtonPositions();
@@ -198,6 +202,12 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
[LocalSliderSetting(min: 0.3f, max: 2f, suffixType: MiraNumberSuffixes.Multiplier, formatString: "0.00", displayValue: true)]
public ConfigEntry TopRightButtonsFactorSlider { get; private set; } = config.Bind("MiraApi.VisualsUi", "MiraApi.TopRightButtonScaleFactor", 1f);
+ ///
+ /// Gets the scale of the match info guide.
+ ///
+ [LocalSliderSetting(min: 0.5f, max: 1f, suffixType: MiraNumberSuffixes.Multiplier, formatString: "0.00", displayValue: true)]
+ public ConfigEntry MatchInfoGuideScaleFactor { get; private set; } = config.Bind("MiraApi.VisualsUi", "MiraApi.MatchInfoGuideScaleFactor", 1f);
+
///
/// Gets the fps specified by the player.
///
diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs
index 7e2b312..4313101 100644
--- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs
+++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs
@@ -38,6 +38,7 @@ internal static class GameOptionsMenuPatch
[HarmonyPatch(nameof(GameOptionsMenu.Initialize))]
public static bool InitPatch(GameOptionsMenu __instance)
{
+ Helpers.CacheAdvancedSettingsRowPrefab(__instance.numberOptionOrigin);
__instance.Children ??= new();
__instance.Children.Clear();
@@ -187,6 +188,66 @@ internal static class GameOptionsMenuPatch
instance.scrollBar.SetYBoundsMax(-num - 1.65f);
}
+ internal static void RefreshForGameMode()
+ {
+ if (CustomGameModeManager.ActiveMode == null ||
+ !GameSettingMenu.Instance ||
+ !GameSettingMenu.Instance.GameSettingsTab ||
+ !MenuState.Instance ||
+ MenuState.Instance.CurrentModIdx != 0 ||
+ GameModeGroups.Count == 0)
+ {
+ return;
+ }
+
+ ToggleGamemodeOptions(CustomGameModeManager.ActiveMode, GameSettingMenu.Instance.GameSettingsTab);
+ GameSettingMenu.Instance.RoleSettingsButton.gameObject.SetActive(
+ CustomGameModeManager.ActiveMode.ShowNormalRoleSettings);
+ }
+
+ internal static List> GetActiveGameSettingsRows(Func formatBool)
+ {
+ var menu = GameSettingMenu.Instance ? GameSettingMenu.Instance.GameSettingsTab : null;
+ if (!menu || menu.Children == null)
+ {
+ return [];
+ }
+
+ var rows = new List>();
+ foreach (var option in menu.Children)
+ {
+ if (!option || !option!.gameObject.activeInHierarchy ||
+ option == menu.MapPicker ||
+ (GameModeOption.OptionBehaviour && option == GameModeOption.OptionBehaviour) ||
+ !option.Data)
+ {
+ continue;
+ }
+
+ var title = option switch
+ {
+ ToggleOption toggle => toggle.TitleText.text,
+ NumberOption number => number.TitleText.text,
+ StringOption str => str.TitleText.text,
+ PlayerOption player => player.TitleText.text,
+ _ => TranslationController.Instance.GetString(option.Data.Title),
+ };
+
+ var value = option switch
+ {
+ ToggleOption toggle => formatBool(toggle.GetBool()),
+ NumberOption number => number.ValueText.text,
+ StringOption str => str.ValueText.text,
+ PlayerOption player => player.ValueText.text,
+ _ => option.Data.GetValueString(option.GetFloat()),
+ };
+
+ rows.Add(new KeyValuePair(title, value));
+ }
+
+ return rows;
+ }
+
private static void CreateSettings(GameOptionsMenu instance, Transform container)
{
var num = 0.713f;
diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
index e6ae2d5..6c1ede7 100644
--- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
+++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs
@@ -1,4 +1,5 @@
using HarmonyLib;
+using MiraAPI.Utilities;
namespace MiraAPI.Patches.Options;
@@ -13,6 +14,11 @@ internal static class GameSettingMenuPatches
[HarmonyPatch(nameof(GameSettingMenu.OnEnable))]
public static void OnEnablePrefix(GameSettingMenu __instance)
{
+ if (__instance.GameSettingsTab && __instance.GameSettingsTab.numberOptionOrigin)
+ {
+ Helpers.CacheAdvancedSettingsRowPrefab(__instance.GameSettingsTab.numberOptionOrigin);
+ }
+
// Add Menu State object
if (!__instance.gameObject.GetComponent())
{
diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs
index 508b3f7..b8196fc 100644
--- a/MiraAPI/Patches/RoleGuidePatches.cs
+++ b/MiraAPI/Patches/RoleGuidePatches.cs
@@ -7,8 +7,11 @@ using System.Linq;
using AmongUs.GameOptions;
using HarmonyLib;
using Innersloth.Assets;
+using MiraAPI.GameModes;
+using MiraAPI.LocalSettings;
using MiraAPI.Modifiers;
using MiraAPI.Modifiers.Types;
+using MiraAPI.Patches.Options;
using MiraAPI.Roles;
using MiraAPI.Translation;
using MiraAPI.Utilities;
@@ -26,27 +29,38 @@ namespace MiraAPI.Patches;
[HarmonyPatch]
public static class RoleGuidePatches
{
+ private const int SettingsTabIndex = 1;
private const int RolesTabIndex = 2;
private const int ModifiersTabIndex = 3;
- private const float SearchDebounceSeconds = 0.3f;
+ private const float MinGuideScaleFactor = 0.5f;
+ private const float MaxGuideScaleFactor = 1f;
+ private const float SearchDebounceSeconds = 0.05f;
private static readonly List RoleEntries = [];
private static readonly List ModifierEntries = [];
+ private static readonly List UntrackedRolePanelDiagnostics = [];
private static GameObject searchComponentContainer;
+ public static PassiveButton SearchIconButton;
private static SpriteRenderer _searchIconIdle;
private static SpriteRenderer _searchIconHover;
+ 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;
private static Scroller _rolesScroller;
private static Scroller _modifiersScroller;
- private static Scroller _advancedInfoTabScroller;
+ private static GameObject _modifierNoResultsMessage;
+ private static Scroller _wikiScroller;
private static TextBoxTMP _searchBoxTmp;
- private static GameObject _currentAdvancedTabObject;
+ private static GameObject _currentWikiPage;
private static TextMeshPro _titleText;
private static SpriteRenderer _titleIcon;
private static bool _suppressSearchRefresh;
@@ -55,14 +69,21 @@ public static class RoleGuidePatches
private static float _openedAt;
private static bool _uiInitialized;
private static MatchInfoGuide? _guide;
+ private static int _activeGuideTabIndex = -1;
private static IEnumerator? _populate;
private static bool _loading;
+ private static Type? _roleEntriesMode;
+ private static bool _roleEntriesInitialized;
+ private static bool _rolePanelsCleaned;
private static LoadingRing? _loadingSpinner;
private static SortingFilter _sortFilter = SortingFilter.EnabledOnly;
private static SortingMethod _sortMethod = SortingMethod.Alphabetical;
private static SortingGroups _sortGrouping = SortingGroups.Ungrouped;
- private static bool IsModifiersTabActive => MatchInfoGuide.Instance.activeTabIndex == ModifiersTabIndex;
+ private static int ActiveGuideTabIndex =>
+ _guide && MatchInfoGuide.Instance == _guide ? MatchInfoGuide.Instance.activeTabIndex : _activeGuideTabIndex;
+
+ private static bool IsModifiersTabActive => ActiveGuideTabIndex == ModifiersTabIndex;
[HarmonyPostfix]
[HarmonyPriority(Priority.First)]
@@ -106,13 +127,29 @@ public static class RoleGuidePatches
[HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Awake))]
public static void Awake(MatchInfoGuide __instance)
{
- __instance.transitionOpen.targetSize = 1.3f;
+ __instance.transitionOpen.targetSize = Helpers.MatchInfoGuideBaseScale;
if (_guide != __instance)
{
ResetState();
}
}
+ public static void ApplyGuideScale(MatchInfoGuide instance, float scaleFactor)
+ {
+ if (!instance || !instance.transitionOpen)
+ {
+ return;
+ }
+
+ var targetSize = Helpers.MatchInfoGuideBaseScale *
+ Mathf.Clamp(scaleFactor, MinGuideScaleFactor, MaxGuideScaleFactor);
+ instance.transitionOpen.targetSize = targetSize;
+ if (instance.MatchInfoParent.activeSelf)
+ {
+ instance.transitionOpen.transform.localScale = Vector3.one * targetSize;
+ }
+ }
+
///
/// Clears all cached state tied to a previous instance.
/// Every game spawns a fresh guide, so anything we cached from the old one is a destroyed Unity object.
@@ -133,7 +170,12 @@ public static class RoleGuidePatches
RoleEntries.Clear();
ModifierEntries.Clear();
+ UntrackedRolePanelDiagnostics.Clear();
+ _roleEntriesMode = null;
+ _roleEntriesInitialized = false;
+ _rolePanelsCleaned = false;
_guide = null;
+ _activeGuideTabIndex = -1;
_uiInitialized = false;
_loading = false;
_appliedSearch = string.Empty;
@@ -143,9 +185,15 @@ public static class RoleGuidePatches
_searchBoxTmp = null!;
_rolesScroller = null!;
_modifiersScroller = null!;
- _advancedInfoTabScroller = null!;
- _currentAdvancedTabObject = null!;
+ _modifierNoResultsMessage = null!;
+ _wikiScroller = null!;
+ _currentWikiPage = null!;
_loadingSpinner = null;
+ ResetSettingsState();
+ SearchIconButton = null!;
+ SearchSortingOrderButton = null!;
+ SearchSortingGroupButton = null!;
+ SearchSortingFilterButton = null!;
_searchIconIdle = null!;
_searchIconHover = null!;
_searchSortingOrderIdle = null!;
@@ -160,6 +208,7 @@ public static class RoleGuidePatches
[HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Awake))]
private static void AwakePostfix(MatchInfoGuide __instance)
{
+ ApplyGuideScale(__instance, LocalSettingsTabSingleton.Instance.MatchInfoGuideScaleFactor.Value);
Coroutines.Start(CoPreload(__instance));
}
@@ -186,11 +235,7 @@ public static class RoleGuidePatches
}
_guide = instance;
- if (RoleEntries.Count == 0)
- {
- BuildEntries();
- }
-
+ BuildEntries();
if (_populate != null)
{
yield break;
@@ -221,6 +266,8 @@ public static class RoleGuidePatches
return false;
}
+ ApplyGuideScale(__instance, LocalSettingsTabSingleton.Instance.MatchInfoGuideScaleFactor.Value);
+
if (Minigame.Instance != null)
{
Minigame.Instance.Close();
@@ -351,6 +398,8 @@ public static class RoleGuidePatches
_sortMethod = MiraApiPlugin.WikiSortingMethod.Value;
_sortGrouping = MiraApiPlugin.WikiSortingGroup.Value;
+ SetNoResultsText(instance.rolesEnabledMessage);
+
if (_populate != null)
{
Coroutines.Stop(_populate);
@@ -368,9 +417,9 @@ public static class RoleGuidePatches
HudManager.Instance.SettingsButton,
searchComponentContainer.transform);
sortingOrderButton.gameObject.SetActive(true);
- var searchSortingOrderButton = sortingOrderButton.GetComponent();
+ SearchSortingOrderButton = sortingOrderButton.GetComponent();
SetupSearchButton(
- searchSortingOrderButton,
+ SearchSortingOrderButton,
"SearchSortingOrderButton",
SwitchMethod,
GetSortMethodSprite(_sortMethod, false),
@@ -383,9 +432,9 @@ public static class RoleGuidePatches
sortingOrderButton,
searchComponentContainer.transform);
sortingGroupButton.gameObject.SetActive(true);
- var searchSortingGroupButton = sortingGroupButton.GetComponent();
+ SearchSortingGroupButton = sortingGroupButton.GetComponent();
SetupSearchButton(
- searchSortingGroupButton,
+ SearchSortingGroupButton,
"SearchSortingGroupButton",
SwitchGrouping,
GetSortGroupSprite(_sortGrouping, false),
@@ -398,9 +447,9 @@ public static class RoleGuidePatches
sortingOrderButton,
searchComponentContainer.transform);
sortingFilterButton.gameObject.SetActive(true);
- var searchSortingFilterButton = sortingFilterButton.GetComponent();
+ SearchSortingFilterButton = sortingFilterButton.GetComponent();
SetupSearchButton(
- searchSortingFilterButton,
+ SearchSortingFilterButton,
"SearchSortingFilterButton",
SwitchFilter,
GetSortFilterSprite(_sortFilter, false),
@@ -419,29 +468,37 @@ public static class RoleGuidePatches
instance.settingsTabs[RolesTabIndex],
instance.settingsTabs[RolesTabIndex].transform.parent);
modifiersTab.name = "ModifiersPanel";
+ _modifierNoResultsMessage = Object.Instantiate(
+ instance.rolesEnabledMessage,
+ modifiersTab.transform,
+ true);
+ _modifierNoResultsMessage.name = "NoResultsFound";
+ _modifierNoResultsMessage.SetActive(false);
+ SetNoResultsText(_modifierNoResultsMessage);
modifiersTab.transform.FindChild("MaskArea")?.transform.localPosition =
new Vector3(-0.0184f, 0.15f, -0.1f);
modifiersTab.transform.GetAllChildren()
.First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5);
_modifiersScroller = modifiersTab.GetComponent();
instance.settingsTabs.Add(modifiersTab);
+ InitializeSettingsPanel(instance);
var wikiTab = Object.Instantiate(
instance.settingsTabs[RolesTabIndex],
instance.settingsTabs[RolesTabIndex].transform.parent);
- wikiTab.name = "AdvancedWikiPanels";
+ wikiTab.name = "WikiPanels";
var wikiMask = wikiTab.transform.FindChild("MaskArea");
wikiMask.transform.localPosition = new Vector3(-0.0184f, 0.425f, -0.1f);
wikiMask.transform.localScale = new Vector3(1, 1.3f, 1);
wikiTab.transform.GetAllChildren()
.First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5);
- _advancedInfoTabScroller = wikiTab.GetComponent();
+ _wikiScroller = wikiTab.GetComponent();
var wikiScrollHitbox = wikiTab.transform.FindChild("Hitbox");
- _advancedInfoTabScroller.ScrollbarYBounds.max = 1.2f;
+ _wikiScroller.ScrollbarYBounds.max = 1.2f;
wikiScrollHitbox.GetComponent().size = new Vector2(5.75f, 2.4f);
wikiScrollHitbox.transform.localPosition += new Vector3(0, 0.3f);
- _advancedInfoTabScroller.ScrollbarY.trackGraphic.transform.localPosition = new Vector3(2.8364f, 0.3649f, -0.2f);
- _advancedInfoTabScroller.ScrollbarY.trackGraphic.transform.localScale = new Vector3(0.0234f, 2.068f, 0.5598f);
+ _wikiScroller.ScrollbarY.trackGraphic.transform.localPosition = new Vector3(2.8364f, 0.3649f, -0.2f);
+ _wikiScroller.ScrollbarY.trackGraphic.transform.localScale = new Vector3(0.0234f, 2.068f, 0.5598f);
var spinnerParent = instance.settingsTabs[RolesTabIndex].transform.parent;
var spinnerPos = spinnerParent.InverseTransformPoint(instance.MatchInfoRoleMaskArea.transform.position);
@@ -453,8 +510,9 @@ public static class RoleGuidePatches
sortingOrderButton,
sortingOrderButton.transform.parent);
searchIconButton.gameObject.SetActive(true);
+ SearchIconButton = searchIconButton.GetComponent();
SetupSearchButton(
- searchIconButton.GetComponent(),
+ SearchIconButton,
"SearchIconButton",
ClearSearchText,
MiraAssets.SearchIconIdleSprite,
@@ -603,9 +661,9 @@ public static class RoleGuidePatches
instance.TabButtons[2].transform.parent);
SetupTabButton(
modifiersButton,
- MiraAssets.WikiRolesButtonIdleSprite,
- MiraAssets.WikiRolesButtonOpenSprite,
- MiraAssets.WikiRolesButtonHoverSprite);
+ MiraAssets.WikiModifiersButtonIdleSprite,
+ MiraAssets.WikiModifiersButtonOpenSprite,
+ MiraAssets.WikiModifiersButtonHoverSprite);
modifiersButton.OnClick = new Button.ButtonClickedEvent();
modifiersButton.OnClick.AddListener((Action)(() => OpenModifiersTab()));
instance.TabButtons.Add(modifiersButton);
@@ -619,17 +677,17 @@ public static class RoleGuidePatches
orderSortingTmp.horizontalAlignment = HorizontalAlignmentOptions.Center;
orderSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0);
- var groupSortingTmp = Object.Instantiate(
+ GroupSortingTmp = Object.Instantiate(
orderSortingTmp,
sortingGroupButton.transform);
- orderSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.GroupingLabel");
- groupSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0);
+ GroupSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.GroupingLabel");
+ GroupSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0);
- var filterSortingTmp = Object.Instantiate(
+ FilterSortingTmp = Object.Instantiate(
orderSortingTmp,
sortingFilterButton.transform);
- filterSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.FilterLabel");
- filterSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0);
+ 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);
@@ -640,19 +698,42 @@ public static class RoleGuidePatches
private static void BuildEntries()
{
- foreach (var modifier in ModifierManager.Modifiers.OfType())
+ if (ModifierEntries.Count == 0)
{
- ModifierEntries.Add(new DetailedPanel(
- modifier.ModifierName,
- modifier.ModifierCategoryTitle,
- modifier.ParentMod.MiraPlugin.GetAbbreviatedModName(),
- null,
- modifier));
+ foreach (var modifier in ModifierManager.Modifiers.OfType())
+ {
+ ModifierEntries.Add(new DetailedPanel(
+ modifier.ModifierName,
+ modifier.ModifierCategoryTitle,
+ modifier.ParentMod.MiraPlugin.GetAbbreviatedModName(),
+ null,
+ modifier));
+ }
}
+ var activeMode = CustomGameModeManager.ActiveMode;
+ var activeModeType = activeMode?.GetType();
+ if (_roleEntriesInitialized && _roleEntriesMode == activeModeType)
+ {
+ return;
+ }
+
+ foreach (var entry in RoleEntries)
+ {
+ if (entry.Panel)
+ {
+ entry.Panel!.gameObject.Destroy();
+ }
+ }
+
+ RoleEntries.Clear();
+ _rolePanelsCleaned = false;
foreach (var roleBehaviour in CustomRoleManager.AllStoredRoleBehaviours.Where(x =>
x.Role is not RoleTypes.Crewmate and not RoleTypes.Impostor and
- not RoleTypes.CrewmateGhost and not RoleTypes.ImpostorGhost))
+ not RoleTypes.CrewmateGhost and not RoleTypes.ImpostorGhost &&
+ CustomRoleUtils.CanSpawnOnCurrentMode(x))
+ .GroupBy(x => x.Role)
+ .Select(group => group.First()))
{
var modId = roleBehaviour is ICustomRole custom
? custom.ParentMod.MiraPlugin.GetAbbreviatedModName()
@@ -664,6 +745,9 @@ public static class RoleGuidePatches
roleBehaviour,
null));
}
+
+ _roleEntriesMode = activeModeType;
+ _roleEntriesInitialized = true;
}
private static void SetupTabButton(MatchInfoGuideTabButton button, Sprite inactive, Sprite selected, Sprite hover)
@@ -703,12 +787,57 @@ public static class RoleGuidePatches
private static void OpenTab(int tabIndex, float delay = 0f)
{
- _advancedInfoTabScroller?.gameObject.SetActive(false);
+ _wikiScroller?.gameObject.SetActive(false);
+ _activeGuideTabIndex = tabIndex;
MatchInfoGuide.Instance.SetActiveTab(tabIndex);
+ if (tabIndex == RolesTabIndex)
+ {
+ BuildEntries();
+ if (!_rolePanelsCleaned)
+ {
+ _rolePanelsCleaned = RemoveUntrackedRolePanels();
+ }
+ }
+
HidePanels(tabIndex == ModifiersTabIndex ? ModifierEntries : RoleEntries);
StartPopulate(delay);
}
+ private static bool RemoveUntrackedRolePanels()
+ {
+ if (_rolesScroller?.Inner == null)
+ {
+ return false;
+ }
+
+ var trackedPanels = RoleEntries
+ .Where(entry => entry.Panel)
+ .Select(entry => entry.Panel!)
+ .ToHashSet();
+ foreach (var panel in _rolesScroller.Inner.GetComponentsInChildren(true))
+ {
+ if (!trackedPanels.Contains(panel))
+ {
+ var panelName = panel.roleName ? panel.roleName.text : panel.name;
+ UntrackedRolePanelDiagnostics.Add(panelName);
+ panel.gameObject.Destroy();
+ }
+ }
+
+ if (UntrackedRolePanelDiagnostics.Count > 0)
+ {
+ var panelNames = string.Join(", ", UntrackedRolePanelDiagnostics.Take(10));
+ var additionalCount = UntrackedRolePanelDiagnostics.Count - 10;
+ Logger.Warning(
+ $"Removed or blocked {UntrackedRolePanelDiagnostics.Count} untracked Match Info role panel(s) in " +
+ $"{CustomGameModeManager.ActiveMode?.GetType().Name ?? "vanilla"} mode: {panelNames}" +
+ (additionalCount > 0 ? $", and {additionalCount} more" : string.Empty));
+ UntrackedRolePanelDiagnostics.Clear();
+ }
+
+ return true;
+ }
+
private static void HidePanels(List entries)
{
foreach (var entry in entries)
@@ -730,6 +859,26 @@ public static class RoleGuidePatches
OpenTab(RolesTabIndex);
}
+ private static void SetNoResultsText(GameObject message)
+ {
+ var text = message.GetComponentInChildren(true);
+ if (!text)
+ {
+ return;
+ }
+
+ text.GetComponent()?.Destroy();
+ text.text = MiraLocaleManager.Get("MiraApi.Wiki.NoResultsFound", "No Results Found");
+ }
+
+ private static void SetNoResultsVisible(GameObject? message, bool visible)
+ {
+ if (message != null && message)
+ {
+ message.SetActive(visible);
+ }
+ }
+
private sealed class DetailedPanel(string title, string category, string modId, RoleBehaviour? role, GameModifier? modifier)
{
public MatchInfoRolePanel? Panel { get; set; }
@@ -741,6 +890,8 @@ public static class RoleGuidePatches
public int Amount { get; set; }
public int Chance { get; set; }
public float Likelihood { get; set; }
+ public bool CanShow { get; set; }
+ public bool CountsReady { get; set; }
public bool Visible { get; set; }
public string GetSortKey(SortingGroups grouping)
@@ -774,8 +925,8 @@ public static class RoleGuidePatches
if (_loadingSpinner != null)
{
_loadingSpinner.gameObject.SetActive(
- _loading && MatchInfoGuide.Instance.activeTabIndex is RolesTabIndex or ModifiersTabIndex &&
- !(_advancedInfoTabScroller && _advancedInfoTabScroller.gameObject.activeSelf));
+ _loading && ActiveGuideTabIndex is RolesTabIndex or ModifiersTabIndex &&
+ !(_wikiScroller && _wikiScroller.gameObject.activeSelf));
}
}
@@ -819,8 +970,16 @@ public static class RoleGuidePatches
});
_loading = false;
UpdateSpinner();
- yield return CoForEachBudgeted(entries, EnsurePanel);
- yield return CoForEachBudgeted(modifiers ? RoleEntries : ModifierEntries, PreloadPanel);
+ if (!modifiers)
+ {
+ RemoveUntrackedRolePanels();
+ yield return null;
+ if (_guide)
+ {
+ RemoveUntrackedRolePanels();
+ }
+ }
+
_populate = null;
}
@@ -849,6 +1008,7 @@ public static class RoleGuidePatches
{
if (entry.Panel)
{
+ SetupPanelButton(entry.Panel!, () => OpenAdvancedWiki(entry));
return;
}
@@ -867,18 +1027,29 @@ public static class RoleGuidePatches
if (entry.Modifier != null)
{
panel.SetModifierPanel(entry.Modifier, entry.Amount, entry.Chance);
- SetupPanelButton(panel, () => DisplayAdvancedWiki(MatchInfoGuide.Instance, entry.Modifier));
}
else
{
panel.SetRolePanel(entry.Role!, entry.Amount, entry.Chance);
- SetupPanelButton(panel, () => DisplayAdvancedWiki(MatchInfoGuide.Instance, entry.Role!));
}
+ SetupPanelButton(panel, () => OpenAdvancedWiki(entry));
panel.gameObject.SetActive(false);
entry.Panel = panel;
}
+ private static void OpenAdvancedWiki(DetailedPanel entry)
+ {
+ if (entry.Modifier != null)
+ {
+ DisplayAdvancedWiki(MatchInfoGuide.Instance, entry.Modifier);
+ }
+ else if (entry.Role != null)
+ {
+ DisplayAdvancedWiki(MatchInfoGuide.Instance, entry.Role);
+ }
+ }
+
private static void PreloadPanel(DetailedPanel entry)
{
if (!entry.Panel)
@@ -923,14 +1094,18 @@ public static class RoleGuidePatches
entry.Amount = amount;
entry.Chance = chance;
entry.Likelihood = amount * chance;
- entry.Visible = ShouldShowPanel(forceShow, amount == 0 || chance == 0, isHidden);
+ entry.CanShow = ShouldShowPanel(forceShow, amount == 0 || chance == 0, isHidden);
+ entry.CountsReady = true;
+ entry.Visible = entry.CanShow;
}
private static List UpdateEntries(List entries, bool modifiers)
{
+ var searchText = GetSearchText();
foreach (var entry in entries)
{
ComputeCounts(entry);
+ entry.Visible = entry.CanShow && MatchesSearch(entry, searchText);
if (entry.Panel)
{
SetPanelCount(entry.Panel!, entry.Amount, entry.Chance, entry.ModId);
@@ -941,34 +1116,51 @@ public static class RoleGuidePatches
}
}
- var searchText = GetSearchText();
- var sorted = entries.Where(p => p.Visible)
- .OrderByDescending(p => p.Title.Equals(searchText, StringComparison.OrdinalIgnoreCase))
- .ThenByDescending(p =>
- p.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
- p.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase));
- var shown = (_sortMethod is SortingMethod.Alphabetical
- ? sorted.ThenBy(p => p.GetSortKey(_sortGrouping))
- : sorted.ThenByDescending(p => p.GetSortKey(_sortGrouping)))
- .ToList();
+ var shown = SortEntries(entries, searchText);
var instance = MatchInfoGuide.Instance;
if (modifiers)
{
_modifiersScroller.SetYBoundsMax(GetGridScrollBounds(shown.Count));
- /*instance.rolesEnabledMessage.SetActive(shown.Count == 0);*/
+ SetNoResultsVisible(_modifierNoResultsMessage, shown.Count == 0);
}
else
{
instance.MatchInfoRoleScroller.SetYBoundsMax(GetGridScrollBounds(shown.Count));
- instance.rolesEnabledMessage.SetActive(shown.Count == 0);
+ SetNoResultsVisible(instance.rolesEnabledMessage, shown.Count == 0);
}
return shown;
}
+ private static List SortEntries(List entries, string searchText)
+ {
+ var sorted = entries.Where(p => p.Visible)
+ .OrderByDescending(p => p.Title.Equals(searchText, StringComparison.OrdinalIgnoreCase))
+ .ThenByDescending(p =>
+ p.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
+ p.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase));
+ return (_sortMethod is SortingMethod.Alphabetical
+ ? sorted.ThenBy(p => p.GetSortKey(_sortGrouping))
+ : sorted.ThenByDescending(p => p.GetSortKey(_sortGrouping)))
+ .ToList();
+ }
+
+ private static bool MatchesSearch(DetailedPanel entry, string searchText)
+ {
+ return searchText.Length == 0 ||
+ entry.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
+ entry.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase);
+ }
+
private static void RefreshActiveTab(bool scrollToTop)
{
+ if (IsSettingsTabActive)
+ {
+ RefreshSettingsTab(scrollToTop);
+ return;
+ }
+
StartPopulate();
if (scrollToTop)
{
@@ -997,9 +1189,85 @@ public static class RoleGuidePatches
}
_appliedSearch = text;
+ if (IsSettingsTabActive)
+ {
+ RefreshSettingsTab(true);
+ yield break;
+ }
+
+ if (TryRefreshSearch(text))
+ {
+ yield break;
+ }
+
RefreshActiveTab(true);
}
+ private static bool TryRefreshSearch(string searchText)
+ {
+ var guide = MatchInfoGuide.Instance;
+ if (!_guide || guide != _guide)
+ {
+ return false;
+ }
+
+ var activeTabIndex = ActiveGuideTabIndex;
+ var entries = activeTabIndex switch
+ {
+ RolesTabIndex => RoleEntries,
+ ModifiersTabIndex => ModifierEntries,
+ _ => null,
+ };
+ if (entries == null || _populate != null || !ArePanelsReady(entries))
+ {
+ return false;
+ }
+
+ foreach (var entry in entries)
+ {
+ entry.Visible = entry.CanShow && MatchesSearch(entry, searchText);
+ if (entry.Panel!.gameObject.activeSelf != entry.Visible)
+ {
+ entry.Panel.gameObject.SetActive(entry.Visible);
+ }
+ }
+
+ var shown = SortEntries(entries, searchText);
+ foreach (var entry in shown)
+ {
+ entry.Panel!.transform.SetAsLastSibling();
+ }
+
+ var scroller = activeTabIndex == ModifiersTabIndex
+ ? _modifiersScroller
+ : guide.MatchInfoRoleScroller;
+ scroller.SetYBoundsMax(GetGridScrollBounds(shown.Count));
+ var noResultsMessage = activeTabIndex == ModifiersTabIndex
+ ? _modifierNoResultsMessage
+ : guide.rolesEnabledMessage;
+ SetNoResultsVisible(noResultsMessage, shown.Count == 0);
+ scroller.ScrollToTop();
+ return true;
+ }
+
+ private static bool ArePanelsReady(List entries)
+ {
+ if (entries.Count == 0)
+ {
+ return false;
+ }
+
+ foreach (var entry in entries)
+ {
+ if (!entry.CountsReady || !entry.Panel)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
private static void ResetSearchBox()
{
if (_searchDebounce != null)
@@ -1035,6 +1303,12 @@ public static class RoleGuidePatches
public static void SwitchFilter()
{
+ if (IsSettingsTabActive)
+ {
+ ChangeSettingsPage(-1);
+ return;
+ }
+
var stepUp = (SortingFilter)((int)_sortFilter + 1);
_sortFilter = Enum.IsDefined(stepUp) ? stepUp : SortingFilter.EnabledOnly;
@@ -1075,6 +1349,11 @@ public static class RoleGuidePatches
public static void SwitchMethod()
{
+ if (IsSettingsTabActive)
+ {
+ return;
+ }
+
var stepUp = (SortingMethod)((int)_sortMethod + 1);
_sortMethod = Enum.IsDefined(stepUp) ? stepUp : SortingMethod.Alphabetical;
@@ -1103,6 +1382,12 @@ public static class RoleGuidePatches
public static void SwitchGrouping()
{
+ if (IsSettingsTabActive)
+ {
+ ChangeSettingsPage(1);
+ return;
+ }
+
var stepUp = (SortingGroups)((int)_sortGrouping + 1);
if (Enum.IsDefined(stepUp))
{
@@ -1175,10 +1460,22 @@ public static class RoleGuidePatches
private static void SetupPanelButton(MatchInfoRolePanel panel, Action onClick)
{
- var collider = panel.roleIcon.gameObject.AddComponent();
+ var collider = panel.roleIcon.gameObject.GetComponent();
+ if (!collider)
+ {
+ collider = panel.roleIcon.gameObject.AddComponent();
+ }
+
collider.size = new Vector2(0.13f, 0.13f);
collider.offset = new Vector2(0, 0);
- var passiveButton = panel.roleIcon.gameObject.AddComponent();
+ collider.enabled = true;
+ var passiveButton = panel.roleIcon.gameObject.GetComponent();
+ if (!passiveButton)
+ {
+ passiveButton = panel.roleIcon.gameObject.AddComponent();
+ }
+
+ passiveButton.enabled = true;
passiveButton.ClickSound = HudManager.Instance.MapButton.ClickSound;
passiveButton.OnMouseOver = new UnityEvent();
passiveButton.OnMouseOver.AddListener(
@@ -1234,6 +1531,9 @@ public static class RoleGuidePatches
[HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.SetActiveTab))]
private static void SetActiveTab(int tabIndex)
{
+ _activeGuideTabIndex = tabIndex;
+ HideNoResultsMessages();
+
if (_titleText && _titleIcon)
{
switch (tabIndex)
@@ -1242,9 +1542,9 @@ public static class RoleGuidePatches
_titleText.text = MiraLocaleManager.Get("MiraApi.Wiki.PlayersTab");
_titleIcon.sprite = MiraAssets.IconPlayerList.LoadAsset();
break;
- case 1:
- _titleText.text = MiraLocaleManager.Get("MiraApi.Wiki.SettingsTab");
- _titleIcon.sprite = MiraAssets.IconSettings.LoadAsset();
+ case SettingsTabIndex:
+ _titleText.text = GetSettingsTitle();
+ _titleIcon.sprite = GetSettingsIcon();
break;
case RolesTabIndex:
_titleText.text = MiraLocaleManager.Get("Roles");
@@ -1263,11 +1563,32 @@ public static class RoleGuidePatches
_titleIcon.SetSizeLimit(0.55f);
}
- _advancedInfoTabScroller?.gameObject.SetActive(false);
+ _wikiScroller?.gameObject.SetActive(false);
searchComponentContainer?.SetActive(true);
+ if (tabIndex == SettingsTabIndex)
+ {
+ EnterSettingsTab();
+ }
+ else
+ {
+ LeaveSettingsTab();
+ }
+
UpdateSpinner();
}
+ private static void HideNoResultsMessages()
+ {
+ var guide = _guide;
+ if (guide != null && guide)
+ {
+ SetNoResultsVisible(guide.rolesEnabledMessage, false);
+ }
+
+ SetNoResultsVisible(_modifierNoResultsMessage, false);
+ SetNoResultsVisible(_settingsNoResults, false);
+ }
+
public static Sprite GetRandomModifierIcon()
{
var activeMods = PlayerControl.LocalPlayer.GetModifiers()
@@ -1307,14 +1628,15 @@ public static class RoleGuidePatches
return MiraAssets.IconNeutral.LoadAsset();
}
- private static bool TryPrepareAdvancedWiki(MatchInfoGuide instance)
+ private static bool TryPrepareWikiPage(MatchInfoGuide instance)
{
instance.settingsTabs[RolesTabIndex].SetActive(false);
_modifiersScroller?.gameObject.SetActive(false);
- if (_currentAdvancedTabObject)
+ LeaveSettingsTab();
+ if (_currentWikiPage)
{
- _currentAdvancedTabObject.SetActive(false);
- _currentAdvancedTabObject.Destroy();
+ _currentWikiPage.SetActive(false);
+ _currentWikiPage.Destroy();
}
if (searchComponentContainer)
@@ -1322,18 +1644,18 @@ public static class RoleGuidePatches
searchComponentContainer.SetActive(false);
}
- if (_advancedInfoTabScroller == null)
+ if (_wikiScroller == null)
{
Warning("Wiki tab is null.");
return false;
}
- _advancedInfoTabScroller.gameObject.SetActive(true);
+ _wikiScroller.gameObject.SetActive(true);
UpdateSpinner();
return true;
}
- private static IEnumerator CoDisplayAdvancedWikiLater(float delay, Action display)
+ private static IEnumerator CoDisplayWikiPageLater(float delay, Action display)
{
yield return new WaitForSeconds(delay);
if (_guide && MatchInfoGuide.Instance && MatchInfoGuide.Instance.MatchInfoParent.activeSelf)
@@ -1347,18 +1669,18 @@ public static class RoleGuidePatches
var remaining = _openedAt + instance.transitionOpen.duration - Time.time;
if (remaining > 0f)
{
- Coroutines.Start(CoDisplayAdvancedWikiLater(remaining, () => DisplayAdvancedWiki(instance, modifier)));
+ Coroutines.Start(CoDisplayWikiPageLater(remaining, () => DisplayAdvancedWiki(instance, modifier)));
return;
}
var name = modifier.ModifierName;
- Info($"Opening advanced tab for {name}.");
- if (!TryPrepareAdvancedWiki(instance))
+ Info($"Opening wiki page for {name}.");
+ if (!TryPrepareWikiPage(instance))
{
return;
}
- _currentAdvancedTabObject = modifier.GetAdvancedWiki(instance, _titleText, _titleIcon, _advancedInfoTabScroller);
+ _currentWikiPage = modifier.GetAdvancedWiki(instance, _titleText, _titleIcon, _wikiScroller);
}
public static void DisplayAdvancedWiki(MatchInfoGuide instance, RoleBehaviour role)
@@ -1366,19 +1688,19 @@ public static class RoleGuidePatches
var remaining = _openedAt + instance.transitionOpen.duration - Time.time;
if (remaining > 0f)
{
- Coroutines.Start(CoDisplayAdvancedWikiLater(remaining, () => DisplayAdvancedWiki(instance, role)));
+ Coroutines.Start(CoDisplayWikiPageLater(remaining, () => DisplayAdvancedWiki(instance, role)));
return;
}
- Info($"Opening advanced tab for {role.GetRoleName()}.");
- if (!TryPrepareAdvancedWiki(instance))
+ Info($"Opening wiki page for {role.GetRoleName()}.");
+ if (!TryPrepareWikiPage(instance))
{
return;
}
if (role is ICustomRole custom)
{
- _currentAdvancedTabObject = custom.GetAdvancedWiki(instance, _titleText, _titleIcon, _advancedInfoTabScroller);
+ _currentWikiPage = custom.GetAdvancedWiki(instance, _titleText, _titleIcon, _wikiScroller);
}
else
{
@@ -1394,11 +1716,11 @@ public static class RoleGuidePatches
}
}
- _currentAdvancedTabObject = Helpers.CreateAdvancedWikiPage(
+ _currentWikiPage = Helpers.CreateAdvancedWikiPage(
instance,
_titleText,
_titleIcon,
- _advancedInfoTabScroller,
+ _wikiScroller,
role.Role.ToString(),
titleTxt,
role.RoleIconColor,
@@ -1412,6 +1734,17 @@ public static class RoleGuidePatches
[HarmonyPatch(typeof(MatchInfoRolePanel), nameof(MatchInfoRolePanel.SetPanel))]
public static bool SetPanel(MatchInfoRolePanel __instance, RoleBehaviour role, int numPerGame, int chancePerGame)
{
+ var rolesInner = _rolesScroller ? _rolesScroller.Inner : null;
+ if (rolesInner &&
+ __instance.transform.IsChildOf(rolesInner) &&
+ !RoleEntries.Any(entry => entry.Panel == __instance))
+ {
+ UntrackedRolePanelDiagnostics.Add(
+ role is ICustomRole untrackedRole ? untrackedRole.RoleName : role.NiceName);
+ __instance.gameObject.Destroy();
+ return false;
+ }
+
__instance.roleCount.text = string.Format(
CultureInfo.InvariantCulture,
"{0} at {1}%",
@@ -1517,6 +1850,391 @@ public static class RoleGuidePatches
{
return customRole.RoleFactionTitle;
}
+
+ // Settings page state.
+ private static Scroller? _settingsScroller;
+ private static GameObject? _settingsNoResults;
+ private static GameObject? _settingsPageObject;
+ private static List _settingsPages = [];
+ private static int _settingsPage; // 0 = vanilla settings, 1..N = _settingsPages[page - 1]
+ private static bool _settingsSearching;
+
+ // Save the sort-button appearance while they are used for page navigation.
+ private static bool _sortButtonsRepurposed;
+ private static string _savedFilterLabel = string.Empty;
+ private static string _savedGroupLabel = string.Empty;
+ private static Vector3 _savedFilterIconScale = Vector3.one;
+ private static Vector3 _savedGroupIconScale = Vector3.one;
+ private static Vector3 _savedFilterLabelPosition = Vector3.zero;
+ private static Vector3 _savedGroupLabelPosition = Vector3.zero;
+
+ private static bool IsSettingsTabActive =>
+ _uiInitialized && MatchInfoGuide.Instance && ActiveGuideTabIndex == SettingsTabIndex;
+
+ private static void ResetSettingsState()
+ {
+ _settingsScroller = null;
+ _settingsNoResults = null;
+ _settingsPageObject = null;
+ _settingsPages = [];
+ _settingsPage = 0;
+ _settingsSearching = false;
+ _sortButtonsRepurposed = false;
+ }
+
+ private static void InitializeSettingsPanel(MatchInfoGuide instance)
+ {
+ var rolesTab = instance.settingsTabs[RolesTabIndex];
+ var settingsTab = Object.Instantiate(rolesTab, rolesTab.transform.parent);
+ settingsTab.name = "SettingsOptionsPanel";
+ settingsTab.transform.FindChild("MaskArea")?.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f);
+ settingsTab.transform.GetAllChildren()
+ .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5);
+ _settingsScroller = settingsTab.GetComponent();
+ settingsTab.SetActive(false);
+
+ _settingsNoResults = Object.Instantiate(
+ instance.rolesEnabledMessage,
+ settingsTab.transform,
+ true);
+ _settingsNoResults.name = "SettingsNoResultsFound";
+ _settingsNoResults.SetActive(false);
+ SetNoResultsText(_settingsNoResults);
+ }
+
+ private static void EnterSettingsTab()
+ {
+ if (!_uiInitialized)
+ {
+ return;
+ }
+
+ RefreshSettingsTab(true);
+ }
+
+ private static void LeaveSettingsTab()
+ {
+ if (_settingsScroller != null)
+ {
+ _settingsScroller.gameObject.SetActive(false);
+ }
+
+ if (_settingsNoResults)
+ {
+ _settingsNoResults!.SetActive(false);
+ }
+
+ RestoreSortButtons();
+ }
+
+ private static void ChangeSettingsPage(int delta)
+ {
+ var total = _settingsPages.Count + 1;
+ if (total <= 1)
+ {
+ return;
+ }
+
+ _settingsPage = (_settingsPage + delta + total) % total;
+ RefreshSettingsTab(true);
+ }
+
+ ///
+ /// Updates the Settings tab with the current option values.
+ ///
+ private static void RefreshSettingsTab(bool scrollToTop)
+ {
+ var guide = MatchInfoGuide.Instance;
+ if (!_uiInitialized || !guide)
+ {
+ return;
+ }
+
+ var vanillaTab = guide.settingsTabs[SettingsTabIndex];
+ vanillaTab.SetActive(false);
+ var vanillaEntries = GameOptionsMenuPatch.GetActiveGameSettingsRows(guide.GetBoolString);
+ if (vanillaEntries.Count == 0)
+ {
+ vanillaEntries = BuildVanillaSettingsEntries(guide);
+ }
+
+ _settingsPages = GuideOptionPages.BuildPages(guide.GetBoolString);
+ if (_settingsPage > _settingsPages.Count)
+ {
+ _settingsPage = 0;
+ }
+
+ var searchText = GetSearchText().Trim();
+ _settingsSearching = searchText.Length > 0;
+
+ if (_settingsSearching)
+ {
+ ShowSettingsEntries(GuideOptionPages.Search(_settingsPages, searchText), true, scrollToTop);
+ vanillaTab.SetActive(false);
+ RepurposeSortButtons(false);
+ }
+ else if (_settingsPage == 0)
+ {
+ ShowSettingsRows(vanillaEntries, scrollToTop);
+ RepurposeSortButtons(true);
+ }
+ else
+ {
+ var page = _settingsPages[_settingsPage - 1];
+ ShowSettingsEntries(
+ page.Entries,
+ page.Groups.Count > 1,
+ scrollToTop,
+ page.Columns,
+ page.Groups.Count > 1);
+ vanillaTab.SetActive(false);
+ RepurposeSortButtons(true);
+ }
+
+ UpdateSettingsTitle();
+ }
+
+ internal static void RefreshSettingsForGameMode()
+ {
+ if (IsSettingsTabActive)
+ {
+ RefreshSettingsTab(false);
+ }
+ }
+
+ private static List> BuildVanillaSettingsEntries(MatchInfoGuide guide)
+ {
+ foreach (var setting in guide.NormalModeSettings.ToArray())
+ {
+ if (setting)
+ {
+ setting.Destroy();
+ }
+ }
+
+ guide.NormalModeSettings.Clear();
+ guide.CreateNormalModeSettings();
+
+ var entries = new List>(guide.NormalModeSettings.Count);
+ foreach (var setting in guide.NormalModeSettings)
+ {
+ if (!setting)
+ {
+ continue;
+ }
+
+ var texts = setting.GetComponentsInChildren(true);
+ if (texts.Length < 2)
+ {
+ Warning($"Could not read vanilla settings row '{setting.name}'.");
+ continue;
+ }
+
+ entries.Add(new KeyValuePair(texts[0].text, texts[1].text));
+ }
+
+ return entries;
+ }
+
+ private static void ShowSettingsEntries(
+ List entries,
+ bool showGroup,
+ bool scrollToTop,
+ int columns = 2,
+ bool groupsAsColumns = false)
+ {
+ var orderedEntries = showGroup
+ ? entries.GroupBy(entry => entry.Group).SelectMany(group => group)
+ : entries;
+ var rows = orderedEntries
+ .Select(entry => new AdvancedSettingsRow(entry.Title, entry.Value, showGroup ? entry.Group : null))
+ .ToList();
+ ShowSettingsRows(rows, scrollToTop, columns, groupsAsColumns);
+ _settingsNoResults?.SetActive(_settingsSearching && entries.Count == 0);
+ }
+
+ private static void ShowSettingsRows(IReadOnlyList> rows, bool scrollToTop)
+ {
+ var advancedRows = rows
+ .Select(row => new AdvancedSettingsRow(row.Key, row.Value, null))
+ .ToList();
+ ShowSettingsRows(advancedRows, scrollToTop);
+ }
+
+ private static void ShowSettingsRows(
+ List rows,
+ bool scrollToTop,
+ int columns = 2,
+ bool groupsAsColumns = false)
+ {
+ var guide = MatchInfoGuide.Instance;
+ if (_settingsScroller == null || !guide || !guide.MatchInfoRolePanelPrefab)
+ {
+ // Use the vanilla settings tab if the custom panel is unavailable.
+ guide?.settingsTabs[SettingsTabIndex].SetActive(true);
+ return;
+ }
+
+ if (_settingsPageObject)
+ {
+ _settingsPageObject!.SetActive(false);
+ _settingsPageObject.Destroy();
+ _settingsPageObject = null;
+ }
+
+ _settingsScroller.gameObject.SetActive(true);
+ _settingsPageObject = Helpers.CreateAdvancedSettingsPage(
+ guide,
+ _settingsScroller,
+ rows,
+ columns,
+ groupsAsColumns);
+ _settingsNoResults?.SetActive(false);
+ if (scrollToTop)
+ {
+ _settingsScroller.ScrollToTop();
+ }
+ }
+
+ private static string GetSettingsTitle()
+ {
+ var baseTitle = MiraLocaleManager.Get("MiraApi.Wiki.SettingsTab");
+ if (!_uiInitialized)
+ {
+ return baseTitle;
+ }
+
+ if (_settingsSearching)
+ {
+ return $"{baseTitle}: {MiraLocaleManager.Get("MiraApi.Wiki.SearchResults", "Search Results")}";
+ }
+
+ if (_settingsPage == 0 || _settingsPages.Count == 0)
+ {
+ var vanillaTitle = $"{MiraLocaleManager.Get("MiraApi.Wiki.VanillaSettings", "Vanilla")} Settings";
+ return $"{vanillaTitle} ({_settingsPage + 1}/{_settingsPages.Count + 1})";
+ }
+
+ var counter = $"({_settingsPage + 1}/{_settingsPages.Count + 1})";
+ var pageTitle = _settingsPages[_settingsPage - 1].Title;
+ var titleAlreadyDescribesSettings = pageTitle.EndsWith("settings", StringComparison.OrdinalIgnoreCase) ||
+ pageTitle.EndsWith("options", StringComparison.OrdinalIgnoreCase);
+ return titleAlreadyDescribesSettings
+ ? $"{pageTitle} {counter}"
+ : $"{pageTitle} Settings {counter}";
+ }
+
+ private static Sprite GetSettingsIcon()
+ {
+ if (!_uiInitialized || _settingsSearching || _settingsPage == 0 || _settingsPages.Count == 0)
+ {
+ return MiraAssets.IconSettings;
+ }
+
+ return _settingsPages[_settingsPage - 1].Sprite;
+ }
+
+ private static void UpdateSettingsTitle()
+ {
+ if (_titleText && IsSettingsTabActive)
+ {
+ _titleText.text = GetSettingsTitle();
+ if (_titleIcon)
+ {
+ _titleIcon.sprite = GetSettingsIcon();
+
+ _titleIcon.SetSizeLimit(0.55f);
+ }
+ }
+ }
+
+ ///
+ /// Turns the Filter button into "previous page" and the Grouping button into "next page",
+ /// and hides the Order button (it only sorts roles/modifiers).
+ ///
+ /// False while searching, since search results are a single page.
+ private static void RepurposeSortButtons(bool paginationVisible)
+ {
+ if (!SearchSortingFilterButton || !SearchSortingGroupButton || !SearchSortingOrderButton)
+ {
+ return;
+ }
+
+ if (!_sortButtonsRepurposed)
+ {
+ _savedFilterLabel = FilterSortingTmp.text;
+ _savedGroupLabel = GroupSortingTmp.text;
+ _savedFilterIconScale = _searchSortingFilterIdle.transform.localScale;
+ _savedGroupIconScale = _searchSortingGroupIdle.transform.localScale;
+ _savedFilterLabelPosition = FilterSortingTmp.transform.localPosition;
+ _savedGroupLabelPosition = GroupSortingTmp.transform.localPosition;
+
+ SetPaginationSprites(_searchSortingFilterIdle, _searchSortingFilterHover, true);
+ SetPaginationSprites(_searchSortingGroupIdle, _searchSortingGroupHover, false);
+ FilterSortingTmp.transform.localPosition = new Vector3(
+ -1.25f,
+ 0f,
+ _savedFilterLabelPosition.z);
+ GroupSortingTmp.transform.localPosition = new Vector3(
+ 0f,
+ 0f,
+ _savedGroupLabelPosition.z);
+ FilterSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.PrevPage", "Prev");
+ GroupSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.NextPage", "Next");
+ _sortButtonsRepurposed = true;
+ }
+
+ SearchSortingOrderButton.gameObject.SetActive(false);
+ SearchSortingFilterButton.gameObject.SetActive(paginationVisible);
+ SearchSortingGroupButton.gameObject.SetActive(paginationVisible);
+ }
+
+ private static void SetPaginationSprites(SpriteRenderer idle, SpriteRenderer hover, bool isLeft)
+ {
+ var idleSprite = isLeft ? MiraAssets.PageLeftIdle.LoadAsset() : MiraAssets.PageRightIdle.LoadAsset();
+ var hoverSprite = isLeft ? MiraAssets.PageLeftHover.LoadAsset() : MiraAssets.PageRightHover.LoadAsset();
+
+ // Match the on-screen size of the sort icon being replaced, since the arrow art differs in size.
+ var currentHeight = idle.sprite ? idle.sprite.bounds.size.y * idle.transform.localScale.y : 0f;
+ var newHeight = idleSprite.bounds.size.y;
+ var scale = currentHeight > 0f && newHeight > 0f ? currentHeight / newHeight : 1f;
+
+ idle.sprite = idleSprite;
+ hover.sprite = hoverSprite;
+ idle.transform.localScale = hover.transform.localScale = new Vector3(scale, scale, 1f);
+ }
+
+ private static void RestoreSortButtons()
+ {
+ if (!_sortButtonsRepurposed)
+ {
+ return;
+ }
+
+ _sortButtonsRepurposed = false;
+ if (!SearchSortingFilterButton || !SearchSortingGroupButton || !SearchSortingOrderButton)
+ {
+ return;
+ }
+
+ _searchSortingFilterIdle.sprite = GetSortFilterSprite(_sortFilter, false);
+ _searchSortingFilterHover.sprite = GetSortFilterSprite(_sortFilter, true);
+ _searchSortingGroupIdle.sprite = GetSortGroupSprite(_sortGrouping, false);
+ _searchSortingGroupHover.sprite = GetSortGroupSprite(_sortGrouping, true);
+ _searchSortingFilterIdle.flipX = _searchSortingFilterHover.flipX = false;
+ _searchSortingGroupIdle.flipX = _searchSortingGroupHover.flipX = false;
+ _searchSortingFilterIdle.transform.localScale =
+ _searchSortingFilterHover.transform.localScale = _savedFilterIconScale;
+ _searchSortingGroupIdle.transform.localScale =
+ _searchSortingGroupHover.transform.localScale = _savedGroupIconScale;
+ FilterSortingTmp.text = _savedFilterLabel;
+ GroupSortingTmp.text = _savedGroupLabel;
+ FilterSortingTmp.transform.localPosition = _savedFilterLabelPosition;
+ GroupSortingTmp.transform.localPosition = _savedGroupLabelPosition;
+ SearchSortingOrderButton.gameObject.SetActive(true);
+ SearchSortingFilterButton.gameObject.SetActive(true);
+ SearchSortingGroupButton.gameObject.SetActive(true);
+ }
}
internal enum SortingGroups
@@ -1537,4 +2255,4 @@ internal enum SortingFilter
EnabledOnly,
DisabledOnly,
AllRoles,
-}
+}
\ No newline at end of file
diff --git a/MiraAPI/Resources/Locale/en_US.xml b/MiraAPI/Resources/Locale/en_US.xml
index 4ee0714..3cff5f4 100644
--- a/MiraAPI/Resources/Locale/en_US.xml
+++ b/MiraAPI/Resources/Locale/en_US.xml
@@ -8,6 +8,7 @@
Show Keybinds on Buttons
Button Scale Factor
Top-Right UI Scale Factor
+ Match Info Guide Scale Factor
Max FPS
Display Wiki on Bottom Row
Enable Sabotage Flashes
@@ -60,4 +61,9 @@ Outlive the timer or eliminate the crew.
Filter
Grouping
Order
+ Vanilla
+ Search Results
+ Previous
+ Next
+ No Results Found
diff --git a/MiraAPI/Resources/Wiki/PageLeftHover.png b/MiraAPI/Resources/Wiki/PageLeftHover.png
new file mode 100644
index 0000000..7fa3b15
Binary files /dev/null and b/MiraAPI/Resources/Wiki/PageLeftHover.png differ
diff --git a/MiraAPI/Resources/Wiki/PageLeftIdle.png b/MiraAPI/Resources/Wiki/PageLeftIdle.png
new file mode 100644
index 0000000..cbedc93
Binary files /dev/null and b/MiraAPI/Resources/Wiki/PageLeftIdle.png differ
diff --git a/MiraAPI/Resources/Wiki/PageRightHover.png b/MiraAPI/Resources/Wiki/PageRightHover.png
new file mode 100644
index 0000000..8ffb435
Binary files /dev/null and b/MiraAPI/Resources/Wiki/PageRightHover.png differ
diff --git a/MiraAPI/Resources/Wiki/PageRightIdle.png b/MiraAPI/Resources/Wiki/PageRightIdle.png
new file mode 100644
index 0000000..0cb09f6
Binary files /dev/null and b/MiraAPI/Resources/Wiki/PageRightIdle.png differ
diff --git a/MiraAPI/Resources/Wiki/SidebarModifiersButtonHover.png b/MiraAPI/Resources/Wiki/SidebarModifiersButtonHover.png
new file mode 100644
index 0000000..090abc5
Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarModifiersButtonHover.png differ
diff --git a/MiraAPI/Resources/Wiki/SidebarModifiersButtonIdle.png b/MiraAPI/Resources/Wiki/SidebarModifiersButtonIdle.png
new file mode 100644
index 0000000..2712441
Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarModifiersButtonIdle.png differ
diff --git a/MiraAPI/Resources/Wiki/SidebarModifiersButtonOpen.png b/MiraAPI/Resources/Wiki/SidebarModifiersButtonOpen.png
new file mode 100644
index 0000000..1127956
Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarModifiersButtonOpen.png differ
diff --git a/MiraAPI/Resources/Wiki/SidebarSettingsButtonHover.png b/MiraAPI/Resources/Wiki/SidebarSettingsButtonHover.png
index 7813904..b4e53a9 100644
Binary files a/MiraAPI/Resources/Wiki/SidebarSettingsButtonHover.png and b/MiraAPI/Resources/Wiki/SidebarSettingsButtonHover.png differ
diff --git a/MiraAPI/Resources/Wiki/SidebarSettingsButtonIdle.png b/MiraAPI/Resources/Wiki/SidebarSettingsButtonIdle.png
index fdf65df..2d040a6 100644
Binary files a/MiraAPI/Resources/Wiki/SidebarSettingsButtonIdle.png and b/MiraAPI/Resources/Wiki/SidebarSettingsButtonIdle.png differ
diff --git a/MiraAPI/Resources/Wiki/SidebarSettingsButtonOpen.png b/MiraAPI/Resources/Wiki/SidebarSettingsButtonOpen.png
index ec5bb14..e750bac 100644
Binary files a/MiraAPI/Resources/Wiki/SidebarSettingsButtonOpen.png and b/MiraAPI/Resources/Wiki/SidebarSettingsButtonOpen.png differ
diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs
index 0feb75c..705bc32 100644
--- a/MiraAPI/Utilities/Assets/MiraAssets.cs
+++ b/MiraAPI/Utilities/Assets/MiraAssets.cs
@@ -205,6 +205,21 @@ public static class MiraAssets
///
public static LoadableResourceAsset WikiRolesButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonOpen.png");
+ ///
+ /// Gets the sprite used for the modifiers button while hovering.
+ ///
+ public static LoadableResourceAsset WikiModifiersButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarModifiersButtonHover.png");
+
+ ///
+ /// Gets the sprite used for the modifiers button while hovering.
+ ///
+ public static LoadableResourceAsset WikiModifiersButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarModifiersButtonIdle.png");
+
+ ///
+ /// Gets the sprite used for the modifiers button while hovering.
+ ///
+ public static LoadableResourceAsset WikiModifiersButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarModifiersButtonOpen.png");
+
///
/// Gets the sprite used for the roles button while hovering.
///
@@ -335,5 +350,25 @@ public static class MiraAssets
///
public static LoadableResourceAsset IconTerminology { get; } = new("MiraAPI.Resources.Wiki.IconTerminology.png");
+ ///
+ /// Gets the Page switching sprite.
+ ///
+ public static LoadableResourceAsset PageLeftHover { get; } = new("MiraAPI.Resources.Wiki.PageLeftHover.png");
+
+ ///
+ /// Gets the Page switching sprite.
+ ///
+ public static LoadableResourceAsset PageLeftIdle { get; } = new("MiraAPI.Resources.Wiki.PageLeftIdle.png");
+
+ ///
+ /// Gets the Page switching sprite.
+ ///
+ public static LoadableResourceAsset PageRightHover { get; } = new("MiraAPI.Resources.Wiki.PageRightHover.png");
+
+ ///
+ /// Gets the Page switching sprite.
+ ///
+ public static LoadableResourceAsset PageRightIdle { get; } = new("MiraAPI.Resources.Wiki.PageRightIdle.png");
+
internal static LoadableResourceAsset BlankSprite { get; } = new("MiraAPI.Resources.BlankSprite.png");
}
diff --git a/MiraAPI/Utilities/GuideOptionsPatches.cs b/MiraAPI/Utilities/GuideOptionsPatches.cs
new file mode 100644
index 0000000..22703b5
--- /dev/null
+++ b/MiraAPI/Utilities/GuideOptionsPatches.cs
@@ -0,0 +1,344 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using MiraAPI.GameModes;
+using MiraAPI.GameOptions;
+using MiraAPI.GameOptions.OptionTypes;
+using MiraAPI.PluginLoading;
+using MiraAPI.Translation;
+using MiraAPI.Utilities.Assets;
+using Reactor.Utilities;
+using UnityEngine;
+
+namespace MiraAPI.Utilities;
+
+internal sealed record GuideOptionEntry(
+ string Title,
+ string Value,
+ AbstractOptionGroup Group,
+ string GroupName,
+ string ModAbbreviation,
+ IReadOnlyList ModSearchTerms);
+
+internal sealed class GuideOptionPage(
+ AbstractOptionGroup group,
+ string title,
+ int order,
+ int menuOrder,
+ uint groupPriority,
+ int columns,
+ string modAbbreviation,
+ IReadOnlyList modSearchTerms,
+ List entries)
+{
+ public AbstractOptionGroup Group { get; } = group;
+
+ public List Groups { get; } = [group];
+
+ public string Title { get; } = title;
+
+ public LoadableAsset Sprite { get; } = group.Sprite;
+
+ public int Order { get; set; } = order;
+
+ public int MenuOrder { get; set; } = menuOrder;
+
+ public uint GroupPriority { get; set; } = groupPriority;
+
+ public int Columns { get; } = columns;
+
+ public string ModAbbreviation { get; } = modAbbreviation;
+
+ public HashSet ModSearchTerms { get; } = new(modSearchTerms, StringComparer.OrdinalIgnoreCase);
+
+ public List Entries { get; } = entries;
+}
+
+internal static class GuideOptionPages
+{
+ private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100));
+
+ public static List BuildPages(Func formatBool)
+ {
+ var pages = new List();
+ var combinedPages = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var mode = CustomGameModeManager.ActiveMode;
+
+ var groups = MiraPluginManager.Instance.PluginsWithOptionsOrGameModes
+ .SelectMany(plugin => plugin.InternalOptionGroups
+ .Where(group => group.ShowInMatchInfoGuide && IsSettingsGroup(group, mode) && SafeGroupVisible(group))
+ .Select(group => (
+ Plugin: plugin,
+ Group: group,
+ ModAbbreviation: plugin.MiraPlugin.GetAbbreviatedModName())))
+ .OrderBy(item => GetMenuOrder(item.Group.ParentMenu))
+ .ThenBy(item => item.Group.GroupPriority)
+ .ThenBy(item => item.Group.GroupName, StringComparer.OrdinalIgnoreCase)
+ .ThenBy(item => item.ModAbbreviation, StringComparer.OrdinalIgnoreCase);
+
+ foreach (var item in groups)
+ {
+ var groupName = item.Group.GroupName.Translate();
+ var modSearchTerms = GetModSearchTerms(item.Plugin, item.Group);
+ var entries = new List();
+
+ foreach (var option in item.Group.Options)
+ {
+ if (!SafeOptionVisible(option))
+ {
+ continue;
+ }
+
+ var entry = TryCreateEntry(
+ option,
+ item.Group,
+ groupName,
+ item.ModAbbreviation,
+ modSearchTerms,
+ formatBool);
+ if (entry != null)
+ {
+ entries.Add(entry);
+ }
+ }
+
+ if (entries.Count > 0)
+ {
+ var pageKey = item.Group.MatchInfoGuidePageKey?.Trim();
+ var combinedKey = string.IsNullOrEmpty(pageKey) ? null : $"{item.Plugin.PluginId}\0{pageKey}";
+ if (combinedKey != null && combinedPages.TryGetValue(combinedKey, out var combinedPage))
+ {
+ combinedPage.Groups.Add(item.Group);
+ combinedPage.Entries.AddRange(entries);
+ combinedPage.ModSearchTerms.UnionWith(modSearchTerms);
+ combinedPage.Order = Math.Min(combinedPage.Order, item.Group.MatchInfoGuidePageOrder);
+ combinedPage.MenuOrder = Math.Min(
+ combinedPage.MenuOrder,
+ GetMenuOrder(item.Group.ParentMenu));
+ combinedPage.GroupPriority = Math.Min(
+ combinedPage.GroupPriority,
+ item.Group.GroupPriority);
+ }
+ else
+ {
+ var page = new GuideOptionPage(
+ item.Group,
+ item.Group.MatchInfoGuidePageTitle,
+ item.Group.MatchInfoGuidePageOrder,
+ GetMenuOrder(item.Group.ParentMenu),
+ item.Group.GroupPriority,
+ Mathf.Clamp(item.Group.MatchInfoGuidePageColumns, 1, 6),
+ item.ModAbbreviation,
+ modSearchTerms,
+ entries);
+ pages.Add(page);
+ if (combinedKey != null)
+ {
+ combinedPages.Add(combinedKey, page);
+ }
+ }
+ }
+ }
+
+ return pages
+ .OrderBy(page => page.MenuOrder)
+ .ThenBy(page => page.GroupPriority)
+ .ThenBy(page => page.Order)
+ .ThenBy(page => page.Group.GroupName, StringComparer.OrdinalIgnoreCase)
+ .ThenBy(page => page.ModAbbreviation, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ }
+
+ private static int GetMenuOrder(MenuCategory menu)
+ {
+ return menu switch
+ {
+ MenuCategory.Game => 0,
+ MenuCategory.CustomOne => 1,
+ MenuCategory.CustomTwo => 2,
+ _ => int.MaxValue,
+ };
+ }
+
+ public static List Search(IEnumerable pages, string query)
+ {
+ query = query.Trim();
+ if (query.Length == 0)
+ {
+ return [];
+ }
+
+ const StringComparison cmp = StringComparison.OrdinalIgnoreCase;
+ var pageList = pages.ToList();
+ var normalizedQuery = NormalizeSearchText(query);
+ var matchingModPages = normalizedQuery.Length < 3
+ ? Array.Empty()
+ : pageList
+ .Where(page => page.ModSearchTerms.Any(term =>
+ NormalizeSearchText(term).Contains(normalizedQuery, StringComparison.Ordinal)))
+ .ToArray();
+
+ if (matchingModPages.Length > 0)
+ {
+ return matchingModPages.SelectMany(page => page.Entries).ToList();
+ }
+
+ return pageList
+ .SelectMany(page => page.Entries
+ .Select(entry => (Entry: entry, Rank: Rank(entry, query, cmp)))
+ .Where(result => result.Rank < int.MaxValue)
+ .OrderBy(result => result.Rank)
+ .Select(result => result.Entry))
+ .ToList();
+ }
+
+ private static string[] GetModSearchTerms(MiraPluginInfo plugin, AbstractOptionGroup group)
+ {
+ var terms = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ plugin.PluginId,
+ plugin.PluginInfo.Metadata.Name,
+ plugin.MiraPlugin.OptionsTitleText,
+ plugin.MiraPlugin.ParentLocaleId,
+ plugin.MiraPlugin.GetAbbreviatedModName(),
+ };
+
+ AddNamespace(terms, plugin.MiraPlugin.GetType());
+ AddNamespace(terms, group.GetType());
+ foreach (var option in group.Options)
+ {
+ AddNamespace(terms, option.GetType());
+ }
+
+ return terms.Where(term => !string.IsNullOrWhiteSpace(term)).ToArray();
+ }
+
+ private static void AddNamespace(HashSet terms, Type type)
+ {
+ if (!string.IsNullOrWhiteSpace(type.Namespace))
+ {
+ terms.Add(type.Namespace);
+ }
+ }
+
+ private static string NormalizeSearchText(string value)
+ {
+ return new string(value.Where(char.IsLetterOrDigit).Select(char.ToLowerInvariant).ToArray());
+ }
+
+ private static int Rank(GuideOptionEntry entry, string query, StringComparison cmp)
+ {
+ if (entry.Title.Equals(query, cmp))
+ {
+ return 0;
+ }
+
+ if (entry.Title.Contains(query, cmp))
+ {
+ return 1;
+ }
+
+ if (entry.GroupName.Contains(query, cmp) || entry.ModAbbreviation.Contains(query, cmp))
+ {
+ return 2;
+ }
+
+ return entry.Value.Contains(query, cmp) ? 3 : int.MaxValue;
+ }
+
+ private static bool IsSettingsGroup(AbstractOptionGroup group, AbstractGameMode? mode)
+ {
+ if (group.ParentMenu is not (MenuCategory.Game or MenuCategory.CustomOne or MenuCategory.CustomTwo))
+ {
+ return false;
+ }
+
+ if (group.OptionableType == null)
+ {
+ return true;
+ }
+
+ return mode != null && typeof(AbstractGameMode).IsAssignableFrom(group.OptionableType) &&
+ mode.GetType().IsAssignableTo(group.OptionableType);
+ }
+
+ private static bool SafeGroupVisible(AbstractOptionGroup group)
+ {
+ try
+ {
+ return group.GroupVisible();
+ }
+ catch (Exception e)
+ {
+ Logger.Warning($"GroupVisible threw for {group.GetType().Name}: {e.Message}");
+ return false;
+ }
+ }
+
+ private static bool SafeOptionVisible(IModdedOption option)
+ {
+ try
+ {
+ return option.Visible();
+ }
+ catch (Exception e)
+ {
+ Logger.Warning($"Visible threw for option '{option.Title}': {e.Message}");
+ return false;
+ }
+ }
+
+ private static GuideOptionEntry? TryCreateEntry(
+ IModdedOption option,
+ AbstractOptionGroup group,
+ string groupName,
+ string modAbbreviation,
+ IReadOnlyList modSearchTerms,
+ Func formatBool)
+ {
+ try
+ {
+ var title = TranslationController.Instance.GetString(option.StringName);
+ var value = FormatValue(option, formatBool);
+ return new GuideOptionEntry(title, value, group, groupName, modAbbreviation, modSearchTerms);
+ }
+ catch (Exception e)
+ {
+ Logger.Warning($"Could not format option '{option.Title}' for the guide: {e.Message}");
+ return null;
+ }
+ }
+
+ private static string FormatValue(IModdedOption option, Func formatBool)
+ {
+ switch (option)
+ {
+ case ModdedToggleOption toggle:
+ return formatBool(toggle.Value);
+
+ case ModdedEnumOption enumOption:
+ return MiraLocaleManager.Get(
+ enumOption.Values[enumOption.Value],
+ enumOption.Values[enumOption.Value]);
+
+ case ModdedNumberOption number:
+ if (number is { NegativeWordValue: not "#", Value: -1 })
+ {
+ return number.NegativeWordValue;
+ }
+
+ if (number is { ZeroWordValue: not "#", Value: 0 })
+ {
+ return number.ZeroWordValue;
+ }
+
+ return TrailingZeroRegex.Replace(number.Data.GetValueString(number.Value), string.Empty);
+
+ default:
+ return option.Data.Type == global::OptionTypes.Checkbox
+ ? formatBool(Math.Abs(option.GetFloatData() - 1f) < 0.0001f)
+ : option.Data.GetValueString(option.GetFloatData());
+ }
+ }
+}
\ No newline at end of file
diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs
index 413fb59..570bb04 100644
--- a/MiraAPI/Utilities/Helpers.cs
+++ b/MiraAPI/Utilities/Helpers.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using HarmonyLib;
+using Il2CppInterop.Runtime;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.OptionTypes;
@@ -25,12 +26,16 @@ using Random = UnityEngine.Random;
namespace MiraAPI.Utilities;
+internal readonly record struct AdvancedSettingsRow(string Title, string Value, AbstractOptionGroup? Group);
+
///
/// A class that contains helper methods.
///
public static class Helpers
{
+ internal const float MatchInfoGuideBaseScale = 1.3f;
private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100));
+ private static NumberOption? _advancedSettingsRowPrefab;
///
/// Gets all of the options stored within the provided type.
@@ -843,7 +848,8 @@ public static class Helpers
grid.localScale = new Vector3(1.3f, 1.3f, 1);
}
- obj.transform.SetParent(parent.Inner.transform);
+ obj.transform.SetParent(parent.Inner.transform, false);
+ obj.transform.localScale = Vector3.one / MatchInfoGuideBaseScale;
obj.transform.localPosition = new Vector3(0f, 0.5f, 0f);
var scrollHeight = variableTextSizing
? desc.textBounds.size.y - 2.5f + maxTextSize + 0.475f
@@ -852,6 +858,444 @@ public static class Helpers
return obj;
}
+ ///
+ /// Caches a persistent clone of the vanilla settings row for use in the Match Info Guide.
+ ///
+ /// The vanilla settings row prefab.
+ internal static void CacheAdvancedSettingsRowPrefab(NumberOption? rowPrefab)
+ {
+ if (!rowPrefab)
+ {
+ foreach (var candidate in Resources.FindObjectsOfTypeAll(Il2CppType.Of()))
+ {
+ var menu = candidate.TryCast();
+ var settingsTab = menu ? menu!.GameSettingsTab : null;
+ if (settingsTab && settingsTab!.numberOptionOrigin)
+ {
+ rowPrefab = settingsTab.numberOptionOrigin;
+ break;
+ }
+ }
+ }
+
+ if (!rowPrefab)
+ {
+ foreach (var candidate in Resources.FindObjectsOfTypeAll(Il2CppType.Of()))
+ {
+ var option = candidate.TryCast();
+ if (!option)
+ {
+ continue;
+ }
+
+ if (option!.TitleText && option.ValueText && option.PlusBtn && option.MinusBtn)
+ {
+ rowPrefab = option;
+ break;
+ }
+ }
+ }
+
+ if (_advancedSettingsRowPrefab || !rowPrefab)
+ {
+ return;
+ }
+
+ var cachedPrefab = Object.Instantiate(rowPrefab!.gameObject);
+ cachedPrefab.name = "MiraAPIAdvancedSettingsRowPrefab";
+ cachedPrefab.transform.SetParent(null, false);
+ cachedPrefab.transform.localPosition = Vector3.zero;
+ cachedPrefab.transform.localRotation = Quaternion.identity;
+ cachedPrefab.SetActive(false);
+ Object.DontDestroyOnLoad(cachedPrefab);
+ _advancedSettingsRowPrefab = cachedPrefab.GetComponent();
+ }
+
+ ///
+ /// Creates a page of vanilla settings rows inside a roles-style scroller.
+ ///
+ /// The guide object.
+ /// The the page is parented to.
+ /// The rows to show: key is the label, value is the displayed value.
+ /// The that holds all the rows. Destroy it to remove the page.
+ public static GameObject CreateAdvancedSettingsPage(
+ MatchInfoGuide guide,
+ Scroller parent,
+ IReadOnlyList> settings)
+ {
+ return CreateAdvancedSettingsPage(
+ guide,
+ parent,
+ settings.Select(setting => new AdvancedSettingsRow(setting.Key, setting.Value, null)).ToList(),
+ 2,
+ false);
+ }
+
+ internal static GameObject CreateAdvancedSettingsPage(
+ MatchInfoGuide guide,
+ Scroller parent,
+ List settings,
+ int columns = 2,
+ bool groupsAsColumns = false)
+ {
+ const float contentTop = 0.95f;
+ const float horizontalOffset = -0.2f;
+ const float headerContentGap = 0.2f;
+ const int contentSortingOrder = 151;
+
+ columns = Mathf.Clamp(columns, 1, 6);
+ var inner = parent.Inner;
+ var layout = inner.GetComponent();
+ if (layout)
+ {
+ layout.enabled = false;
+ }
+
+ var columnGap = layout ? Mathf.Clamp(layout.spacing.x, 0.1f, 0.6f) : 0.3f;
+ const int baseColumnCount = 2;
+ var baseColumnWidth = layout ? Mathf.Clamp(layout.cellSize.x, 2f, 3.2f) : 2.7f;
+ var availableWidth = baseColumnWidth * baseColumnCount + columnGap * (baseColumnCount - 1);
+ var columnWidth = Mathf.Clamp(
+ (availableWidth - (columns - 1) * columnGap) / columns,
+ 1f,
+ 3.2f);
+ var layer = inner.gameObject.layer;
+ var page = new GameObject("AdvancedSettingsPage") { layer = layer };
+ page.transform.SetParent(inner, false);
+ page.transform.localPosition = Vector3.zero;
+ page.SetActive(false);
+
+ var gameSettingsMenu = GameSettingMenu.Instance;
+ CacheAdvancedSettingsRowPrefab(gameSettingsMenu ? gameSettingsMenu.GameSettingsTab.numberOptionOrigin : null);
+ var rowPrefab = _advancedSettingsRowPrefab;
+ if (!rowPrefab)
+ {
+ Error("Could not render MiraAPI settings rows because the vanilla NumberOption prefab was not cached.");
+ return page;
+ }
+
+ var spriteBounds = rowPrefab!.GetComponentsInChildren(true)
+ .Where(renderer =>
+ renderer.enabled &&
+ renderer.sprite &&
+ renderer.transform != rowPrefab.PlusBtn.transform &&
+ !renderer.transform.IsChildOf(rowPrefab.PlusBtn.transform) &&
+ renderer.transform != rowPrefab.MinusBtn.transform &&
+ !renderer.transform.IsChildOf(rowPrefab.MinusBtn.transform))
+ .Select(renderer => renderer.bounds)
+ .ToArray();
+ if (spriteBounds.Length == 0)
+ {
+ Error("Could not render MiraAPI settings rows because the vanilla NumberOption prefab has no sprites.");
+ return page;
+ }
+
+ var prefabBounds = spriteBounds.Skip(1).Aggregate(
+ spriteBounds[0],
+ (bounds, next) =>
+ {
+ bounds.Encapsulate(next);
+ return bounds;
+ });
+ var rowScale = (columnWidth / prefabBounds.size.x) * 1.22f;
+ var rowHeight = prefabBounds.size.y * rowScale;
+ var rowPitch = 0.45f * rowScale;
+ var groupedSettings = groupsAsColumns
+ ? settings
+ .Where(setting => setting.Group != null)
+ .GroupBy(setting => setting.Group!)
+ .ToList()
+ : [];
+ groupsAsColumns = groupedSettings.Count > 1;
+ if (groupsAsColumns)
+ {
+ rowScale = (columnWidth / prefabBounds.size.x) * (columns > 2 ? 1.15f : 1f);
+ rowHeight = prefabBounds.size.y * rowScale;
+ rowPitch = 0.45f * rowScale;
+ }
+
+ var groupColumns = new Dictionary();
+ var groupTopOffsets = new Dictionary();
+ var groupRows = new Dictionary();
+ if (groupsAsColumns)
+ {
+ var bandCount = Mathf.CeilToInt(groupedSettings.Count / (float)columns);
+ var bandHeights = new float[bandCount];
+ for (var i = 0; i < groupedSettings.Count; i++)
+ {
+ var group = groupedSettings[i].Key;
+ var band = i / columns;
+ groupColumns[group] = i % columns;
+ bandHeights[band] = Mathf.Max(
+ bandHeights[band],
+ rowPitch * (groupedSettings[i].Count() + 1) + headerContentGap + 0.25f);
+ }
+
+ var bandOffset = 0f;
+ for (var band = 0; band < bandCount; band++)
+ {
+ for (var i = band * columns; i < Mathf.Min((band + 1) * columns, groupedSettings.Count); i++)
+ {
+ groupTopOffsets[groupedSettings[i].Key] = bandOffset;
+ groupRows[groupedSettings[i].Key] = 0;
+ }
+
+ bandOffset += bandHeights[band];
+ }
+ }
+
+ var contentBottom = float.PositiveInfinity;
+ var previousGroup = (AbstractOptionGroup?)null;
+ var rowIndex = 0;
+ var column = 0;
+ var groupSpacing = 0f;
+
+ for (var i = 0; i < settings.Count; i++)
+ {
+ if (settings[i].Group != previousGroup && settings[i].Group is { } group)
+ {
+ if (groupsAsColumns)
+ {
+ column = groupColumns[group];
+ }
+ else
+ {
+ if (previousGroup != null)
+ {
+ groupSpacing += 0.2f;
+ }
+
+ if (column != 0)
+ {
+ rowIndex++;
+ column = 0;
+ }
+ }
+
+ var headerSource = group.Header;
+ if (!headerSource && gameSettingsMenu && gameSettingsMenu.GameSettingsTab)
+ {
+ headerSource = gameSettingsMenu.GameSettingsTab.categoryHeaderOrigin;
+ }
+
+ if (!headerSource)
+ {
+ foreach (var candidate in Resources.FindObjectsOfTypeAll(Il2CppType.Of()))
+ {
+ var candidateHeader = candidate.TryCast();
+ if (candidateHeader && candidateHeader!.Background && candidateHeader.Title)
+ {
+ headerSource = candidateHeader;
+ break;
+ }
+ }
+ }
+
+ if (headerSource)
+ {
+ const float headerScale = 0.46f;
+ var headerWidth = groupsAsColumns
+ ? columnWidth / headerScale
+ : (columns * columnWidth + (columns - 1) * columnGap) / headerScale;
+ var header = Object.Instantiate(headerSource!, page.transform, false);
+ header.transform.localScale = Vector3.one * headerScale;
+ if (groupsAsColumns)
+ {
+ var totalWidth = columns * columnWidth + (columns - 1) * columnGap;
+ var groupColumn = groupColumns[group];
+ var groupLeft = -totalWidth / 2f + horizontalOffset +
+ groupColumn * (columnWidth + columnGap);
+ header.transform.localPosition = new Vector3(
+ groupLeft + (columnWidth / 2f) - 0.25f,
+ contentTop - groupTopOffsets[group],
+ -1f);
+ }
+ else
+ {
+ header.transform.localPosition = new Vector3(
+ horizontalOffset - 0.5f,
+ contentTop - (rowIndex * rowPitch) - groupSpacing,
+ -1f);
+ }
+
+ header.SetHeader(MiraLocaleManager.GetOrCreateLocaleString(group.GroupName), 20);
+ if (header.Title)
+ {
+ var titleTranslator = header.Title.GetComponent();
+ if (titleTranslator)
+ {
+ Object.Destroy(titleTranslator);
+ }
+
+ header.Title.text = group.GroupName.Translate();
+ header.Title.color = Color.white;
+ header.Title.horizontalAlignment = HorizontalAlignmentOptions.Left;
+ var titleTransform = header.Title.rectTransform;
+ var titlePosition = titleTransform.localPosition;
+ titlePosition.x = 0.5f;
+ titleTransform.localPosition = titlePosition;
+ titleTransform.SetSizeWithCurrentAnchors(
+ RectTransform.Axis.Horizontal,
+ headerWidth - 0.3f);
+ }
+
+ if (header.Background)
+ {
+ header.Background.sprite = MiraAssets.CategoryHeader.LoadAsset();
+ header.Background.drawMode = SpriteDrawMode.Sliced;
+ header.Background.color = group.GroupColor;
+ header.Background.transform.localPosition = new Vector3(0.5f, -0.1833f, 0f);
+ header.Background.size = new Vector2(headerWidth, header.Background.size.y);
+ }
+
+ if (header.Divider)
+ {
+ header.Divider.color = group.GroupColor;
+ }
+
+ header.gameObject.SetActive(true);
+ foreach (var renderer in header.GetComponentsInChildren(true))
+ {
+ renderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
+ renderer.material.SetInt(PlayerMaterial.MaskLayer, 50);
+ renderer.sortingOrder = contentSortingOrder;
+ if (renderer.enabled && renderer.sprite)
+ {
+ contentBottom = Mathf.Min(
+ contentBottom,
+ inner.InverseTransformPoint(renderer.bounds.min).y);
+ }
+ }
+
+ foreach (var text in header.GetComponentsInChildren(true))
+ {
+ text.fontMaterial.SetFloat(ShaderID.StencilComp, 3f);
+ text.fontMaterial.SetFloat(ShaderID.Stencil, 50f);
+ text.renderer.sortingOrder = contentSortingOrder;
+ if (text.enabled)
+ {
+ text.ForceMeshUpdate();
+ contentBottom = Mathf.Min(
+ contentBottom,
+ inner.InverseTransformPoint(text.bounds.min).y);
+ }
+ }
+
+ if (!groupsAsColumns)
+ {
+ rowIndex++;
+ }
+ }
+
+ previousGroup = group;
+ }
+
+ var originX = -((columns * columnWidth) + ((columns - 1) * columnGap)) / 2f +
+ (column * (columnWidth + columnGap)) +
+ horizontalOffset;
+ var centerY = groupsAsColumns && settings[i].Group is { } rowGroup
+ ? contentTop - groupTopOffsets[rowGroup] -
+ (rowPitch * (groupRows[rowGroup] + 1)) - headerContentGap - (rowHeight / 2f)
+ : contentTop - (rowHeight / 2f) - (rowIndex * rowPitch) - groupSpacing;
+ var row = Object.Instantiate(rowPrefab, page.transform);
+ row.enabled = false;
+ row.PlusBtn.gameObject.SetActive(false);
+ row.MinusBtn.gameObject.SetActive(false);
+
+ foreach (var button in row.GetComponentsInChildren(true))
+ {
+ button.enabled = false;
+ }
+
+ foreach (var collider in row.GetComponentsInChildren(true))
+ {
+ collider.enabled = false;
+ }
+
+ row.transform.localScale = rowPrefab.transform.localScale * rowScale;
+ row.transform.localPosition = new Vector3(
+ originX + (columnWidth / 2f) - (prefabBounds.center.x * rowScale),
+ centerY - (prefabBounds.center.y * rowScale),
+ 0f);
+ row.TitleText.text = settings[i].Title;
+ row.ValueText.text = settings[i].Value;
+
+ foreach (var text in row.GetComponentsInChildren(true))
+ {
+ if (text.TryGetComponent(out var translator))
+ {
+ translator.enabled = false;
+ Object.Destroy(translator);
+ }
+
+ text.fontMaterial.SetFloat(ShaderID.StencilComp, 3f);
+ text.fontMaterial.SetFloat(ShaderID.Stencil, 50f);
+ text.renderer.sortingOrder = contentSortingOrder;
+ if (text.enabled)
+ {
+ text.ForceMeshUpdate();
+ }
+ }
+
+ foreach (var renderer in row.GetComponentsInChildren(true))
+ {
+ renderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
+ renderer.material.SetInt(PlayerMaterial.MaskLayer, 50);
+ renderer.sortingOrder = contentSortingOrder;
+ if (renderer.enabled &&
+ renderer.sprite &&
+ renderer.transform != row.PlusBtn.transform &&
+ !renderer.transform.IsChildOf(row.PlusBtn.transform) &&
+ renderer.transform != row.MinusBtn.transform &&
+ !renderer.transform.IsChildOf(row.MinusBtn.transform))
+ {
+ contentBottom = Mathf.Min(
+ contentBottom,
+ inner.InverseTransformPoint(renderer.bounds.min).y);
+ }
+ }
+
+ foreach (var text in row.GetComponentsInChildren(true))
+ {
+ if (text.enabled)
+ {
+ contentBottom = Mathf.Min(
+ contentBottom,
+ inner.InverseTransformPoint(text.bounds.min).y);
+ }
+ }
+
+ row.gameObject.SetActive(true);
+ if (groupsAsColumns && settings[i].Group is { } renderedGroup)
+ {
+ groupRows[renderedGroup]++;
+ }
+ else
+ {
+ column++;
+ if (column == columns)
+ {
+ column = 0;
+ rowIndex++;
+ }
+ }
+ }
+
+ var maskArea = parent.transform.FindChild("MaskArea")?.GetComponent();
+ var maskBottom = maskArea
+ ? inner.InverseTransformPoint(maskArea!.bounds.min).y
+ : -0.75f;
+ var contentOverflow = float.IsPositiveInfinity(contentBottom)
+ ? 0f
+ : maskBottom - contentBottom;
+ var scrollRange = contentOverflow > 0.05f
+ ? contentOverflow + 0.05f
+ : 0f;
+ parent.SetYBoundsMax(Mathf.Clamp(scrollRange, 0f, 999f));
+ page.SetActive(true);
+ return page;
+ }
+
private static void SetupAbilityPanel(MatchInfoRolePanel panel, AdvancedWikiAbilityDescription ability, bool variableTextSizing = false)
{
panel.roleCount.text = ability.AbilityType;
@@ -877,4 +1321,4 @@ public static class Helpers
panel.roleCount.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f);
panel.roleIcon.transform.localScale = new Vector3(3.5f, 3.5f, 1f);
}
-}
+}
\ No newline at end of file