diff --git a/AmongUs.props b/AmongUs.props index 0819a88..b00f7ff 100644 --- a/AmongUs.props +++ b/AmongUs.props @@ -2,7 +2,7 @@ - + diff --git a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs index 8cd0139..6a7f3a0 100644 --- a/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs +++ b/MiraAPI.Example/Modifiers/Freezer/FreezeModifier.cs @@ -1,5 +1,4 @@ -using Il2CppSystem; -using MiraAPI.Example.Roles; +using MiraAPI.Example.Roles; using MiraAPI.Modifiers.Types; using MiraAPI.Translation; using UnityEngine; @@ -28,7 +27,7 @@ public class FreezeModifier : TimedModifier if (Player.AmOwner || PlayerControl.LocalPlayer.Data.Role is FreezerRole) { - Player.cosmetics.SetOutline(true, new Nullable(Palette.LightBlue)); + Player.cosmetics.SetOutline(true, new Il2CppSystem.Nullable(Palette.LightBlue)); } } diff --git a/MiraAPI/GameModes/GameModeOption.cs b/MiraAPI/GameModes/GameModeOption.cs index 34ec457..c58030a 100644 --- a/MiraAPI/GameModes/GameModeOption.cs +++ b/MiraAPI/GameModes/GameModeOption.cs @@ -39,7 +39,7 @@ public static class GameModeOption } } - internal static readonly StringNames GamemodeName = MiraLocaleManager.GetOrCreateLocaleString("Gamemode"); + internal static readonly StringNames GamemodeName = MiraLocaleManager.GetOrCreateLocaleString("MiraApi.Gamemode"); internal static readonly StringNames CustomName = MiraLocaleManager.GetOrCreateLocaleString("Custom"); internal static readonly Dictionary Values = new() { diff --git a/MiraAPI/GameOptions/IModdedOption.cs b/MiraAPI/GameOptions/IModdedOption.cs index 099194e..fdf71a6 100644 --- a/MiraAPI/GameOptions/IModdedOption.cs +++ b/MiraAPI/GameOptions/IModdedOption.cs @@ -121,4 +121,24 @@ public interface IModdedOption /// public interface IModdedOptionList : IReadOnlyList { + /// + /// Gets the function to name the keys of the individual options in the configuration file. + /// + /// Return to use the default of the property's name with its index appended. + /// + Func? ConfigName { get; init; } +} + +/// +/// Interface for list of options. +/// +/// The type of . +public interface IModdedOptionList : IModdedOptionList + where T : IModdedOption +{ + /// + new T this[int index] { get; } + + /// + new IEnumerator GetEnumerator(); } diff --git a/MiraAPI/GameOptions/ISummarizedOptions.cs b/MiraAPI/GameOptions/ISummarizedOptions.cs new file mode 100644 index 0000000..7873902 --- /dev/null +++ b/MiraAPI/GameOptions/ISummarizedOptions.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace MiraAPI.GameOptions; + +/// +/// Allows an option group to compress noisy option blocks in the wiki "Options" section. +/// Implementers can hide specific option keys and replace them with one or more summary lines. +/// +public interface ISummarizedOptions +{ + /// + /// Gets the option string keys (StringNames) to omit from the wiki options list (e.g., a large set of related options). + /// + IReadOnlySet WikiHiddenOptionKeys { get; } + + /// + /// Gets summary lines to insert when the first hidden option would have appeared. + /// Lines should already be formatted as "Title: Value". + /// + IEnumerable GetWikiOptionSummaryLines(); +} diff --git a/MiraAPI/GameOptions/ModdedOptionsManager.cs b/MiraAPI/GameOptions/ModdedOptionsManager.cs index 387a1f8..b893040 100644 --- a/MiraAPI/GameOptions/ModdedOptionsManager.cs +++ b/MiraAPI/GameOptions/ModdedOptionsManager.cs @@ -198,9 +198,11 @@ public static class ModdedOptionsManager return; } + Func configName = optionList.ConfigName ?? (i => property.Name + (i + 1)); + for (int i = 0; i < optionList.Count; i++) { - RegisterOption(optionList[i], group, property.Name + i, pluginInfo); + RegisterOption(optionList[i], group, configName(i), pluginInfo); } } @@ -277,7 +279,7 @@ public static class ModdedOptionsManager option.Visible = () => visibilityFunc(i); } - RegisterOption(option, group, property.Name + i, pluginInfo); + RegisterOption(option, group, property.Name + (i + 1), pluginInfo); } } diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs index 6f9e8d5..257ae6e 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedOptionList.cs @@ -9,7 +9,7 @@ namespace MiraAPI.GameOptions.OptionTypes; /// Represents a modded option list. /// /// The type of options. -public class ModdedOptionList : IModdedOptionList +public class ModdedOptionList : IModdedOptionList where T : IModdedOption { /// @@ -20,6 +20,9 @@ public class ModdedOptionList : IModdedOptionList /// public IReadOnlyList Options { get; } + /// + public Func? ConfigName { get; init; } + /// /// Initializes a new instance of the class. /// @@ -41,10 +44,10 @@ public class ModdedOptionList : IModdedOptionList Options = [.. options]; } - /// - public IEnumerator GetEnumerator() + /// + public IEnumerator GetEnumerator() { - return ((IEnumerable)Options).GetEnumerator(); + return Options.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() @@ -52,6 +55,11 @@ public class ModdedOptionList : IModdedOptionList return Options.GetEnumerator(); } + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)Options).GetEnumerator(); + } + /// /// Indexes the option of type . /// diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs index 32c736e..646de56 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs @@ -1,6 +1,6 @@ using System; +using System.Collections.Generic; using System.Linq; -using Il2CppSystem.Collections.Generic; using MiraAPI.Networking; using MiraAPI.Translation; using MiraAPI.Utilities; @@ -43,7 +43,7 @@ public class ModdedPlayerOption : ModdedOption data.Title = StringName; data.Type = global::OptionTypes.Player; - Values = new List(); + Values = new(); AllowNone = allowNone; Value = DefaultValue; } @@ -60,17 +60,16 @@ public class ModdedPlayerOption : ModdedOption playerOption.name = $"{ParentMod!.OptionsTitleText}.PlayerOption.{TranslationController.Instance.GetString(StringName)}"; - var filteredList = GetFilteredPlayers(); - Values = filteredList.ToIl2CppList(); + Values = GetFilteredPlayers(); playerOption.SetUpFromData(Data, 20); playerOption.OnValueChanged = (Il2CppSystem.Action)ValueChanged; playerOption.Title = StringName; playerOption.TitleText.text = Title.Translate(); - playerOption.Values = Values; + playerOption.Values = Values.ToIl2CppList(); playerOption.Value = Value; - playerOption.playerIndex = filteredList.FindIndex(p => p.PlayerId == Value); + playerOption.playerIndex = Values.FindIndex(p => p.PlayerId == Value); OptionBehaviour = playerOption; ModdedOptionsManager.CreatedPlayerOptions.TryAdd(playerOption, this); @@ -85,7 +84,7 @@ public class ModdedPlayerOption : ModdedOption /// Returns a player ran through the filter. /// /// A list of filtered players. - public System.Collections.Generic.List GetFilteredPlayers() + public List GetFilteredPlayers() { return [.. GameData.Instance.AllPlayers.ToArray().Where(x => PlayerFilter?.Invoke(x) ?? true)]; } diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index d68a87d..4da8968 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -4,6 +4,7 @@ using BepInEx; using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; +using MiraAPI.Patches; using MiraAPI.Patches.Roles; using MiraAPI.PluginLoading; using MiraAPI.Translation; @@ -63,9 +64,30 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin private static MiraPluginManager? PluginManager { get; set; } internal Harmony Harmony { get; } = new(Id); + public static ConfigEntry WikiSortingGroup { get; private set; } + + public static ConfigEntry WikiSortingMethod { get; private set; } + + public static ConfigEntry WikiSortingFilter { get; private set; } + /// public override void Load() { + WikiSortingGroup = Config.Bind( + "Wiki", + "WikiSortingGroup", + SortingGroups.Ungrouped, + "Determines how clusters of roles / modifiers are grouped together, if at all."); + WikiSortingMethod = Config.Bind( + "Wiki", + "WikiSortingMethod", + SortingMethod.Alphabetical, + "Determines if the list of roles / modifiers are organized in ascending or descending order."); + WikiSortingFilter = Config.Bind( + "Wiki", + "WikiSortingFilter", + SortingFilter.AllRoles, + "Determines what roles / modifiers are displayed at once."); Harmony.PatchAll(); RoleOptionsCollectionPatch.PatchRoleMethods(Harmony); UiResetEvents.Initialize(); diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 0017fd9..3daeb56 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -1,7 +1,13 @@ using System; +using System.Collections.Generic; +using Il2CppInterop.Runtime.Attributes; using MiraAPI.GameOptions; using MiraAPI.PluginLoading; +using MiraAPI.Roles; +using MiraAPI.Translation; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; +using TMPro; using UnityEngine; namespace MiraAPI.Modifiers; @@ -44,16 +50,98 @@ public abstract class BaseModifier : IOptionable x => x.InternalModifiers.Exists(y => y.TypeId == TypeId)) ?? throw new InvalidOperationException("Modifier is not registered."); + /// + /// Gets the id part used to build the modifier's translation keys. It is recommended to simply call it what the modifier is, as other mods may utilize it. + /// + public virtual string IdPart => GetType().Name; + + /// + /// Gets the id part used to build the modifier's translation keys. It is recommended to call it ModGuid.Modifier.Type. + /// + public virtual string IdPrefix => GetType().Namespace!; + /// /// Gets the modifier name. /// - public abstract string ModifierName { get; } + public virtual string ModifierName => MiraLocaleManager.Get(ModifierNameLocale); + + /// + /// Gets the modifier's name id for localization. + /// + public virtual string ModifierNameLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart); + + /// + /// Gets the medium description of the modifier. Used in the modifier guide and options menu. + /// + public virtual string ModifierMedDescription => MiraLocaleManager.Get(ModifierMedDescriptionLocale, GetDescription()); + + /// + /// Gets the modifier's medium description id for localization. + /// + public virtual string ModifierMedDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "MedDescription"); + + /// + /// Gets the wiki description of the modifier. Used in the wiki and normally appends the options text as well. + /// + public virtual string ModifierWikiDescription => MiraLocaleManager.Get(ModifierWikiDescriptionLocale, GetDescription()) + + Helpers.GetOptionsText(GetType()); + + /// + /// Gets the modifier's wiki description id for localization. + /// + public virtual string ModifierWikiDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "WikiDescription"); + + /// + /// Gets the category of the modifier. Used in the wiki. + /// + public virtual string ModifierCategoryTitle => MiraLocaleManager.Get("Modifier"); + + /// + /// Gets whether the modifier is forcibly shown or disabled in the wiki screen. + /// + /// if the modifier is always displayed, otherwise if it is never displayable, or if it is dictated by amount and chance. + public virtual bool? ForceShowModifierOnWiki => false; + + /// + /// Gets the information to display in the wiki. + /// + /// The guide object. + /// The title text object. + /// The title's icon. + /// The scroller parent. + /// The of the wiki. + public virtual GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, SpriteRenderer titleIcon, Scroller parent) + { + var abilities = WikiAbilities; + return Helpers.CreateAdvancedWikiPage( + guide, + titleText, + titleIcon, + parent, + ModifierNameLocale, + $"{ModifierName} ({ModifierCategoryTitle})".TrimStart(), + ModifierIcon ?? MiraAssets.IconNeutral.LoadAsset(), + ModifierWikiDescription, + abilities); + } + + /// + /// Gets the list of abilities or other similar information to display below the text of a modifier's wiki page. + /// + [HideFromIl2Cpp] + public virtual List WikiAbilities => []; /// /// Gets the modifier icon. Useless if is . /// public virtual LoadableAsset? ModifierIcon => null; + /// + /// Gets the for the Modifier Icon. + /// + [HideFromIl2Cpp] + public virtual TMP_SpriteAsset IconTmp => null!; + /// /// Gets a value indicating whether the modifier is hidden on the UI. Will be hidden either way if no description is provided. /// @@ -69,6 +157,11 @@ public abstract class BaseModifier : IOptionable /// public virtual Color FreeplayFileColor => Color.gray; + /// + /// Gets the of the modifier, used in the wiki and other UI outside freeplay. + /// + public virtual Color GeneralColor => FreeplayFileColor; + /// /// Gets a value indicating whether the modifier is unique. If , the player can only have one instance of this modifier. /// diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index f574fd3..b432920 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -9,6 +9,9 @@ namespace MiraAPI.Modifiers.Types; [MiraIgnore] public abstract class GameModifier : BaseModifier { + /// + public override bool? ForceShowModifierOnWiki => null; + /// public override bool ShowInFreeplay => true; diff --git a/MiraAPI/Patches/Events/VanillaButtonPatches.cs b/MiraAPI/Patches/Events/VanillaButtonPatches.cs index 6b12a22..70a218b 100644 --- a/MiraAPI/Patches/Events/VanillaButtonPatches.cs +++ b/MiraAPI/Patches/Events/VanillaButtonPatches.cs @@ -23,10 +23,18 @@ public static class VanillaButtonPatches { playerTarget = role.Cast().currentTarget; } + else if (role.Role is RoleTypes.Detective) + { + playerTarget = role.Cast().currentTarget; + } else if (role.Role is RoleTypes.GuardianAngel) { playerTarget = role.Cast().currentTarget; } + else if (role.Role is RoleTypes.SpiritGuide) + { + playerTarget = role.Cast().currentTarget; + } else if (role.Role is RoleTypes.Engineer) { ventTarget = role.Cast().currentTarget; @@ -53,6 +61,10 @@ public static class VanillaButtonPatches { playerTarget = role.Cast().currentTarget; } + else if (role.Role is RoleTypes.SpiritGuide) + { + playerTarget = role.Cast().currentTarget; + } var genericEvent = new VanillaButtonClickEvent(__instance, playerTarget); MiraEventManager.InvokeEvent(genericEvent); diff --git a/MiraAPI/Patches/GameModes/HudPatches.cs b/MiraAPI/Patches/GameModes/HudPatches.cs index d237967..3c2d815 100644 --- a/MiraAPI/Patches/GameModes/HudPatches.cs +++ b/MiraAPI/Patches/GameModes/HudPatches.cs @@ -1,6 +1,5 @@ using System.Collections; using HarmonyLib; -using Il2CppSystem; using MiraAPI.GameModes; using MiraAPI.Translation; using Reactor.Utilities; @@ -16,7 +15,7 @@ internal static class HudPatches [HarmonyPostfix, HarmonyPatch(nameof(HudManager.Update))] public static void HudUpdatePatch(HudManager __instance) { - if (GameManager.Instance != null && GameManager.Instance.GameHasStarted && + if (GameManager.Instance && GameManager.Instance.GameHasStarted && CustomGameModeManager.ActiveMode != null) { CustomGameModeManager.ActiveMode.HudUpdate(__instance); @@ -32,7 +31,7 @@ internal static class HudPatches private static IEnumerator CoPostHudStart(HudManager instance) { - yield return new WaitUntil((Func)(() => GameManager.Instance != null)); + yield return new WaitUntil((Il2CppSystem.Func)(() => GameManager.Instance != null)); if (GameManager.Instance.IsHideAndSeek()) yield return null; var infoPane = instance.gameObject.transform.FindChild("LobbyInfoPane"); @@ -42,7 +41,7 @@ internal static class HudPatches var modelText = modeLabel!.Find("Text_TMP").gameObject; var modelTextClone = Object.Instantiate(modelText, modeLabel); Object.Destroy(modelTextClone.GetComponent()); - modelTextClone.GetComponent().text = "Gamemode"; + modelTextClone.GetComponent().text = MiraLocaleManager.Get("MiraApi.Gamemode", "Gamemode"); var gmText = modeValue!.Find("GameModeText").gameObject; var gmTextClone = Object.Instantiate(gmText, modeValue); gmText.SetActive(false); diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index 38ba561..e65f4c3 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -1,10 +1,10 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq; using BepInEx.Unity.IL2CPP.Utils.Collections; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; -using Il2CppSystem.Collections.Generic; using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Modifiers; @@ -28,9 +28,9 @@ internal static class GameOptionsMenuPatch private static readonly List VanillaHeaders = new(); private static readonly List VanillaOptions = new(); private static readonly List MainOptions = new(); - private static readonly System.Collections.Generic.Dictionary> GameModeGroups = []; - private static readonly System.Collections.Generic.Dictionary> GameModeOptions = []; - private static readonly System.Collections.Generic.Dictionary> GameModeHeaders = []; + private static readonly Dictionary> GameModeGroups = []; + private static readonly Dictionary> GameModeOptions = []; + private static readonly Dictionary> GameModeHeaders = []; private static TextMeshPro _gamemodeDescription = null!; private static SpriteRenderer _modIcon = null!; @@ -38,7 +38,7 @@ internal static class GameOptionsMenuPatch [HarmonyPatch(nameof(GameOptionsMenu.Initialize))] public static bool InitPatch(GameOptionsMenu __instance) { - __instance.Children ??= new List(); + __instance.Children ??= new(); __instance.Children.Clear(); if (MenuState.Instance.CurrentModIdx == 0) @@ -324,8 +324,8 @@ internal static class GameOptionsMenuPatch .ToList(); GameModeGroups.Add(mode, [.. filteredGroups]); - var optionBehaviours = new System.Collections.Generic.List(); - var categoryHeaders = new System.Collections.Generic.List(); + var optionBehaviours = new List(); + var categoryHeaders = new List(); var newNum = mode.ShowNormalGameSettings ? num : num - additionalVanillaScrollNum; foreach (var group in filteredGroups) { @@ -406,7 +406,7 @@ internal static class GameOptionsMenuPatch __instance.scrollBar.SetYBoundsMax(-num - 1.65f); } - private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, ref float num, ref System.Collections.Generic.List optionBehaviours, ref System.Collections.Generic.List categoryHeaders) + private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, ref float num, ref List optionBehaviours, ref List categoryHeaders) { var categoryHeaderMasked = Object.Instantiate( menu.categoryHeaderOrigin, @@ -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 64d9119..0b7f5b9 100644 --- a/MiraAPI/Patches/Options/OptionsPatches.cs +++ b/MiraAPI/Patches/Options/OptionsPatches.cs @@ -1,11 +1,9 @@ using HarmonyLib; -using Il2CppSystem; using MiraAPI.GameOptions; using MiraAPI.Roles; using MiraAPI.Translation; using MiraAPI.Utilities; using UnityEngine; -using Object = Il2CppSystem.Object; namespace MiraAPI.Patches.Options; @@ -165,7 +163,7 @@ public static class OptionsPatches return true; } - __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty()).Translate(); + __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title).Translate(); return false; } @@ -314,8 +312,8 @@ public static class OptionsPatches return true; } - __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty()).Translate(); - __instance.ValueText.text = TranslationController.Instance.GetString(__instance.Values[__instance.Value], Array.Empty()); + __instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title).Translate(); + __instance.ValueText.text = TranslationController.Instance.GetString(__instance.Values[__instance.Value]); return false; } diff --git a/MiraAPI/Patches/Options/PlayerOptionPatches.cs b/MiraAPI/Patches/Options/PlayerOptionPatches.cs index 9cf0134..ab19fce 100644 --- a/MiraAPI/Patches/Options/PlayerOptionPatches.cs +++ b/MiraAPI/Patches/Options/PlayerOptionPatches.cs @@ -26,8 +26,8 @@ public static class PlayerOptionPatches { filteredPlayers.Sort((a, b) => a.PlayerId.CompareTo(b.PlayerId)); + moddedPlrOpt.Values = filteredPlayers; __instance.Values = filteredPlayers.ToIl2CppList(); - moddedPlrOpt.Values = __instance.Values; __instance.playerIndex = filteredPlayers.FindIndex(p => p.PlayerId == __instance.Value); __instance.SetValueText(); } diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 6b17ea3..92080e8 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -1,12 +1,19 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using AmongUs.GameOptions; using HarmonyLib; using Innersloth.Assets; +using MiraAPI.Modifiers; +using MiraAPI.Modifiers.Types; using MiraAPI.Roles; +using MiraAPI.Translation; using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using Reactor.Utilities; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; @@ -19,32 +26,86 @@ 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 GameObject SearchComponentContainer; + public static PassiveButton SearchIconButton; + private static SpriteRenderer _searchIconIdle; + private static SpriteRenderer _searchIconHover; + public static TextMeshPro OrderSortingTmp; + public static PassiveButton SearchSortingOrderButton; + private static SpriteRenderer _searchSortingOrderIdle; + private static SpriteRenderer _searchSortingOrderHover; + public static TextMeshPro GroupSortingTmp; + public static PassiveButton SearchSortingGroupButton; + private static SpriteRenderer _searchSortingGroupIdle; + private static SpriteRenderer _searchSortingGroupHover; + public static TextMeshPro FilterSortingTmp; + public static PassiveButton SearchSortingFilterButton; + private static SpriteRenderer _searchSortingFilterIdle; + private static SpriteRenderer _searchSortingFilterHover; + private static Scroller _rolesScroller; + private static Scroller _modifiersScroller; + private static Scroller _advancedInfoTabScroller; + private static TextBoxTMP _searchBoxTmp; + private static GameObject _currentAdvancedTabObject; + private static TextMeshPro _titleText; + private static SpriteRenderer _titleIcon; + private static bool _suppressSearchRefresh; + private static IEnumerator? _searchDebounce; + private static string _appliedSearch = string.Empty; + 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)] [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.CreateNormalModeSettings))] [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); - __instance.matchInfoPlayersMaskArea.transform.parent.GetAllChildren().First(x => x.name.Contains("BG_Gradient")) + 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 = - __instance.matchInfoSettingsMaskArea.transform.parent.GetAllChildren() - .First(x => x.name.Contains("BG_Gradient")).GetComponent() + settingsenuGradient.GetComponent() .maskInteraction = - __instance.MatchInfoRoleMaskArea.transform.parent.GetAllChildren() - .First(x => x.name.Contains("BG_Gradient")).GetComponent() + roleMenuGradient.GetComponent() .maskInteraction = SpriteMaskInteraction.VisibleInsideMask; - var wikiTab = Object.Instantiate(__instance.settingsTabs[2], __instance.settingsTabs[2].transform.parent); - advancedWikiTab = wikiTab.GetComponent(); + roleMenuGradient.transform.localScale = new Vector3(0.5297f, 0.1565f, 1); + roleMenuGradient.transform.localPosition = new Vector3(0, -0.62f, -5); } [HarmonyPrefix] @@ -53,6 +114,107 @@ 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!; + _titleIcon = 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] @@ -60,6 +222,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; @@ -84,55 +251,73 @@ 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.localPosition += new Vector3(0.5f, 0); + _titleText.transform.GetComponent().Destroy(); } } - - if (titleText) + if (!_titleIcon) { - titleText.text = TranslationController.Instance.GetString(StringNames.MatchInfoGuideTitle); + var titleIconObj = new GameObject("TitleIcon"); + _titleIcon = titleIconObj.AddComponent(); + _titleIcon.transform.SetParent(_titleText.transform.parent); + _titleIcon.transform.localPosition = new Vector3(-2.75f, 1.4f, 0); + titleIconObj.layer = _titleText.gameObject.layer; } - if (GameManager.Instance.TryCast() != null) + if (_titleText) { + _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) { - __instance.numOfTabs = 3; - __instance.TabButtons[0].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); - - __instance.matchInfoPlayersMaskArea.transform.parent.GetAllChildren().First(x => x.name.Contains("BG_Gradient")) - .GetComponent() - .maskInteraction = - __instance.matchInfoSettingsMaskArea.transform.parent.GetAllChildren() - .First(x => x.name.Contains("BG_Gradient")).GetComponent() - .maskInteraction = - __instance.MatchInfoRoleMaskArea.transform.parent.GetAllChildren() - .First(x => x.name.Contains("BG_Gradient")).GetComponent() - .maskInteraction = SpriteMaskInteraction.VisibleInsideMask; - __instance.MatchInfoRoleMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); - __instance.matchInfoSettingsMaskArea.material.SetInt(PlayerMaterial.MaskLayer, 50); - var wikiTab = Object.Instantiate(__instance.settingsTabs[2], __instance.settingsTabs[2].transform.parent); - wikiTab.transform.FindChild("MaskArea")?.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); - advancedWikiTab = wikiTab.GetComponent(); - DisplayNormalRoleSettings(__instance, true); - } - else - { - DisplayNormalRoleSettings(__instance, false); + __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) { @@ -142,124 +327,891 @@ 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(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; } - private static readonly Dictionary RolePanels = []; - private static Scroller advancedWikiTab; - private static GameObject currentAdvancedTabObject; - private static TextMeshPro titleText; - - public static void DisplayNormalRoleSettings(MatchInfoGuide instance, bool reset) + private static void Initialize(MatchInfoGuide instance) { - if (reset) + if (_uiInitialized) { - RolePanels.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()); + return; + } - var hoverColor = new Color32(255, 255, 255, 150); - foreach (var roleBehaviour in RoleManager.Instance.AllRoles.ToArray().OrderBy(x => x.GetRoleName())) + _sortFilter = MiraApiPlugin.WikiSortingFilter.Value; + _sortMethod = MiraApiPlugin.WikiSortingMethod.Value; + _sortGrouping = MiraApiPlugin.WikiSortingGroup.Value; + + if (_populate != null) + { + Coroutines.Stop(_populate); + _populate = null; + } + + _guide = instance; + _loading = false; + SearchComponentContainer = new GameObject(); + SearchComponentContainer.transform.SetParent(instance.MatchInfoParent.transform); + SearchComponentContainer.transform.localPosition = Vector3.zero; + instance.MatchInfoParent.transform.GetChild(1).transform.SetParent(SearchComponentContainer.transform); + + var sortingOrderButton = Object.Instantiate( + HudManager.Instance.SettingsButton, + SearchComponentContainer.transform); + sortingOrderButton.gameObject.SetActive(true); + SearchSortingOrderButton = sortingOrderButton.GetComponent(); + SetupSearchButton( + SearchSortingOrderButton, + "SearchSortingOrderButton", + SwitchMethod, + GetSortMethodSprite(_sortMethod, false), + GetSortMethodSprite(_sortMethod, true), + new Vector3(2.4f, 0.765f, -0.1f), + out _searchSortingOrderIdle, + out _searchSortingOrderHover); + + var sortingGroupButton = Object.Instantiate( + sortingOrderButton, + SearchComponentContainer.transform); + sortingGroupButton.gameObject.SetActive(true); + SearchSortingGroupButton = sortingGroupButton.GetComponent(); + SetupSearchButton( + SearchSortingGroupButton, + "SearchSortingGroupButton", + SwitchGrouping, + GetSortGroupSprite(_sortGrouping, false), + GetSortGroupSprite(_sortGrouping, true), + new Vector3(1.7f, 0.765f, -0.1f), + out _searchSortingGroupIdle, + out _searchSortingGroupHover); + + var sortingFilterButton = Object.Instantiate( + sortingOrderButton, + SearchComponentContainer.transform); + sortingFilterButton.gameObject.SetActive(true); + SearchSortingFilterButton = sortingFilterButton.GetComponent(); + SetupSearchButton( + SearchSortingFilterButton, + "SearchSortingFilterButton", + SwitchFilter, + GetSortFilterSprite(_sortFilter, false), + GetSortFilterSprite(_sortFilter, true), + 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"; + var wikiMask = wikiTab.transform.FindChild("MaskArea"); + wikiMask.transform.localPosition = new Vector3(-0.0184f, 0.425f, -0.1f); + wikiMask.transform.localScale = new Vector3(1, 1.3f, 1); + wikiTab.transform.GetAllChildren() + .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5); + _advancedInfoTabScroller = wikiTab.GetComponent(); + var wikiScrollHitbox = wikiTab.transform.FindChild("Hitbox"); + _advancedInfoTabScroller.ScrollbarYBounds.max = 1.2f; + wikiScrollHitbox.GetComponent().size = new Vector2(5.75f, 2.4f); + wikiScrollHitbox.transform.localPosition += new Vector3(0, 0.3f); + _advancedInfoTabScroller.ScrollbarY.trackGraphic.transform.localPosition = new Vector3(2.8364f, 0.3649f, -0.2f); + _advancedInfoTabScroller.ScrollbarY.trackGraphic.transform.localScale = new Vector3(0.0234f, 2.068f, 0.5598f); + + var spinnerParent = instance.settingsTabs[RolesTabIndex].transform.parent; + var spinnerPos = spinnerParent.InverseTransformPoint(instance.MatchInfoRoleMaskArea.transform.position); + 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, + SearchComponentContainer.transform); + 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 (roleBehaviour.Role is not RoleTypes.Crewmate and not RoleTypes.Impostor and - not RoleTypes.CrewmateGhost and - not RoleTypes.ImpostorGhost) + if (GetSearchText() != string.Empty) { - var panel = Object.Instantiate( - instance.MatchInfoRolePanelPrefab, - instance.settingsTabs[2].GetComponent().Inner); - 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; - })); + _searchIconIdle.sprite = MiraAssets.SearchClearIdleSprite; + _searchIconHover.sprite = MiraAssets.SearchClearHoverSprite; + } + else + { + _searchIconIdle.sprite = MiraAssets.SearchIconIdleSprite; + _searchIconHover.sprite = MiraAssets.SearchIconHoverSprite; + } - passiveButton.OnClick = new Button.ButtonClickedEvent(); - passiveButton.OnClick.AddListener( - (Action)(() => { DisplayAdvancedWiki(instance, roleBehaviour); })); - RolePanels.Add(roleBehaviour, panel); + 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); + + OrderSortingTmp = Object.Instantiate( + _titleText, + sortingOrderButton.transform); + OrderSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.OrderLabel"); + OrderSortingTmp.fontSize = OrderSortingTmp.fontSizeMax = 3f; + OrderSortingTmp.alignment = TextAlignmentOptions.Center; + OrderSortingTmp.horizontalAlignment = HorizontalAlignmentOptions.Center; + OrderSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0); + + GroupSortingTmp = Object.Instantiate( + OrderSortingTmp, + sortingGroupButton.transform); + OrderSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.GroupingLabel"); + GroupSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0); + + FilterSortingTmp = Object.Instantiate( + OrderSortingTmp, + sortingFilterButton.transform); + FilterSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.FilterLabel"); + FilterSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0); + + playerButton.transform.localPosition = new Vector3(-3.6f, 0.656f, -0.2f); + settingButton.transform.localPosition = new Vector3(-3.6f, 0.056f, -0.2f); + rolesButton.transform.localPosition = new Vector3(-3.6f, -0.544f, -0.2f); + 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); + var settingBtnInactive = button.inactiveSprites.GetComponent(); + settingBtnInactive.sprite = inactive; + settingBtnInactive.transform.GetChild(0).gameObject.SetActive(false); + var settingBtnSelected = button.selectedSprites.GetComponent(); + settingBtnSelected.sprite = selected; + settingBtnSelected.transform.GetChild(0).gameObject.SetActive(false); + var settingBtnHighlight = button.activeSprites.GetComponent(); + settingBtnHighlight.sprite = hover; + settingBtnInactive.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); + buttonCollider.offset = Vector2.zero; + button.transform.localScale = new Vector3(0.7f, 0.7f, 1); + } + + 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(); + inactiveSprite.sprite = inactive; + hoverSprite = button.activeSprites.GetComponent(); + hoverSprite.sprite = hover; + button.OnClick = new Button.ButtonClickedEvent(); + button.OnClick.AddListener(onClick); + + button.transform.localPosition = pos; + button.transform.localScale = Vector3.one; + button.transform.GetChild(2).gameObject.SetActive(false); + } + + private static void OpenTab(int tabIndex, float delay = 0f) + { + _advancedInfoTabScroller?.gameObject.SetActive(false); + MatchInfoGuide.Instance.SetActiveTab(tabIndex); + HidePanels(tabIndex == ModifiersTabIndex ? ModifierEntries : RoleEntries); + StartPopulate(delay); + } + + private static void HidePanels(List entries) + { + foreach (var entry in entries) + { + if (entry.Panel && entry.Panel!.gameObject.activeSelf) + { + entry.Panel.gameObject.SetActive(false); + } + } + } + + public static void OpenModifiersTab() => OpenTab(ModifiersTabIndex); + + public static void OpenRolesTab() => OpenTab(RolesTabIndex); + + public sealed class DetailedPanel(string title, string category, string modId, RoleBehaviour? role, GameModifier? modifier) + { + public MatchInfoRolePanel? Panel { get; set; } + public string Title { get; } = title; + public string Category { get; } = category; + public string ModId { get; } = modId; + public RoleBehaviour? Role { get; } = role; + public GameModifier? Modifier { get; } = modifier; + public int Amount { get; set; } + public int Chance { get; set; } + public float Likelihood { get; set; } + public bool Visible { get; set; } + + public string GetSortKey(SortingGroups grouping) + { + 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 string GetSearchText() + { + return _searchBoxTmp && _searchBoxTmp.outputText ? _searchBoxTmp.outputText.text : string.Empty; + } + + private static bool ShouldShowPanel(bool? forceShow, bool isDisabled, bool isHidden) + { + return forceShow ?? + !(isHidden || (isDisabled && _sortFilter is SortingFilter.EnabledOnly) || + (!isDisabled && _sortFilter is SortingFilter.DisabledOnly)); + } + + private static void UpdateSpinner() + { + if (_loadingSpinner != null) + { + _loadingSpinner.gameObject.SetActive( + _loading && MatchInfoGuide.Instance.activeTabIndex is RolesTabIndex or ModifiersTabIndex && + !(_advancedInfoTabScroller && _advancedInfoTabScroller.gameObject.activeSelf)); + } + } + + 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) + { + Coroutines.Stop(_populate); + } + + _loading = true; + UpdateSpinner(); + _populate = Coroutines.Start(CoPopulate(delay)); + } + + private static IEnumerator CoPopulate(float delay) + { + if (delay > 0f) + { + yield return new WaitForSeconds(delay); + } + else + { + yield return null; + } + + if (!_guide) + { + yield break; + } + + var modifiers = IsModifiersTabActive; + var entries = modifiers ? ModifierEntries : RoleEntries; + var shown = UpdateEntries(entries, modifiers); + 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); } } } - advancedWikiTab?.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 num = 0; - foreach (var (roleData, panel) in RolePanels) + var instance = MatchInfoGuide.Instance; + if (modifiers) { - var amount = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleData.Role); - var chance = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(roleData.Role); - var forciblyShow = roleData is ICustomRole custom ? custom.ForceShowRoleOnWiki : null; - if (amount == 0 || chance == 0 || (Enum.IsDefined(roleData.Role) && roleData.IsRoleBlacklisted()) || - (roleData is ICustomRole custom2 && ((!custom2.CanSpawnOnCurrentMode() && forciblyShow == null) || - (forciblyShow.HasValue && !forciblyShow.Value)))) + _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); + if (Enum.IsDefined(stepUp)) + { + _sortFilter = stepUp; + } + else + { + _sortFilter = SortingFilter.EnabledOnly; + } + + MiraApiPlugin.WikiSortingFilter.Value = _sortFilter; + + _searchSortingFilterIdle.sprite = GetSortFilterSprite(_sortFilter, false); + _searchSortingFilterHover.sprite = GetSortFilterSprite(_sortFilter, true); + + RefreshActiveTab(false); + } + + private static Sprite GetSortFilterSprite(SortingFilter value, bool hover) + { + if (hover) + { + switch (value) { - panel.gameObject.SetActive(false); - continue; + case SortingFilter.AllRoles: + return MiraAssets.VisibleAllHoverSprite; + case SortingFilter.DisabledOnly: + return MiraAssets.VisibleOffHoverSprite; + default: + return MiraAssets.VisibleOnHoverSprite; } - - panel.gameObject.SetActive(true); - panel.SetPanel( - roleData, - amount, - chance); - num++; } - if (num == 0) + switch (value) { - instance.rolesEnabledMessage.SetActive(true); + case SortingFilter.AllRoles: + return MiraAssets.VisibleAllIdleSprite; + case SortingFilter.DisabledOnly: + return MiraAssets.VisibleOffIdleSprite; + default: + return MiraAssets.VisibleOnIdleSprite; + } + } + + public static void SwitchMethod() + { + var stepUp = (SortingMethod)((int)_sortMethod + 1); + if (Enum.IsDefined(stepUp)) + { + _sortMethod = stepUp; + } + else + { + _sortMethod = SortingMethod.Alphabetical; } - instance.MatchInfoRoleScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); - instance.MatchInfoRoleScroller.SetYBoundsMax(Mathf.Clamp(Mathf.Ceil(num / 2f) * 1.3f - 1.5f, 0f, 999f)); - if (reset) + MiraApiPlugin.WikiSortingMethod.Value = _sortMethod; + + _searchSortingOrderIdle.sprite = GetSortMethodSprite(_sortMethod, false); + _searchSortingOrderHover.sprite = GetSortMethodSprite(_sortMethod, true); + + RefreshActiveTab(false); + } + + private static Sprite GetSortMethodSprite(SortingMethod value, bool hover) + { + if (hover) { - instance.CreatePlayerEntries(); + return value is SortingMethod.Alphabetical + ? MiraAssets.SortingAzHoverSprite + : MiraAssets.SortingZaHoverSprite; } + + return _sortMethod is SortingMethod.Alphabetical + ? MiraAssets.SortingAzIdleSprite + : MiraAssets.SortingZaIdleSprite; + } + + public static void SwitchGrouping() + { + var stepUp = (SortingGroups)((int)_sortGrouping + 1); + if (Enum.IsDefined(stepUp)) + { + _sortGrouping = stepUp; + } + else + { + _sortGrouping = SortingGroups.Ungrouped; + } + + MiraApiPlugin.WikiSortingGroup.Value = _sortGrouping; + + _searchSortingGroupIdle.sprite = GetSortGroupSprite(_sortGrouping, false); + _searchSortingGroupHover.sprite = GetSortGroupSprite(_sortGrouping, true); + + RefreshActiveTab(false); + } + + private static Sprite GetSortGroupSprite(SortingGroups value, bool hover) + { + if (hover) + { + switch (value) + { + case SortingGroups.Faction: + return MiraAssets.GroupFactionHoverSprite; + case SortingGroups.AmountChance: + return MiraAssets.GroupCountHoverSprite; + default: + return MiraAssets.GroupNoneHoverSprite; + } + } + + switch (value) + { + case SortingGroups.Faction: + return MiraAssets.GroupFactionIdleSprite; + case SortingGroups.AmountChance: + return MiraAssets.GroupCountIdleSprite; + default: + return MiraAssets.GroupNoneIdleSprite; + } + } + + private static void CopyTextLayout(TextMeshPro source, TextMeshPro target) + { + target.font = source.font; + target.fontSharedMaterial = source.fontSharedMaterial; + target.fontSize = source.fontSize; + target.fontSizeMin = source.fontSizeMin; + target.fontSizeMax = source.fontSizeMax; + target.enableAutoSizing = source.enableAutoSizing; + target.overflowMode = source.overflowMode; + target.alignment = source.alignment; + target.horizontalAlignment = source.horizontalAlignment; + target.verticalAlignment = source.verticalAlignment; + target.margin = source.margin; + target.enableWordWrapping = source.enableWordWrapping; + var sourceRect = source.rectTransform; + var targetRect = target.rectTransform; + targetRect.anchorMin = sourceRect.anchorMin; + targetRect.anchorMax = sourceRect.anchorMax; + targetRect.pivot = sourceRect.pivot; + targetRect.sizeDelta = sourceRect.sizeDelta; + targetRect.anchoredPosition = sourceRect.anchoredPosition; + targetRect.localScale = sourceRect.localScale; + targetRect.localRotation = sourceRect.localRotation; + targetRect.localPosition = sourceRect.localPosition; + } + + private static void SetupPanelButton(MatchInfoRolePanel panel, Action onClick) + { + var collider = panel.roleIcon.gameObject.AddComponent(); + collider.size = new Vector2(0.13f, 0.13f); + collider.offset = new Vector2(0, 0); + var passiveButton = panel.roleIcon.gameObject.AddComponent(); + passiveButton.ClickSound = HudManager.Instance.MapButton.ClickSound; + passiveButton.OnMouseOver = new UnityEvent(); + passiveButton.OnMouseOver.AddListener( + (UnityAction)(() => { panel.roleIcon.color = new Color32(255, 255, 255, 150); })); + passiveButton.OnMouseOut = new UnityEvent(); + passiveButton.OnMouseOut.AddListener((UnityAction)(() => { panel.roleIcon.color = Color.white; })); + + passiveButton.OnClick = new Button.ButtonClickedEvent(); + passiveButton.OnClick.AddListener(onClick); } [HarmonyPrefix] @@ -282,10 +1234,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) { @@ -301,41 +1256,158 @@ public static class RoleGuidePatches [HarmonyPostfix] [HarmonyPriority(Priority.First)] [HarmonyPatch(typeof(MatchInfoGuide), nameof(MatchInfoGuide.SetActiveTab))] - private static void SetActiveTab() + private static void SetActiveTab(int tabIndex) { - if (titleText) + if (_titleText && _titleIcon) { - titleText.text = TranslationController.Instance.GetString(StringNames.MatchInfoGuideTitle); + switch (tabIndex) + { + case 0: + _titleText.text = MiraLocaleManager.Get("MiraApi.Wiki.PlayersTab"); + _titleIcon.sprite = MiraAssets.IconPlayerList.LoadAsset(); + break; + case 1: + _titleText.text = MiraLocaleManager.Get("MiraApi.Wiki.SettingsTab"); + _titleIcon.sprite = MiraAssets.IconSettings.LoadAsset(); + break; + case RolesTabIndex: + _titleText.text = MiraLocaleManager.Get("Roles"); + _titleIcon.sprite = GetRandomRoleIcon(); + break; + case ModifiersTabIndex: + _titleText.text = MiraLocaleManager.Get("Modifiers"); + _titleIcon.sprite = GetRandomModifierIcon(); + break; + default: + _titleText.text = TranslationController.Instance.GetString(StringNames.MatchInfoGuideTitle); + _titleIcon.sprite = MiraAssets.IconPlayerList.LoadAsset(); + break; + } + + _titleIcon.SetSizeLimit(0.55f); } - advancedWikiTab?.gameObject.SetActive(false); + _advancedInfoTabScroller?.gameObject.SetActive(false); + SearchComponentContainer?.SetActive(true); + UpdateSpinner(); + } + + public static Sprite GetRandomModifierIcon() + { + var activeMods = PlayerControl.LocalPlayer.GetModifiers() + .Where(x => !x.HideOnUi && x.ModifierIcon != null).ToList(); + if (activeMods.Count > 0) + { + return activeMods.Random()!.ModifierIcon!.LoadAsset(); + } + + return GetRandomFactionIcon(); + } + + public static Sprite GetRandomRoleIcon() + { + var aliveRole = PlayerControl.LocalPlayer.Data?.Role; + if (aliveRole != null && aliveRole.RoleIconSolid != null) + { + return aliveRole.RoleIconSolid; + } + + return GetRandomFactionIcon(); + } + + public static Sprite GetRandomFactionIcon() + { + var random = UnityEngine.Random.Range(1, 4); + if (random == 1) + { + return MiraAssets.IconCrewmate.LoadAsset(); + } + + if (random == 2) + { + return MiraAssets.IconImpostor.LoadAsset(); + } + + return MiraAssets.IconNeutral.LoadAsset(); + } + + private static bool TryPrepareAdvancedWiki(MatchInfoGuide instance) + { + instance.settingsTabs[RolesTabIndex].SetActive(false); + _modifiersScroller?.gameObject.SetActive(false); + if (_currentAdvancedTabObject) + { + _currentAdvancedTabObject.SetActive(false); + _currentAdvancedTabObject.Destroy(); + } + + if (SearchComponentContainer) + { + SearchComponentContainer.SetActive(false); + } + + 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 remaining = _openedAt + instance.transitionOpen.duration - Time.time; + if (remaining > 0f) + { + Coroutines.Start(CoDisplayAdvancedWikiLater(remaining, () => DisplayAdvancedWiki(instance, modifier))); + return; + } + + var name = modifier.ModifierName; + Info($"Opening advanced tab for {name}."); + if (!TryPrepareAdvancedWiki(instance)) + { + return; + } + + _currentAdvancedTabObject = modifier.GetAdvancedWiki(instance, _titleText, _titleIcon, _advancedInfoTabScroller); } public static void DisplayAdvancedWiki(MatchInfoGuide instance, RoleBehaviour role) { - Warning($"Opening advanced tab for {role.GetRoleName()}."); - instance.settingsTabs[2].SetActive(false); - advancedWikiTab?.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 (advancedWikiTab == 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, advancedWikiTab); + _currentAdvancedTabObject = custom.GetAdvancedWiki(instance, _titleText, _titleIcon, _advancedInfoTabScroller); } else { - advancedWikiTab.ScrollToTop(); - var titleTxt = role.GetRoleName() + $" ({TranslationController.Instance.GetString(role.TeamType is RoleTeamTypes.Crewmate ? StringNames.Crewmate : StringNames.Impostor)})"; + var titleTxt = $"{role.GetRoleName()}" + + $" ({TranslationController.Instance.GetString(role.TeamType is RoleTeamTypes.Crewmate ? StringNames.Crewmate : StringNames.Impostor)})"; var description = TranslationController.Instance.GetString(role.BlurbNameLong); if (description.Contains("STRMISS")) { @@ -346,16 +1418,16 @@ public static class RoleGuidePatches } } - currentAdvancedTabObject = Helpers.CreateAdvancedWikiTab( + _currentAdvancedTabObject = Helpers.CreateAdvancedWikiPage( instance, + _titleText, + _titleIcon, + _advancedInfoTabScroller, role.Role.ToString(), titleTxt, + role.RoleIconColor, description, - titleText, - out var desc); - currentAdvancedTabObject.transform.SetParent(advancedWikiTab.Inner.transform); - currentAdvancedTabObject.transform.localPosition = new Vector3(0f, 0f, 0f); - advancedWikiTab.SetYBoundsMax(Mathf.Clamp(desc.textBounds.size.y - 2, 0f, 999f)); + []); } } @@ -364,18 +1436,23 @@ public static class RoleGuidePatches [HarmonyPatch(typeof(MatchInfoRolePanel), nameof(MatchInfoRolePanel.SetPanel))] public static bool SetPanel(MatchInfoRolePanel __instance, RoleBehaviour role, int numPerGame, int chancePerGame) { - __instance.roleCount.text = string.Format(CultureInfo.InvariantCulture, "{0} at {1}%", numPerGame.ToString(CultureInfo.InvariantCulture), 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 = $"{customRole.RoleFactionTitle}\n" + customRole.RoleMedDescription; + __instance.roleDescription.text = + $"{role.GetCategoryTitle()}\n" + customRole.RoleMedDescription; __instance.roleIcon.sprite = customRole.Configuration.Icon?.LoadAsset(); __instance.roleCount.text += $" ({customRole.ParentMod.MiraPlugin.GetAbbreviatedModName()})"; } else { __instance.roleName.text = role.NiceName; - __instance.roleDescription.text = $"{TranslationController.Instance.GetString(role.TeamType is RoleTeamTypes.Crewmate ? StringNames.Crewmate : StringNames.Impostor)}\n" + role.BlurbMed; + __instance.roleDescription.text = $"{role.GetCategoryTitle()}\n" + role.BlurbMed; __instance.roleIcon.sprite = role.RoleIconColor; __instance.roleCount.text += " (AU)"; } @@ -388,4 +1465,100 @@ public static class RoleGuidePatches __instance.roleIcon.transform.localScale = new Vector3(4f, 4f, 1f); 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) + { + 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(); + 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; + SetPanelCount(instance, numPerGame, chancePerGame, "AU"); + } + + instance.roleIcon.SetSizeLimit(0.13f); + instance.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50); + instance.roleName.fontMaterial.SetFloat(instance.STENCIL_NAME, 50f); + instance.roleDescription.fontMaterial.SetFloat(instance.STENCIL_NAME, 50f); + instance.roleCount.fontMaterial.SetFloat(instance.STENCIL_NAME, 50f); + instance.roleIcon.transform.localScale = new Vector3(4f, 4f, 1f); + } + + public static void SetModifierPanel( + this MatchInfoRolePanel instance, + GameModifier modifier, + int numPerGame, + int chancePerGame) + { + instance.roleName.text = modifier.ModifierName; + instance.roleDescription.text = + $"{modifier.ModifierCategoryTitle}\n" + modifier.ModifierMedDescription; + instance.roleIcon.sprite = modifier.ModifierIcon?.LoadAsset(); + SetPanelCount(instance, numPerGame, chancePerGame, modifier.ParentMod.MiraPlugin.GetAbbreviatedModName()); + + instance.roleIcon.SetSizeLimit(0.13f); + instance.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50); + instance.roleName.fontMaterial.SetFloat(instance.STENCIL_NAME, 50f); + instance.roleDescription.fontMaterial.SetFloat(instance.STENCIL_NAME, 50f); + instance.roleCount.fontMaterial.SetFloat(instance.STENCIL_NAME, 50f); + instance.roleIcon.transform.localScale = new Vector3(4f, 4f, 1f); + } + + public static string GetCategoryTitle(this RoleBehaviour role) + { + if (role is ICustomRole customRole) + { + return customRole.GetCategoryTitle(); + } + + return TranslationController.Instance.GetString( + role.TeamType is RoleTeamTypes.Crewmate ? StringNames.Crewmate : StringNames.Impostor); + } + + public static string GetCategoryTitle(this ICustomRole customRole) + { + return customRole.RoleFactionTitle; + } +} + +public enum SortingGroups +{ + Ungrouped, + Faction, + AmountChance, +} + +public enum SortingMethod +{ + Alphabetical, + AlphabeticalDescending, +} + +public enum SortingFilter +{ + EnabledOnly, + DisabledOnly, + AllRoles, } diff --git a/MiraAPI/Patches/Roles/KillButtonPatches.cs b/MiraAPI/Patches/Roles/KillButtonPatches.cs index fc32a7a..9693346 100644 --- a/MiraAPI/Patches/Roles/KillButtonPatches.cs +++ b/MiraAPI/Patches/Roles/KillButtonPatches.cs @@ -1,5 +1,4 @@ using HarmonyLib; -using Il2CppSystem; using MiraAPI.Networking; using MiraAPI.Roles; using UnityEngine; @@ -31,13 +30,13 @@ public static class KillButtonPatches if (__instance.currentTarget && __instance.currentTarget != target) { - __instance.currentTarget.cosmetics.SetOutline(false, new Nullable(Color.clear)); + __instance.currentTarget.cosmetics.SetOutline(false, new Il2CppSystem.Nullable(Color.clear)); } __instance.currentTarget = target; if (__instance.currentTarget) { - __instance.currentTarget.cosmetics.SetOutline(true, new Nullable(customRole.Configuration.KillButtonOutlineColor)); + __instance.currentTarget.cosmetics.SetOutline(true, new Il2CppSystem.Nullable(customRole.Configuration.KillButtonOutlineColor)); __instance.SetEnabled(); return false; } diff --git a/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs b/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs index 1a845fc..c1e9da7 100644 --- a/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs +++ b/MiraAPI/Patches/Roles/LogicRoleSelectionHnsPatch.cs @@ -1,8 +1,8 @@ using System; +using System.Collections.Generic; using System.Linq; using AmongUs.GameOptions; using HarmonyLib; -using Il2CppSystem.Collections.Generic; using MiraAPI.Roles; namespace MiraAPI.Patches.Roles; @@ -14,7 +14,7 @@ public static class LogicRoleSelectionHnsPatch [HarmonyPatch(nameof(LogicRoleSelectionHnS.AssignRolesForTeam))] public static bool AssignRolesForTeam( LogicRoleSelectionHnS __instance, - List players, + Il2CppSystem.Collections.Generic.List players, IGameOptions opts, RoleTeamTypes team, int teamMax, @@ -58,7 +58,7 @@ public static class LogicRoleSelectionHnsPatch case RoleTeamTypes.Impostor: Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment"); - var newImpostors = new List(); + var newImpostors = new Il2CppSystem.Collections.Generic.List(); // Specified Seeker if (__instance.hnsManager.LogicOptionsHnS.HasImpostorPlayerID() && @@ -113,7 +113,7 @@ public static class LogicRoleSelectionHnsPatch return false; } - public static void AssignRolesFromList(List players, int teamMax, List roleList, ref int rolesAssigned) + public static void AssignRolesFromList(Il2CppSystem.Collections.Generic.List players, int teamMax, List roleList, ref int rolesAssigned) { while (roleList.Count > 0 && players.Count > 0 && rolesAssigned < teamMax) { @@ -128,7 +128,7 @@ public static class LogicRoleSelectionHnsPatch } private static void AddGuaranteedRoles( - System.Collections.Generic.IEnumerable guaranteedRoles, + IEnumerable guaranteedRoles, IGameOptions opts, List list) { @@ -146,7 +146,7 @@ public static class LogicRoleSelectionHnsPatch } } - private static void AddPotentialRoles(System.Collections.Generic.IEnumerable source, IGameOptions opts, List list) + private static void AddPotentialRoles(IEnumerable source, IGameOptions opts, List list) { var roleOptions = opts.RoleOptions; var potentialRoles = source.Where(x => !x.IsDead).Select(role => new RoleManager.RoleAssignmentData( diff --git a/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs b/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs index 69b4089..f047fd3 100644 --- a/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs +++ b/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs @@ -2,7 +2,6 @@ using System.Runtime.CompilerServices; using HarmonyLib; using Hazel; -using Il2CppSystem.Text; namespace MiraAPI.Patches.Stubs; @@ -45,9 +44,9 @@ public static class RoleBehaviourStubs /// The object. /// The to append the task hint to. [HarmonyReversePatch] - [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.AppendTaskHint), typeof(StringBuilder))] + [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.AppendTaskHint), typeof(Il2CppSystem.Text.StringBuilder))] [MethodImpl(MethodImplOptions.NoInlining)] - public static void AppendTaskHint(RoleBehaviour instance, StringBuilder taskStringBuilder) + public static void AppendTaskHint(RoleBehaviour instance, Il2CppSystem.Text.StringBuilder taskStringBuilder) { // nothing needed } 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/Presets/SavePresetPopup.cs b/MiraAPI/Presets/SavePresetPopup.cs index 8df354c..b9932ff 100644 --- a/MiraAPI/Presets/SavePresetPopup.cs +++ b/MiraAPI/Presets/SavePresetPopup.cs @@ -9,7 +9,10 @@ using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; +// TODO: for the love of fucking god please add a namespace on the asset bundle AND here - Atony. +/* namespace MiraAPI.Presets; +*/ [RegisterInIl2Cpp] [SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity convention.")] diff --git a/MiraAPI/Resources/Locale/en_US.xml b/MiraAPI/Resources/Locale/en_US.xml index 7cd2c1c..ba60484 100644 --- a/MiraAPI/Resources/Locale/en_US.xml +++ b/MiraAPI/Resources/Locale/en_US.xml @@ -17,6 +17,7 @@ Custom Category 2 Configure modifiers and their settings here! Apply game settings for this mod! + Gamemode Classic The classic Among Us experience! Find the Impostors or deceive the Crew. @@ -43,4 +44,19 @@ Outlive the timer or eliminate the crew. Page Page Main + Ability + Indirect Ability + Interaction Ability + Radius Ability + Menu Ability + Meeting Ability + Selective Meeting Ability + Meeting Menu Ability + Options + Player List + Game Settings + Search... + Filter + Grouping + Order diff --git a/MiraAPI/Resources/Wiki/GroupCountHover.png b/MiraAPI/Resources/Wiki/GroupCountHover.png new file mode 100644 index 0000000..0e8ec76 Binary files /dev/null and b/MiraAPI/Resources/Wiki/GroupCountHover.png differ diff --git a/MiraAPI/Resources/Wiki/GroupCountIdle.png b/MiraAPI/Resources/Wiki/GroupCountIdle.png new file mode 100644 index 0000000..5bf5f98 Binary files /dev/null and b/MiraAPI/Resources/Wiki/GroupCountIdle.png differ diff --git a/MiraAPI/Resources/Wiki/GroupFactionHover.png b/MiraAPI/Resources/Wiki/GroupFactionHover.png new file mode 100644 index 0000000..864e94d Binary files /dev/null and b/MiraAPI/Resources/Wiki/GroupFactionHover.png differ diff --git a/MiraAPI/Resources/Wiki/GroupFactionIdle.png b/MiraAPI/Resources/Wiki/GroupFactionIdle.png new file mode 100644 index 0000000..1d50880 Binary files /dev/null and b/MiraAPI/Resources/Wiki/GroupFactionIdle.png differ diff --git a/MiraAPI/Resources/Wiki/GroupNoneHover.png b/MiraAPI/Resources/Wiki/GroupNoneHover.png new file mode 100644 index 0000000..dd303b2 Binary files /dev/null and b/MiraAPI/Resources/Wiki/GroupNoneHover.png differ diff --git a/MiraAPI/Resources/Wiki/GroupNoneIdle.png b/MiraAPI/Resources/Wiki/GroupNoneIdle.png new file mode 100644 index 0000000..c215f2f Binary files /dev/null and b/MiraAPI/Resources/Wiki/GroupNoneIdle.png differ diff --git a/MiraAPI/Resources/Wiki/IconCrewmate.png b/MiraAPI/Resources/Wiki/IconCrewmate.png new file mode 100644 index 0000000..61f706f Binary files /dev/null and b/MiraAPI/Resources/Wiki/IconCrewmate.png differ diff --git a/MiraAPI/Resources/Wiki/IconImpostor.png b/MiraAPI/Resources/Wiki/IconImpostor.png new file mode 100644 index 0000000..6100cd1 Binary files /dev/null and b/MiraAPI/Resources/Wiki/IconImpostor.png differ diff --git a/MiraAPI/Resources/Wiki/IconNeutral.png b/MiraAPI/Resources/Wiki/IconNeutral.png new file mode 100644 index 0000000..984041c Binary files /dev/null and b/MiraAPI/Resources/Wiki/IconNeutral.png differ diff --git a/MiraAPI/Resources/Wiki/IconPlayerList.png b/MiraAPI/Resources/Wiki/IconPlayerList.png new file mode 100644 index 0000000..234baa0 Binary files /dev/null and b/MiraAPI/Resources/Wiki/IconPlayerList.png differ diff --git a/MiraAPI/Resources/Wiki/IconSettings.png b/MiraAPI/Resources/Wiki/IconSettings.png new file mode 100644 index 0000000..50452dc Binary files /dev/null and b/MiraAPI/Resources/Wiki/IconSettings.png differ diff --git a/MiraAPI/Resources/Wiki/IconTerminology.png b/MiraAPI/Resources/Wiki/IconTerminology.png new file mode 100644 index 0000000..8b95d62 Binary files /dev/null and b/MiraAPI/Resources/Wiki/IconTerminology.png differ diff --git a/MiraAPI/Resources/Wiki/SearchClearHover.png b/MiraAPI/Resources/Wiki/SearchClearHover.png new file mode 100644 index 0000000..a23a1da Binary files /dev/null and b/MiraAPI/Resources/Wiki/SearchClearHover.png differ diff --git a/MiraAPI/Resources/Wiki/SearchClearIdle.png b/MiraAPI/Resources/Wiki/SearchClearIdle.png new file mode 100644 index 0000000..7f50a9b Binary files /dev/null and b/MiraAPI/Resources/Wiki/SearchClearIdle.png differ diff --git a/MiraAPI/Resources/Wiki/SearchIconHover.png b/MiraAPI/Resources/Wiki/SearchIconHover.png new file mode 100644 index 0000000..d64f9af Binary files /dev/null and b/MiraAPI/Resources/Wiki/SearchIconHover.png differ diff --git a/MiraAPI/Resources/Wiki/SearchIconIdle.png b/MiraAPI/Resources/Wiki/SearchIconIdle.png new file mode 100644 index 0000000..d16f7f9 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SearchIconIdle.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarPlayersButtonHover.png b/MiraAPI/Resources/Wiki/SidebarPlayersButtonHover.png new file mode 100644 index 0000000..c4bd871 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarPlayersButtonHover.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarPlayersButtonIdle.png b/MiraAPI/Resources/Wiki/SidebarPlayersButtonIdle.png new file mode 100644 index 0000000..6c991be Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarPlayersButtonIdle.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarPlayersButtonOpen.png b/MiraAPI/Resources/Wiki/SidebarPlayersButtonOpen.png new file mode 100644 index 0000000..6e6ccbf Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarPlayersButtonOpen.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarRolesButtonHover.png b/MiraAPI/Resources/Wiki/SidebarRolesButtonHover.png new file mode 100644 index 0000000..8bfb8d1 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarRolesButtonHover.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarRolesButtonIdle.png b/MiraAPI/Resources/Wiki/SidebarRolesButtonIdle.png new file mode 100644 index 0000000..c75d077 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarRolesButtonIdle.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarRolesButtonOpen.png b/MiraAPI/Resources/Wiki/SidebarRolesButtonOpen.png new file mode 100644 index 0000000..162bf07 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarRolesButtonOpen.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarSettingsButtonHover.png b/MiraAPI/Resources/Wiki/SidebarSettingsButtonHover.png new file mode 100644 index 0000000..7813904 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarSettingsButtonHover.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarSettingsButtonIdle.png b/MiraAPI/Resources/Wiki/SidebarSettingsButtonIdle.png new file mode 100644 index 0000000..fdf65df Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarSettingsButtonIdle.png differ diff --git a/MiraAPI/Resources/Wiki/SidebarSettingsButtonOpen.png b/MiraAPI/Resources/Wiki/SidebarSettingsButtonOpen.png new file mode 100644 index 0000000..ec5bb14 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SidebarSettingsButtonOpen.png differ diff --git a/MiraAPI/Resources/Wiki/SortingAzHover.png b/MiraAPI/Resources/Wiki/SortingAzHover.png new file mode 100644 index 0000000..d9779c6 Binary files /dev/null and b/MiraAPI/Resources/Wiki/SortingAzHover.png differ diff --git a/MiraAPI/Resources/Wiki/SortingAzIdle.png b/MiraAPI/Resources/Wiki/SortingAzIdle.png new file mode 100644 index 0000000..5efb21b Binary files /dev/null and b/MiraAPI/Resources/Wiki/SortingAzIdle.png differ diff --git a/MiraAPI/Resources/Wiki/SortingZaHover.png b/MiraAPI/Resources/Wiki/SortingZaHover.png new file mode 100644 index 0000000..c9d817a Binary files /dev/null and b/MiraAPI/Resources/Wiki/SortingZaHover.png differ diff --git a/MiraAPI/Resources/Wiki/SortingZaIdle.png b/MiraAPI/Resources/Wiki/SortingZaIdle.png new file mode 100644 index 0000000..199071a Binary files /dev/null and b/MiraAPI/Resources/Wiki/SortingZaIdle.png differ diff --git a/MiraAPI/Resources/Wiki/VisibleAllHover.png b/MiraAPI/Resources/Wiki/VisibleAllHover.png new file mode 100644 index 0000000..9380f83 Binary files /dev/null and b/MiraAPI/Resources/Wiki/VisibleAllHover.png differ diff --git a/MiraAPI/Resources/Wiki/VisibleAllIdle.png b/MiraAPI/Resources/Wiki/VisibleAllIdle.png new file mode 100644 index 0000000..9ddcaee Binary files /dev/null and b/MiraAPI/Resources/Wiki/VisibleAllIdle.png differ diff --git a/MiraAPI/Resources/Wiki/VisibleOffHover.png b/MiraAPI/Resources/Wiki/VisibleOffHover.png new file mode 100644 index 0000000..ecfaefe Binary files /dev/null and b/MiraAPI/Resources/Wiki/VisibleOffHover.png differ diff --git a/MiraAPI/Resources/Wiki/VisibleOffIdle.png b/MiraAPI/Resources/Wiki/VisibleOffIdle.png new file mode 100644 index 0000000..a0c8ea9 Binary files /dev/null and b/MiraAPI/Resources/Wiki/VisibleOffIdle.png differ diff --git a/MiraAPI/Resources/Wiki/VisibleOnHover.png b/MiraAPI/Resources/Wiki/VisibleOnHover.png new file mode 100644 index 0000000..a4e5be8 Binary files /dev/null and b/MiraAPI/Resources/Wiki/VisibleOnHover.png differ diff --git a/MiraAPI/Resources/Wiki/VisibleOnIdle.png b/MiraAPI/Resources/Wiki/VisibleOnIdle.png new file mode 100644 index 0000000..30f1067 Binary files /dev/null and b/MiraAPI/Resources/Wiki/VisibleOnIdle.png differ diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs index 060b1a5..0548eaa 100644 --- a/MiraAPI/Roles/CustomRoleManager.cs +++ b/MiraAPI/Roles/CustomRoleManager.cs @@ -45,6 +45,11 @@ public static class CustomRoleManager /// public static IReadOnlyList CustomRoleBehaviours { get; private set; } = []; + /// + /// Gets the of all roles as objects. + /// + public static IReadOnlyList AllStoredRoleBehaviours { get; private set; } = []; + /// /// Gets the of custom roles as objects. /// @@ -52,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; @@ -77,6 +83,7 @@ public static class CustomRoleManager CustomRoleBehaviours = [.. CustomRoles.Values]; CustomMiraRoles = [.. CustomRoles.Values.OfType()]; + AllStoredRoleBehaviours = [.. RoleManager.Instance.AllRoles]; } internal static void RegisterRoleTypes(List roles, MiraPluginInfo pluginInfo) @@ -99,6 +106,7 @@ public static class CustomRoleManager } pluginInfo.InternalRoles.Add((ushort)role.Role, role); + RoleParentMods[roleType] = pluginInfo; } catch (Exception ex) { @@ -205,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 0030024..4d5e247 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -1,12 +1,15 @@ using System; +using System.Collections.Generic; using System.Text; using BepInEx.Configuration; +using Il2CppInterop.Runtime.Attributes; using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.PluginLoading; using MiraAPI.Translation; using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; using TMPro; using UnityEngine; @@ -68,9 +71,10 @@ public interface ICustomRole : IOptionable string RoleLongDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "TabDescription"); /// - /// Gets the wiki description of the role. Used in the wiki, but not currently required. + /// Gets the wiki description of the role. Used in the wiki and normally appends the options text as well. /// - string RoleWikiDescription => MiraLocaleManager.Get(RoleWikiDescriptionLocale, RoleLongDescription); + string RoleWikiDescription => MiraLocaleManager.Get(RoleWikiDescriptionLocale, RoleLongDescription) + + Helpers.GetOptionsText(GetType()); /// /// Gets the role's wiki description id for localization. @@ -314,26 +318,34 @@ public interface ICustomRole : IOptionable /// /// The guide object. /// The title text object. + /// The title's icon. /// The scroller parent. /// The of the wiki. - GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, Scroller parent) + GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, SpriteRenderer titleIcon, Scroller parent) { - parent.ScrollToTop(); - var obj = Helpers.CreateAdvancedWikiTab( + var abilities = WikiAbilities; + return Helpers.CreateAdvancedWikiPage( guide, - RoleNameLocale, - RoleName + $" ({RoleFactionTitle})", - RoleWikiDescription, titleText, - out var desc); - obj.transform.SetParent(parent.Inner.transform); - obj.transform.localPosition = new Vector3(0f, 0f, 0f); - parent.SetYBoundsMax(Mathf.Clamp(desc.textBounds.size.y - 2, 0f, 999f)); - return obj; + titleIcon, + parent, + RoleNameLocale, + $"{RoleName} ({RoleFactionTitle})", + Configuration.Icon ?? MiraAssets.IconNeutral.LoadAsset(), + RoleWikiDescription, + abilities); } /// /// Gets the function that determines whether the role should be toggled on or off in the game settings. /// Func VisibleInSettings => () => true; + + /// + /// Gets the list of abilities or other similar information to display below the text of a role's wiki page. + /// + [HideFromIl2Cpp] + public List WikiAbilities => []; } + +public record struct AdvancedWikiAbilityDescription(string Name, string AbilityType, string Description, LoadableAsset Icon); diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index bec994a..0feb75c 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); @@ -160,5 +160,180 @@ public static class MiraAssets /// public static LoadableResourceAsset ChatOpenSprite { get; } = new("MiraAPI.Resources.NormalChatOpen.png"); + /// + /// Gets the sprite used for players button while hovering. + /// + public static LoadableResourceAsset WikiPlayersButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarPlayersButtonHover.png"); + + /// + /// Gets the sprite used for the players button while hovering. + /// + public static LoadableResourceAsset WikiPlayersButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarPlayersButtonIdle.png"); + + /// + /// Gets the sprite used for the players button while hovering. + /// + public static LoadableResourceAsset WikiPlayersButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarPlayersButtonOpen.png"); + + /// + /// Gets the sprite used for the settings button while hovering. + /// + public static LoadableResourceAsset WikiSettingsButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarSettingsButtonHover.png"); + + /// + /// Gets the sprite used for the settings button while hovering. + /// + public static LoadableResourceAsset WikiSettingsButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarSettingsButtonIdle.png"); + + /// + /// Gets the sprite used for the settings button while hovering. + /// + public static LoadableResourceAsset WikiSettingsButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarSettingsButtonOpen.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset WikiRolesButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset WikiRolesButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset WikiRolesButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonOpen.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset GroupCountHoverSprite { get; } = new("MiraAPI.Resources.Wiki.GroupCountHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset GroupCountIdleSprite { get; } = new("MiraAPI.Resources.Wiki.GroupCountIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset GroupFactionHoverSprite { get; } = new("MiraAPI.Resources.Wiki.GroupFactionHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset GroupFactionIdleSprite { get; } = new("MiraAPI.Resources.Wiki.GroupFactionIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset GroupNoneHoverSprite { get; } = new("MiraAPI.Resources.Wiki.GroupNoneHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset GroupNoneIdleSprite { get; } = new("MiraAPI.Resources.Wiki.GroupNoneIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset SearchClearHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SearchClearHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset SearchClearIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SearchClearIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset SearchIconHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SearchIconHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset SearchIconIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SearchIconIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset SortingAzHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SortingAzHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset SortingAzIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SortingAzIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset SortingZaHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SortingZaHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset SortingZaIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SortingZaIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset VisibleAllHoverSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleAllHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset VisibleAllIdleSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleAllIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset VisibleOffHoverSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOffHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset VisibleOffIdleSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOffIdle.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset VisibleOnHoverSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOnHover.png"); + + /// + /// Gets the sprite used for the roles button while hovering. + /// + public static LoadableResourceAsset VisibleOnIdleSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOnIdle.png"); + + /// + /// Gets the sprite used for crewmates. + /// + public static LoadableResourceAsset IconCrewmate { get; } = new("MiraAPI.Resources.Wiki.IconCrewmate.png"); + + /// + /// Gets the sprite used for impostors. + /// + public static LoadableResourceAsset IconImpostor { get; } = new("MiraAPI.Resources.Wiki.IconImpostor.png"); + + /// + /// Gets the sprite used for neutrals. + /// + public static LoadableResourceAsset IconNeutral { get; } = new("MiraAPI.Resources.Wiki.IconNeutral.png"); + + /// + /// Gets the sprite used for the player list. + /// + public static LoadableResourceAsset IconPlayerList { get; } = new("MiraAPI.Resources.Wiki.IconPlayerList.png"); + + /// + /// Gets the sprite used for settings. + /// + public static LoadableResourceAsset IconSettings { get; } = new("MiraAPI.Resources.Wiki.IconSettings.png"); + + /// + /// Gets the sprite used for terminology. + /// + public static LoadableResourceAsset IconTerminology { get; } = new("MiraAPI.Resources.Wiki.IconTerminology.png"); + internal static LoadableResourceAsset BlankSprite { get; } = new("MiraAPI.Resources.BlankSprite.png"); } diff --git a/MiraAPI/Utilities/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 f2d6d0c..a39a6c6 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -226,7 +226,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); } @@ -355,8 +355,8 @@ public static class Extensions public static void ClearGarbageCollector() { Resources.UnloadUnusedAssets(); - Il2CppSystem.GC.Collect(); - GC.Collect(); + /*Il2CppSystem.GC.Collect(); + GC.Collect();*/ } /// diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index eded3d3..773a4ae 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -1,17 +1,27 @@ -using System.Collections; +using System; +using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; 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; namespace MiraAPI.Utilities; @@ -20,6 +30,125 @@ 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 group = ModdedOptionsManager.Groups.FirstOrDefault(x => x.OptionableType == classType); + + return group?.Children; + } + + public static string GetOptionsText(Type 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; + } + + var lines = new List(); + var insertedSummary = false; + foreach (var option in options) + { + StringNames? key = option switch + { + ModdedToggleOption t => t.StringName, + ModdedEnumOption e => e.StringName, + ModdedNumberOption n => n.StringName, + _ => null, + }; + + 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: + lines.Add( + TranslationController.Instance.GetString(toggleOption.StringName) + ": " + + toggleOption.Value); + break; + /*case ModdedMultiSelectOption enumOption: + if (!enumOption.Visible()) + { + continue; + } + + builder.AppendLine(enumOption.Title + ": " + enumOption.Values[enumOption.Value]); + break;*/ + case ModdedEnumOption enumOption: + lines.Add( + TranslationController.Instance.GetString(enumOption.StringName) + ": " + + MiraLocaleManager.Get( + enumOption.Values[enumOption.Value], + enumOption.Values[enumOption.Value])); + break; + case ModdedNumberOption numberOption: + 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 }) + { + lines.Add(title + $": {numberOption.NegativeWordValue}"); + } + else if (numberOption is { ZeroWordValue: not "#", Value: 0 }) + { + lines.Add(title + $": {numberOption.ZeroWordValue}"); + } + else + { + 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(); + } + /// /// Get all living players. /// @@ -228,7 +357,7 @@ public static class Helpers var popper = HudManager.Instance.Notifier; var newMessage = Object.Instantiate(popper.notificationMessageOrigin, Vector3.zero, Quaternion.identity, popper.transform); newMessage.transform.localPosition = localPos; - newMessage.SetUp(text, spr ?? null, color, new System.Action(() => popper.OnMessageDestroy(newMessage))); + newMessage.SetUp(text, spr ?? null, color, new Action(() => popper.OnMessageDestroy(newMessage))); popper.lastMessageKey = -1; popper.ShiftMessages(); popper.AddMessageToQueue(newMessage); @@ -280,9 +409,12 @@ public static class Helpers { var results = new CppCollections.List(); Physics2D.OverlapCircle(source, radius, filter, results); - return [.. results.ToArray() - .Where(collider2D => collider2D.CompareTag("DeadBody")) - .Select(collider2D => collider2D.GetComponent())]; + return + [ + .. results.ToArray() + .Where(collider2D => collider2D.CompareTag("DeadBody")) + .Select(collider2D => collider2D.GetComponent()) + ]; } /// @@ -299,9 +431,12 @@ public static class Helpers { var results = new CppCollections.List(); Physics2D.OverlapCircle(source, radius, filter, results); - return [.. results.ToArray() - .Where(collider2D => colliderTag == null || collider2D.CompareTag(colliderTag)) - .Select(collider2D => collider2D.GetComponent())]; + return + [ + .. results.ToArray() + .Where(collider2D => colliderTag == null || collider2D.CompareTag(colliderTag)) + .Select(collider2D => collider2D.GetComponent()) + ]; } /// @@ -320,15 +455,18 @@ public static class Helpers return !ignoreColliders ? newList - : [.. from player in newList - let vector = player.GetTruePosition() - source - let magnitude = vector.magnitude - where !PhysicsHelpers.AnyNonTriggersBetween( - source, - vector.normalized, - magnitude, - Constants.ShipAndObjectsMask) - select player]; + : + [ + .. from player in newList + let vector = player.GetTruePosition() - source + let magnitude = vector.magnitude + where !PhysicsHelpers.AnyNonTriggersBetween( + source, + vector.normalized, + magnitude, + Constants.ShipAndObjectsMask) + select player + ]; } /// @@ -389,17 +527,16 @@ public static class Helpers Constants.ShipAndObjectsMask)) select playerControl); - outputList.Sort( - (a, b) => - { - var magnitude2 = (a.GetTruePosition() - source).magnitude; - var magnitude3 = (b.GetTruePosition() - source).magnitude; - return magnitude2 > magnitude3 - ? 1 - : magnitude2 < magnitude3 - ? -1 - : 0; - }); + outputList.Sort((a, b) => + { + var magnitude2 = (a.GetTruePosition() - source).magnitude; + var magnitude3 = (b.GetTruePosition() - source).magnitude; + return magnitude2 > magnitude3 + ? 1 + : magnitude2 < magnitude3 + ? -1 + : 0; + }); return outputList; } @@ -552,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). /// @@ -570,14 +741,19 @@ public static class Helpers /// The guide panel to attach the tab to. /// The name of the tab. /// The title of the tab. + /// The icon of the page. /// The description of the tab. /// The instance of the title. + /// The instance for the title. /// The instance of the description. /// The of the tab. - public static GameObject CreateAdvancedWikiTab(MatchInfoGuide guide, string objName, string title, string description, TextMeshPro titleTmp, out TextMeshPro descriptionTmp) + public static GameObject CreateAdvancedWikiTab(MatchInfoGuide guide, string objName, string title, Sprite icon, string description, TextMeshPro titleTmp, SpriteRenderer titleIcon, out TextMeshPro descriptionTmp) { var obj = new GameObject(objName); + titleTmp.richText = true; titleTmp.text = title; + titleIcon.sprite = icon; + titleIcon.SetSizeLimit(0.55f); descriptionTmp = Object.Instantiate(guide.MatchInfoRolePanelPrefab.roleCount, obj.transform); descriptionTmp.fontSizeMin = descriptionTmp.fontSizeMax = descriptionTmp.fontSize = 2f; var sourceTmp = HudManager.Instance.TaskPanel.taskText; @@ -591,4 +767,103 @@ 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 instance for the title. + /// The the page should be parented to. + /// The name of the page object. + /// The title of the page. + /// The icon of the page. + /// The description of the page. + /// The abilities to display in a grid below the description. + /// Whether to use variable text sizing for ability descriptions (allows different heights). + /// The of the wiki page. + public static GameObject CreateAdvancedWikiPage( + MatchInfoGuide guide, + TextMeshPro titleTmp, + SpriteRenderer titleIcon, + Scroller parent, + string objName, + string title, + Sprite icon, + string description, + IReadOnlyCollection abilities, + bool variableTextSizing = false) + { + parent.ScrollToTop(); + var obj = CreateAdvancedWikiTab( + guide, + objName, + title, + icon, + description, + titleTmp, + titleIcon, + 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, 0.5f, 0f); + var scrollHeight = variableTextSizing + ? (desc.textBounds.size.y - 2.5f) + maxTextSize + 0.475f + : desc.textBounds.size.y - 2.5f + Mathf.Ceil(num / 2f) * 1.45f; + parent.SetYBoundsMax(Mathf.Clamp(scrollHeight, 0f, 999f)); + return obj; + } + + 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; + } +} diff --git a/MiraAPI/Voting/VotingUtils.cs b/MiraAPI/Voting/VotingUtils.cs index 3d3e206..ad9e32c 100644 --- a/MiraAPI/Voting/VotingUtils.cs +++ b/MiraAPI/Voting/VotingUtils.cs @@ -300,10 +300,7 @@ public static class VotingUtils // If modified, these will visually change. votes = @event.Votes; - MeetingHud.Instance.TitleText.text = - TranslationController.Instance.GetString( - StringNames.MeetingVotingResults, - Il2CppSystem.Array.Empty()); + MeetingHud.Instance.TitleText.text = TranslationController.Instance.GetString(StringNames.MeetingVotingResults); var delays = new Dictionary(); var num = 0;