diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 0ca73e0..cf108b3 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, - 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; + parent, + ModifierNameLocale, + $"{this.GetTmpIcon()} {ModifierName} ({ModifierCategoryTitle})".TrimStart(), + ModifierWikiDescription, + abilities); } /// 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/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..e3b134a 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -1,5 +1,7 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using AmongUs.GameOptions; @@ -11,6 +13,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; @@ -23,6 +26,13 @@ namespace MiraAPI.Patches; [HarmonyPatch] 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 = []; + public static PassiveButton SearchIconButton; private static SpriteRenderer _searchIconIdle; private static SpriteRenderer _searchIconHover; @@ -36,10 +46,25 @@ 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 _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; + 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; [HarmonyPostfix] [HarmonyPriority(Priority.First)] @@ -47,20 +72,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 +101,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] @@ -81,6 +109,106 @@ 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] + [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.Awake))] + private static void AwakePostfix(MatchInfoGuide __instance) + { + 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)) + { + yield return null; + } + + if (!instance || GameManager.Instance.TryCast() == null) + { + yield break; + } + + _guide = instance; + if (RoleEntries.Count == 0) + { + BuildEntries(); + } + + 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] @@ -88,6 +216,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; @@ -112,239 +245,64 @@ 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; if (regGame) { + if (!_uiInitialized) + { + Initialize(__instance); + } + + if (RoleEntries.Count == 0) + { + BuildEntries(); + } + if (__instance.NormalModeSettings.Count == 0) { - var sortingOrderButton = Object.Instantiate( - HudManager.Instance.SettingsButton, - __instance.TabButtons[2].transform.parent); - SearchSortingOrderButton = sortingOrderButton.GetComponent(); - 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(); - 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(); - 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); - __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); - __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); - _modifiersScroller = 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); - _advancedInfoTabScroller = wikiTab.GetComponent(); - - DisplayNormalRoleSettings(__instance, true); - - var searchIconButton = Object.Instantiate( - sortingOrderButton, - sortingOrderButton.transform.parent); - SearchIconButton = searchIconButton.GetComponent(); - 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, - __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; - 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( - (UnityAction)(() => - { - var text = searchBoxTmp.outputText.text; - if (text != string.Empty) - { - _searchIconIdle.sprite = MiraAssets.SearchClearIdleSprite; - _searchIconHover.sprite = MiraAssets.SearchClearHoverSprite; - } - else - { - _searchIconIdle.sprite = MiraAssets.SearchIconIdleSprite; - _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; - } - - 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); - - var playerButton = __instance.TabButtons[0]; - FixTabButton( - playerButton, - MiraAssets.WikiPlayersButtonIdleSprite, - MiraAssets.WikiPlayersButtonOpenSprite, - MiraAssets.WikiPlayersButtonHoverSprite); - - var settingButton = __instance.TabButtons[1]; - 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 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())); - __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) { @@ -354,19 +312,319 @@ public static class RoleGuidePatches } PlayerControl.LocalPlayer.NetTransform.Halt(); + HidePanels(RoleEntries); + HidePanels(ModifierEntries); __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]); - __instance.SetActiveTab(regGame ? 2 : 0); + _openedAt = Time.time; + 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); + if (regGame) + { + OpenTab(RolesTabIndex, __instance.transitionOpen.duration); + } + return false; } - public static void FixTabButton(MatchInfoGuideTabButton button, Sprite inactive, Sprite selected, Sprite hover) + private static void Initialize(MatchInfoGuide instance) + { + if (_uiInitialized) + { + return; + } + + if (_populate != null) + { + Coroutines.Stop(_populate); + _populate = null; + } + + _guide = instance; + _loading = false; + var sortingOrderButton = Object.Instantiate( + HudManager.Instance.SettingsButton, + instance.TabButtons[2].transform.parent); + sortingOrderButton.gameObject.SetActive(true); + 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); + sortingGroupButton.gameObject.SetActive(true); + SearchSortingGroupButton = sortingGroupButton.GetComponent(); + SetupSearchButton( + SearchSortingGroupButton, + "SearchSortingGroupButton", + SwitchGrouping, + MiraAssets.GroupNoneIdleSprite, + MiraAssets.GroupNoneHoverSprite, + new Vector3(1.7f, 0.765f, -0.1f), + out _searchSortingGroupIdle, + out _searchSortingGroupHover); + + var sortingFilterButton = Object.Instantiate( + sortingOrderButton, + sortingOrderButton.transform.parent); + sortingFilterButton.gameObject.SetActive(true); + 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.gameObject.SetActive(true); + 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.gameObject.SetActive(true); + _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; + } + + QueueSearchRefresh(); + })); + 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); + _uiInitialized = true; + } + + 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(); button.transform.GetChild(0).gameObject.SetActive(false); @@ -378,6 +636,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); @@ -385,7 +644,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(); @@ -400,128 +659,352 @@ public static class RoleGuidePatches button.transform.GetChild(2).gameObject.SetActive(false); } - public static void OpenModifiersTab() + private static void OpenTab(int tabIndex, float delay = 0f) { _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; + MatchInfoGuide.Instance.SetActiveTab(tabIndex); + HidePanels(tabIndex == ModifiersTabIndex ? ModifierEntries : RoleEntries); + StartPopulate(delay); } - public static void OpenRolesTab() + private static void HidePanels(List entries) { - _advancedInfoTabScroller?.gameObject.SetActive(false); - MatchInfoGuide.Instance.SetActiveTab(2); - if (_needsToggledRoleRefresh) + foreach (var entry in entries) { - ToggleRoleVisibility(); - } - - if (_needsRoleRefresh) - { - var text = string.Empty; - try + if (entry.Panel && entry.Panel!.gameObject.activeSelf) { - text = searchBoxTmp.outputText.text; + entry.Panel.gameObject.SetActive(false); } - 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 static void OpenModifiersTab() => OpenTab(ModifiersTabIndex); - public sealed class DetailedPanel(MatchInfoRolePanel panel, string title, string category, string modId) + public static void OpenRolesTab() => OpenTab(RolesTabIndex); + + public sealed class DetailedPanel(string title, string category, string modId, RoleBehaviour? role, GameModifier? modifier) { - public MatchInfoRolePanel Panel = panel; - public string Title = title; - public string Category = category; - public string ModId = modId; + 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 LikelyhoodOfRole { get; set; } + public float Likelihood { get; set; } + public bool Visible { 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; - - public static void ClearSearchText() + private static string GetSearchText() { - _searchIconIdle.sprite = MiraAssets.SearchIconIdleSprite; - _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; - try + 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 void UpdateSpinner() + { + if (_loadingSpinner != null) { - searchBoxTmp.outputText.SetText(string.Empty); - searchBoxTmp.SetText(string.Empty); + _loadingSpinner.gameObject.SetActive( + _loading && MatchInfoGuide.Instance.activeTabIndex is RolesTabIndex or ModifiersTabIndex && + !(_advancedInfoTabScroller && _advancedInfoTabScroller.gameObject.activeSelf)); } - catch + } + + private static float GetGridScrollBounds(int count) + { + return Mathf.Clamp(Mathf.Ceil(count / 2f) * 1.3f - 1.5f, 0f, 999f); + } + + private static void StartPopulate(float delay = 0f) + { + if (_populate != null) { - // ignored + Coroutines.Stop(_populate); } - var text = string.Empty; + _loading = true; + UpdateSpinner(); + _populate = Coroutines.Start(CoPopulate(delay)); + } - if (MatchInfoGuide.Instance.activeTabIndex != 3) + private static IEnumerator CoPopulate(float delay) + { + if (delay > 0f) { - SortAllRoles(text); - _rolesScroller.ScrollToTop(); - _needsModifierRefresh = true; + yield return new WaitForSeconds(delay); } else { - SortAllModifiers(text); - _modifiersScroller.ScrollToTop(); - _needsRoleRefresh = true; + yield return null; } + + if (!_guide) + { + yield break; + } + + var modifiers = IsModifiersTabActive; + var entries = modifiers ? ModifierEntries : RoleEntries; + var shown = UpdateEntries(entries, modifiers); + yield return CoForEachBudgeted(shown, entry => + { + EnsurePanel(entry); + entry.Panel!.transform.SetAsLastSibling(); + if (!entry.Panel.gameObject.activeSelf) + { + entry.Panel.gameObject.SetActive(true); + } + }); + _loading = false; + UpdateSpinner(); + yield return CoForEachBudgeted(entries, EnsurePanel); + yield return CoForEachBudgeted(modifiers ? RoleEntries : ModifierEntries, PreloadPanel); + _populate = null; + } + + private static IEnumerator CoForEachBudgeted(IEnumerable collection, Action action) + { + var fps = Application.targetFrameRate > 0 ? Application.targetFrameRate : 60; + var budget = Math.Max(1L, 1000L / (fps * 4)); + var timer = new Stopwatch(); + timer.Start(); + foreach (var item in collection) + { + action(item); + if (timer.ElapsedMilliseconds > budget) + { + timer.Restart(); + yield return null; + if (!_guide) + { + yield break; + } + } + } + } + + private static void EnsurePanel(DetailedPanel entry) + { + if (entry.Panel) + { + return; + } + + 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); + SetupPanelButton(panel, () => DisplayAdvancedWiki(MatchInfoGuide.Instance, entry.Modifier)); + } + else + { + panel.SetRolePanel(entry.Role!, entry.Amount, entry.Chance); + SetupPanelButton(panel, () => DisplayAdvancedWiki(MatchInfoGuide.Instance, entry.Role!)); + } + + panel.gameObject.SetActive(false); + entry.Panel = panel; + } + + private static void PreloadPanel(DetailedPanel entry) + { + if (!entry.Panel) + { + 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) + { + StartPopulate(); + if (scrollToTop) + { + (IsModifiersTabActive ? _modifiersScroller : _rolesScroller).ScrollToTop(); + } + } + + 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; + if (_searchIconIdle) + { + _searchIconIdle.sprite = MiraAssets.SearchIconIdleSprite; + } + + if (_searchIconHover) + { + _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; + } + + if (_searchBoxTmp && GetSearchText().Length > 0) + { + _suppressSearchRefresh = true; + _searchBoxTmp.Clear(); + _suppressSearchRefresh = false; + } + } + + public static void ClearSearchText() + { + ResetSearchBox(); + 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 +1020,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,387 +1073,50 @@ public static class RoleGuidePatches break; } - 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 DisplayNormalRoleSettings(MatchInfoGuide instance, bool reset) + private static void CopyTextLayout(TextMeshPro source, TextMeshPro target) { - var inner = instance.settingsTabs[2].GetComponent().Inner; - 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()); - - var hoverColor = new Color32(255, 255, 255, 150); - foreach (var modifier in ModifierManager.Modifiers.OfType()) - { - var panel = Object.Instantiate( - instance.MatchInfoRolePanelPrefab, - _modifiersScroller.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 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); - 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); })); - RolePanels.Add( - roleBehaviour, - new DetailedPanel( - panel, - roleBehaviour.GetRoleName(), - roleBehaviour.GetCategoryTitle(), - abbreviation)); - } - } - } - - _advancedInfoTabScroller?.gameObject.SetActive(false); - - if (MatchInfoGuide.Instance.activeTabIndex != 3) - { - ToggleRoleVisibility(); - } - else - { - ToggleModifierVisibility(); - } - - if (reset) - { - instance.CreatePlayerEntries(); - } + 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; } - - public static void ToggleModifierVisibility() + private static void SetupPanelButton(MatchInfoRolePanel panel, Action onClick) { - 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; - } + 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; })); - panel.gameObject.SetActive(true); - num++; - } - - var text = string.Empty; - try - { - text = searchBoxTmp.outputText.text; - } - catch - { - // ignored - } - - SortAllModifiers(text); - _needsRoleRefresh = true; - - /*var instance = MatchInfoGuide.Instance; - instance.rolesEnabledMessage.SetActive(num == 0);*/ - - _modifiersScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); - } - - public static void ToggleRoleVisibility() - { - var num = 0; - foreach (var (roleData, holder) in RolePanels) - { - var panel = holder.Panel; - int amount; - int chance; - int forciblyShow = 0; - 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; - } - } - else - { - amount = - GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleData.Role); - chance = - GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(roleData.Role); - } - - panel.SetRolePanel( - roleData, - amount, - chance); - holder.Amount = amount; - holder.Chance = chance; - holder.LikelyhoodOfRole = 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) - { - panel.gameObject.SetActive(false); - continue; - } - - panel.gameObject.SetActive(true); - num++; - } - - var text = string.Empty; - try - { - text = searchBoxTmp.outputText.text; - } - catch - { - // ignored - } - - SortAllRoles(text); - - var instance = MatchInfoGuide.Instance; - instance.rolesEnabledMessage.SetActive(num == 0); - - instance.MatchInfoRoleScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); - } - - 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(); - } - } - - 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(); - } - } - - public static Func, string> GetSortingOrder() - { - 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 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) - { - 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)}"; - } + passiveButton.OnClick = new Button.ButtonClickedEvent(); + passiveButton.OnClick.AddListener(onClick); } [HarmonyPrefix] @@ -1021,10 +1139,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,79 +1163,103 @@ 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; } } _advancedInfoTabScroller?.gameObject.SetActive(false); + UpdateSpinner(); + } + + private static bool TryPrepareAdvancedWiki(MatchInfoGuide instance) + { + instance.settingsTabs[RolesTabIndex].SetActive(false); + _modifiersScroller?.gameObject.SetActive(false); + if (_currentAdvancedTabObject) + { + _currentAdvancedTabObject.SetActive(false); + _currentAdvancedTabObject.Destroy(); + } + + if (_advancedInfoTabScroller == null) + { + Warning("Wiki tab is null."); + return false; + } + + _advancedInfoTabScroller.gameObject.SetActive(true); + UpdateSpinner(); + 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 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) + var remaining = _openedAt + instance.transitionOpen.duration - Time.time; + if (remaining > 0f) { - currentAdvancedTabObject.SetActive(false); - currentAdvancedTabObject.Destroy(); - } - - if (_advancedInfoTabScroller == null) - { - Warning($"Wiki tab is null."); + Coroutines.Start(CoDisplayAdvancedWikiLater(remaining, () => DisplayAdvancedWiki(instance, modifier))); return; } - currentAdvancedTabObject = modifier.GetAdvancedWiki(instance, titleText, _advancedInfoTabScroller); + var name = modifier.ModifierName; + Info($"Opening advanced tab for {name}."); + if (!TryPrepareAdvancedWiki(instance)) + { + return; + } + + _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) + var remaining = _openedAt + instance.transitionOpen.duration - Time.time; + if (remaining > 0f) { - currentAdvancedTabObject.SetActive(false); - currentAdvancedTabObject.Destroy(); + Coroutines.Start(CoDisplayAdvancedWikiLater(remaining, () => DisplayAdvancedWiki(instance, role))); + return; } - if (_advancedInfoTabScroller == null) + Info($"Opening advanced tab for {role.GetRoleName()}."); + if (!TryPrepareAdvancedWiki(instance)) { - 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() + + 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")) @@ -1126,16 +1271,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)); + []); } } @@ -1174,31 +1317,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); @@ -1215,16 +1363,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); 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); } } 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... 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())); } /// diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index dc38423..307b6ca 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, - 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; + parent, + RoleNameLocale, + $"{this.GetTmpIcon()} {RoleName} ({RoleFactionTitle})", + RoleWikiDescription, + abilities); } /// 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; } diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 1a8f962..7a3381c 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); } diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 3584588..8472e6b 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -6,16 +6,19 @@ 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; using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Modifiers; using MiraAPI.Roles; using MiraAPI.Translation; using MiraAPI.Utilities.Assets; using Rewired; using TMPro; using UnityEngine; +using UnityEngine.UI; using MethodBase = System.Reflection.MethodBase; using Object = UnityEngine.Object; using Random = UnityEngine.Random; @@ -27,83 +30,67 @@ namespace MiraAPI.Utilities; /// public static class Helpers { + private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); + 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; @@ -116,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(); } @@ -718,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). /// @@ -743,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; @@ -757,4 +763,97 @@ 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. + /// Whether to use variable text sizing for ability descriptions (allows different heights). + /// The of the wiki page. + public static GameObject CreateAdvancedWikiPage( + MatchInfoGuide guide, + TextMeshPro titleTmp, + Scroller parent, + string objName, + string title, + string description, + IReadOnlyCollection abilities, + bool variableTextSizing = false) + { + parent.ScrollToTop(); + var obj = CreateAdvancedWikiTab( + guide, + objName, + title, + description, + titleTmp, + out var desc); + var num = 0; + var maxTextSize = 0f; + 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++; + 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); + grid.localScale = new Vector3(1.3f, 1.3f, 1); + } + + obj.transform.SetParent(parent.Inner.transform); + obj.transform.localPosition = new Vector3(0f, 0f, 0f); + 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, bool variableTextSizing = false) + { + 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); + 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); + + 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); + } } diff --git a/MiraAPI/Utilities/LoadingRing.cs b/MiraAPI/Utilities/LoadingRing.cs new file mode 100644 index 0000000..3f8af87 --- /dev/null +++ b/MiraAPI/Utilities/LoadingRing.cs @@ -0,0 +1,88 @@ +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) + { + if (!_ringSprite) + { + _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(); + 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; + } +}