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)); } } }