Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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);
}
}
}
28 changes: 26 additions & 2 deletions Silksong.ModMenu/Elements/TextButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ 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 = string.Empty;
}

/// <summary>
/// Create a text button with a description below it.
/// </summary>
public TextButton(LocalizedText text, LocalizedText description)
: this(text)
Comment thread
flibber-hk marked this conversation as resolved.
Outdated
{
DescriptionText.LocalizedText = description;
}

/// <summary>
Expand All @@ -54,10 +66,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