mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-01-30 18:22:52 +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();
|
||||
PathMappings.Clear();
|
||||
ClonedMenuMappings.Clear();
|
||||
|
||||
// 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
|
||||
@ -172,13 +173,16 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(component);
|
||||
}
|
||||
|
||||
var activator = avatarGameObject.GetComponent<AvatarActivator>();
|
||||
if (activator != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(activator);
|
||||
}
|
||||
|
||||
ClonedMenuMappings.Clear();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
[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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEditor.IMGUI.Controls;
|
||||
@ -21,6 +22,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
set => _treeView.Avatar = value;
|
||||
}
|
||||
|
||||
public ModularAvatarMenuInstaller TargetInstaller
|
||||
{
|
||||
get => _treeView.TargetInstaller;
|
||||
set => _treeView.TargetInstaller = value;
|
||||
}
|
||||
|
||||
public Action<VRCExpressionsMenu> OnMenuSelected = (menu) => { };
|
||||
|
||||
private void Awake()
|
||||
@ -51,12 +58,13 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
_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>();
|
||||
window.titleContent = new GUIContent("Select menu");
|
||||
|
||||
window.Avatar = Avatar;
|
||||
window.TargetInstaller = Installer;
|
||||
window.OnMenuSelected = OnSelect;
|
||||
|
||||
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 OnDoubleclickSelect = () => { };
|
||||
|
||||
private List<VRCExpressionsMenu> _menuItems = new List<VRCExpressionsMenu>();
|
||||
private HashSet<VRCExpressionsMenu> _visitedMenus = new HashSet<VRCExpressionsMenu>();
|
||||
|
||||
private MenuTree _menuTree;
|
||||
private Stack<VRCExpressionsMenu> _visitedMenuStack = new Stack<VRCExpressionsMenu>();
|
||||
|
||||
public AvMenuTreeView(TreeViewState state) : base(state)
|
||||
{
|
||||
}
|
||||
@ -98,49 +121,59 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
OnDoubleclickSelect.Invoke();
|
||||
}
|
||||
|
||||
protected override TreeViewItem BuildRoot()
|
||||
protected override TreeViewItem BuildRoot()
|
||||
{
|
||||
_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);
|
||||
var root = new TreeViewItem {id = -1, depth = -1, displayName = "<root>"};
|
||||
|
||||
var treeItems = new List<TreeViewItem>();
|
||||
treeItems.Add(new TreeViewItem
|
||||
{id = 0, depth = 0, displayName = $"{Avatar.gameObject.name} ({Avatar.expressionsMenu.name})"});
|
||||
|
||||
_visitedMenuStack.Push(Avatar.expressionsMenu);
|
||||
|
||||
TraverseMenu(1, treeItems, Avatar.expressionsMenu);
|
||||
|
||||
SetupParentsAndChildrenFromDepths(root, treeItems);
|
||||
|
||||
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
|
||||
&& control.subMenu != null && !_visitedMenus.Contains(control.subMenu))
|
||||
{
|
||||
items.Add(new TreeViewItem
|
||||
if (child.menu == null) continue;
|
||||
string displayName = child.installer == null ?
|
||||
$"{child.menuName} ({child.menu.name})" :
|
||||
$"{child.menuName} ({child.menu.name}) InstallerObject : {child.installer.name}";
|
||||
items.Add(
|
||||
new TreeViewItem
|
||||
{
|
||||
id = _menuItems.Count,
|
||||
id = items.Count,
|
||||
depth = depth,
|
||||
displayName = $"{control.name} ({control.subMenu.name})"
|
||||
});
|
||||
_menuItems.Add(control.subMenu);
|
||||
_visitedMenus.Add(control.subMenu);
|
||||
|
||||
TraverseMenu(depth + 1, items, control.subMenu);
|
||||
}
|
||||
displayName = displayName
|
||||
}
|
||||
);
|
||||
_menuItems.Add(child.menu);
|
||||
_visitedMenuStack.Push(child.menu);
|
||||
TraverseMenu(depth + 1, items, child.menu);
|
||||
_visitedMenuStack.Pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
@ -22,11 +23,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
private HashSet<VRCExpressionsMenu> _avatarMenus;
|
||||
|
||||
private Dictionary<VRCExpressionsMenu, List<ModularAvatarMenuInstaller>> _menuInstallersMap;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_installer = (ModularAvatarMenuInstaller) target;
|
||||
|
||||
FindMenus();
|
||||
FindMenuInstallers();
|
||||
}
|
||||
|
||||
private void SetupMenuEditor()
|
||||
@ -47,7 +51,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
_menuToAppend = _installer.menuToAppend;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnInnerInspectorGUI()
|
||||
{
|
||||
SetupMenuEditor();
|
||||
@ -95,7 +98,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
var avatar = RuntimeUtil.FindAvatarInParents(_installer.transform);
|
||||
if (avatar != null && GUILayout.Button(G("menuinstall.selectmenu")))
|
||||
{
|
||||
AvMenuTreeViewWindow.Show(avatar, menu =>
|
||||
AvMenuTreeViewWindow.Show(avatar, _installer, menu =>
|
||||
{
|
||||
installTo.objectReferenceValue = menu;
|
||||
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)
|
||||
@ -212,26 +276,25 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
if (visitedMenus.Contains(menu)) return ValidateExpressionMenuIconResult.Success;
|
||||
visitedMenus.Add(menu);
|
||||
|
||||
foreach (VRCExpressionsMenu.Control control in menu.controls)
|
||||
{
|
||||
foreach (VRCExpressionsMenu.Control control in menu.controls) {
|
||||
// Control
|
||||
ValidateExpressionMenuIconResult result = Util.ValidateExpressionMenuIcon(control.icon);
|
||||
if (result != ValidateExpressionMenuIconResult.Success) return result;
|
||||
|
||||
|
||||
// Labels
|
||||
foreach (VRCExpressionsMenu.Control.Label label in control.labels)
|
||||
{
|
||||
foreach (VRCExpressionsMenu.Control.Label label in control.labels) {
|
||||
ValidateExpressionMenuIconResult labelResult = Util.ValidateExpressionMenuIcon(label.icon);
|
||||
if (labelResult != ValidateExpressionMenuIconResult.Success) return labelResult;
|
||||
}
|
||||
|
||||
|
||||
// SubMenu
|
||||
if (control.type != VRCExpressionsMenu.Control.ControlType.SubMenu) continue;
|
||||
ValidateExpressionMenuIconResult subMenuResult = ValidateExpressionMenuIcon(control.subMenu, visitedMenus);
|
||||
if (subMenuResult != ValidateExpressionMenuIconResult.Success) return subMenuResult;
|
||||
}
|
||||
|
||||
|
||||
return ValidateExpressionMenuIconResult.Success;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ using VRC.SDK3.Avatars.Components;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
internal class MenuInstallHook
|
||||
@ -16,39 +17,52 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
);
|
||||
|
||||
private Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> _clonedMenus;
|
||||
private Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> _installTargets;
|
||||
|
||||
|
||||
private VRCExpressionsMenu _rootMenu;
|
||||
|
||||
private MenuTree _menuTree;
|
||||
private Stack<ModularAvatarMenuInstaller> _visitedInstallerStack;
|
||||
|
||||
public void OnPreprocessAvatar(GameObject avatarRoot)
|
||||
{
|
||||
var menuInstallers = avatarRoot.GetComponentsInChildren<ModularAvatarMenuInstaller>(true)
|
||||
.Where(c => c.enabled)
|
||||
ModularAvatarMenuInstaller[] menuInstallers = avatarRoot.GetComponentsInChildren<ModularAvatarMenuInstaller>(true)
|
||||
.Where(menuInstaller => menuInstaller.enabled)
|
||||
.ToArray();
|
||||
if (menuInstallers.Length == 0) return;
|
||||
|
||||
|
||||
_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>();
|
||||
AssetDatabase.CreateAsset(menu, Util.GenerateAssetPath());
|
||||
avatar.expressionsMenu = menu;
|
||||
_clonedMenus[menu] = menu;
|
||||
}
|
||||
|
||||
_rootMenu = avatar.expressionsMenu;
|
||||
_menuTree = new MenuTree(avatar);
|
||||
_menuTree.TraverseAvatarMenu();
|
||||
|
||||
avatar.expressionsMenu = CloneMenu(avatar.expressionsMenu);
|
||||
_installTargets = new Dictionary<VRCExpressionsMenu, VRCExpressionsMenu>(_clonedMenus);
|
||||
|
||||
foreach (var install in menuInstallers)
|
||||
|
||||
foreach (ModularAvatarMenuInstaller installer 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;
|
||||
|
||||
@ -57,30 +71,48 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
installer.installTargetMenu = _rootMenu;
|
||||
}
|
||||
|
||||
if (installer.installTargetMenu == null || installer.menuToAppend == null) return;
|
||||
if (!_installTargets.TryGetValue(installer.installTargetMenu, out var targetMenu)) return;
|
||||
if (_installTargets.ContainsKey(installer.menuToAppend)) return;
|
||||
if (installTarget == null)
|
||||
{
|
||||
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
|
||||
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
|
||||
var newMenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
||||
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));
|
||||
targetMenu.controls.RemoveRange(keepCount,
|
||||
targetMenu.controls.Count - keepCount
|
||||
);
|
||||
|
||||
targetMenu.controls.Add(new VRCExpressionsMenu.Control()
|
||||
targetMenu.controls.Add(new VRCExpressionsMenu.Control
|
||||
{
|
||||
name = "More",
|
||||
type = VRCExpressionsMenu.Control.ControlType.SubMenu,
|
||||
subMenu = newMenu,
|
||||
parameter = new VRCExpressionsMenu.Control.Parameter()
|
||||
parameter = new VRCExpressionsMenu.Control.Parameter
|
||||
{
|
||||
name = ""
|
||||
},
|
||||
@ -89,11 +121,11 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
labels = Array.Empty<VRCExpressionsMenu.Control.Label>()
|
||||
});
|
||||
|
||||
_installTargets[installer.installTargetMenu] = newMenu;
|
||||
_clonedMenus[installer.installTargetMenu] = newMenu;
|
||||
targetMenu = newMenu;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu)
|
||||
{
|
||||
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);
|
||||
AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath());
|
||||
remapped[menu] = newMenu;
|
||||
ClonedMenuMappings.Add(menu, newMenu);
|
||||
|
||||
foreach (var control in newMenu.controls)
|
||||
{
|
||||
|
@ -6,15 +6,15 @@ namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
[AddComponentMenu("Modular Avatar/MA Menu Installer")]
|
||||
public class ModularAvatarMenuInstaller : AvatarTagComponent
|
||||
{
|
||||
public VRCExpressionsMenu menuToAppend;
|
||||
{
|
||||
public VRCExpressionsMenu menuToAppend;
|
||||
public VRCExpressionsMenu installTargetMenu;
|
||||
|
||||
|
||||
// ReSharper disable once Unity.RedundantEventFunction
|
||||
void Start()
|
||||
{
|
||||
// Ensure that unity generates an enable checkbox
|
||||
}
|
||||
{
|
||||
// Ensure that unity generates an enable checkbox
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user