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 System.Collections.Generic;
|
||||||
using nadena.dev.modular_avatar.core;
|
using nadena.dev.modular_avatar.core;
|
||||||
using nadena.dev.modular_avatar.core.editor.menu;
|
using nadena.dev.modular_avatar.core.editor.menu;
|
||||||
|
using nadena.dev.modular_avatar.core.menu;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEditor.VersionControl;
|
using UnityEditor.VersionControl;
|
||||||
@ -13,12 +14,14 @@ namespace modular_avatar_tests.VirtualMenuTests
|
|||||||
{
|
{
|
||||||
private Texture2D testTex;
|
private Texture2D testTex;
|
||||||
private List<UnityEngine.Object> toDestroy;
|
private List<UnityEngine.Object> toDestroy;
|
||||||
|
private int controlIndex;
|
||||||
|
|
||||||
public override void Setup()
|
public override void Setup()
|
||||||
{
|
{
|
||||||
base.Setup();
|
base.Setup();
|
||||||
testTex = new Texture2D(1, 1);
|
testTex = new Texture2D(1, 1);
|
||||||
toDestroy = new List<UnityEngine.Object>();
|
toDestroy = new List<UnityEngine.Object>();
|
||||||
|
controlIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Teardown()
|
public override void Teardown()
|
||||||
@ -280,14 +283,14 @@ namespace modular_avatar_tests.VirtualMenuTests
|
|||||||
|
|
||||||
Assert.AreEqual(2, virtualMenu.ResolvedMenu.Count);
|
Assert.AreEqual(2, virtualMenu.ResolvedMenu.Count);
|
||||||
var rootMenu = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
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.AreEqual(1, rootMenu.Controls.Count);
|
||||||
Assert.AreSame(RootMenu.Instance, rootMenu.NodeKey);
|
Assert.AreSame(RootMenu.Instance, rootMenu.NodeKey);
|
||||||
AssertControlEquals(item.Control, rootMenu.Controls[0]);
|
AssertControlEquals(item.Control, rootMenu.Controls[0]);
|
||||||
Assert.AreSame(item_node, rootMenu.Controls[0].SubmenuNode);
|
Assert.AreSame(item_node, rootMenu.Controls[0].SubmenuNode);
|
||||||
|
|
||||||
Assert.AreEqual(1, item_node.Controls.Count);
|
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]);
|
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.RadialPuppet,
|
||||||
VRCExpressionsMenu.Control.ControlType.FourAxisPuppet,
|
VRCExpressionsMenu.Control.ControlType.FourAxisPuppet,
|
||||||
VRCExpressionsMenu.Control.ControlType.TwoAxisPuppet,
|
VRCExpressionsMenu.Control.ControlType.TwoAxisPuppet,
|
||||||
VRCExpressionsMenu.Control.ControlType.OneAxisPuppet,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
control.type = types[Random.Range(0, types.Length)];
|
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 = new VRCExpressionsMenu.Control.Parameter();
|
||||||
control.parameter.name = "Test Parameter " + GUID.Generate();
|
control.parameter.name = "Test Parameter " + GUID.Generate();
|
||||||
control.icon = new Texture2D(1, 1);
|
control.icon = new Texture2D(1, 1);
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
"com.unity.ugui": "1.0.0",
|
"com.unity.ugui": "1.0.0",
|
||||||
"com.unity.xr.oculus.standalone": "2.38.4",
|
"com.unity.xr.oculus.standalone": "2.38.4",
|
||||||
"com.unity.xr.openvr.standalone": "2.0.5",
|
"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",
|
"nadena.dev.modular-avatar": "0.0.1",
|
||||||
"com.unity.modules.ai": "1.0.0",
|
"com.unity.modules.ai": "1.0.0",
|
||||||
"com.unity.modules.androidjni": "1.0.0",
|
"com.unity.modules.androidjni": "1.0.0",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using nadena.dev.modular_avatar.core;
|
using nadena.dev.modular_avatar.core;
|
||||||
|
using nadena.dev.modular_avatar.core.menu;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using VRC.SDK3.Avatars.Components;
|
using VRC.SDK3.Avatars.Components;
|
||||||
|
|
||||||
@ -137,7 +138,7 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting
|
|||||||
private static List<ErrorLog> CheckInternal(ModularAvatarMenuInstaller mi)
|
private static List<ErrorLog> CheckInternal(ModularAvatarMenuInstaller mi)
|
||||||
{
|
{
|
||||||
// TODO - check that target menu is in the avatar
|
// 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>()
|
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)
|
internal static T ReportingObject<T>(UnityEngine.Object obj, Func<T> action)
|
||||||
{
|
{
|
||||||
CurrentReport._references.Push(obj);
|
if (obj != null) CurrentReport._references.Push(obj);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return action();
|
return action();
|
||||||
@ -347,7 +347,7 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
CurrentReport._references.Pop();
|
if (obj != null) CurrentReport._references.Pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using nadena.dev.modular_avatar.core.editor.menu;
|
using nadena.dev.modular_avatar.core.editor.menu;
|
||||||
|
using nadena.dev.modular_avatar.core.menu;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEditor.IMGUI.Controls;
|
using UnityEditor.IMGUI.Controls;
|
||||||
@ -148,7 +149,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
return root;
|
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
|
IEnumerable<VirtualControl> children = node.Controls
|
||||||
.Where(control => control.type == VRCExpressionsMenu.Control.ControlType.SubMenu &&
|
.Where(control => control.type == VRCExpressionsMenu.Control.ControlType.SubMenu &&
|
||||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using nadena.dev.modular_avatar.core.editor.menu;
|
using nadena.dev.modular_avatar.core.editor.menu;
|
||||||
|
using nadena.dev.modular_avatar.core.menu;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using VRC.SDK3.Avatars.Components;
|
using VRC.SDK3.Avatars.Components;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using nadena.dev.modular_avatar.core.menu;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using VRC.SDK3.Avatars.Components;
|
using VRC.SDK3.Avatars.Components;
|
||||||
@ -23,17 +24,18 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
type = VRCExpressionsMenu.Control.ControlType.SubMenu,
|
type = VRCExpressionsMenu.Control.ControlType.SubMenu,
|
||||||
name = "Avatar Menu"
|
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");
|
Undo.RecordObject(avatar, "Convert menu");
|
||||||
avatar.expressionsMenu = null;
|
avatar.expressionsMenu = null;
|
||||||
|
|
||||||
rootMenu.gameObject.AddComponent<ModularAvatarMenuInstaller>();
|
rootMenu.gameObject.AddComponent<ModularAvatarMenuInstaller>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MenuSource ConvertSubmenu(
|
private static MenuSourceComponent ConvertSubmenu(
|
||||||
GameObject parentObj,
|
GameObject parentObj,
|
||||||
VRCExpressionsMenu.Control sourceControl,
|
VRCExpressionsMenu.Control sourceControl,
|
||||||
Dictionary<VRCExpressionsMenu, MenuSource> convertedMenus
|
Dictionary<VRCExpressionsMenu, MenuSourceComponent> convertedMenus
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var itemObj = new GameObject();
|
var itemObj = new GameObject();
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using nadena.dev.modular_avatar.core.menu;
|
||||||
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@ -12,57 +15,116 @@ namespace nadena.dev.modular_avatar.core.editor.menu
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sentinel object to represent the avatar root menu (for avatars which don't have a root menu)
|
/// Sentinel object to represent the avatar root menu (for avatars which don't have a root menu)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class RootMenu
|
internal sealed class RootMenu : MenuSource
|
||||||
{
|
{
|
||||||
public static readonly RootMenu Instance = new RootMenu();
|
public static readonly RootMenu Instance = new RootMenu();
|
||||||
|
|
||||||
private RootMenu()
|
private RootMenu()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public void Visit(NodeContext context)
|
||||||
/// 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)
|
|
||||||
{
|
{
|
||||||
NodeKey = nodeKey;
|
// we initialize the root node manually
|
||||||
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class VirtualControl : VRCExpressionsMenu.Control
|
class NodeContextImpl : NodeContext
|
||||||
{
|
{
|
||||||
/// <summary>
|
[CanBeNull]
|
||||||
/// VirtualControls do not reference real VRCExpressionsMenu objects, but rather virtual MenuNodes.
|
internal delegate VirtualMenuNode NodeForDelegate(object menu);
|
||||||
/// </summary>
|
|
||||||
internal MenuNode SubmenuNode;
|
|
||||||
|
|
||||||
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;
|
_node = node;
|
||||||
this.type = control.type;
|
_nodeFor = nodeFor;
|
||||||
this.parameter = new Parameter() {name = control?.parameter?.name};
|
_menuToInstallerMap = menuToInstallerMap;
|
||||||
this.value = control.value;
|
}
|
||||||
this.icon = control.icon;
|
|
||||||
this.style = control.style;
|
public void PushNode(VRCExpressionsMenu expMenu)
|
||||||
this.subMenu = null;
|
{
|
||||||
this.subParameters = control.subParameters?.Select(p => new VRCExpressionsMenu.Control.Parameter()
|
if (expMenu == null) return;
|
||||||
|
if (_visited.Contains(expMenu)) return;
|
||||||
|
_visited.Add(expMenu);
|
||||||
|
|
||||||
|
foreach (var control in expMenu.controls)
|
||||||
{
|
{
|
||||||
name = p.name
|
PushControl(control);
|
||||||
})?.ToArray();
|
}
|
||||||
this.labels = control.labels?.ToArray();
|
|
||||||
|
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
|
private Dictionary<ModularAvatarMenuInstaller, List<ModularAvatarMenuInstallTarget>> _installerToTargetComponent
|
||||||
= new Dictionary<ModularAvatarMenuInstaller, List<ModularAvatarMenuInstallTarget>>();
|
= new Dictionary<ModularAvatarMenuInstaller, List<ModularAvatarMenuInstallTarget>>();
|
||||||
|
|
||||||
/// <summary>
|
private Dictionary<object, VirtualMenuNode> _resolvedMenu = new Dictionary<object, VirtualMenuNode>();
|
||||||
/// 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>();
|
|
||||||
|
|
||||||
// TODO: immutable?
|
// TODO: immutable?
|
||||||
public Dictionary<object, MenuNode> ResolvedMenu => _resolvedMenu;
|
public Dictionary<object, VirtualMenuNode> ResolvedMenu => _resolvedMenu;
|
||||||
public MenuNode RootMenuNode => ResolvedMenu[RootMenuKey];
|
public VirtualMenuNode RootMenuNode => ResolvedMenu[RootMenuKey];
|
||||||
|
|
||||||
|
private Queue<Action> _pendingGeneration = new Queue<Action>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes the VirtualMenu.
|
/// Initializes the VirtualMenu.
|
||||||
@ -121,12 +179,10 @@ namespace nadena.dev.modular_avatar.core.editor.menu
|
|||||||
if (rootMenu != null)
|
if (rootMenu != null)
|
||||||
{
|
{
|
||||||
RootMenuKey = rootMenu;
|
RootMenuKey = rootMenu;
|
||||||
ImportMenu(rootMenu);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
RootMenuKey = RootMenu.Instance;
|
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>
|
/// <summary>
|
||||||
/// Registers a menu installer with this virtual menu. Because we need the full set of components indexed to
|
/// 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.
|
/// 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>
|
/// </summary>
|
||||||
internal void FreezeMenu()
|
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)
|
internal VRCExpressionsMenu SerializeMenu(Action<UnityEngine.Object> SaveAsset)
|
||||||
@ -265,203 +361,5 @@ namespace nadena.dev.modular_avatar.core.editor.menu
|
|||||||
return 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 System.Collections.Generic;
|
||||||
|
using nadena.dev.modular_avatar.core.menu;
|
||||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||||
using VRC.SDKBase;
|
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.
|
/// We can also end up with a loop between install targets; in this case, we break the loop at an arbitrary point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class ModularAvatarMenuInstallTarget : MenuSource
|
internal class ModularAvatarMenuInstallTarget : MenuSourceComponent
|
||||||
{
|
{
|
||||||
public ModularAvatarMenuInstaller installer;
|
public ModularAvatarMenuInstaller installer;
|
||||||
|
|
||||||
private static HashSet<MenuSource> _recursing = new HashSet<MenuSource>();
|
public override void Visit(NodeContext context)
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
HashSet<MenuSource> oldRecursing = _recursing;
|
context.PushNode(installer);
|
||||||
_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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,30 +1,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using nadena.dev.modular_avatar.core.menu;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core
|
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
|
public enum SubmenuSource
|
||||||
{
|
{
|
||||||
MenuAsset,
|
MenuAsset,
|
||||||
@ -32,7 +13,7 @@ namespace nadena.dev.modular_avatar.core
|
|||||||
}
|
}
|
||||||
|
|
||||||
[AddComponentMenu("Modular Avatar/MA Menu Item")]
|
[AddComponentMenu("Modular Avatar/MA Menu Item")]
|
||||||
public class ModularAvatarMenuItem : MenuSource
|
public class ModularAvatarMenuItem : MenuSourceComponent
|
||||||
{
|
{
|
||||||
public VRCExpressionsMenu.Control Control;
|
public VRCExpressionsMenu.Control Control;
|
||||||
public SubmenuSource MenuSource;
|
public SubmenuSource MenuSource;
|
||||||
@ -40,98 +21,32 @@ namespace nadena.dev.modular_avatar.core
|
|||||||
public ModularAvatarMenuInstaller menuSource_installer;
|
public ModularAvatarMenuInstaller menuSource_installer;
|
||||||
public GameObject menuSource_otherObjectChildren;
|
public GameObject menuSource_otherObjectChildren;
|
||||||
|
|
||||||
internal override VRCExpressionsMenu.Control[] GenerateMenu()
|
public override void Visit(NodeContext context)
|
||||||
{
|
{
|
||||||
switch (Control.type)
|
var cloned = new VirtualControl(Control);
|
||||||
{
|
cloned.subMenu = null;
|
||||||
case VRCExpressionsMenu.Control.ControlType.SubMenu:
|
cloned.name = gameObject.name;
|
||||||
return GenerateSubmenu();
|
|
||||||
default:
|
|
||||||
return new[]
|
|
||||||
{Control};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _recursing = false;
|
if (cloned.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
|
||||||
private VRCExpressionsMenu _cachedMenu;
|
|
||||||
|
|
||||||
private VRCExpressionsMenu.Control[] GenerateSubmenu()
|
|
||||||
{
|
|
||||||
List<VRCExpressionsMenu.Control> controls = null;
|
|
||||||
switch (MenuSource)
|
|
||||||
{
|
{
|
||||||
case SubmenuSource.MenuAsset:
|
switch (this.MenuSource)
|
||||||
controls = Control.subMenu?.controls?.ToList();
|
|
||||||
break;
|
|
||||||
case SubmenuSource.Children:
|
|
||||||
{
|
{
|
||||||
var menuRoot = menuSource_otherObjectChildren == null
|
case SubmenuSource.MenuAsset:
|
||||||
? gameObject
|
cloned.SubmenuNode = context.NodeFor(this.Control.subMenu);
|
||||||
: menuSource_otherObjectChildren;
|
break;
|
||||||
controls = new List<VRCExpressionsMenu.Control>();
|
case SubmenuSource.Children:
|
||||||
foreach (Transform child in menuRoot.transform)
|
|
||||||
{
|
{
|
||||||
var menuSource = child.GetComponent<MenuSource>();
|
var root = this.menuSource_otherObjectChildren != null
|
||||||
if (menuSource != null && child.gameObject.activeSelf && menuSource.enabled)
|
? this.menuSource_otherObjectChildren
|
||||||
{
|
: this.gameObject;
|
||||||
controls.AddRange(menuSource.GenerateMenu());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
context.PushControl(cloned);
|
||||||
{
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
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"
|
"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": {
|
"nadena.dev.modular-avatar": {
|
||||||
"version": "file:nadena.dev.modular-avatar",
|
"version": "file:nadena.dev.modular-avatar",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
|
@ -9,13 +9,14 @@
|
|||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"com.vrchat.avatars": {
|
"com.vrchat.avatars": {
|
||||||
"version": "3.1.10",
|
"version": "3.1.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"com.vrchat.base": "3.1.x"
|
"com.vrchat.base": "3.1.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"com.vrchat.base": {
|
"com.vrchat.base": {
|
||||||
"version": "3.1.10"
|
"version": "3.1.11",
|
||||||
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
"com.vrchat.core.vpm-resolver": {
|
"com.vrchat.core.vpm-resolver": {
|
||||||
"version": "0.1.17"
|
"version": "0.1.17"
|
||||||
|
@ -28,16 +28,7 @@ GraphicsSettings:
|
|||||||
m_LensFlare:
|
m_LensFlare:
|
||||||
m_Mode: 1
|
m_Mode: 1
|
||||||
m_Shader: {fileID: 102, guid: 0000000000000000f000000000000000, type: 0}
|
m_Shader: {fileID: 102, guid: 0000000000000000f000000000000000, type: 0}
|
||||||
m_AlwaysIncludedShaders:
|
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_PreloadedShaders: []
|
m_PreloadedShaders: []
|
||||||
m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000,
|
m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000,
|
||||||
type: 0}
|
type: 0}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user