Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Silksong.ModMenu/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -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.
-->
<Version>0.5.1</Version>
<Version>0.5.2</Version>
</PropertyGroup>
</Project>
100 changes: 11 additions & 89 deletions Silksong.ModMenu/Elements/DynamicDescriptionChoiceElement.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -28,7 +24,7 @@ LocalizedText rightDescription
)
: base(label, model, description)
{
RightText = SetupRightDescription(DescriptionText, ChoiceText);
RightText = SetupRightDescription(DescriptionText, ChoiceText, SelectableComponent);
RightText.LocalizedText = rightDescription;
}

Expand All @@ -55,7 +51,11 @@ Func<T, LocalizedText> getRightDescription
/// </summary>
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(
Expand Down Expand Up @@ -85,6 +85,10 @@ private static Text SetupRightDescription(Text descriptionText, Text choiceText)
cpbl.originalPosition = rightDesc.transform.localPosition;
}

selectable
.gameObject.GetOrAddComponent<MenuSelectableAnimationProxy>()
.Animators.Add(rightDesc.GetComponent<Animator>());

return rightText;
}

Expand All @@ -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<MenuSelectable>(nameof(MenuSelectable.descriptionText)),
i => i.MatchLdsfld<MenuSelectable>(nameof(MenuSelectable._showPropId)),
i => i.MatchCallOrCallvirt<Animator>(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<MenuSelectable>(nameof(MenuSelectable.descriptionText)),
i => i.MatchLdsfld<MenuSelectable>(nameof(MenuSelectable._hidePropId)),
i => i.MatchCallOrCallvirt<Animator>(nameof(Animator.SetTrigger))
)
)
{
cursor.Emit(OpCodes.Ldloc, locIndex);
cursor.EmitDelegate(AnimateDown);
}
}

private static void AnimateUp(MenuSelectable selectable)
{
GameObject? rightDesc = selectable.gameObject.FindChild(
DynamicDescriptionChoiceElement<object>.RIGHT_DESCRIPTION_NAME
);

if (rightDesc != null)
{
Animator anim = rightDesc.GetComponent<Animator>();
anim.ResetTrigger(MenuSelectable._hidePropId);
anim.SetTrigger(MenuSelectable._showPropId);
}
}

private static void AnimateDown(MenuSelectable selectable)
{
GameObject? rightDesc = selectable.gameObject.FindChild(
DynamicDescriptionChoiceElement<object>.RIGHT_DESCRIPTION_NAME
);

if (rightDesc != null)
{
Animator anim = rightDesc.GetComponent<Animator>();
anim.ResetTrigger(MenuSelectable._showPropId);
anim.SetTrigger(MenuSelectable._hidePropId);
}
}
}
25 changes: 22 additions & 3 deletions Silksong.ModMenu/Elements/TextButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class TextButton : SelectableElement
/// <summary>
/// Construct a text button with the given text.
/// </summary>
public TextButton(LocalizedText text)
public TextButton(LocalizedText text, LocalizedText description)
: base(MenuPrefabs.Get().NewTextButtonContainer(out var menuButton), menuButton)
{
Container.name = text.Text;
Expand All @@ -36,8 +36,15 @@ public TextButton(LocalizedText text)

ButtonText = menuButton.gameObject.FindChild("Menu Button Text")!.GetComponent<Text>();
ButtonText.LocalizedText = text;

DescriptionText = MenuButton.gameObject.FindChild("Description")!.GetComponent<Text>();
DescriptionText.LocalizedText = description;
}

/// <inheritdoc cref="TextButton.TextButton(LocalizedText, LocalizedText)"/>
public TextButton(LocalizedText text)
: this(text, string.Empty) { }

/// <summary>
/// The action(s) to perform when this button is selected.
/// This takes place on the UI Thread and so must be relatively instantaneous.
Expand All @@ -54,10 +61,22 @@ public TextButton(LocalizedText text)
/// </summary>
public readonly Text ButtonText;

/// <summary>
/// The text element for the description text.
/// </summary>
public readonly Text DescriptionText;

/// <inheritdoc/>
public override void SetMainColor(Color color) => ButtonText.color = color;
public override void SetMainColor(Color color)
{
ButtonText.color = color;
DescriptionText.color = color;
}

/// <inheritdoc/>
public override void SetFontSizes(FontSizes fontSizes) =>
public override void SetFontSizes(FontSizes fontSizes)
{
ButtonText.fontSize = FontSizeConstants.LabelSize(fontSizes);
DescriptionText.fontSize = FontSizeConstants.DescriptionSize(fontSizes);
}
}
38 changes: 26 additions & 12 deletions Silksong.ModMenu/Internal/MenuPrefabs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,24 +68,13 @@ private MenuPrefabs(UIManager uiManager)
.RemoveComponent<ChangeTextFontScaleOnHandHeld>();
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<AutoLocalizeTextUI>();
buttonChild.FindChild("Menu Button Text")!.RemoveComponent<ChangeTextFontScaleOnHandHeld>();

textLabelTemplate = Object.Instantiate(
optionsScreen.FindChild("Content/GameOptions/GameOptionsButton/Menu Button Text")!
);
textLabelTemplate.SetActive(false);
textLabelTemplate.RemoveComponent<ChangeTextFontScaleOnHandHeld>();
textLabelTemplate.name = "TextLabel";
textLabelTemplate.GetComponent<Text>().raycastTarget = false;
Object.DontDestroyOnLoad(textLabelTemplate);

textChoiceTemplate = Object.Instantiate(
Expand All @@ -109,6 +98,31 @@ private MenuPrefabs(UIManager uiManager)
choiceChild.FindChild("Menu Option Text")!.RemoveComponent<ChangeTextFontScaleOnHandHeld>();
choiceChild.FindChild("Description")!.RemoveComponent<ChangeTextFontScaleOnHandHeld>();

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<AutoLocalizeTextUI>();
buttonChild.FindChild("Menu Button Text")!.RemoveComponent<ChangeTextFontScaleOnHandHeld>();

// Add a (centered) description to the menu button
GameObject clonedDescription = Object.Instantiate(choiceChild.FindChild("Description")!);
RectTransform clonedDescTransform = clonedDescription.GetComponent<RectTransform>();
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<Text>();
clonedDescText.alignment = TextAnchor.MiddleCenter;
buttonChild.GetComponent<MenuButton>().descriptionText =
clonedDescText.GetComponent<Animator>();

textInputTemplate = Object.Instantiate(textChoiceTemplate);
textInputTemplate.SetActive(false);
textInputTemplate.name = "TextInputContainer";
Expand Down
105 changes: 105 additions & 0 deletions Silksong.ModMenu/Internal/MenuSelectableAnimationProxy.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,50 @@
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;

/// <summary>
/// Component to animate objects (such as a description and fleurs) on an element which is not a MenuSelectable,
/// when the element is selected.
/// </summary>
/// <remarks>
/// This component should not be added multiple times; instead, multiple animators should be added to
/// the <see cref="Animators"/> list of a single MenuSelectableAnimationProxy component.
/// </remarks>
[MonoDetourTargets(typeof(MenuSelectable))]
internal class MenuSelectableAnimationProxy
: MonoBehaviour,
ISelectHandler,
IDeselectHandler,
ICancelHandler
{
private Selectable _selectable;
private bool _isMenuSelectable;

public List<Animator> Animators = [];

void Awake()
{
_selectable = GetComponent<Selectable>();
// 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));
}

Expand Down Expand Up @@ -66,6 +89,11 @@ private IEnumerator ValidateDeselect(BaseEventData eventData, bool force)

public void OnSelect(BaseEventData eventData)
{
if (_isMenuSelectable)
{
return;
}

if (Animators.Count == 0)
{
return;
Expand All @@ -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<MenuSelectable>(nameof(MenuSelectable.descriptionText)),
i => i.MatchLdsfld<MenuSelectable>(nameof(MenuSelectable._showPropId)),
i => i.MatchCallOrCallvirt<Animator>(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<MenuSelectable>(nameof(MenuSelectable.descriptionText)),
i => i.MatchLdsfld<MenuSelectable>(nameof(MenuSelectable._hidePropId)),
i => i.MatchCallOrCallvirt<Animator>(nameof(Animator.SetTrigger))
)
)
{
cursor.Emit(OpCodes.Ldloc, locIndex);
cursor.EmitDelegate(AnimateDown);
}
}

private static void AnimateUp(MenuSelectable selectable)
{
MenuSelectableAnimationProxy proxy =
selectable.gameObject.GetComponent<MenuSelectableAnimationProxy>();
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<MenuSelectableAnimationProxy>();
if (proxy != null)
{
foreach (Animator anim in proxy.Animators)
{
anim.ResetTrigger(MenuSelectable._showPropId);
anim.SetTrigger(MenuSelectable._hidePropId);
}
}
}
}
Loading