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:
らいちちゃん 2023-01-02 16:42:54 +09:00 committed by GitHub
parent 549ce8f0d3
commit a361789c43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 464 additions and 67 deletions

View File

@ -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")]

View File

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

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: aeaeff9c3af44683bb2f8f5fe6c5791d
timeCreated: 1671016064

View File

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

View File

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

View File

@ -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;

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

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: effd4557902f4578af42d3bdfb7f876d
timeCreated: 1670746991

View File

@ -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)
{

View File

@ -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
}
}
}