mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-04-24 05:19:00 +08:00
feat: add support for nested Menu Installer operation
With this change it is now possible for Menu Installer to specify as its target a menu installed by another Menu Installer, or a submenu thereof. This allows prefabs to inject extension entries into other prefab menus.
This commit is contained in:
parent
549ce8f0d3
commit
a361789c43
@ -131,6 +131,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
BoneDatabase.ResetBones();
|
BoneDatabase.ResetBones();
|
||||||
PathMappings.Clear();
|
PathMappings.Clear();
|
||||||
|
ClonedMenuMappings.Clear();
|
||||||
|
|
||||||
// Sometimes people like to nest one avatar in another, when transplanting clothing. To avoid issues
|
// Sometimes people like to nest one avatar in another, when transplanting clothing. To avoid issues
|
||||||
// with inconsistently determining the avatar root, we'll go ahead and remove the extra sub-avatars
|
// with inconsistently determining the avatar root, we'll go ahead and remove the extra sub-avatars
|
||||||
@ -172,13 +173,16 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
UnityEngine.Object.DestroyImmediate(component);
|
UnityEngine.Object.DestroyImmediate(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
var activator = avatarGameObject.GetComponent<AvatarActivator>();
|
var activator = avatarGameObject.GetComponent<AvatarActivator>();
|
||||||
if (activator != null)
|
if (activator != null)
|
||||||
{
|
{
|
||||||
UnityEngine.Object.DestroyImmediate(activator);
|
UnityEngine.Object.DestroyImmediate(activator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClonedMenuMappings.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "PossibleNullReferenceException")]
|
[SuppressMessage("ReSharper", "PossibleNullReferenceException")]
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||||
|
|
||||||
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
|
{
|
||||||
|
internal static class ClonedMenuMappings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Map to link the cloned menu from the clone source.
|
||||||
|
/// If one menu is specified for multiple installers, they are replicated separately, so there is a one-to-many relationship.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Dictionary<VRCExpressionsMenu, ImmutableList<VRCExpressionsMenu>> ClonedMappings =
|
||||||
|
new Dictionary<VRCExpressionsMenu, ImmutableList<VRCExpressionsMenu>>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map to link the clone source from the cloned menu.
|
||||||
|
/// Map is the opposite of ClonedMappings.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> OriginalMapping =
|
||||||
|
new Dictionary<VRCExpressionsMenu, VRCExpressionsMenu>();
|
||||||
|
|
||||||
|
public static void Clear()
|
||||||
|
{
|
||||||
|
ClonedMappings.Clear();
|
||||||
|
OriginalMapping.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Add(VRCExpressionsMenu original, VRCExpressionsMenu clonedMenu)
|
||||||
|
{
|
||||||
|
if (!ClonedMappings.TryGetValue(original, out ImmutableList<VRCExpressionsMenu> clonedMenus))
|
||||||
|
{
|
||||||
|
clonedMenus = ImmutableList<VRCExpressionsMenu>.Empty;
|
||||||
|
}
|
||||||
|
ClonedMappings[original] = clonedMenus.Add(clonedMenu);
|
||||||
|
OriginalMapping[clonedMenu] = original;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetClonedMenus(VRCExpressionsMenu original, out ImmutableList<VRCExpressionsMenu> clonedMenus)
|
||||||
|
{
|
||||||
|
return ClonedMappings.TryGetValue(original, out clonedMenus);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VRCExpressionsMenu GetOriginal(VRCExpressionsMenu cloned)
|
||||||
|
{
|
||||||
|
return OriginalMapping.TryGetValue(cloned, out VRCExpressionsMenu original) ? original : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: aeaeff9c3af44683bb2f8f5fe6c5791d
|
||||||
|
timeCreated: 1671016064
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEditor.IMGUI.Controls;
|
using UnityEditor.IMGUI.Controls;
|
||||||
@ -21,6 +22,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
set => _treeView.Avatar = value;
|
set => _treeView.Avatar = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ModularAvatarMenuInstaller TargetInstaller
|
||||||
|
{
|
||||||
|
get => _treeView.TargetInstaller;
|
||||||
|
set => _treeView.TargetInstaller = value;
|
||||||
|
}
|
||||||
|
|
||||||
public Action<VRCExpressionsMenu> OnMenuSelected = (menu) => { };
|
public Action<VRCExpressionsMenu> OnMenuSelected = (menu) => { };
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
@ -51,12 +58,13 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
_treeView.OnGUI(new Rect(0, 0, position.width, position.height));
|
_treeView.OnGUI(new Rect(0, 0, position.width, position.height));
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void Show(VRCAvatarDescriptor Avatar, Action<VRCExpressionsMenu> OnSelect)
|
internal static void Show(VRCAvatarDescriptor Avatar, ModularAvatarMenuInstaller Installer, Action<VRCExpressionsMenu> OnSelect)
|
||||||
{
|
{
|
||||||
var window = GetWindow<AvMenuTreeViewWindow>();
|
var window = GetWindow<AvMenuTreeViewWindow>();
|
||||||
window.titleContent = new GUIContent("Select menu");
|
window.titleContent = new GUIContent("Select menu");
|
||||||
|
|
||||||
window.Avatar = Avatar;
|
window.Avatar = Avatar;
|
||||||
|
window.TargetInstaller = Installer;
|
||||||
window.OnMenuSelected = OnSelect;
|
window.OnMenuSelected = OnSelect;
|
||||||
|
|
||||||
window.Show();
|
window.Show();
|
||||||
@ -77,12 +85,27 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ModularAvatarMenuInstaller _targetInstaller;
|
||||||
|
|
||||||
|
public ModularAvatarMenuInstaller TargetInstaller
|
||||||
|
{
|
||||||
|
get => _targetInstaller;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_targetInstaller = value;
|
||||||
|
Reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal Action<VRCExpressionsMenu> OnSelect = (menu) => { };
|
internal Action<VRCExpressionsMenu> OnSelect = (menu) => { };
|
||||||
internal Action OnDoubleclickSelect = () => { };
|
internal Action OnDoubleclickSelect = () => { };
|
||||||
|
|
||||||
private List<VRCExpressionsMenu> _menuItems = new List<VRCExpressionsMenu>();
|
private List<VRCExpressionsMenu> _menuItems = new List<VRCExpressionsMenu>();
|
||||||
private HashSet<VRCExpressionsMenu> _visitedMenus = new HashSet<VRCExpressionsMenu>();
|
private HashSet<VRCExpressionsMenu> _visitedMenus = new HashSet<VRCExpressionsMenu>();
|
||||||
|
|
||||||
|
private MenuTree _menuTree;
|
||||||
|
private Stack<VRCExpressionsMenu> _visitedMenuStack = new Stack<VRCExpressionsMenu>();
|
||||||
|
|
||||||
public AvMenuTreeView(TreeViewState state) : base(state)
|
public AvMenuTreeView(TreeViewState state) : base(state)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@ -98,49 +121,59 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
OnDoubleclickSelect.Invoke();
|
OnDoubleclickSelect.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override TreeViewItem BuildRoot()
|
protected override TreeViewItem BuildRoot()
|
||||||
{
|
{
|
||||||
_menuItems.Clear();
|
_menuItems.Clear();
|
||||||
_visitedMenus.Clear();
|
_visitedMenuStack.Clear();
|
||||||
|
|
||||||
if (Avatar.expressionsMenu == null)
|
_menuTree = new MenuTree(Avatar);
|
||||||
|
_menuTree.TraverseAvatarMenu();
|
||||||
|
foreach (ModularAvatarMenuInstaller installer in Avatar.gameObject.GetComponentsInChildren<ModularAvatarMenuInstaller>(true))
|
||||||
{
|
{
|
||||||
return new TreeViewItem(0, -1, "No menu");
|
if (installer == TargetInstaller) continue;
|
||||||
|
_menuTree.TraverseMenuInstaller(installer);
|
||||||
}
|
}
|
||||||
|
|
||||||
_visitedMenus.Add(Avatar.expressionsMenu);
|
var root = new TreeViewItem(-1, -1, "<root>");
|
||||||
|
List<TreeViewItem> treeItems = new List<TreeViewItem>
|
||||||
|
{
|
||||||
|
new TreeViewItem
|
||||||
|
{
|
||||||
|
id = 0,
|
||||||
|
depth = 0,
|
||||||
|
displayName = $"{Avatar.gameObject.name} ({(Avatar.expressionsMenu == null ? "None" : Avatar.expressionsMenu.name)})"
|
||||||
|
}
|
||||||
|
};
|
||||||
_menuItems.Add(Avatar.expressionsMenu);
|
_menuItems.Add(Avatar.expressionsMenu);
|
||||||
var root = new TreeViewItem {id = -1, depth = -1, displayName = "<root>"};
|
_visitedMenuStack.Push(Avatar.expressionsMenu);
|
||||||
|
|
||||||
var treeItems = new List<TreeViewItem>();
|
|
||||||
treeItems.Add(new TreeViewItem
|
|
||||||
{id = 0, depth = 0, displayName = $"{Avatar.gameObject.name} ({Avatar.expressionsMenu.name})"});
|
|
||||||
|
|
||||||
TraverseMenu(1, treeItems, Avatar.expressionsMenu);
|
TraverseMenu(1, treeItems, Avatar.expressionsMenu);
|
||||||
|
|
||||||
SetupParentsAndChildrenFromDepths(root, treeItems);
|
SetupParentsAndChildrenFromDepths(root, treeItems);
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TraverseMenu(int depth, List<TreeViewItem> items, VRCExpressionsMenu menu)
|
private void TraverseMenu(int depth, List<TreeViewItem> items, VRCExpressionsMenu menu)
|
||||||
{
|
{
|
||||||
foreach (var control in menu.controls)
|
IEnumerable<MenuTree.ChildElement> children = _menuTree.GetChildren(menu)
|
||||||
|
.Where(child => !_visitedMenuStack.Contains(child.menu));
|
||||||
|
foreach (MenuTree.ChildElement child in children)
|
||||||
{
|
{
|
||||||
if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu
|
if (child.menu == null) continue;
|
||||||
&& control.subMenu != null && !_visitedMenus.Contains(control.subMenu))
|
string displayName = child.installer == null ?
|
||||||
{
|
$"{child.menuName} ({child.menu.name})" :
|
||||||
items.Add(new TreeViewItem
|
$"{child.menuName} ({child.menu.name}) InstallerObject : {child.installer.name}";
|
||||||
|
items.Add(
|
||||||
|
new TreeViewItem
|
||||||
{
|
{
|
||||||
id = _menuItems.Count,
|
id = items.Count,
|
||||||
depth = depth,
|
depth = depth,
|
||||||
displayName = $"{control.name} ({control.subMenu.name})"
|
displayName = displayName
|
||||||
});
|
}
|
||||||
_menuItems.Add(control.subMenu);
|
);
|
||||||
_visitedMenus.Add(control.subMenu);
|
_menuItems.Add(child.menu);
|
||||||
|
_visitedMenuStack.Push(child.menu);
|
||||||
TraverseMenu(depth + 1, items, control.subMenu);
|
TraverseMenu(depth + 1, items, child.menu);
|
||||||
}
|
_visitedMenuStack.Pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using VRC.SDK3.Avatars.Components;
|
using VRC.SDK3.Avatars.Components;
|
||||||
@ -22,11 +23,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
private HashSet<VRCExpressionsMenu> _avatarMenus;
|
private HashSet<VRCExpressionsMenu> _avatarMenus;
|
||||||
|
|
||||||
|
private Dictionary<VRCExpressionsMenu, List<ModularAvatarMenuInstaller>> _menuInstallersMap;
|
||||||
|
|
||||||
private void OnEnable()
|
private void OnEnable()
|
||||||
{
|
{
|
||||||
_installer = (ModularAvatarMenuInstaller) target;
|
_installer = (ModularAvatarMenuInstaller) target;
|
||||||
|
|
||||||
FindMenus();
|
FindMenus();
|
||||||
|
FindMenuInstallers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetupMenuEditor()
|
private void SetupMenuEditor()
|
||||||
@ -47,7 +51,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
_menuToAppend = _installer.menuToAppend;
|
_menuToAppend = _installer.menuToAppend;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnInnerInspectorGUI()
|
protected override void OnInnerInspectorGUI()
|
||||||
{
|
{
|
||||||
SetupMenuEditor();
|
SetupMenuEditor();
|
||||||
@ -95,7 +98,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
var avatar = RuntimeUtil.FindAvatarInParents(_installer.transform);
|
var avatar = RuntimeUtil.FindAvatarInParents(_installer.transform);
|
||||||
if (avatar != null && GUILayout.Button(G("menuinstall.selectmenu")))
|
if (avatar != null && GUILayout.Button(G("menuinstall.selectmenu")))
|
||||||
{
|
{
|
||||||
AvMenuTreeViewWindow.Show(avatar, menu =>
|
AvMenuTreeViewWindow.Show(avatar, _installer, menu =>
|
||||||
{
|
{
|
||||||
installTo.objectReferenceValue = menu;
|
installTo.objectReferenceValue = menu;
|
||||||
serializedObject.ApplyModifiedProperties();
|
serializedObject.ApplyModifiedProperties();
|
||||||
@ -200,9 +203,70 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsMenuReachable(VRCAvatarDescriptor avatar, VRCExpressionsMenu menu)
|
private void FindMenuInstallers()
|
||||||
{
|
{
|
||||||
return _avatarMenus == null || _avatarMenus.Contains(menu);
|
if (targets.Length > 1)
|
||||||
|
{
|
||||||
|
_menuInstallersMap = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_menuInstallersMap = new Dictionary<VRCExpressionsMenu, List<ModularAvatarMenuInstaller>>();
|
||||||
|
var avatar = RuntimeUtil.FindAvatarInParents(((Component)target).transform);
|
||||||
|
if (avatar == null) return;
|
||||||
|
var menuInstallers = avatar.GetComponentsInChildren<ModularAvatarMenuInstaller>(true)
|
||||||
|
.Where(menuInstaller => menuInstaller.enabled && menuInstaller.menuToAppend != null);
|
||||||
|
foreach (ModularAvatarMenuInstaller menuInstaller in menuInstallers)
|
||||||
|
{
|
||||||
|
if (menuInstaller == target) continue;
|
||||||
|
var visitedMenus = new HashSet<VRCExpressionsMenu>();
|
||||||
|
var queue = new Queue<VRCExpressionsMenu>();
|
||||||
|
queue.Enqueue(menuInstaller.menuToAppend);
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
VRCExpressionsMenu parent = queue.Dequeue();
|
||||||
|
var controls = parent.controls.Where(control => control.type == VRCExpressionsMenu.Control.ControlType.SubMenu && control.subMenu != null);
|
||||||
|
foreach (VRCExpressionsMenu.Control control in controls)
|
||||||
|
{
|
||||||
|
// Do not filter in LINQ to avoid closure allocation
|
||||||
|
if (visitedMenus.Contains(control.subMenu)) continue;
|
||||||
|
if (!_menuInstallersMap.TryGetValue(control.subMenu, out List<ModularAvatarMenuInstaller> fromInstallers))
|
||||||
|
{
|
||||||
|
fromInstallers = new List<ModularAvatarMenuInstaller>();
|
||||||
|
_menuInstallersMap[control.subMenu] = fromInstallers;
|
||||||
|
}
|
||||||
|
|
||||||
|
fromInstallers.Add(menuInstaller);
|
||||||
|
visitedMenus.Add(control.subMenu);
|
||||||
|
queue.Enqueue(control.subMenu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsMenuReachable(VRCAvatarDescriptor avatar, VRCExpressionsMenu menu, HashSet<ModularAvatarMenuInstaller> visitedInstaller = null)
|
||||||
|
{
|
||||||
|
if (_avatarMenus == null || _avatarMenus.Contains(menu)) return true;
|
||||||
|
|
||||||
|
if (_menuInstallersMap == null) return true;
|
||||||
|
if (visitedInstaller == null) visitedInstaller = new HashSet<ModularAvatarMenuInstaller> { (ModularAvatarMenuInstaller)target };
|
||||||
|
|
||||||
|
if (!_menuInstallersMap.TryGetValue(menu, out List<ModularAvatarMenuInstaller> installers)) return false;
|
||||||
|
foreach (ModularAvatarMenuInstaller installer in installers)
|
||||||
|
{
|
||||||
|
// Root is always reachable if installTargetMenu is null
|
||||||
|
if (installer.installTargetMenu == null) return true;
|
||||||
|
// Even in a circular structure, it may be possible to reach root by another path.
|
||||||
|
if (visitedInstaller.Contains(installer)) continue;
|
||||||
|
visitedInstaller.Add(installer);
|
||||||
|
if (IsMenuReachable(avatar, installer.installTargetMenu, visitedInstaller))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ValidateExpressionMenuIconResult ValidateExpressionMenuIcon(VRCExpressionsMenu menu, HashSet<VRCExpressionsMenu> visitedMenus = null)
|
private static ValidateExpressionMenuIconResult ValidateExpressionMenuIcon(VRCExpressionsMenu menu, HashSet<VRCExpressionsMenu> visitedMenus = null)
|
||||||
@ -212,26 +276,25 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
if (visitedMenus.Contains(menu)) return ValidateExpressionMenuIconResult.Success;
|
if (visitedMenus.Contains(menu)) return ValidateExpressionMenuIconResult.Success;
|
||||||
visitedMenus.Add(menu);
|
visitedMenus.Add(menu);
|
||||||
|
|
||||||
foreach (VRCExpressionsMenu.Control control in menu.controls)
|
foreach (VRCExpressionsMenu.Control control in menu.controls) {
|
||||||
{
|
|
||||||
// Control
|
// Control
|
||||||
ValidateExpressionMenuIconResult result = Util.ValidateExpressionMenuIcon(control.icon);
|
ValidateExpressionMenuIconResult result = Util.ValidateExpressionMenuIcon(control.icon);
|
||||||
if (result != ValidateExpressionMenuIconResult.Success) return result;
|
if (result != ValidateExpressionMenuIconResult.Success) return result;
|
||||||
|
|
||||||
// Labels
|
// Labels
|
||||||
foreach (VRCExpressionsMenu.Control.Label label in control.labels)
|
foreach (VRCExpressionsMenu.Control.Label label in control.labels) {
|
||||||
{
|
|
||||||
ValidateExpressionMenuIconResult labelResult = Util.ValidateExpressionMenuIcon(label.icon);
|
ValidateExpressionMenuIconResult labelResult = Util.ValidateExpressionMenuIcon(label.icon);
|
||||||
if (labelResult != ValidateExpressionMenuIconResult.Success) return labelResult;
|
if (labelResult != ValidateExpressionMenuIconResult.Success) return labelResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubMenu
|
// SubMenu
|
||||||
if (control.type != VRCExpressionsMenu.Control.ControlType.SubMenu) continue;
|
if (control.type != VRCExpressionsMenu.Control.ControlType.SubMenu) continue;
|
||||||
ValidateExpressionMenuIconResult subMenuResult = ValidateExpressionMenuIcon(control.subMenu, visitedMenus);
|
ValidateExpressionMenuIconResult subMenuResult = ValidateExpressionMenuIcon(control.subMenu, visitedMenus);
|
||||||
if (subMenuResult != ValidateExpressionMenuIconResult.Success) return subMenuResult;
|
if (subMenuResult != ValidateExpressionMenuIconResult.Success) return subMenuResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ValidateExpressionMenuIconResult.Success;
|
return ValidateExpressionMenuIconResult.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,6 +7,7 @@ using VRC.SDK3.Avatars.Components;
|
|||||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||||
using Object = UnityEngine.Object;
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core.editor
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
{
|
{
|
||||||
internal class MenuInstallHook
|
internal class MenuInstallHook
|
||||||
@ -16,39 +17,52 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
);
|
);
|
||||||
|
|
||||||
private Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> _clonedMenus;
|
private Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> _clonedMenus;
|
||||||
private Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> _installTargets;
|
|
||||||
|
|
||||||
private VRCExpressionsMenu _rootMenu;
|
private VRCExpressionsMenu _rootMenu;
|
||||||
|
|
||||||
|
private MenuTree _menuTree;
|
||||||
|
private Stack<ModularAvatarMenuInstaller> _visitedInstallerStack;
|
||||||
|
|
||||||
public void OnPreprocessAvatar(GameObject avatarRoot)
|
public void OnPreprocessAvatar(GameObject avatarRoot)
|
||||||
{
|
{
|
||||||
var menuInstallers = avatarRoot.GetComponentsInChildren<ModularAvatarMenuInstaller>(true)
|
ModularAvatarMenuInstaller[] menuInstallers = avatarRoot.GetComponentsInChildren<ModularAvatarMenuInstaller>(true)
|
||||||
.Where(c => c.enabled)
|
.Where(menuInstaller => menuInstaller.enabled)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
if (menuInstallers.Length == 0) return;
|
if (menuInstallers.Length == 0) return;
|
||||||
|
|
||||||
|
|
||||||
_clonedMenus = new Dictionary<VRCExpressionsMenu, VRCExpressionsMenu>();
|
_clonedMenus = new Dictionary<VRCExpressionsMenu, VRCExpressionsMenu>();
|
||||||
|
_visitedInstallerStack = new Stack<ModularAvatarMenuInstaller>();
|
||||||
|
|
||||||
|
VRCAvatarDescriptor avatar = avatarRoot.GetComponent<VRCAvatarDescriptor>();
|
||||||
|
|
||||||
var avatar = avatarRoot.GetComponent<VRCAvatarDescriptor>();
|
if (avatar.expressionsMenu == null)
|
||||||
|
|
||||||
if (avatar.expressionsMenu == null)
|
|
||||||
{
|
{
|
||||||
var menu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
var menu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
||||||
AssetDatabase.CreateAsset(menu, Util.GenerateAssetPath());
|
AssetDatabase.CreateAsset(menu, Util.GenerateAssetPath());
|
||||||
avatar.expressionsMenu = menu;
|
avatar.expressionsMenu = menu;
|
||||||
|
_clonedMenus[menu] = menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
_rootMenu = avatar.expressionsMenu;
|
_rootMenu = avatar.expressionsMenu;
|
||||||
|
_menuTree = new MenuTree(avatar);
|
||||||
|
_menuTree.TraverseAvatarMenu();
|
||||||
|
|
||||||
avatar.expressionsMenu = CloneMenu(avatar.expressionsMenu);
|
avatar.expressionsMenu = CloneMenu(avatar.expressionsMenu);
|
||||||
_installTargets = new Dictionary<VRCExpressionsMenu, VRCExpressionsMenu>(_clonedMenus);
|
|
||||||
|
foreach (ModularAvatarMenuInstaller installer in menuInstallers)
|
||||||
foreach (var install in menuInstallers)
|
|
||||||
{
|
{
|
||||||
InstallMenu(install);
|
_menuTree.TraverseMenuInstaller(installer);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(null))
|
||||||
|
{
|
||||||
|
InstallMenu(childElement.installer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InstallMenu(ModularAvatarMenuInstaller installer)
|
private void InstallMenu(ModularAvatarMenuInstaller installer, VRCExpressionsMenu installTarget = null)
|
||||||
{
|
{
|
||||||
if (!installer.enabled) return;
|
if (!installer.enabled) return;
|
||||||
|
|
||||||
@ -57,30 +71,48 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
installer.installTargetMenu = _rootMenu;
|
installer.installTargetMenu = _rootMenu;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (installer.installTargetMenu == null || installer.menuToAppend == null) return;
|
if (installTarget == null)
|
||||||
if (!_installTargets.TryGetValue(installer.installTargetMenu, out var targetMenu)) return;
|
{
|
||||||
if (_installTargets.ContainsKey(installer.menuToAppend)) return;
|
installTarget = installer.installTargetMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installer.installTargetMenu == null || installer.menuToAppend == null) return;
|
||||||
|
if (!_clonedMenus.TryGetValue(installTarget, out var targetMenu)) return;
|
||||||
|
|
||||||
// Clone before appending to sanitize menu icons
|
// Clone before appending to sanitize menu icons
|
||||||
targetMenu.controls.AddRange(CloneMenu(installer.menuToAppend).controls);
|
targetMenu.controls.AddRange(CloneMenu(installer.menuToAppend).controls);
|
||||||
|
|
||||||
while (targetMenu.controls.Count > VRCExpressionsMenu.MAX_CONTROLS)
|
SplitMenu(installer, targetMenu);
|
||||||
|
|
||||||
|
if (_visitedInstallerStack.Contains(installer)) return;
|
||||||
|
_visitedInstallerStack.Push(installer);
|
||||||
|
foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(installer))
|
||||||
|
{
|
||||||
|
InstallMenu(childElement.installer, childElement.parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
_visitedInstallerStack.Pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SplitMenu(ModularAvatarMenuInstaller installer, VRCExpressionsMenu targetMenu)
|
||||||
|
{
|
||||||
|
while (targetMenu.controls.Count > VRCExpressionsMenu.MAX_CONTROLS)
|
||||||
{
|
{
|
||||||
// Split target menu
|
// Split target menu
|
||||||
var newMenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
var newMenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
||||||
AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath());
|
AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath());
|
||||||
var keepCount = VRCExpressionsMenu.MAX_CONTROLS - 1;
|
const int keepCount = VRCExpressionsMenu.MAX_CONTROLS - 1;
|
||||||
newMenu.controls.AddRange(targetMenu.controls.Skip(keepCount));
|
newMenu.controls.AddRange(targetMenu.controls.Skip(keepCount));
|
||||||
targetMenu.controls.RemoveRange(keepCount,
|
targetMenu.controls.RemoveRange(keepCount,
|
||||||
targetMenu.controls.Count - keepCount
|
targetMenu.controls.Count - keepCount
|
||||||
);
|
);
|
||||||
|
|
||||||
targetMenu.controls.Add(new VRCExpressionsMenu.Control()
|
targetMenu.controls.Add(new VRCExpressionsMenu.Control
|
||||||
{
|
{
|
||||||
name = "More",
|
name = "More",
|
||||||
type = VRCExpressionsMenu.Control.ControlType.SubMenu,
|
type = VRCExpressionsMenu.Control.ControlType.SubMenu,
|
||||||
subMenu = newMenu,
|
subMenu = newMenu,
|
||||||
parameter = new VRCExpressionsMenu.Control.Parameter()
|
parameter = new VRCExpressionsMenu.Control.Parameter
|
||||||
{
|
{
|
||||||
name = ""
|
name = ""
|
||||||
},
|
},
|
||||||
@ -89,11 +121,11 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
labels = Array.Empty<VRCExpressionsMenu.Control.Label>()
|
labels = Array.Empty<VRCExpressionsMenu.Control.Label>()
|
||||||
});
|
});
|
||||||
|
|
||||||
_installTargets[installer.installTargetMenu] = newMenu;
|
_clonedMenus[installer.installTargetMenu] = newMenu;
|
||||||
targetMenu = newMenu;
|
targetMenu = newMenu;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu)
|
private VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu)
|
||||||
{
|
{
|
||||||
if (menu == null) return null;
|
if (menu == null) return null;
|
||||||
|
209
Packages/nadena.dev.modular-avatar/Editor/MenuTree.cs
Normal file
209
Packages/nadena.dev.modular-avatar/Editor/MenuTree.cs
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEngine;
|
||||||
|
using VRC.SDK3.Avatars.Components;
|
||||||
|
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||||
|
using static VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionsMenu.Control;
|
||||||
|
|
||||||
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
|
{
|
||||||
|
internal class MenuTree
|
||||||
|
{
|
||||||
|
|
||||||
|
public struct ChildElement
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parent menu control name
|
||||||
|
/// </summary>
|
||||||
|
public string menuName;
|
||||||
|
public VRCExpressionsMenu menu;
|
||||||
|
public VRCExpressionsMenu parent;
|
||||||
|
/// <summary>
|
||||||
|
/// Installer to install this menu. Is null if the this menu is not installed by the installer.
|
||||||
|
/// </summary>
|
||||||
|
public ModularAvatarMenuInstaller installer;
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the this submenu is added directly by the installer
|
||||||
|
/// </summary>
|
||||||
|
public bool isInstallerRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly HashSet<VRCExpressionsMenu> _included;
|
||||||
|
|
||||||
|
private readonly VRCExpressionsMenu _rootMenu;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map to link child menus from parent menu
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<VRCExpressionsMenu, ImmutableList<ChildElement>> _menuChildrenMap;
|
||||||
|
|
||||||
|
public MenuTree(VRCAvatarDescriptor descriptor)
|
||||||
|
{
|
||||||
|
_rootMenu = descriptor.expressionsMenu;
|
||||||
|
_included = new HashSet<VRCExpressionsMenu>();
|
||||||
|
_menuChildrenMap = new Dictionary<VRCExpressionsMenu, ImmutableList<ChildElement>>();
|
||||||
|
|
||||||
|
if (_rootMenu == null)
|
||||||
|
{
|
||||||
|
// If the route menu is null, create a temporary menu indicating the route
|
||||||
|
_rootMenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
||||||
|
}
|
||||||
|
|
||||||
|
_included.Add(_rootMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TraverseAvatarMenu()
|
||||||
|
{
|
||||||
|
if (_rootMenu == null) return;
|
||||||
|
TraverseMenu(_rootMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TraverseMenuInstaller(ModularAvatarMenuInstaller installer)
|
||||||
|
{
|
||||||
|
if (!installer.enabled) return;
|
||||||
|
if (installer.menuToAppend == null) return;
|
||||||
|
TraverseMenu(installer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableList<ChildElement> GetChildren(VRCExpressionsMenu parent)
|
||||||
|
{
|
||||||
|
if (parent == null) parent = _rootMenu;
|
||||||
|
return !_menuChildrenMap.TryGetValue(parent, out ImmutableList<ChildElement> immutableList) ? ImmutableList<ChildElement>.Empty : immutableList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ChildElement> GetChildInstallers(ModularAvatarMenuInstaller parentInstaller)
|
||||||
|
{
|
||||||
|
HashSet<VRCExpressionsMenu> visitedMenus = new HashSet<VRCExpressionsMenu>();
|
||||||
|
Queue<VRCExpressionsMenu> queue = new Queue<VRCExpressionsMenu>();
|
||||||
|
if (parentInstaller != null && parentInstaller.menuToAppend == null) yield break;
|
||||||
|
if (parentInstaller == null)
|
||||||
|
{
|
||||||
|
queue.Enqueue(_rootMenu);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (parentInstaller.menuToAppend == null) yield break;
|
||||||
|
foreach (KeyValuePair<string, VRCExpressionsMenu> childMenu in GetChildMenus(parentInstaller.menuToAppend))
|
||||||
|
{
|
||||||
|
queue.Enqueue(childMenu.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
VRCExpressionsMenu parentMenu = queue.Dequeue();
|
||||||
|
if (visitedMenus.Contains(parentMenu)) continue;
|
||||||
|
visitedMenus.Add(parentMenu);
|
||||||
|
HashSet<ModularAvatarMenuInstaller> returnedInstallers = new HashSet<ModularAvatarMenuInstaller>();
|
||||||
|
foreach (ChildElement childElement in GetChildren(parentMenu))
|
||||||
|
{
|
||||||
|
if (!childElement.isInstallerRoot)
|
||||||
|
{
|
||||||
|
queue.Enqueue(childElement.menu);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One installer may add multiple children, so filter to return only one.
|
||||||
|
if (returnedInstallers.Contains(childElement.installer)) continue;
|
||||||
|
returnedInstallers.Add(childElement.installer);
|
||||||
|
yield return childElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void TraverseMenu(VRCExpressionsMenu root)
|
||||||
|
{
|
||||||
|
foreach (KeyValuePair<string, VRCExpressionsMenu> childMenu in GetChildMenus(root))
|
||||||
|
{
|
||||||
|
TraverseMenu(root, new ChildElement
|
||||||
|
{
|
||||||
|
menuName = childMenu.Key,
|
||||||
|
menu = childMenu.Value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TraverseMenu(ModularAvatarMenuInstaller installer)
|
||||||
|
{
|
||||||
|
IEnumerable<KeyValuePair<string, VRCExpressionsMenu>> childMenus = GetChildMenus(installer.menuToAppend);
|
||||||
|
IEnumerable<VRCExpressionsMenu> parents = Enumerable.Empty<VRCExpressionsMenu>();
|
||||||
|
if (installer.installTargetMenu != null &&
|
||||||
|
ClonedMenuMappings.TryGetClonedMenus(installer.installTargetMenu, out ImmutableList<VRCExpressionsMenu> parentMenus))
|
||||||
|
{
|
||||||
|
parents = parentMenus;
|
||||||
|
}
|
||||||
|
|
||||||
|
VRCExpressionsMenu[] parentsMenus = parents.DefaultIfEmpty(installer.installTargetMenu).ToArray();
|
||||||
|
bool hasChildMenu = false;
|
||||||
|
/*
|
||||||
|
* Installer adds the controls in specified menu to the installation destination.
|
||||||
|
* So, since the specified menu itself does not exist as a child menu,
|
||||||
|
* and the child menus of the specified menu are the actual child menus, a single installer may add multiple child menus.
|
||||||
|
*/
|
||||||
|
foreach (KeyValuePair<string, VRCExpressionsMenu> childMenu in childMenus)
|
||||||
|
{
|
||||||
|
hasChildMenu = true;
|
||||||
|
ChildElement childElement = new ChildElement
|
||||||
|
{
|
||||||
|
menuName = childMenu.Key,
|
||||||
|
menu = childMenu.Value,
|
||||||
|
installer = installer,
|
||||||
|
isInstallerRoot = true
|
||||||
|
};
|
||||||
|
foreach (VRCExpressionsMenu parentMenu in parentsMenus)
|
||||||
|
{
|
||||||
|
TraverseMenu(parentMenu, childElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChildMenu) return;
|
||||||
|
/*
|
||||||
|
* If the specified menu does not have any submenus, it is not mapped as a child menu and the Installer information itself is not registered.
|
||||||
|
* Therefore, register elements that do not have child menus themselves, but only have information about the installer.
|
||||||
|
*/
|
||||||
|
foreach (VRCExpressionsMenu parentMenu in parentsMenus)
|
||||||
|
{
|
||||||
|
TraverseMenu(parentMenu, new ChildElement
|
||||||
|
{
|
||||||
|
installer = installer,
|
||||||
|
isInstallerRoot = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TraverseMenu(VRCExpressionsMenu parent, ChildElement childElement)
|
||||||
|
{
|
||||||
|
if (parent == null) parent = _rootMenu;
|
||||||
|
childElement.parent = parent;
|
||||||
|
if (!_menuChildrenMap.TryGetValue(parent, out ImmutableList<ChildElement> children))
|
||||||
|
{
|
||||||
|
children = ImmutableList<ChildElement>.Empty;
|
||||||
|
_menuChildrenMap[parent] = children;
|
||||||
|
}
|
||||||
|
|
||||||
|
_menuChildrenMap[parent] = children.Add(childElement);
|
||||||
|
if (childElement.menu == null) return;
|
||||||
|
if (_included.Contains(childElement.menu)) return;
|
||||||
|
_included.Add(childElement.menu);
|
||||||
|
foreach (KeyValuePair<string, VRCExpressionsMenu> childMenu in GetChildMenus(childElement.menu))
|
||||||
|
{
|
||||||
|
TraverseMenu(childElement.menu, new ChildElement
|
||||||
|
{
|
||||||
|
menuName = childMenu.Key,
|
||||||
|
menu = childMenu.Value,
|
||||||
|
installer = childElement.installer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<KeyValuePair<string, VRCExpressionsMenu>> GetChildMenus(VRCExpressionsMenu expressionsMenu)
|
||||||
|
{
|
||||||
|
return expressionsMenu.controls
|
||||||
|
.Where(control => control.type == ControlType.SubMenu && control.subMenu != null)
|
||||||
|
.Select(control => new KeyValuePair<string, VRCExpressionsMenu>(control.name, control.subMenu));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: effd4557902f4578af42d3bdfb7f876d
|
||||||
|
timeCreated: 1670746991
|
@ -192,6 +192,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
newMenu = Object.Instantiate(menu);
|
newMenu = Object.Instantiate(menu);
|
||||||
AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath());
|
AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath());
|
||||||
remapped[menu] = newMenu;
|
remapped[menu] = newMenu;
|
||||||
|
ClonedMenuMappings.Add(menu, newMenu);
|
||||||
|
|
||||||
foreach (var control in newMenu.controls)
|
foreach (var control in newMenu.controls)
|
||||||
{
|
{
|
||||||
|
@ -6,15 +6,15 @@ namespace nadena.dev.modular_avatar.core
|
|||||||
{
|
{
|
||||||
[AddComponentMenu("Modular Avatar/MA Menu Installer")]
|
[AddComponentMenu("Modular Avatar/MA Menu Installer")]
|
||||||
public class ModularAvatarMenuInstaller : AvatarTagComponent
|
public class ModularAvatarMenuInstaller : AvatarTagComponent
|
||||||
{
|
{
|
||||||
public VRCExpressionsMenu menuToAppend;
|
public VRCExpressionsMenu menuToAppend;
|
||||||
public VRCExpressionsMenu installTargetMenu;
|
public VRCExpressionsMenu installTargetMenu;
|
||||||
|
|
||||||
|
|
||||||
// ReSharper disable once Unity.RedundantEventFunction
|
// ReSharper disable once Unity.RedundantEventFunction
|
||||||
void Start()
|
void Start()
|
||||||
{
|
{
|
||||||
// Ensure that unity generates an enable checkbox
|
// Ensure that unity generates an enable checkbox
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user