diff --git a/Packages/nadena.dev.modular-avatar/Editor/AvatarProcessor.cs b/Packages/nadena.dev.modular-avatar/Editor/AvatarProcessor.cs index 78622461..77de548b 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/AvatarProcessor.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/AvatarProcessor.cs @@ -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(); if (activator != null) { UnityEngine.Object.DestroyImmediate(activator); } + + ClonedMenuMappings.Clear(); } + + } [SuppressMessage("ReSharper", "PossibleNullReferenceException")] diff --git a/Packages/nadena.dev.modular-avatar/Editor/ClonedMenuMappings.cs b/Packages/nadena.dev.modular-avatar/Editor/ClonedMenuMappings.cs new file mode 100644 index 00000000..a886193e --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/ClonedMenuMappings.cs @@ -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 + { + /// + /// 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. + /// + private static readonly Dictionary> ClonedMappings = + new Dictionary>(); + + /// + /// Map to link the clone source from the cloned menu. + /// Map is the opposite of ClonedMappings. + /// + private static readonly Dictionary OriginalMapping = + new Dictionary(); + + public static void Clear() + { + ClonedMappings.Clear(); + OriginalMapping.Clear(); + } + + public static void Add(VRCExpressionsMenu original, VRCExpressionsMenu clonedMenu) + { + if (!ClonedMappings.TryGetValue(original, out ImmutableList clonedMenus)) + { + clonedMenus = ImmutableList.Empty; + } + ClonedMappings[original] = clonedMenus.Add(clonedMenu); + OriginalMapping[clonedMenu] = original; + } + + public static bool TryGetClonedMenus(VRCExpressionsMenu original, out ImmutableList clonedMenus) + { + return ClonedMappings.TryGetValue(original, out clonedMenus); + } + + public static VRCExpressionsMenu GetOriginal(VRCExpressionsMenu cloned) + { + return OriginalMapping.TryGetValue(cloned, out VRCExpressionsMenu original) ? original : null; + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/ClonedMenuMappings.cs.meta b/Packages/nadena.dev.modular-avatar/Editor/ClonedMenuMappings.cs.meta new file mode 100644 index 00000000..656acaab --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/ClonedMenuMappings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: aeaeff9c3af44683bb2f8f5fe6c5791d +timeCreated: 1671016064 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuTreeView.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuTreeView.cs index b947801c..5f73a280 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuTreeView.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuTreeView.cs @@ -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 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 OnSelect) + internal static void Show(VRCAvatarDescriptor Avatar, ModularAvatarMenuInstaller Installer, Action OnSelect) { var window = GetWindow(); 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 OnSelect = (menu) => { }; internal Action OnDoubleclickSelect = () => { }; private List _menuItems = new List(); private HashSet _visitedMenus = new HashSet(); + private MenuTree _menuTree; + private Stack _visitedMenuStack = new Stack(); + 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(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, ""); + List treeItems = new List + { + 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 = ""}; - - var treeItems = new List(); - 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 items, VRCExpressionsMenu menu) + private void TraverseMenu(int depth, List items, VRCExpressionsMenu menu) { - foreach (var control in menu.controls) + IEnumerable 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(); } } } diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs index b9566fe1..f7ca7bcc 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs @@ -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 _avatarMenus; + private Dictionary> _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>(); + var avatar = RuntimeUtil.FindAvatarInParents(((Component)target).transform); + if (avatar == null) return; + var menuInstallers = avatar.GetComponentsInChildren(true) + .Where(menuInstaller => menuInstaller.enabled && menuInstaller.menuToAppend != null); + foreach (ModularAvatarMenuInstaller menuInstaller in menuInstallers) + { + if (menuInstaller == target) continue; + var visitedMenus = new HashSet(); + var queue = new Queue(); + 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 fromInstallers)) + { + fromInstallers = new List(); + _menuInstallersMap[control.subMenu] = fromInstallers; + } + + fromInstallers.Add(menuInstaller); + visitedMenus.Add(control.subMenu); + queue.Enqueue(control.subMenu); + } + } + } + } + + private bool IsMenuReachable(VRCAvatarDescriptor avatar, VRCExpressionsMenu menu, HashSet visitedInstaller = null) + { + if (_avatarMenus == null || _avatarMenus.Contains(menu)) return true; + + if (_menuInstallersMap == null) return true; + if (visitedInstaller == null) visitedInstaller = new HashSet { (ModularAvatarMenuInstaller)target }; + + if (!_menuInstallersMap.TryGetValue(menu, out List 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 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; } + } } \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/MenuInstallHook.cs b/Packages/nadena.dev.modular-avatar/Editor/MenuInstallHook.cs index 95ca0dc3..c469fd2e 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/MenuInstallHook.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/MenuInstallHook.cs @@ -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 _clonedMenus; - private Dictionary _installTargets; + private VRCExpressionsMenu _rootMenu; + private MenuTree _menuTree; + private Stack _visitedInstallerStack; + public void OnPreprocessAvatar(GameObject avatarRoot) { - var menuInstallers = avatarRoot.GetComponentsInChildren(true) - .Where(c => c.enabled) + ModularAvatarMenuInstaller[] menuInstallers = avatarRoot.GetComponentsInChildren(true) + .Where(menuInstaller => menuInstaller.enabled) .ToArray(); if (menuInstallers.Length == 0) return; + _clonedMenus = new Dictionary(); + _visitedInstallerStack = new Stack(); + + VRCAvatarDescriptor avatar = avatarRoot.GetComponent(); - var avatar = avatarRoot.GetComponent(); - - if (avatar.expressionsMenu == null) + if (avatar.expressionsMenu == null) { var menu = ScriptableObject.CreateInstance(); 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(_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(); 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() }); - _installTargets[installer.installTargetMenu] = newMenu; + _clonedMenus[installer.installTargetMenu] = newMenu; targetMenu = newMenu; } } - + private VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu) { if (menu == null) return null; diff --git a/Packages/nadena.dev.modular-avatar/Editor/MenuTree.cs b/Packages/nadena.dev.modular-avatar/Editor/MenuTree.cs new file mode 100644 index 00000000..cde6ab69 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/MenuTree.cs @@ -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 + { + /// + /// Parent menu control name + /// + public string menuName; + public VRCExpressionsMenu menu; + public VRCExpressionsMenu parent; + /// + /// Installer to install this menu. Is null if the this menu is not installed by the installer. + /// + public ModularAvatarMenuInstaller installer; + /// + /// Whether the this submenu is added directly by the installer + /// + public bool isInstallerRoot; + } + + private readonly HashSet _included; + + private readonly VRCExpressionsMenu _rootMenu; + + /// + /// Map to link child menus from parent menu + /// + private readonly Dictionary> _menuChildrenMap; + + public MenuTree(VRCAvatarDescriptor descriptor) + { + _rootMenu = descriptor.expressionsMenu; + _included = new HashSet(); + _menuChildrenMap = new Dictionary>(); + + if (_rootMenu == null) + { + // If the route menu is null, create a temporary menu indicating the route + _rootMenu = ScriptableObject.CreateInstance(); + } + + _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 GetChildren(VRCExpressionsMenu parent) + { + if (parent == null) parent = _rootMenu; + return !_menuChildrenMap.TryGetValue(parent, out ImmutableList immutableList) ? ImmutableList.Empty : immutableList; + } + + public IEnumerable GetChildInstallers(ModularAvatarMenuInstaller parentInstaller) + { + HashSet visitedMenus = new HashSet(); + Queue queue = new Queue(); + if (parentInstaller != null && parentInstaller.menuToAppend == null) yield break; + if (parentInstaller == null) + { + queue.Enqueue(_rootMenu); + } + else + { + if (parentInstaller.menuToAppend == null) yield break; + foreach (KeyValuePair 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 returnedInstallers = new HashSet(); + 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 childMenu in GetChildMenus(root)) + { + TraverseMenu(root, new ChildElement + { + menuName = childMenu.Key, + menu = childMenu.Value + }); + } + } + + private void TraverseMenu(ModularAvatarMenuInstaller installer) + { + IEnumerable> childMenus = GetChildMenus(installer.menuToAppend); + IEnumerable parents = Enumerable.Empty(); + if (installer.installTargetMenu != null && + ClonedMenuMappings.TryGetClonedMenus(installer.installTargetMenu, out ImmutableList 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 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 children)) + { + children = ImmutableList.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 childMenu in GetChildMenus(childElement.menu)) + { + TraverseMenu(childElement.menu, new ChildElement + { + menuName = childMenu.Key, + menu = childMenu.Value, + installer = childElement.installer + }); + } + } + + private static IEnumerable> GetChildMenus(VRCExpressionsMenu expressionsMenu) + { + return expressionsMenu.controls + .Where(control => control.type == ControlType.SubMenu && control.subMenu != null) + .Select(control => new KeyValuePair(control.name, control.subMenu)); + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/MenuTree.cs.meta b/Packages/nadena.dev.modular-avatar/Editor/MenuTree.cs.meta new file mode 100644 index 00000000..dc5993a1 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/MenuTree.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: effd4557902f4578af42d3bdfb7f876d +timeCreated: 1670746991 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/RenameParametersHook.cs b/Packages/nadena.dev.modular-avatar/Editor/RenameParametersHook.cs index 94ecab72..e22d3a07 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/RenameParametersHook.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/RenameParametersHook.cs @@ -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) { diff --git a/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuInstaller.cs b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuInstaller.cs index 4c38dc65..826162f9 100644 --- a/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuInstaller.cs +++ b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuInstaller.cs @@ -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 + } } } \ No newline at end of file