From 1528db1312f1274ae9f98a8f7267a8d6066c539a Mon Sep 17 00:00:00 2001 From: bd_ Date: Thu, 23 Feb 2023 19:23:33 +0900 Subject: [PATCH] rewrite the virtual menu system to be more extensible --- .../VirtualMenuTests/VirtualMenuTests.cs | 10 +- Packages/manifest.json | 1 + .../ErrorReporting/ComponentValidation.cs | 3 +- .../Editor/ErrorReporting/ErrorLog.cs | 4 +- .../Editor/Inspector/AvMenuTreeView.cs | 3 +- .../Editor/Inspector/MenuInstallerEditor.cs | 1 + .../Editor/MenuExtractor.cs | 8 +- .../Editor/MenuGeneration/VirtualMenu.cs | 432 +++++++----------- .../Runtime/ModularAvatarMenuGroup.cs | 18 + .../Runtime/ModularAvatarMenuGroup.cs.meta | 3 + .../Runtime/ModularAvatarMenuInstallTarget.cs | 49 +- .../Runtime/ModularAvatarMenuItem.cs | 123 +---- .../Runtime/menu.meta | 4 + .../Runtime/menu/VirtualMenuAPI.cs | 174 +++++++ .../Runtime/menu/VirtualMenuAPI.cs.meta | 3 + Packages/packages-lock.json | 7 + Packages/vpm-manifest.json | 7 +- ProjectSettings/GraphicsSettings.asset | 11 +- 18 files changed, 421 insertions(+), 440 deletions(-) create mode 100644 Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuGroup.cs create mode 100644 Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuGroup.cs.meta create mode 100644 Packages/nadena.dev.modular-avatar/Runtime/menu.meta create mode 100644 Packages/nadena.dev.modular-avatar/Runtime/menu/VirtualMenuAPI.cs create mode 100644 Packages/nadena.dev.modular-avatar/Runtime/menu/VirtualMenuAPI.cs.meta diff --git a/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/VirtualMenuTests.cs b/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/VirtualMenuTests.cs index ec407a59..bcb41689 100644 --- a/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/VirtualMenuTests.cs +++ b/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/VirtualMenuTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using nadena.dev.modular_avatar.core; using nadena.dev.modular_avatar.core.editor.menu; +using nadena.dev.modular_avatar.core.menu; using NUnit.Framework; using UnityEditor; using UnityEditor.VersionControl; @@ -13,12 +14,14 @@ namespace modular_avatar_tests.VirtualMenuTests { private Texture2D testTex; private List toDestroy; + private int controlIndex; public override void Setup() { base.Setup(); testTex = new Texture2D(1, 1); toDestroy = new List(); + controlIndex = 0; } public override void Teardown() @@ -280,14 +283,14 @@ namespace modular_avatar_tests.VirtualMenuTests Assert.AreEqual(2, virtualMenu.ResolvedMenu.Count); var rootMenu = virtualMenu.ResolvedMenu[RootMenu.Instance]; - var item_node = virtualMenu.ResolvedMenu[item]; + var item_node = virtualMenu.ResolvedMenu[new MenuNodesUnder(item.gameObject)]; Assert.AreEqual(1, rootMenu.Controls.Count); Assert.AreSame(RootMenu.Instance, rootMenu.NodeKey); AssertControlEquals(item.Control, rootMenu.Controls[0]); Assert.AreSame(item_node, rootMenu.Controls[0].SubmenuNode); Assert.AreEqual(1, item_node.Controls.Count); - Assert.AreSame(item, item_node.NodeKey); + Assert.AreEqual(new MenuNodesUnder(item.gameObject), item_node.NodeKey); AssertControlEquals(menu_a.controls[0], item_node.Controls[0]); } @@ -558,11 +561,10 @@ namespace modular_avatar_tests.VirtualMenuTests VRCExpressionsMenu.Control.ControlType.RadialPuppet, VRCExpressionsMenu.Control.ControlType.FourAxisPuppet, VRCExpressionsMenu.Control.ControlType.TwoAxisPuppet, - VRCExpressionsMenu.Control.ControlType.OneAxisPuppet, }; control.type = types[Random.Range(0, types.Length)]; - control.name = "Test Control " + GUID.Generate(); + control.name = "Test Control " + controlIndex++; control.parameter = new VRCExpressionsMenu.Control.Parameter(); control.parameter.name = "Test Parameter " + GUID.Generate(); control.icon = new Texture2D(1, 1); diff --git a/Packages/manifest.json b/Packages/manifest.json index 8a81d421..97863409 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -11,6 +11,7 @@ "com.unity.ugui": "1.0.0", "com.unity.xr.oculus.standalone": "2.38.4", "com.unity.xr.openvr.standalone": "2.0.5", + "de.thryrallo.vrc.avatar-performance-tools": "https://github.com/Thryrallo/VRC-Avatar-Performance-Tools.git", "nadena.dev.modular-avatar": "0.0.1", "com.unity.modules.ai": "1.0.0", "com.unity.modules.androidjni": "1.0.0", diff --git a/Packages/nadena.dev.modular-avatar/Editor/ErrorReporting/ComponentValidation.cs b/Packages/nadena.dev.modular-avatar/Editor/ErrorReporting/ComponentValidation.cs index 9b098396..ae9652fc 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/ErrorReporting/ComponentValidation.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/ErrorReporting/ComponentValidation.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using nadena.dev.modular_avatar.core; +using nadena.dev.modular_avatar.core.menu; using UnityEngine; using VRC.SDK3.Avatars.Components; @@ -137,7 +138,7 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting private static List CheckInternal(ModularAvatarMenuInstaller mi) { // TODO - check that target menu is in the avatar - if (mi.menuToAppend == null && mi.GetComponent() == null) + if (mi.menuToAppend == null && mi.GetComponent() == null) { return new List() { diff --git a/Packages/nadena.dev.modular-avatar/Editor/ErrorReporting/ErrorLog.cs b/Packages/nadena.dev.modular-avatar/Editor/ErrorReporting/ErrorLog.cs index 6fea2c85..26b98951 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/ErrorReporting/ErrorLog.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/ErrorReporting/ErrorLog.cs @@ -334,7 +334,7 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting internal static T ReportingObject(UnityEngine.Object obj, Func action) { - CurrentReport._references.Push(obj); + if (obj != null) CurrentReport._references.Push(obj); try { return action(); @@ -347,7 +347,7 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting } finally { - CurrentReport._references.Pop(); + if (obj != null) CurrentReport._references.Pop(); } } diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuTreeView.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuTreeView.cs index dbd8a682..3e089c77 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuTreeView.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuTreeView.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using nadena.dev.modular_avatar.core.editor.menu; +using nadena.dev.modular_avatar.core.menu; using NUnit.Framework; using UnityEditor; using UnityEditor.IMGUI.Controls; @@ -148,7 +149,7 @@ namespace nadena.dev.modular_avatar.core.editor return root; } - private void TraverseMenu(int depth, List items, MenuNode node) + private void TraverseMenu(int depth, List items, VirtualMenuNode node) { IEnumerable children = node.Controls .Where(control => control.type == VRCExpressionsMenu.Control.ControlType.SubMenu && diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs index ede7df51..db17b650 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using nadena.dev.modular_avatar.core.editor.menu; +using nadena.dev.modular_avatar.core.menu; using UnityEditor; using UnityEngine; using VRC.SDK3.Avatars.Components; diff --git a/Packages/nadena.dev.modular-avatar/Editor/MenuExtractor.cs b/Packages/nadena.dev.modular-avatar/Editor/MenuExtractor.cs index 22861d5e..ec2ff744 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/MenuExtractor.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/MenuExtractor.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using nadena.dev.modular_avatar.core.menu; using UnityEditor; using UnityEngine; using VRC.SDK3.Avatars.Components; @@ -23,17 +24,18 @@ namespace nadena.dev.modular_avatar.core.editor type = VRCExpressionsMenu.Control.ControlType.SubMenu, name = "Avatar Menu" }; - var rootMenu = ConvertSubmenu(gameObj, fakeControl, new Dictionary()); + var rootMenu = ConvertSubmenu(gameObj, fakeControl, + new Dictionary()); Undo.RecordObject(avatar, "Convert menu"); avatar.expressionsMenu = null; rootMenu.gameObject.AddComponent(); } - private static MenuSource ConvertSubmenu( + private static MenuSourceComponent ConvertSubmenu( GameObject parentObj, VRCExpressionsMenu.Control sourceControl, - Dictionary convertedMenus + Dictionary convertedMenus ) { var itemObj = new GameObject(); diff --git a/Packages/nadena.dev.modular-avatar/Editor/MenuGeneration/VirtualMenu.cs b/Packages/nadena.dev.modular-avatar/Editor/MenuGeneration/VirtualMenu.cs index 73ff5f71..d82f6183 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/MenuGeneration/VirtualMenu.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/MenuGeneration/VirtualMenu.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using JetBrains.Annotations; +using nadena.dev.modular_avatar.core.menu; using nadena.dev.modular_avatar.editor.ErrorReporting; using UnityEditor; using UnityEngine; @@ -12,57 +15,116 @@ namespace nadena.dev.modular_avatar.core.editor.menu /// /// Sentinel object to represent the avatar root menu (for avatars which don't have a root menu) /// - internal sealed class RootMenu + internal sealed class RootMenu : MenuSource { public static readonly RootMenu Instance = new RootMenu(); private RootMenu() { } - } - /// - /// A MenuNode represents a single VRCExpressionsMenu, prior to overflow splitting. MenuNodes form a directed graph, - /// which may contain cycles, and may include contributions from multiple MenuInstallers, or from the base avatar - /// menu. - /// - internal class MenuNode - { - internal List Controls = new List(); - - /// - /// The primary (serialized) object that contributed to this menu; if we want to add more items to it, we look - /// here. This can currently be either a VRCExpressionsMenu, a MAMenuItem, or a RootMenu. - /// - internal readonly object NodeKey; - - internal MenuNode(object nodeKey) + public void Visit(NodeContext context) { - NodeKey = nodeKey; + // we initialize the root node manually + throw new NotImplementedException(); } } - internal class VirtualControl : VRCExpressionsMenu.Control + class NodeContextImpl : NodeContext { - /// - /// VirtualControls do not reference real VRCExpressionsMenu objects, but rather virtual MenuNodes. - /// - internal MenuNode SubmenuNode; + [CanBeNull] + internal delegate VirtualMenuNode NodeForDelegate(object menu); - internal VirtualControl(VRCExpressionsMenu.Control control) + private readonly ImmutableDictionary> + _menuToInstallerMap; + + private readonly VirtualMenuNode _node; + private readonly NodeForDelegate _nodeFor; + private readonly HashSet _visited = new HashSet(); + + public NodeContextImpl( + VirtualMenuNode node, + NodeForDelegate nodeFor, + ImmutableDictionary> menuToInstallerMap) { - this.name = control.name; - this.type = control.type; - this.parameter = new Parameter() {name = control?.parameter?.name}; - this.value = control.value; - this.icon = control.icon; - this.style = control.style; - this.subMenu = null; - this.subParameters = control.subParameters?.Select(p => new VRCExpressionsMenu.Control.Parameter() + _node = node; + _nodeFor = nodeFor; + _menuToInstallerMap = menuToInstallerMap; + } + + public void PushNode(VRCExpressionsMenu expMenu) + { + if (expMenu == null) return; + if (_visited.Contains(expMenu)) return; + _visited.Add(expMenu); + + foreach (var control in expMenu.controls) { - name = p.name - })?.ToArray(); - this.labels = control.labels?.ToArray(); + PushControl(control); + } + + if (_menuToInstallerMap.TryGetValue(expMenu, out var installers)) + { + foreach (var installer in installers) + { + PushNode(installer); + } + } + } + + public void PushNode(MenuSource source) + { + if (source == null) return; + if (_visited.Contains(source)) return; + _visited.Add(source); + + BuildReport.ReportingObject(source as UnityEngine.Object, () => source.Visit(this)); + } + + public void PushNode(ModularAvatarMenuInstaller installer) + { + if (installer == null) return; + if (_visited.Contains(installer)) return; + _visited.Add(installer); + + BuildReport.ReportingObject(installer, () => + { + var menuSourceComp = installer.GetComponent(); + if (menuSourceComp != null) + { + PushNode(menuSourceComp); + } + else if (installer.menuToAppend != null) + { + PushNode(installer.menuToAppend); + } + }); + } + + public void PushControl(VRCExpressionsMenu.Control control) + { + var virtualControl = new VirtualControl(control); + + virtualControl.SubmenuNode = NodeFor(control.subMenu); + + PushControl(virtualControl); + } + + public void PushControl(VirtualControl control) + { + _node.Controls.Add(control); + } + + public VirtualMenuNode NodeFor(VRCExpressionsMenu menu) + { + if (menu == null) return null; + return _nodeFor(menu); + } + + public VirtualMenuNode NodeFor(MenuSource source) + { + if (source == null) return null; + return _nodeFor(source); } } @@ -100,17 +162,13 @@ namespace nadena.dev.modular_avatar.core.editor.menu private Dictionary> _installerToTargetComponent = new Dictionary>(); - /// - /// Maps from either VRCEXpressionsMenu objects or MenuItems to menu nodes. The ROOT_MENU here is a special - /// object used to mark contributors to the avatar root menu. - /// - private Dictionary _menuNodeMap = new Dictionary(); - - private Dictionary _resolvedMenu = new Dictionary(); + private Dictionary _resolvedMenu = new Dictionary(); // TODO: immutable? - public Dictionary ResolvedMenu => _resolvedMenu; - public MenuNode RootMenuNode => ResolvedMenu[RootMenuKey]; + public Dictionary ResolvedMenu => _resolvedMenu; + public VirtualMenuNode RootMenuNode => ResolvedMenu[RootMenuKey]; + + private Queue _pendingGeneration = new Queue(); /// /// Initializes the VirtualMenu. @@ -121,12 +179,10 @@ namespace nadena.dev.modular_avatar.core.editor.menu if (rootMenu != null) { RootMenuKey = rootMenu; - ImportMenu(rootMenu); } else { RootMenuKey = RootMenu.Instance; - _menuNodeMap[RootMenu.Instance] = new MenuNode(RootMenu.Instance); } } @@ -162,27 +218,6 @@ namespace nadena.dev.modular_avatar.core.editor.menu } } - private MenuNode ImportMenu(VRCExpressionsMenu menu, object menuKey = null) - { - if (menuKey == null) menuKey = menu; - if (_menuNodeMap.TryGetValue(menuKey, out var subMenuNode)) return subMenuNode; - - var node = new MenuNode(menuKey); - _menuNodeMap[menuKey] = node; - foreach (var control in menu.controls) - { - var virtualControl = new VirtualControl(control); - if (control.subMenu != null) - { - virtualControl.SubmenuNode = ImportMenu(control.subMenu); - } - - node.Controls.Add(virtualControl); - } - - return node; - } - /// /// Registers a menu installer with this virtual menu. Because we need the full set of components indexed to /// determine the effects of this menu installer, further processing is deferred until we freeze the menu. @@ -224,7 +259,68 @@ namespace nadena.dev.modular_avatar.core.editor.menu /// internal void FreezeMenu() { - ResolveNode(RootMenuKey); + ImmutableDictionary> menuToInstallerFiltered = + _targetMenuToInstaller + .Select(kvp => new KeyValuePair>( + kvp.Key, + kvp.Value.Where(i => !_installerToTargetComponent.ContainsKey(i)).ToImmutableList() + )) + .Where(kvp => !kvp.Value.IsEmpty) + .ToImmutableDictionary(); + + var RootNode = new VirtualMenuNode(RootMenuKey); + _resolvedMenu[RootMenuKey] = RootNode; + + var rootContext = new NodeContextImpl(RootNode, NodeFor, menuToInstallerFiltered); + if (RootMenuKey is VRCExpressionsMenu menu) + { + foreach (var control in menu.controls) + { + rootContext.PushControl(control); + } + } + + if (menuToInstallerFiltered.TryGetValue(RootMenuKey, out var installers)) + { + foreach (var installer in installers) + { + rootContext.PushNode(installer); + } + } + + while (_pendingGeneration.Count > 0) + { + _pendingGeneration.Dequeue()(); + } + + VirtualMenuNode NodeFor(object key) + { + if (_resolvedMenu.TryGetValue(key, out var node)) return node; + node = new VirtualMenuNode(key); + _resolvedMenu[key] = node; + + _pendingGeneration.Enqueue(() => + { + BuildReport.ReportingObject(key as UnityEngine.Object, () => + { + var context = new NodeContextImpl(node, NodeFor, menuToInstallerFiltered); + if (key is VRCExpressionsMenu expMenu) + { + context.PushNode(expMenu); + } + else if (key is MenuSource source) + { + context.PushNode(source); + } + else + { + // TODO warning + } + }); + }); + + return node; + } } internal VRCExpressionsMenu SerializeMenu(Action SaveAsset) @@ -265,203 +361,5 @@ namespace nadena.dev.modular_avatar.core.editor.menu return menu; } } - - private HashSet _sourceTrace = null; - - private MenuNode ResolveNode(object nodeKey) - { - if (_resolvedMenu.TryGetValue(nodeKey, out var node)) return node; - - if (nodeKey is ModularAvatarMenuItem item) - { - return ResolveSubmenuItem(item); - } - - if (nodeKey is VRCExpressionsMenu menu) - { - ImportMenu(menu); - } - - if (_menuNodeMap.TryGetValue(nodeKey, out node)) - { - _resolvedMenu[nodeKey] = node; - } - else - { - node = new MenuNode(nodeKey); - _menuNodeMap[nodeKey] = node; - _resolvedMenu[nodeKey] = node; - } - - - // Find any menu installers which target this node, and recursively include them. - // Note that we're also recursing through MenuNodes, and should not consider the objects visited on - // different submenus when cutting off cycles. - var priorTrace = _sourceTrace; - _sourceTrace = new HashSet(); - try - { - // We use a stack here to maintain the expected order of elements. Consider if we have three menu - // installers as follows: - // A -> root - // B -> root - // C -> A - // We'll first push [B, A], then visit A. At this point we'll push C back on the stack, so we visit - // [A, C, B] in the end. - Stack installers = new Stack(); - if (_targetMenuToInstaller.TryGetValue(nodeKey, out var rootInstallers)) - { - foreach (var i in rootInstallers.Select(x => x).Reverse()) - { - if (_installerToTargetComponent.ContainsKey(i)) continue; - installers.Push(i); - } - } - - while (installers.Count > 0) - { - var next = installers.Pop(); - if (_sourceTrace.Contains(next)) continue; - _sourceTrace.Add(next); - - BuildReport.ReportingObject(next, () => ResolveInstaller(node, next, installers)); - } - - // Resolve any submenus - foreach (var virtualControl in node.Controls) - { - if (virtualControl.SubmenuNode != null) - { - virtualControl.SubmenuNode = ResolveNode(virtualControl.SubmenuNode.NodeKey); - } - } - } - finally - { - _sourceTrace = priorTrace; - } - - return node; - } - - private MenuNode ResolveSubmenuItem(ModularAvatarMenuItem item) - { - return BuildReport.ReportingObject(item, () => - { - MenuNode node = new MenuNode(item); - _resolvedMenu[item] = node; - - switch (item.MenuSource) - { - case SubmenuSource.MenuAsset: - { - if (item.Control.subMenu != null) - { - node.Controls = ResolveNode(item.Control.subMenu).Controls; - } - - break; - } - case SubmenuSource.Children: - { - var transformRoot = item.menuSource_otherObjectChildren != null - ? item.menuSource_otherObjectChildren.transform - : item.transform; - foreach (Transform child in transformRoot) - { - if (!child.gameObject.activeSelf) continue; - - var source = child.GetComponent(); - if (source == null) continue; - - if (source is ModularAvatarMenuItem subItem) - { - var control = new VirtualControl(subItem.Control); - if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu) - { - control.SubmenuNode = ResolveNode(subItem); - } - - control.name = subItem.gameObject.name; - node.Controls.Add(control); - } - else if (source is ModularAvatarMenuInstallTarget target && target.installer != null) - { - ResolveInstaller(node, target.installer, new Stack()); - } - else - { - // TODO validation - } - } - - break; - } - default: - // TODO validation - break; - } - - return node; - }); - } - - private void ResolveInstaller(MenuNode node, ModularAvatarMenuInstaller installer, - Stack installers) - { - if (installer == null || !installer.enabled) return; - - var menuSource = installer.GetComponent(); - - if (menuSource == null) - { - var expMenu = installer.menuToAppend; - if (expMenu == null) return; - var controls = expMenu.controls; - if (controls == null) return; - - foreach (var control in controls) - { - var virtualControl = new VirtualControl(control); - if (control.subMenu != null) - { - virtualControl.SubmenuNode = ResolveNode(control.subMenu); - } - - node.Controls.Add(virtualControl); - } - - if (_targetMenuToInstaller.TryGetValue(expMenu, out var subInstallers)) - { - foreach (var subInstaller in subInstallers.Select(x => x).Reverse()) - { - if (_installerToTargetComponent.ContainsKey(subInstaller)) continue; - installers.Push(subInstaller); - } - } - } - else if (menuSource is ModularAvatarMenuInstallTarget target) - { - if (target.installer != null) - { - installers.Push(target.installer); - } - } - else if (menuSource is ModularAvatarMenuItem item) - { - var virtualControl = new VirtualControl(item.Control); - virtualControl.name = item.gameObject.name; - node.Controls.Add(virtualControl); - if (virtualControl.type == VRCExpressionsMenu.Control.ControlType.SubMenu) - { - virtualControl.SubmenuNode = ResolveNode(item); - } - } - else - { - BuildReport.Log(ReportLevel.Error, "virtual_menu.unknown_source_type", - strings: new object[] {menuSource.GetType().ToString()}); - } - } } } \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuGroup.cs b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuGroup.cs new file mode 100644 index 00000000..91da3f00 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuGroup.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using nadena.dev.modular_avatar.core.menu; +using UnityEngine; +using VRC.SDK3.Avatars.ScriptableObjects; + +namespace nadena.dev.modular_avatar.core +{ + public class ModularAvatarMenuGroup : MenuSourceComponent + { + private bool recursing = false; + + public override void Visit(NodeContext context) + { + context.PushNode(new MenuNodesUnder(gameObject)); + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuGroup.cs.meta b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuGroup.cs.meta new file mode 100644 index 00000000..96715265 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuGroup.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 97e46a47dd8a425eb4ce9411defe313d +timeCreated: 1677080023 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuInstallTarget.cs b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuInstallTarget.cs index 9b5e7755..da61cf34 100644 --- a/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuInstallTarget.cs +++ b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuInstallTarget.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using nadena.dev.modular_avatar.core.menu; using VRC.SDK3.Avatars.ScriptableObjects; using VRC.SDKBase; @@ -14,55 +15,13 @@ namespace nadena.dev.modular_avatar.core /// /// We can also end up with a loop between install targets; in this case, we break the loop at an arbitrary point. /// - internal class ModularAvatarMenuInstallTarget : MenuSource + internal class ModularAvatarMenuInstallTarget : MenuSourceComponent { public ModularAvatarMenuInstaller installer; - private static HashSet _recursing = new HashSet(); - - internal delegate T Returning(); - - /** - * Temporarily clears the list of install targets we're recursing through. This is useful if we need to generate - * a submenu; these have their own recursion stack, and we shouldn't truncate the set of controls registered on - * a different submenu that happens to transclude the same point. - */ - internal static T PushRecursing(Returning callback) + public override void Visit(NodeContext context) { - HashSet oldRecursing = _recursing; - _recursing = new HashSet(); - try - { - return callback(); - } - finally - { - _recursing = oldRecursing; - } - } - - internal override VRCExpressionsMenu.Control[] GenerateMenu() - { - if (installer == null) return new VRCExpressionsMenu.Control[] { }; - - _recursing.Add(this); - try - { - var source = installer.GetComponent(); - if (source != null) - { - return source.GenerateMenu(); - } - else - { - // ReSharper disable once Unity.NoNullPropagation - return installer.menuToAppend?.controls?.ToArray() ?? new VRCExpressionsMenu.Control[] { }; - } - } - finally - { - _recursing.Remove(this); - } + context.PushNode(installer); } } } \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuItem.cs b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuItem.cs index f3de9ba9..6aeb6b06 100644 --- a/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuItem.cs +++ b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuItem.cs @@ -1,30 +1,11 @@ using System.Collections.Generic; using System.Linq; +using nadena.dev.modular_avatar.core.menu; using UnityEngine; using VRC.SDK3.Avatars.ScriptableObjects; namespace nadena.dev.modular_avatar.core { - [DisallowMultipleComponent] - public abstract class MenuSource : AvatarTagComponent - { - /** - * Generates the menu items for this menu source object. Submenus are not required to be persisted as assets; - * this will be handled by the caller if necessary. - * - * Note that this method might be called outside of a build context (e.g. from custom inspectors). - */ - internal abstract VRCExpressionsMenu.Control[] GenerateMenu(); - - protected override void OnValidate() - { - base.OnValidate(); - - RuntimeUtil.InvalidateMenu(); - } - } - - public enum SubmenuSource { MenuAsset, @@ -32,7 +13,7 @@ namespace nadena.dev.modular_avatar.core } [AddComponentMenu("Modular Avatar/MA Menu Item")] - public class ModularAvatarMenuItem : MenuSource + public class ModularAvatarMenuItem : MenuSourceComponent { public VRCExpressionsMenu.Control Control; public SubmenuSource MenuSource; @@ -40,98 +21,32 @@ namespace nadena.dev.modular_avatar.core public ModularAvatarMenuInstaller menuSource_installer; public GameObject menuSource_otherObjectChildren; - internal override VRCExpressionsMenu.Control[] GenerateMenu() + public override void Visit(NodeContext context) { - switch (Control.type) - { - case VRCExpressionsMenu.Control.ControlType.SubMenu: - return GenerateSubmenu(); - default: - return new[] - {Control}; - } - } + var cloned = new VirtualControl(Control); + cloned.subMenu = null; + cloned.name = gameObject.name; - private bool _recursing = false; - private VRCExpressionsMenu _cachedMenu; - - private VRCExpressionsMenu.Control[] GenerateSubmenu() - { - List controls = null; - switch (MenuSource) + if (cloned.type == VRCExpressionsMenu.Control.ControlType.SubMenu) { - case SubmenuSource.MenuAsset: - controls = Control.subMenu?.controls?.ToList(); - break; - case SubmenuSource.Children: + switch (this.MenuSource) { - var menuRoot = menuSource_otherObjectChildren == null - ? gameObject - : menuSource_otherObjectChildren; - controls = new List(); - foreach (Transform child in menuRoot.transform) + case SubmenuSource.MenuAsset: + cloned.SubmenuNode = context.NodeFor(this.Control.subMenu); + break; + case SubmenuSource.Children: { - var menuSource = child.GetComponent(); - if (menuSource != null && child.gameObject.activeSelf && menuSource.enabled) - { - controls.AddRange(menuSource.GenerateMenu()); - } - } + var root = this.menuSource_otherObjectChildren != null + ? this.menuSource_otherObjectChildren + : this.gameObject; - break; + cloned.SubmenuNode = context.NodeFor(new MenuNodesUnder(root)); + break; + } } - /* - case SubmenuSource.MenuInstaller: - controls = menuSource_installer.installTargetMenu?.controls?.ToList(); - break; - case SubmenuSource.OtherMenuItem: - if (_recursing || menuSource_otherSource == null) - { - return new VRCExpressionsMenu.Control[] { }; - } - else - { - _recursing = true; - try - { - return menuSource_otherSource.GenerateMenu(); - } - finally - { - _recursing = false; - } - } - */ } - if (controls == null) - { - return new VRCExpressionsMenu.Control[] { }; - } - - if (_cachedMenu == null) _cachedMenu = ScriptableObject.CreateInstance(); - _cachedMenu.controls = controls; - - var control = CloneControl(Control); - control.name = gameObject.name; - control.subMenu = _cachedMenu; - - return new[] {control}; - } - - private static VRCExpressionsMenu.Control CloneControl(VRCExpressionsMenu.Control control) - { - return new VRCExpressionsMenu.Control() - { - type = control.type, - parameter = control.parameter, - labels = control.labels.ToArray(), - subParameters = control.subParameters.ToArray(), - icon = control.icon, - name = control.name, - value = control.value, - subMenu = control.subMenu - }; + context.PushControl(cloned); } } } \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Runtime/menu.meta b/Packages/nadena.dev.modular-avatar/Runtime/menu.meta new file mode 100644 index 00000000..f598c3ac --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Runtime/menu.meta @@ -0,0 +1,4 @@ +fileFormatVersion: 2 +guid: 1a87fa2d043c4c519d21645034348559 +timeCreated: 1677147240 +folderAsset: yes diff --git a/Packages/nadena.dev.modular-avatar/Runtime/menu/VirtualMenuAPI.cs b/Packages/nadena.dev.modular-avatar/Runtime/menu/VirtualMenuAPI.cs new file mode 100644 index 00000000..7e6aa8b6 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Runtime/menu/VirtualMenuAPI.cs @@ -0,0 +1,174 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using VRC.SDK3.Avatars.ScriptableObjects; + +// Internal runtime API for the Virtual Menu system. +// +// IMPORTANT: This API is currently considered unstable. Due to C# protection rules, we are required to make classes +// here public, but be aware that they may change without warning in the future. +namespace nadena.dev.modular_avatar.core.menu +{ + /// + /// A MenuNode represents a single VRCExpressionsMenu, prior to overflow splitting. MenuNodes form a directed graph, + /// which may contain cycles, and may include contributions from multiple MenuInstallers, or from the base avatar + /// menu. + /// + public class VirtualMenuNode + { + public List Controls = new List(); + + /// + /// The primary (serialized) object that contributed to this menu; if we want to add more items to it, we look + /// here. This can currently be either a VRCExpressionsMenu, a MAMenuItem, or a RootMenu. + /// + public readonly object NodeKey; + + internal VirtualMenuNode(object nodeKey) + { + NodeKey = nodeKey; + } + } + + /** + * A single control on a MenuNode. The main difference between this and a true VRCExpressionsMenu.Control is that + * we use a MenuNode instead of a VRCExpressionsMenu for submenus. + */ + public class VirtualControl : VRCExpressionsMenu.Control + { + /// + /// VirtualControls do not reference real VRCExpressionsMenu objects, but rather virtual MenuNodes. + /// + public VirtualMenuNode SubmenuNode; + + internal VirtualControl(VRCExpressionsMenu.Control control) + { + this.name = control.name; + this.type = control.type; + this.parameter = new Parameter() {name = control?.parameter?.name}; + this.value = control.value; + this.icon = control.icon; + this.style = control.style; + this.subMenu = null; + this.subParameters = control.subParameters?.Select(p => new VRCExpressionsMenu.Control.Parameter() + { + name = p.name + })?.ToArray(); + this.labels = control.labels?.ToArray(); + } + } + + /// + /// Helper MenuSource which includes all children of a given GameObject containing MenuSourceComponents as menu + /// items. Implements equality based on the GameObject in question. + /// + internal class MenuNodesUnder : MenuSource + { + private readonly GameObject root; + + public MenuNodesUnder(GameObject root) + { + this.root = root; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return root == ((MenuNodesUnder) obj).root; + } + + public override int GetHashCode() + { + return (root != null ? root.GetHashCode() : 0); + } + + public void Visit(NodeContext context) + { + foreach (Transform t in root.transform) + { + var source = t.GetComponent(); + if (source != null) context.PushNode(source); + } + } + } + + /// + /// The NodeContext provides callbacks for MenuSource visitors to append controls and/or other node types to a menu + /// node. + /// + public interface NodeContext + { + /// + /// Pushes the contents of this expressions menu asset onto the current menu node, handling loops and menu + /// installer invocations. + /// + /// + void PushNode(VRCExpressionsMenu expMenu); + + /// + /// Pushes the contents of this menu source onto the current menu node. + /// + /// + void PushNode(MenuSource source); + + /// + /// Pushes this menu installer onto this node + /// + /// + void PushNode(ModularAvatarMenuInstaller installer); + + /// + /// Pushes a single expressions menu control onto the current menu node. Converts submenus into menu nodes + /// automatically. + /// + /// + void PushControl(VRCExpressionsMenu.Control control); + + /// + /// Pushes a single expressions menu control onto the current menu node. + /// + /// + void PushControl(VirtualControl control); + + /// + /// Returns the menu node for a given VRCExpressionsMenu asset. This node may not be populated at the time this + /// node returns. + /// + /// + /// + VirtualMenuNode NodeFor(VRCExpressionsMenu menu); + + /// + /// Returns the menu node for a given menu source asset. The contents of the node may not yet be populated. + /// + /// + /// + VirtualMenuNode NodeFor(MenuSource menu); + } + + /// + /// An object which can contribute controls to a menu. + /// + public interface MenuSource + { + void Visit(NodeContext context); + } + + /// + /// A component which can be used to generate menu items. + /// + [DisallowMultipleComponent] + public abstract class MenuSourceComponent : AvatarTagComponent, MenuSource + { + protected override void OnValidate() + { + base.OnValidate(); + + RuntimeUtil.InvalidateMenu(); + } + + public abstract void Visit(NodeContext context); + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Runtime/menu/VirtualMenuAPI.cs.meta b/Packages/nadena.dev.modular-avatar/Runtime/menu/VirtualMenuAPI.cs.meta new file mode 100644 index 00000000..12485cd8 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Runtime/menu/VirtualMenuAPI.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1defe88300684bc38ee24944075d540e +timeCreated: 1677147255 \ No newline at end of file diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 0ddfff93..6c082976 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -158,6 +158,13 @@ "com.unity.nuget.newtonsoft-json": "2.0.2" } }, + "de.thryrallo.vrc.avatar-performance-tools": { + "version": "https://github.com/Thryrallo/VRC-Avatar-Performance-Tools.git", + "depth": 0, + "source": "git", + "dependencies": {}, + "hash": "ba9c16e482e7a376db18e9a85e24fabc04649d37" + }, "nadena.dev.modular-avatar": { "version": "file:nadena.dev.modular-avatar", "depth": 0, diff --git a/Packages/vpm-manifest.json b/Packages/vpm-manifest.json index f2d34349..2fa362a1 100644 --- a/Packages/vpm-manifest.json +++ b/Packages/vpm-manifest.json @@ -9,13 +9,14 @@ }, "locked": { "com.vrchat.avatars": { - "version": "3.1.10", + "version": "3.1.11", "dependencies": { - "com.vrchat.base": "3.1.x" + "com.vrchat.base": "3.1.11" } }, "com.vrchat.base": { - "version": "3.1.10" + "version": "3.1.11", + "dependencies": {} }, "com.vrchat.core.vpm-resolver": { "version": "0.1.17" diff --git a/ProjectSettings/GraphicsSettings.asset b/ProjectSettings/GraphicsSettings.asset index cfce0ab1..b6bb0ac5 100644 --- a/ProjectSettings/GraphicsSettings.asset +++ b/ProjectSettings/GraphicsSettings.asset @@ -28,16 +28,7 @@ GraphicsSettings: m_LensFlare: m_Mode: 1 m_Shader: {fileID: 102, guid: 0000000000000000f000000000000000, type: 0} - m_AlwaysIncludedShaders: - - {fileID: 7, guid: 0000000000000000f000000000000000, type: 0} - - {fileID: 15104, guid: 0000000000000000f000000000000000, type: 0} - - {fileID: 15105, guid: 0000000000000000f000000000000000, type: 0} - - {fileID: 15106, guid: 0000000000000000f000000000000000, type: 0} - - {fileID: 10753, guid: 0000000000000000f000000000000000, type: 0} - - {fileID: 10770, guid: 0000000000000000f000000000000000, type: 0} - - {fileID: 16000, guid: 0000000000000000f000000000000000, type: 0} - - {fileID: 16001, guid: 0000000000000000f000000000000000, type: 0} - - {fileID: 17000, guid: 0000000000000000f000000000000000, type: 0} + m_AlwaysIncludedShaders: [] m_PreloadedShaders: [] m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0}