mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-05-10 05:09:05 +08:00
rewrite the virtual menu system to be more extensible
This commit is contained in:
parent
88f90b6c9a
commit
1528db1312
@ -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<UnityEngine.Object> toDestroy;
|
||||
private int controlIndex;
|
||||
|
||||
public override void Setup()
|
||||
{
|
||||
base.Setup();
|
||||
testTex = new Texture2D(1, 1);
|
||||
toDestroy = new List<UnityEngine.Object>();
|
||||
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);
|
||||
|
@ -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",
|
||||
|
@ -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<ErrorLog> CheckInternal(ModularAvatarMenuInstaller mi)
|
||||
{
|
||||
// TODO - check that target menu is in the avatar
|
||||
if (mi.menuToAppend == null && mi.GetComponent<MenuSource>() == null)
|
||||
if (mi.menuToAppend == null && mi.GetComponent<MenuSourceComponent>() == null)
|
||||
{
|
||||
return new List<ErrorLog>()
|
||||
{
|
||||
|
@ -334,7 +334,7 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting
|
||||
|
||||
internal static T ReportingObject<T>(UnityEngine.Object obj, Func<T> 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<TreeViewItem> items, MenuNode node)
|
||||
private void TraverseMenu(int depth, List<TreeViewItem> items, VirtualMenuNode node)
|
||||
{
|
||||
IEnumerable<VirtualControl> children = node.Controls
|
||||
.Where(control => control.type == VRCExpressionsMenu.Control.ControlType.SubMenu &&
|
||||
|
@ -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;
|
||||
|
@ -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<VRCExpressionsMenu, MenuSource>());
|
||||
var rootMenu = ConvertSubmenu(gameObj, fakeControl,
|
||||
new Dictionary<VRCExpressionsMenu, MenuSourceComponent>());
|
||||
Undo.RecordObject(avatar, "Convert menu");
|
||||
avatar.expressionsMenu = null;
|
||||
|
||||
rootMenu.gameObject.AddComponent<ModularAvatarMenuInstaller>();
|
||||
}
|
||||
|
||||
private static MenuSource ConvertSubmenu(
|
||||
private static MenuSourceComponent ConvertSubmenu(
|
||||
GameObject parentObj,
|
||||
VRCExpressionsMenu.Control sourceControl,
|
||||
Dictionary<VRCExpressionsMenu, MenuSource> convertedMenus
|
||||
Dictionary<VRCExpressionsMenu, MenuSourceComponent> convertedMenus
|
||||
)
|
||||
{
|
||||
var itemObj = new GameObject();
|
||||
|
@ -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
|
||||
/// <summary>
|
||||
/// Sentinel object to represent the avatar root menu (for avatars which don't have a root menu)
|
||||
/// </summary>
|
||||
internal sealed class RootMenu
|
||||
internal sealed class RootMenu : MenuSource
|
||||
{
|
||||
public static readonly RootMenu Instance = new RootMenu();
|
||||
|
||||
private RootMenu()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal class MenuNode
|
||||
{
|
||||
internal List<VirtualControl> Controls = new List<VirtualControl>();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// VirtualControls do not reference real VRCExpressionsMenu objects, but rather virtual MenuNodes.
|
||||
/// </summary>
|
||||
internal MenuNode SubmenuNode;
|
||||
[CanBeNull]
|
||||
internal delegate VirtualMenuNode NodeForDelegate(object menu);
|
||||
|
||||
internal VirtualControl(VRCExpressionsMenu.Control control)
|
||||
private readonly ImmutableDictionary<object, ImmutableList<ModularAvatarMenuInstaller>>
|
||||
_menuToInstallerMap;
|
||||
|
||||
private readonly VirtualMenuNode _node;
|
||||
private readonly NodeForDelegate _nodeFor;
|
||||
private readonly HashSet<object> _visited = new HashSet<object>();
|
||||
|
||||
public NodeContextImpl(
|
||||
VirtualMenuNode node,
|
||||
NodeForDelegate nodeFor,
|
||||
ImmutableDictionary<object, ImmutableList<ModularAvatarMenuInstaller>> 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<MenuSourceComponent>();
|
||||
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<ModularAvatarMenuInstaller, List<ModularAvatarMenuInstallTarget>> _installerToTargetComponent
|
||||
= new Dictionary<ModularAvatarMenuInstaller, List<ModularAvatarMenuInstallTarget>>();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private Dictionary<object, MenuNode> _menuNodeMap = new Dictionary<object, MenuNode>();
|
||||
|
||||
private Dictionary<object, MenuNode> _resolvedMenu = new Dictionary<object, MenuNode>();
|
||||
private Dictionary<object, VirtualMenuNode> _resolvedMenu = new Dictionary<object, VirtualMenuNode>();
|
||||
|
||||
// TODO: immutable?
|
||||
public Dictionary<object, MenuNode> ResolvedMenu => _resolvedMenu;
|
||||
public MenuNode RootMenuNode => ResolvedMenu[RootMenuKey];
|
||||
public Dictionary<object, VirtualMenuNode> ResolvedMenu => _resolvedMenu;
|
||||
public VirtualMenuNode RootMenuNode => ResolvedMenu[RootMenuKey];
|
||||
|
||||
private Queue<Action> _pendingGeneration = new Queue<Action>();
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
internal void FreezeMenu()
|
||||
{
|
||||
ResolveNode(RootMenuKey);
|
||||
ImmutableDictionary<object, ImmutableList<ModularAvatarMenuInstaller>> menuToInstallerFiltered =
|
||||
_targetMenuToInstaller
|
||||
.Select(kvp => new KeyValuePair<object, ImmutableList<ModularAvatarMenuInstaller>>(
|
||||
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<UnityEngine.Object> SaveAsset)
|
||||
@ -265,203 +361,5 @@ namespace nadena.dev.modular_avatar.core.editor.menu
|
||||
return menu;
|
||||
}
|
||||
}
|
||||
|
||||
private HashSet<object> _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<object>();
|
||||
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<ModularAvatarMenuInstaller> installers = new Stack<ModularAvatarMenuInstaller>();
|
||||
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<MenuSource>();
|
||||
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<ModularAvatarMenuInstaller>());
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO validation
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// TODO validation
|
||||
break;
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
private void ResolveInstaller(MenuNode node, ModularAvatarMenuInstaller installer,
|
||||
Stack<ModularAvatarMenuInstaller> installers)
|
||||
{
|
||||
if (installer == null || !installer.enabled) return;
|
||||
|
||||
var menuSource = installer.GetComponent<MenuSource>();
|
||||
|
||||
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()});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97e46a47dd8a425eb4ce9411defe313d
|
||||
timeCreated: 1677080023
|
@ -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.
|
||||
/// </summary>
|
||||
internal class ModularAvatarMenuInstallTarget : MenuSource
|
||||
internal class ModularAvatarMenuInstallTarget : MenuSourceComponent
|
||||
{
|
||||
public ModularAvatarMenuInstaller installer;
|
||||
|
||||
private static HashSet<MenuSource> _recursing = new HashSet<MenuSource>();
|
||||
|
||||
internal delegate T Returning<T>();
|
||||
|
||||
/**
|
||||
* 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<T>(Returning<T> callback)
|
||||
public override void Visit(NodeContext context)
|
||||
{
|
||||
HashSet<MenuSource> oldRecursing = _recursing;
|
||||
_recursing = new HashSet<MenuSource>();
|
||||
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<MenuSource>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<VRCExpressionsMenu.Control> 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<VRCExpressionsMenu.Control>();
|
||||
foreach (Transform child in menuRoot.transform)
|
||||
case SubmenuSource.MenuAsset:
|
||||
cloned.SubmenuNode = context.NodeFor(this.Control.subMenu);
|
||||
break;
|
||||
case SubmenuSource.Children:
|
||||
{
|
||||
var menuSource = child.GetComponent<MenuSource>();
|
||||
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<VRCExpressionsMenu>();
|
||||
_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);
|
||||
}
|
||||
}
|
||||
}
|
4
Packages/nadena.dev.modular-avatar/Runtime/menu.meta
Normal file
4
Packages/nadena.dev.modular-avatar/Runtime/menu.meta
Normal file
@ -0,0 +1,4 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a87fa2d043c4c519d21645034348559
|
||||
timeCreated: 1677147240
|
||||
folderAsset: yes
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class VirtualMenuNode
|
||||
{
|
||||
public List<VirtualControl> Controls = new List<VirtualControl>();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// VirtualControls do not reference real VRCExpressionsMenu objects, but rather virtual MenuNodes.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper MenuSource which includes all children of a given GameObject containing MenuSourceComponents as menu
|
||||
/// items. Implements equality based on the GameObject in question.
|
||||
/// </summary>
|
||||
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<MenuSourceComponent>();
|
||||
if (source != null) context.PushNode(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The NodeContext provides callbacks for MenuSource visitors to append controls and/or other node types to a menu
|
||||
/// node.
|
||||
/// </summary>
|
||||
public interface NodeContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Pushes the contents of this expressions menu asset onto the current menu node, handling loops and menu
|
||||
/// installer invocations.
|
||||
/// </summary>
|
||||
/// <param name="expMenu"></param>
|
||||
void PushNode(VRCExpressionsMenu expMenu);
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the contents of this menu source onto the current menu node.
|
||||
/// </summary>
|
||||
/// <param name="source"></param>
|
||||
void PushNode(MenuSource source);
|
||||
|
||||
/// <summary>
|
||||
/// Pushes this menu installer onto this node
|
||||
/// </summary>
|
||||
/// <param name="installer"></param>
|
||||
void PushNode(ModularAvatarMenuInstaller installer);
|
||||
|
||||
/// <summary>
|
||||
/// Pushes a single expressions menu control onto the current menu node. Converts submenus into menu nodes
|
||||
/// automatically.
|
||||
/// </summary>
|
||||
/// <param name="control"></param>
|
||||
void PushControl(VRCExpressionsMenu.Control control);
|
||||
|
||||
/// <summary>
|
||||
/// Pushes a single expressions menu control onto the current menu node.
|
||||
/// </summary>
|
||||
/// <param name="control"></param>
|
||||
void PushControl(VirtualControl control);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the menu node for a given VRCExpressionsMenu asset. This node may not be populated at the time this
|
||||
/// node returns.
|
||||
/// </summary>
|
||||
/// <param name="menu"></param>
|
||||
/// <returns></returns>
|
||||
VirtualMenuNode NodeFor(VRCExpressionsMenu menu);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the menu node for a given menu source asset. The contents of the node may not yet be populated.
|
||||
/// </summary>
|
||||
/// <param name="menu"></param>
|
||||
/// <returns></returns>
|
||||
VirtualMenuNode NodeFor(MenuSource menu);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An object which can contribute controls to a menu.
|
||||
/// </summary>
|
||||
public interface MenuSource
|
||||
{
|
||||
void Visit(NodeContext context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A component which can be used to generate menu items.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public abstract class MenuSourceComponent : AvatarTagComponent, MenuSource
|
||||
{
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
|
||||
RuntimeUtil.InvalidateMenu();
|
||||
}
|
||||
|
||||
public abstract void Visit(NodeContext context);
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1defe88300684bc38ee24944075d540e
|
||||
timeCreated: 1677147255
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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}
|
||||
|
Loading…
x
Reference in New Issue
Block a user