diff --git a/Silksong.ModMenu/Directory.Build.props b/Silksong.ModMenu/Directory.Build.props index b58d1d5..a463834 100644 --- a/Silksong.ModMenu/Directory.Build.props +++ b/Silksong.ModMenu/Directory.Build.props @@ -11,6 +11,6 @@ It should follow the format major.minor.patch (semantic versioning). If you publish your mod as a library to NuGet, this version will also be used as the package version. --> - 0.5.1 + 0.5.2 diff --git a/Silksong.ModMenu/Elements/DynamicDescriptionChoiceElement.cs b/Silksong.ModMenu/Elements/DynamicDescriptionChoiceElement.cs index 69bf968..65a33ac 100644 --- a/Silksong.ModMenu/Elements/DynamicDescriptionChoiceElement.cs +++ b/Silksong.ModMenu/Elements/DynamicDescriptionChoiceElement.cs @@ -1,9 +1,5 @@ using System; -using Mono.Cecil.Cil; -using MonoDetour; -using MonoDetour.Cil; -using MonoDetour.HookGen; -using MonoMod.Cil; +using Silksong.ModMenu.Internal; using Silksong.ModMenu.Models; using Silksong.UnityHelper.Extensions; using UnityEngine; @@ -28,7 +24,7 @@ LocalizedText rightDescription ) : base(label, model, description) { - RightText = SetupRightDescription(DescriptionText, ChoiceText); + RightText = SetupRightDescription(DescriptionText, ChoiceText, SelectableComponent); RightText.LocalizedText = rightDescription; } @@ -55,7 +51,11 @@ Func getRightDescription /// public readonly Text RightText; - private static Text SetupRightDescription(Text descriptionText, Text choiceText) + private static Text SetupRightDescription( + Text descriptionText, + Text choiceText, + Selectable selectable + ) { GameObject desc = descriptionText.gameObject; GameObject rightDesc = UObject.Instantiate( @@ -85,6 +85,10 @@ private static Text SetupRightDescription(Text descriptionText, Text choiceText) cpbl.originalPosition = rightDesc.transform.localPosition; } + selectable + .gameObject.GetOrAddComponent() + .Animators.Add(rightDesc.GetComponent()); + return rightText; } @@ -102,85 +106,3 @@ public override void SetMainColor(Color color) RightText.color = color; } } - -// Separate class because this can't be done on a generic class -[MonoDetourTargets(typeof(MenuSelectable))] -internal static class RightDescriptionChoiceElementHooks -{ - [MonoDetourHookInitialize] - private static void Hook() - { - Md.UnityEngine.UI.MenuSelectable.OnSelect.ILHook(HookOnSelect); - Md.UnityEngine.UI.MenuSelectable.ValidateDeselect.ILHookMoveNext(HookOnDeselect); - } - - private static void HookOnSelect(ILManipulationInfo info) - { - ILCursor cursor = new(info.Context); - while ( - cursor.TryGotoNext( - MoveType.After, - // this.descriptionText.SetTrigger(MenuSelectable._showPropId) - i => i.MatchLdarg(0), - i => i.MatchLdfld(nameof(MenuSelectable.descriptionText)), - i => i.MatchLdsfld(nameof(MenuSelectable._showPropId)), - i => i.MatchCallOrCallvirt(nameof(Animator.SetTrigger)) - ) - ) - { - cursor.Emit(OpCodes.Ldarg_0); - cursor.EmitDelegate(AnimateUp); - } - } - - private static void HookOnDeselect(ILManipulationInfo info) - { - ILCursor cursor = new(info.Context); - - int locIndex = 1; - while ( - cursor.TryGotoNext( - MoveType.After, - // this.descriptionText.SetTrigger(MenuSelectable._hidePropId) - // It is Ldloc rather than Ldarg.0 because it is the IEnumerator.MoveNext method - // that is being hooked - i => i.MatchLdloc(out locIndex), - i => i.MatchLdfld(nameof(MenuSelectable.descriptionText)), - i => i.MatchLdsfld(nameof(MenuSelectable._hidePropId)), - i => i.MatchCallOrCallvirt(nameof(Animator.SetTrigger)) - ) - ) - { - cursor.Emit(OpCodes.Ldloc, locIndex); - cursor.EmitDelegate(AnimateDown); - } - } - - private static void AnimateUp(MenuSelectable selectable) - { - GameObject? rightDesc = selectable.gameObject.FindChild( - DynamicDescriptionChoiceElement.RIGHT_DESCRIPTION_NAME - ); - - if (rightDesc != null) - { - Animator anim = rightDesc.GetComponent(); - anim.ResetTrigger(MenuSelectable._hidePropId); - anim.SetTrigger(MenuSelectable._showPropId); - } - } - - private static void AnimateDown(MenuSelectable selectable) - { - GameObject? rightDesc = selectable.gameObject.FindChild( - DynamicDescriptionChoiceElement.RIGHT_DESCRIPTION_NAME - ); - - if (rightDesc != null) - { - Animator anim = rightDesc.GetComponent(); - anim.ResetTrigger(MenuSelectable._showPropId); - anim.SetTrigger(MenuSelectable._hidePropId); - } - } -} diff --git a/Silksong.ModMenu/Elements/TextButton.cs b/Silksong.ModMenu/Elements/TextButton.cs index 553f3ce..e21e46d 100644 --- a/Silksong.ModMenu/Elements/TextButton.cs +++ b/Silksong.ModMenu/Elements/TextButton.cs @@ -15,7 +15,7 @@ public class TextButton : SelectableElement /// /// Construct a text button with the given text. /// - public TextButton(LocalizedText text) + public TextButton(LocalizedText text, LocalizedText description) : base(MenuPrefabs.Get().NewTextButtonContainer(out var menuButton), menuButton) { Container.name = text.Text; @@ -36,8 +36,15 @@ public TextButton(LocalizedText text) ButtonText = menuButton.gameObject.FindChild("Menu Button Text")!.GetComponent(); ButtonText.LocalizedText = text; + + DescriptionText = MenuButton.gameObject.FindChild("Description")!.GetComponent(); + DescriptionText.LocalizedText = description; } + /// + public TextButton(LocalizedText text) + : this(text, string.Empty) { } + /// /// The action(s) to perform when this button is selected. /// This takes place on the UI Thread and so must be relatively instantaneous. @@ -54,10 +61,22 @@ public TextButton(LocalizedText text) /// public readonly Text ButtonText; + /// + /// The text element for the description text. + /// + public readonly Text DescriptionText; + /// - public override void SetMainColor(Color color) => ButtonText.color = color; + public override void SetMainColor(Color color) + { + ButtonText.color = color; + DescriptionText.color = color; + } /// - public override void SetFontSizes(FontSizes fontSizes) => + public override void SetFontSizes(FontSizes fontSizes) + { ButtonText.fontSize = FontSizeConstants.LabelSize(fontSizes); + DescriptionText.fontSize = FontSizeConstants.DescriptionSize(fontSizes); + } } diff --git a/Silksong.ModMenu/Internal/MenuPrefabs.cs b/Silksong.ModMenu/Internal/MenuPrefabs.cs index 546abf1..3baf1bf 100644 --- a/Silksong.ModMenu/Internal/MenuPrefabs.cs +++ b/Silksong.ModMenu/Internal/MenuPrefabs.cs @@ -68,24 +68,13 @@ private MenuPrefabs(UIManager uiManager) .RemoveComponent(); Object.DontDestroyOnLoad(keyBindTemplate); - textButtonTemplate = Object.Instantiate(optionsScreen.FindChild("Content/GameOptions")!); - textButtonTemplate.SetActive(false); - textButtonTemplate.name = "TextButtonContainer"; - Object.DontDestroyOnLoad(textButtonTemplate); - - var buttonChild = textButtonTemplate.FindChild("GameOptionsButton")!; - buttonChild.name = "TextButton"; - // We have to remove this component as it's on a different GameObject to the Text, - // so it won't be removed by the LocalizedTextExtensions - buttonChild.RemoveComponent(); - buttonChild.FindChild("Menu Button Text")!.RemoveComponent(); - textLabelTemplate = Object.Instantiate( optionsScreen.FindChild("Content/GameOptions/GameOptionsButton/Menu Button Text")! ); textLabelTemplate.SetActive(false); textLabelTemplate.RemoveComponent(); textLabelTemplate.name = "TextLabel"; + textLabelTemplate.GetComponent().raycastTarget = false; Object.DontDestroyOnLoad(textLabelTemplate); textChoiceTemplate = Object.Instantiate( @@ -109,6 +98,31 @@ private MenuPrefabs(UIManager uiManager) choiceChild.FindChild("Menu Option Text")!.RemoveComponent(); choiceChild.FindChild("Description")!.RemoveComponent(); + textButtonTemplate = Object.Instantiate(optionsScreen.FindChild("Content/GameOptions")!); + textButtonTemplate.SetActive(false); + textButtonTemplate.name = "TextButtonContainer"; + Object.DontDestroyOnLoad(textButtonTemplate); + + var buttonChild = textButtonTemplate.FindChild("GameOptionsButton")!; + buttonChild.name = "TextButton"; + // We have to remove this component as it's on a different GameObject to the Text, + // so it won't be removed by the LocalizedTextExtensions + buttonChild.RemoveComponent(); + buttonChild.FindChild("Menu Button Text")!.RemoveComponent(); + + // Add a (centered) description to the menu button + GameObject clonedDescription = Object.Instantiate(choiceChild.FindChild("Description")!); + RectTransform clonedDescTransform = clonedDescription.GetComponent(); + clonedDescTransform.SetParent(buttonChild.transform, worldPositionStays: false); + clonedDescTransform.anchorMin = new Vector2(0.5f, 0.5f); + clonedDescTransform.anchorMax = new Vector2(0.5f, 0.5f); + clonedDescTransform.pivot = new Vector2(0.5f, 0.5f); + clonedDescription.name = "Description"; + Text clonedDescText = clonedDescription.GetComponent(); + clonedDescText.alignment = TextAnchor.MiddleCenter; + buttonChild.GetComponent().descriptionText = + clonedDescText.GetComponent(); + textInputTemplate = Object.Instantiate(textChoiceTemplate); textInputTemplate.SetActive(false); textInputTemplate.name = "TextInputContainer"; diff --git a/Silksong.ModMenu/Internal/MenuSelectableAnimationProxy.cs b/Silksong.ModMenu/Internal/MenuSelectableAnimationProxy.cs index c03cb24..4964490 100644 --- a/Silksong.ModMenu/Internal/MenuSelectableAnimationProxy.cs +++ b/Silksong.ModMenu/Internal/MenuSelectableAnimationProxy.cs @@ -1,11 +1,25 @@ using System.Collections; using System.Collections.Generic; +using Mono.Cecil.Cil; +using MonoDetour; +using MonoDetour.Cil; +using MonoDetour.HookGen; +using MonoMod.Cil; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace Silksong.ModMenu.Internal; +/// +/// Component to animate objects (such as a description and fleurs) on an element which is not a MenuSelectable, +/// when the element is selected. +/// +/// +/// This component should not be added multiple times; instead, multiple animators should be added to +/// the list of a single MenuSelectableAnimationProxy component. +/// +[MonoDetourTargets(typeof(MenuSelectable))] internal class MenuSelectableAnimationProxy : MonoBehaviour, ISelectHandler, @@ -13,15 +27,24 @@ internal class MenuSelectableAnimationProxy ICancelHandler { private Selectable _selectable; + private bool _isMenuSelectable; + public List Animators = []; void Awake() { _selectable = GetComponent(); + // If the element is already a menu selectable, then the animation will break + // if the usual method is used, so we delegate to the IL hook. + _isMenuSelectable = _selectable is MenuSelectable; } public void OnDeselect(BaseEventData eventData) { + if (_isMenuSelectable) + { + return; + } StartCoroutine(ValidateDeselect(eventData, false)); } @@ -66,6 +89,11 @@ private IEnumerator ValidateDeselect(BaseEventData eventData, bool force) public void OnSelect(BaseEventData eventData) { + if (_isMenuSelectable) + { + return; + } + if (Animators.Count == 0) { return; @@ -87,4 +115,81 @@ public void OnCancel(BaseEventData eventData) { StartCoroutine(ValidateDeselect(eventData, true)); } + + [MonoDetourHookInitialize] + private static void Hook() + { + Md.UnityEngine.UI.MenuSelectable.OnSelect.ILHook(HookOnSelect); + Md.UnityEngine.UI.MenuSelectable.ValidateDeselect.ILHookMoveNext(HookOnDeselect); + } + + private static void HookOnSelect(ILManipulationInfo info) + { + ILCursor cursor = new(info.Context); + while ( + cursor.TryGotoNext( + MoveType.After, + // this.descriptionText.SetTrigger(MenuSelectable._showPropId) + i => i.MatchLdarg(0), + i => i.MatchLdfld(nameof(MenuSelectable.descriptionText)), + i => i.MatchLdsfld(nameof(MenuSelectable._showPropId)), + i => i.MatchCallOrCallvirt(nameof(Animator.SetTrigger)) + ) + ) + { + cursor.Emit(OpCodes.Ldarg_0); + cursor.EmitDelegate(AnimateUp); + } + } + + private static void HookOnDeselect(ILManipulationInfo info) + { + ILCursor cursor = new(info.Context); + + int locIndex = 1; + while ( + cursor.TryGotoNext( + MoveType.After, + // this.descriptionText.SetTrigger(MenuSelectable._hidePropId) + // It is Ldloc rather than Ldarg.0 because it is the IEnumerator.MoveNext method + // that is being hooked + i => i.MatchLdloc(out locIndex), + i => i.MatchLdfld(nameof(MenuSelectable.descriptionText)), + i => i.MatchLdsfld(nameof(MenuSelectable._hidePropId)), + i => i.MatchCallOrCallvirt(nameof(Animator.SetTrigger)) + ) + ) + { + cursor.Emit(OpCodes.Ldloc, locIndex); + cursor.EmitDelegate(AnimateDown); + } + } + + private static void AnimateUp(MenuSelectable selectable) + { + MenuSelectableAnimationProxy proxy = + selectable.gameObject.GetComponent(); + if (proxy != null) + { + foreach (Animator anim in proxy.Animators) + { + anim.ResetTrigger(MenuSelectable._hidePropId); + anim.SetTrigger(MenuSelectable._showPropId); + } + } + } + + private static void AnimateDown(MenuSelectable selectable) + { + MenuSelectableAnimationProxy proxy = + selectable.gameObject.GetComponent(); + if (proxy != null) + { + foreach (Animator anim in proxy.Animators) + { + anim.ResetTrigger(MenuSelectable._showPropId); + anim.SetTrigger(MenuSelectable._hidePropId); + } + } + } } diff --git a/Silksong.ModMenuTesting/Tests/StandardElementsTest.cs b/Silksong.ModMenuTesting/Tests/StandardElementsTest.cs index 302e5ea..9fbde99 100644 --- a/Silksong.ModMenuTesting/Tests/StandardElementsTest.cs +++ b/Silksong.ModMenuTesting/Tests/StandardElementsTest.cs @@ -19,7 +19,7 @@ internal class StandardElementsTest : ModMenuTest internal static IEnumerable CreateUnboundElements() { { - TextButton button = new("The Text Button"); + TextButton button = new("The Text Button", "Here is a text button"); button.OnSubmit += () => Log($"Pressed text button"); yield return button; } @@ -34,10 +34,11 @@ internal static IEnumerable CreateUnboundElements() { ListChoiceModel listChoiceModel = new(["First", "Second", "Third"]); - ChoiceElement choiceElement = new( + ChoiceElement choiceElement = new DynamicDescriptionChoiceElement( "The List Choice", listChoiceModel, - "Here is where to choose option(s)" + "Here is where to choose option(s)", + s => $"This is the {s.ToLowerInvariant()} option" ); listChoiceModel.OnValueChanged += v => Log($"List choice -> {v}"); yield return choiceElement; @@ -45,8 +46,7 @@ internal static IEnumerable CreateUnboundElements() { TextLabel label = new("The Label"); - // Commented out because this is bugged ATM - // yield return label; + yield return label; } {