Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5d34f78
Created parent class for phone menus
TheDavSmasher Jul 9, 2026
7123412
Added protected methods to add panels to custom Phone Menu
TheDavSmasher Jul 9, 2026
98a0d3e
Changed to use Register Panels
TheDavSmasher Jul 9, 2026
3f952da
Added documentation and reverse implicit operator to BasicEntry
TheDavSmasher Jul 12, 2026
cf37448
Merge branch 'dev' into phone-menu-extension
TheDavSmasher Jul 12, 2026
3f2a328
Added MenuDepth property
TheDavSmasher Jul 12, 2026
1660dd7
Renamed variables and removed variable
TheDavSmasher Jul 12, 2026
48112c6
Added on mouse actions
TheDavSmasher Jul 12, 2026
17db947
Added base nameplate appearance method
TheDavSmasher Jul 12, 2026
59db03f
Made Entry Selection check default to false
TheDavSmasher Jul 12, 2026
1719bbf
Added local variable, properties, and delegate type
TheDavSmasher Jul 12, 2026
86912d7
Made menuEntries protected with initial value and BasicEntry sealed
TheDavSmasher Jul 12, 2026
04329e1
Added generic to only have certain types of entries
TheDavSmasher Jul 12, 2026
d652036
Created Paginable Menu subclass based on TOU GuesserMenu
TheDavSmasher Jul 12, 2026
6175628
Moved interface out of class
TheDavSmasher Jul 13, 2026
49e5061
Created interfaces to define entries stored within the menu
TheDavSmasher Jul 13, 2026
88e17c4
Marked method as Obsolete to prevent it from being used.
TheDavSmasher Jul 13, 2026
a5ec81b
Created basic MultiSelect Custom Menu
TheDavSmasher Jul 13, 2026
e6621bd
Added overload for single selections
TheDavSmasher Jul 13, 2026
810facf
Added canRepeat parameter
TheDavSmasher Jul 13, 2026
4fb9897
Made CustomPlayerMenu extend MultiSelectMenu
TheDavSmasher Jul 13, 2026
54f594f
Added 2 selections overload
TheDavSmasher Jul 13, 2026
9cfd8d6
Clear selections on clicking back
TheDavSmasher Jul 13, 2026
b3501b4
Added TODO comments for confirming multi-selection
TheDavSmasher Jul 13, 2026
dc90903
Added shouldConfirm to single selection Begins with default false
TheDavSmasher Jul 13, 2026
8700a2b
Added onSelection field to use earlier
TheDavSmasher Jul 13, 2026
7f023eb
Created UiElement for confirm button
TheDavSmasher Jul 13, 2026
24443c9
Renamed parameter to match overload
TheDavSmasher Jul 13, 2026
bae3ff8
Added documentation to new classes and methods
TheDavSmasher Jul 13, 2026
8246e58
Added base Begin for Paginable menu
TheDavSmasher Jul 13, 2026
6b3ce19
Merge branch 'dev' into phone-menu-extension
AtonyGit Jul 18, 2026
a776edc
Added case for selecting a panel past the max selections
TheDavSmasher Jul 18, 2026
d603f5c
Called SetNameplateAppearance on total selections equal case as well …
TheDavSmasher Jul 18, 2026
638db81
Renamed parameters to match/improve documentation
TheDavSmasher Jul 18, 2026
6ca0b25
Fixed documentation typo lol
TheDavSmasher Jul 19, 2026
ca27188
Merge branch 'dev' into pr/95
AtonyGit Jul 24, 2026
695bbf8
Registered menu types in Il2Cpp
TheDavSmasher Jul 24, 2026
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
222 changes: 222 additions & 0 deletions MiraAPI/Hud/CustomMultiSelectMenu.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Patches.Stubs;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Attributes;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
#pragma warning disable S2365 // Properties should not make collection or array copies

namespace MiraAPI.Hud;

/// <summary>
/// Multi-select <see cref="CustomPhoneMenu"/> using the <see cref="ShapeshifterPanel"/> as a base.
/// </summary>
/// <param name="il2CppPtr">Used by Il2Cpp. Do not use constructor, this is a <see cref="MonoBehaviour"/>.</param>
/// <typeparam name="TEntry">The type of object each entry represents.</typeparam>
[RegisterInIl2Cpp]
[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention")]
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Unity Convention")]
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")]
public abstract class CustomMultiSelectMenu<TEntry>(IntPtr il2CppPtr)
: CustomPhoneMenu<CustomMultiSelectMenu<TEntry>.MenuEntry>(il2CppPtr) where TEntry : class
{
private int totalSelections;
private bool shouldConfirm;
private bool canRepeat;
private readonly List<MenuEntry> selectedEntries = [];

private LoadableAsset<Sprite>? hoverSelectSprite;
private LoadableAsset<Sprite>? hoverDeselectSprite;
private Color? activeColor;
private Color? hoverSelectColor;
private Color? hoverDeselectColor;

public UiElement confirmButton;

private Action<List<TEntry>>? onSelection;

public record MenuEntry(ShapeshifterPanel Panel, TEntry Entry) : IMenuEntry;

/// <summary>
/// Creates a <typeparamref name="TMenu"/>.
/// </summary>
/// <typeparam name="TMenu">The type of <see cref="CustomMultiSelectMenu{TEntry}"/>.</typeparam>
/// <param name="activeColor">The <see cref="Color"/> to use when an entry is selected but not hovered over.</param>
/// <param name="hoverSelectSprite">The <see cref="Sprite"/> to use when an entry is hovered over while not selected.</param>
/// <param name="hoverSelectColor">The <see cref="Color"/> to use when an entry is hovered over while not selected.</param>
/// <param name="hoverDeselectSprite">The <see cref="Sprite"/> to use when an entry is hovered over while selected.</param>
/// <param name="hoverDeselectColor">The <see cref="Color"/> to use when an entry is hovered over while selected.</param>
/// <param name="onMouseOut">Function that can optionally be run when the mouse is moved outside a menu panel.</param>
/// <param name="onMouseOver">Function that can optionally be run when the mouse is moved over a menu panel.</param>
/// <returns>New <typeparamref name="TMenu"/> object.</returns>
protected static TMenu Create<TMenu>(
Color? activeColor = null,
LoadableAsset<Sprite>? hoverSelectSprite = null,
Color? hoverSelectColor = null,
LoadableAsset<Sprite>? hoverDeselectSprite = null,
Color? hoverDeselectColor = null,
PanelButtonOnMouse? onMouseOut = null,
PanelButtonOnMouse? onMouseOver = null
) where TMenu : CustomMultiSelectMenu<TEntry>
{
TMenu customMenu = Create<TMenu>(onMouseOut, onMouseOver);

customMenu.confirmButton = null!; // TODO: create/add confirm button

var button = customMenu.confirmButton.GetComponent<PassiveButton>();
button.OnClick.RemoveAllListeners();
button.OnClick.AddListener((UnityAction)customMenu.OnCompleteSelection);

customMenu.activeColor = activeColor;

customMenu.hoverSelectSprite = hoverSelectSprite;
customMenu.hoverSelectColor = hoverSelectColor;

customMenu.hoverDeselectSprite = hoverDeselectSprite ?? hoverSelectSprite;
customMenu.hoverDeselectColor = hoverDeselectColor ?? hoverSelectColor;

return customMenu;
}

/// <summary>
/// Setup the given <paramref name="panel"/> once created, given its index, entry, and onClick action.
/// </summary>
/// <param name="panel">The panel to configure.</param>
/// <param name="i">The <paramref name="panel"/>'s index.</param>
/// <param name="entry">The <paramref name="panel"/>'s <typeparamref name="TEntry"/> entry.</param>
/// <param name="onClick">Action to perform when the <paramref name="panel"/> is clicked on.</param>
protected abstract void SetupPanelEntry(ShapeshifterPanel panel, int i, TEntry entry, Action onClick);

/// <summary>
/// Begins/opens the custom multi-select menu.
/// </summary>
/// <param name="entries">All entries to give the custom menu.</param>
/// <param name="onClick">Function called when all selections are made/confirmed.</param>
/// <param name="totalSelections">The number of selections required.</param>
/// <param name="shouldConfirm">Whether the entire selection should be confirmed manually.</param>
/// <param name="canRepeat">If the same entry can be selected multiple times, else unselect entry on click.</param>
[HideFromIl2Cpp]
protected void Begin(IEnumerable<TEntry> entries, Action<List<TEntry>?> onClick, int totalSelections, bool shouldConfirm, bool canRepeat = false)
{
MinigameStubs.Begin(this, null);

this.totalSelections = totalSelections;
this.shouldConfirm = shouldConfirm;
this.canRepeat = canRepeat;
this.onSelection = onClick;

var back = backButton.GetComponent<PassiveButton>();
back.OnClick.AddListener((UnityAction)(() =>
{
selectedEntries.Clear();
onClick(null);
}));

DebugAnalytics.Instance.Analytics.MinigameOpened(PlayerControl.LocalPlayer.Data, TaskType);
var list2 = new Il2CppSystem.Collections.Generic.List<UiElement>();
RegisterPanels(
entries,
(shapeshifterPanel, i, entry) =>
{
var num = i % 3;
var num2 = i / 3;
shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f);
SetupPanelEntry(shapeshifterPanel, i, entry, () => OnEntryClick(entry));
list2.Add(shapeshifterPanel.Button);
},
(p, e) => new MenuEntry(p, e));
ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2);
}

/// <summary>
/// Begins/opens the custom menu, only requiring a single selection.
/// </summary>
/// <param name="entries">All entries to give the custom menu.</param>
/// <param name="onClick">Function called when the selection is made.</param>
/// <param name="shouldConfirm">Whether the set of both selections should be confirmed manually.</param>
[HideFromIl2Cpp]
protected void Begin(IEnumerable<TEntry> entries, Action<TEntry?> onClick, bool shouldConfirm)
{
Begin(entries, list => onClick(list?[0]), 1, shouldConfirm);
}

/// <summary>
/// Begins/opens the custom menu, requiring two selections.
/// </summary>
/// <param name="entries">All entries to give the custom menu.</param>
/// <param name="onClick">Function called when both selections are made.</param>
/// <param name="shouldConfirm">Whether the set of both selections should be confirmed manually.</param>
/// <param name="canRepeat">If the same entry can be selected both times, else unselect entry on click.</param>
[HideFromIl2Cpp]
protected void Begin(IEnumerable<TEntry> entries, Action<TEntry?, TEntry?> onClick, bool shouldConfirm, bool canRepeat = false)
{
Begin(entries, list => onClick(list?[0], list?[1]), 2, shouldConfirm, canRepeat);
}

private void OnEntryClick(TEntry entry)
{
MenuEntry menuEntry = MenuEntries.First(e => e.Entry == entry);

if (!canRepeat && selectedEntries.Remove(menuEntry)) // Unselect previous choice
{
// TODO: if confirm button is enabled, disable it
SetNameplateAppearance(menuEntry, false);
return;
}

if (selectedEntries.Count >= totalSelections) // Do not go past total selections.
{
return;
}

selectedEntries.Add(menuEntry);
if (selectedEntries.Count <= totalSelections) // Add choice to list of selections
{
SetNameplateAppearance(menuEntry, true);
}
if (selectedEntries.Count < totalSelections)
{
return;
}

// Total selections reached
if (shouldConfirm)
{
// TODO: enable confirm button
return;
}

OnCompleteSelection();
selectedEntries.Clear();
}

private void OnCompleteSelection()
{
onSelection!(selectedEntries.Select(se => se.Entry).ToList());
}

/// <inheritdoc/>
protected override bool IsEntrySelected(IMenuEntry entry)
{
return selectedEntries.Any(e => e.Panel == entry.Panel);
}

/// <inheritdoc cref="CustomPhoneMenu.SetNameplateAppearance(IMenuEntry, LoadableAsset{Sprite}?, Color?, Color?)"/>
/// <param name="menuEntry"></param>
/// <param name="isSelected">Whether the menu entry is actively selected.</param>
[HideFromIl2Cpp]
protected void SetNameplateAppearance(IMenuEntry menuEntry, bool isSelected)
{
SetNameplateAppearance(
menuEntry,
isSelected ? hoverDeselectSprite : hoverSelectSprite,
isSelected ? hoverDeselectColor : hoverSelectColor,
isSelected ? activeColor : Color.clear);
}
}
Loading
Loading