mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-05-10 13:19:01 +08:00
implement the core virtual menu infrastructure
This commit is contained in:
parent
d212dabc27
commit
0bc1c6c88e
@ -15,13 +15,13 @@ namespace modular_avatar_tests
|
||||
private const string MinimalAvatarGuid = "60d3416d1f6af4a47bf9056aefc38333";
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
public virtual void Setup()
|
||||
{
|
||||
objects = new List<GameObject>();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void Teardown()
|
||||
public virtual void Teardown()
|
||||
{
|
||||
foreach (var obj in objects)
|
||||
{
|
||||
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 10cb5a7057c1452d8e1caf299c31b05b
|
||||
timeCreated: 1676981463
|
@ -0,0 +1,553 @@
|
||||
using System.Collections.Generic;
|
||||
using nadena.dev.modular_avatar.core;
|
||||
using nadena.dev.modular_avatar.core.editor.menu;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
||||
namespace modular_avatar_tests.VirtualMenuTests
|
||||
{
|
||||
public class VirtualMenuTests : TestBase
|
||||
{
|
||||
private Texture2D testTex;
|
||||
private List<UnityEngine.Object> toDestroy;
|
||||
|
||||
public override void Setup()
|
||||
{
|
||||
base.Setup();
|
||||
testTex = new Texture2D(1, 1);
|
||||
toDestroy = new List<UnityEngine.Object>();
|
||||
}
|
||||
|
||||
public override void Teardown()
|
||||
{
|
||||
base.Teardown();
|
||||
Object.DestroyImmediate(testTex);
|
||||
foreach (var obj in toDestroy)
|
||||
{
|
||||
Object.DestroyImmediate(obj);
|
||||
}
|
||||
}
|
||||
|
||||
private T Create<T>(string name = null) where T : ScriptableObject
|
||||
{
|
||||
if (name == null) name = GUID.Generate().ToString();
|
||||
|
||||
T val = ScriptableObject.CreateInstance<T>();
|
||||
val.name = name;
|
||||
|
||||
toDestroy.Add(val);
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmptyMenu()
|
||||
{
|
||||
var virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
Assert.AreEqual(1, virtualMenu.ResolvedMenu.Count);
|
||||
var root = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
Assert.AreEqual(0, root.Controls.Count);
|
||||
Assert.AreSame(RootMenu.Instance, root.NodeKey);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicMenu()
|
||||
{
|
||||
var rootMenu = Create<VRCExpressionsMenu>();
|
||||
|
||||
rootMenu.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl(),
|
||||
GenerateTestControl()
|
||||
};
|
||||
|
||||
var virtualMenu = new VirtualMenu(rootMenu);
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
Assert.AreEqual(1, virtualMenu.ResolvedMenu.Count);
|
||||
var root = virtualMenu.ResolvedMenu[rootMenu];
|
||||
Assert.AreEqual(2, root.Controls.Count);
|
||||
Assert.AreSame(rootMenu, root.NodeKey);
|
||||
AssertControlEquals(rootMenu.controls[0], root.Controls[0]);
|
||||
AssertControlEquals(rootMenu.controls[1], root.Controls[1]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNativeMenuWithCycles()
|
||||
{
|
||||
var rootMenu = Create<VRCExpressionsMenu>("root");
|
||||
var sub1 = Create<VRCExpressionsMenu>("sub1");
|
||||
var sub2 = Create<VRCExpressionsMenu>("sub2");
|
||||
|
||||
rootMenu.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestSubmenu(sub1)
|
||||
};
|
||||
|
||||
sub1.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestSubmenu(sub2)
|
||||
};
|
||||
|
||||
sub2.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestSubmenu(rootMenu)
|
||||
};
|
||||
|
||||
var virtualMenu = new VirtualMenu(rootMenu);
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
Assert.AreEqual(3, virtualMenu.ResolvedMenu.Count);
|
||||
var rootNode = virtualMenu.ResolvedMenu[rootMenu];
|
||||
var sub1Node = virtualMenu.ResolvedMenu[sub1];
|
||||
var sub2Node = virtualMenu.ResolvedMenu[sub2];
|
||||
|
||||
Assert.AreEqual(1, rootNode.Controls.Count);
|
||||
Assert.AreSame(rootMenu, rootNode.NodeKey);
|
||||
Assert.AreSame(sub1Node, rootNode.Controls[0].SubmenuNode);
|
||||
Assert.IsNull(rootNode.Controls[0].subMenu);
|
||||
|
||||
Assert.AreEqual(1, sub1Node.Controls.Count);
|
||||
Assert.AreSame(sub1, sub1Node.NodeKey);
|
||||
Assert.AreSame(sub2Node, sub1Node.Controls[0].SubmenuNode);
|
||||
Assert.IsNull(sub1Node.Controls[0].subMenu);
|
||||
|
||||
Assert.AreEqual(1, sub2Node.Controls.Count);
|
||||
Assert.AreSame(sub2, sub2Node.NodeKey);
|
||||
Assert.AreSame(rootNode, sub2Node.Controls[0].SubmenuNode);
|
||||
Assert.IsNull(sub2Node.Controls[0].subMenu);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicMenuInstaller()
|
||||
{
|
||||
VRCExpressionsMenu testMenu = Create<VRCExpressionsMenu>();
|
||||
testMenu.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl(),
|
||||
GenerateTestControl()
|
||||
};
|
||||
|
||||
var installer = CreateInstaller("test");
|
||||
installer.menuToAppend = testMenu;
|
||||
|
||||
var virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer);
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
Assert.AreEqual(1, virtualMenu.ResolvedMenu.Count);
|
||||
var root = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
Assert.AreEqual(2, root.Controls.Count);
|
||||
Assert.AreSame(RootMenu.Instance, root.NodeKey);
|
||||
AssertControlEquals(testMenu.controls[0], root.Controls[0]);
|
||||
AssertControlEquals(testMenu.controls[1], root.Controls[1]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMenuItemInstaller()
|
||||
{
|
||||
var installer = CreateInstaller("test");
|
||||
installer.menuToAppend = Create<VRCExpressionsMenu>();
|
||||
|
||||
var item = installer.gameObject.AddComponent<ModularAvatarMenuItem>();
|
||||
item.Control = GenerateTestControl();
|
||||
|
||||
var virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer);
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
item.Control.name = "test";
|
||||
|
||||
Assert.AreEqual(1, virtualMenu.ResolvedMenu.Count);
|
||||
var root = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
Assert.AreEqual(1, root.Controls.Count);
|
||||
Assert.AreSame(RootMenu.Instance, root.NodeKey);
|
||||
AssertControlEquals(item.Control, root.Controls[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInstallOntoInstaller()
|
||||
{
|
||||
var installer_a = CreateInstaller("a");
|
||||
var installer_b = CreateInstaller("b");
|
||||
var installer_c = CreateInstaller("c");
|
||||
|
||||
var menu_a = Create<VRCExpressionsMenu>();
|
||||
var menu_b = Create<VRCExpressionsMenu>();
|
||||
var menu_c = Create<VRCExpressionsMenu>();
|
||||
|
||||
menu_a.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
menu_b.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
menu_c.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
|
||||
installer_a.menuToAppend = menu_a;
|
||||
installer_b.menuToAppend = menu_b;
|
||||
installer_c.menuToAppend = menu_c;
|
||||
|
||||
installer_c.installTargetMenu = installer_a.menuToAppend;
|
||||
|
||||
VirtualMenu virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer_a);
|
||||
virtualMenu.RegisterMenuInstaller(installer_b);
|
||||
virtualMenu.RegisterMenuInstaller(installer_c);
|
||||
|
||||
virtualMenu.FreezeMenu();
|
||||
Assert.AreEqual(1, virtualMenu.ResolvedMenu.Count);
|
||||
var rootMenu = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
Assert.AreEqual(3, rootMenu.Controls.Count);
|
||||
Assert.AreSame(RootMenu.Instance, rootMenu.NodeKey);
|
||||
AssertControlEquals(menu_a.controls[0], rootMenu.Controls[0]);
|
||||
AssertControlEquals(menu_c.controls[0], rootMenu.Controls[1]);
|
||||
AssertControlEquals(menu_b.controls[0], rootMenu.Controls[2]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInstallSubmenu()
|
||||
{
|
||||
var installer_a = CreateInstaller("a");
|
||||
|
||||
var menu_a = Create<VRCExpressionsMenu>("a");
|
||||
var menu_b = Create<VRCExpressionsMenu>("b");
|
||||
menu_a.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestSubmenu(menu_b)
|
||||
};
|
||||
menu_b.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
|
||||
installer_a.menuToAppend = menu_a;
|
||||
|
||||
VirtualMenu virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer_a);
|
||||
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
Assert.AreEqual(2, virtualMenu.ResolvedMenu.Count);
|
||||
var rootMenu = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
var subMenu = virtualMenu.ResolvedMenu[menu_b];
|
||||
Assert.AreSame(subMenu, rootMenu.Controls[0].SubmenuNode);
|
||||
Assert.AreSame(RootMenu.Instance, rootMenu.NodeKey);
|
||||
Assert.AreSame(menu_b, subMenu.NodeKey);
|
||||
Assert.AreEqual(1, subMenu.Controls.Count);
|
||||
AssertControlEquals(menu_b.controls[0], subMenu.Controls[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestYankInstaller()
|
||||
{
|
||||
var installer_a = CreateInstaller("a");
|
||||
var installer_b = CreateInstaller("b");
|
||||
|
||||
var menu_a = Create<VRCExpressionsMenu>("a");
|
||||
menu_a.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
installer_a.menuToAppend = menu_a;
|
||||
|
||||
var item = installer_b.gameObject.AddComponent<ModularAvatarMenuItem>();
|
||||
item.Control = GenerateTestControl();
|
||||
item.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
|
||||
item.MenuSource = SubmenuSource.Children;
|
||||
|
||||
var child = CreateChild(item.gameObject, "child");
|
||||
var childItem = child.AddComponent<ModularAvatarMenuInstallTarget>();
|
||||
childItem.installer = installer_a;
|
||||
|
||||
VirtualMenu virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer_a);
|
||||
virtualMenu.RegisterMenuInstaller(installer_b);
|
||||
virtualMenu.RegisterMenuInstallTarget(childItem);
|
||||
|
||||
virtualMenu.FreezeMenu();
|
||||
item.Control.name = "b";
|
||||
|
||||
Assert.AreEqual(2, virtualMenu.ResolvedMenu.Count);
|
||||
var rootMenu = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
var item_node = virtualMenu.ResolvedMenu[item];
|
||||
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);
|
||||
AssertControlEquals(menu_a.controls[0], item_node.Controls[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenMenuInstallersLoop_LoopIsTerminated()
|
||||
{
|
||||
var installer_a = CreateInstaller("a");
|
||||
var installer_b = CreateInstaller("b");
|
||||
|
||||
var menu_a = Create<VRCExpressionsMenu>("a");
|
||||
menu_a.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
installer_a.menuToAppend = menu_a;
|
||||
|
||||
var menu_b = Create<VRCExpressionsMenu>("b");
|
||||
menu_b.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
installer_b.menuToAppend = menu_b;
|
||||
|
||||
installer_a.installTargetMenu = menu_b;
|
||||
installer_b.installTargetMenu = menu_a;
|
||||
|
||||
VirtualMenu virtualMenu = new VirtualMenu(menu_a);
|
||||
virtualMenu.RegisterMenuInstaller(installer_a);
|
||||
virtualMenu.RegisterMenuInstaller(installer_b);
|
||||
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
Assert.AreEqual(1, virtualMenu.ResolvedMenu.Count);
|
||||
var rootMenu = virtualMenu.ResolvedMenu[menu_a];
|
||||
var menu_a_node = virtualMenu.ResolvedMenu[menu_a];
|
||||
Assert.AreEqual(3, rootMenu.Controls.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExternalSubmenuSource_WithMenuInstaller()
|
||||
{
|
||||
var installer_a = CreateInstaller("a");
|
||||
var installer_b = CreateInstaller("b");
|
||||
|
||||
var menu_a = Create<VRCExpressionsMenu>("a");
|
||||
menu_a.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
|
||||
var menu_b = Create<VRCExpressionsMenu>("b");
|
||||
menu_b.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
|
||||
var item_a = installer_a.gameObject.AddComponent<ModularAvatarMenuItem>();
|
||||
item_a.Control = GenerateTestControl();
|
||||
item_a.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
|
||||
item_a.MenuSource = SubmenuSource.External;
|
||||
item_a.Control.subMenu = menu_a;
|
||||
|
||||
installer_b.menuToAppend = menu_b;
|
||||
installer_b.installTargetMenu = menu_a;
|
||||
|
||||
VirtualMenu virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer_a);
|
||||
virtualMenu.RegisterMenuInstaller(installer_b);
|
||||
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
var rootNode = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
Assert.AreEqual(VRCExpressionsMenu.Control.ControlType.SubMenu, rootNode.Controls[0].type);
|
||||
var menu_a_node = rootNode.Controls[0].SubmenuNode;
|
||||
AssertControlEquals(menu_a.controls[0], menu_a_node.Controls[0]);
|
||||
AssertControlEquals(menu_b.controls[0], menu_a_node.Controls[1]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MenuItem_NestedSubmenuNodes()
|
||||
{
|
||||
var installer_a = CreateInstaller("root");
|
||||
var root_item = installer_a.gameObject.AddComponent<ModularAvatarMenuItem>();
|
||||
root_item.Control = GenerateTestControl();
|
||||
root_item.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
|
||||
root_item.MenuSource = SubmenuSource.Children;
|
||||
|
||||
var mid_obj = CreateChild(root_item.gameObject, "mid");
|
||||
var mid_item = mid_obj.AddComponent<ModularAvatarMenuItem>();
|
||||
mid_item.Control = GenerateTestControl();
|
||||
mid_item.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
|
||||
mid_item.MenuSource = SubmenuSource.Children;
|
||||
|
||||
var leaf_obj = CreateChild(mid_obj, "leaf");
|
||||
var leaf_item = leaf_obj.AddComponent<ModularAvatarMenuItem>();
|
||||
leaf_item.Control = GenerateTestControl();
|
||||
|
||||
VirtualMenu virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer_a);
|
||||
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
var rootNode = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
Assert.AreEqual(VRCExpressionsMenu.Control.ControlType.SubMenu, rootNode.Controls[0].type);
|
||||
var mid_node = rootNode.Controls[0].SubmenuNode;
|
||||
var leaf_node = mid_node.Controls[0].SubmenuNode;
|
||||
|
||||
Assert.AreEqual(1, rootNode.Controls.Count);
|
||||
Assert.AreEqual(1, mid_node.Controls.Count);
|
||||
Assert.AreEqual(1, leaf_node.Controls.Count);
|
||||
|
||||
root_item.Control.name = "root";
|
||||
mid_item.Control.name = "mid";
|
||||
leaf_item.Control.name = "leaf";
|
||||
|
||||
AssertControlEquals(root_item.Control, rootNode.Controls[0]);
|
||||
AssertControlEquals(mid_item.Control, mid_node.Controls[0]);
|
||||
AssertControlEquals(leaf_item.Control, leaf_node.Controls[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MenuItem_RemoteReference()
|
||||
{
|
||||
var installer_a = CreateInstaller("root");
|
||||
var root_item = installer_a.gameObject.AddComponent<ModularAvatarMenuItem>();
|
||||
|
||||
var extern_root = CreateRoot("test");
|
||||
var extern_obj = CreateChild(extern_root, "control");
|
||||
var extern_item = extern_obj.AddComponent<ModularAvatarMenuItem>();
|
||||
extern_item.Control = GenerateTestControl();
|
||||
|
||||
root_item.Control = GenerateTestControl();
|
||||
root_item.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
|
||||
root_item.MenuSource = SubmenuSource.Children;
|
||||
root_item.menuSource_otherObjectChildren = extern_root;
|
||||
|
||||
VirtualMenu virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer_a);
|
||||
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
root_item.Control.name = "root";
|
||||
extern_item.Control.name = "control";
|
||||
|
||||
var rootNode = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
Assert.AreEqual(VRCExpressionsMenu.Control.ControlType.SubMenu, rootNode.Controls[0].type);
|
||||
var extern_node = rootNode.Controls[0].SubmenuNode;
|
||||
|
||||
Assert.AreEqual(1, rootNode.Controls.Count);
|
||||
Assert.AreEqual(1, extern_node.Controls.Count);
|
||||
|
||||
AssertControlEquals(root_item.Control, rootNode.Controls[0]);
|
||||
AssertControlEquals(extern_item.Control, extern_node.Controls[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InstallerInstallsInstallTarget()
|
||||
{
|
||||
var installer_a = CreateInstaller("a");
|
||||
var installer_b = CreateInstaller("b");
|
||||
|
||||
var menu_b = Create<VRCExpressionsMenu>("b");
|
||||
menu_b.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
installer_b.menuToAppend = menu_b;
|
||||
|
||||
var item_a = installer_a.gameObject.AddComponent<ModularAvatarMenuInstallTarget>();
|
||||
item_a.installer = installer_b;
|
||||
|
||||
VirtualMenu virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer_a);
|
||||
virtualMenu.RegisterMenuInstaller(installer_b);
|
||||
virtualMenu.RegisterMenuInstallTarget(item_a);
|
||||
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
var rootNode = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
Assert.AreEqual(1, rootNode.Controls.Count);
|
||||
AssertControlEquals(menu_b.controls[0], rootNode.Controls[0]);
|
||||
}
|
||||
|
||||
ModularAvatarMenuInstaller CreateInstaller(string name)
|
||||
{
|
||||
GameObject obj = new GameObject();
|
||||
obj.name = name;
|
||||
|
||||
var installer = obj.AddComponent<ModularAvatarMenuInstaller>();
|
||||
installer.name = name;
|
||||
|
||||
toDestroy.Add(obj);
|
||||
|
||||
return installer;
|
||||
}
|
||||
|
||||
VRCExpressionsMenu.Control GenerateTestSubmenu(VRCExpressionsMenu menu)
|
||||
{
|
||||
var control = GenerateTestControl();
|
||||
control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
|
||||
control.subMenu = menu;
|
||||
|
||||
return control;
|
||||
}
|
||||
|
||||
VRCExpressionsMenu.Control GenerateTestControl()
|
||||
{
|
||||
var control = new VRCExpressionsMenu.Control();
|
||||
|
||||
VRCExpressionsMenu.Control.ControlType[] types = new[]
|
||||
{
|
||||
VRCExpressionsMenu.Control.ControlType.Button,
|
||||
// VRCExpressionsMenu.Control.ControlType.SubMenu,
|
||||
VRCExpressionsMenu.Control.ControlType.Toggle,
|
||||
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.parameter = new VRCExpressionsMenu.Control.Parameter();
|
||||
control.parameter.name = "Test Parameter " + GUID.Generate();
|
||||
control.icon = new Texture2D(1, 1);
|
||||
control.labels = new[]
|
||||
{
|
||||
new VRCExpressionsMenu.Control.Label()
|
||||
{
|
||||
name = "label",
|
||||
icon = testTex
|
||||
}
|
||||
};
|
||||
control.subParameters = new[]
|
||||
{
|
||||
new VRCExpressionsMenu.Control.Parameter()
|
||||
{
|
||||
name = "Test Sub Parameter " + GUID.Generate()
|
||||
}
|
||||
};
|
||||
control.value = 0.42f;
|
||||
control.style = VRCExpressionsMenu.Control.Style.Style3;
|
||||
|
||||
return control;
|
||||
}
|
||||
|
||||
void AssertControlEquals(VRCExpressionsMenu.Control expected, VRCExpressionsMenu.Control actual)
|
||||
{
|
||||
Assert.AreEqual(expected.type, actual.type);
|
||||
Assert.AreEqual(expected.name, actual.name);
|
||||
Assert.AreEqual(expected.parameter.name, actual.parameter.name);
|
||||
Assert.AreNotSame(expected.parameter, actual.parameter);
|
||||
Assert.AreEqual(expected.icon, actual.icon);
|
||||
Assert.AreEqual(expected.labels.Length, actual.labels.Length);
|
||||
Assert.AreEqual(expected.labels[0].name, actual.labels[0].name);
|
||||
Assert.AreEqual(expected.labels[0].icon, actual.labels[0].icon);
|
||||
Assert.AreEqual(expected.subParameters.Length, actual.subParameters.Length);
|
||||
Assert.AreEqual(expected.subParameters[0].name, actual.subParameters[0].name);
|
||||
Assert.AreNotSame(expected.subParameters[0], actual.subParameters[0]);
|
||||
Assert.AreEqual(expected.value, actual.value);
|
||||
Assert.AreEqual(expected.style, actual.style);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c96564b3caa04c63aa9c7c4ce05429eb
|
||||
timeCreated: 1676981475
|
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace nadena.dev.modular_avatar.editor.ErrorReporting
|
||||
{
|
||||
/// <summary>
|
||||
/// These exceptions will not be logged in the error report.
|
||||
/// </summary>
|
||||
public class NominalException : Exception
|
||||
{
|
||||
public NominalException()
|
||||
{
|
||||
}
|
||||
|
||||
protected NominalException([NotNull] SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
|
||||
public NominalException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public NominalException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 152d780db95c4e408240ec3cd4dad60e
|
||||
timeCreated: 1676979798
|
@ -5,7 +5,7 @@ using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
[CustomEditor(typeof(MAMenuItem))]
|
||||
[CustomEditor(typeof(ModularAvatarMenuItem))]
|
||||
internal class MAMenuItemInspector : MAEditorBase
|
||||
{
|
||||
private SerializedProperty prop_submenu_source;
|
||||
@ -14,9 +14,10 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
prop_control = serializedObject.FindProperty(nameof(MAMenuItem.Control));
|
||||
prop_submenu_source = serializedObject.FindProperty(nameof(MAMenuItem.MenuSource));
|
||||
prop_otherObjChildren = serializedObject.FindProperty(nameof(MAMenuItem.menuSource_otherObjectChildren));
|
||||
prop_control = serializedObject.FindProperty(nameof(ModularAvatarMenuItem.Control));
|
||||
prop_submenu_source = serializedObject.FindProperty(nameof(ModularAvatarMenuItem.MenuSource));
|
||||
prop_otherObjChildren =
|
||||
serializedObject.FindProperty(nameof(ModularAvatarMenuItem.menuSource_otherObjectChildren));
|
||||
}
|
||||
|
||||
private void DrawControlSettings(SerializedProperty control, string name = null,
|
||||
@ -25,7 +26,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
if (name != null)
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var targetGameObject = ((MAMenuItem) target).gameObject;
|
||||
var targetGameObject = ((ModularAvatarMenuItem) target).gameObject;
|
||||
var newName = EditorGUILayout.TextField("Name", targetGameObject.name);
|
||||
if (EditorGUI.EndChangeCheck() && commitName != null)
|
||||
{
|
||||
@ -51,7 +52,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
if (!multiEdit)
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var targetGameObject = ((MAMenuItem) target).gameObject;
|
||||
var targetGameObject = ((ModularAvatarMenuItem) target).gameObject;
|
||||
name = targetGameObject.name;
|
||||
commitName = newName =>
|
||||
{
|
||||
@ -68,7 +69,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
if (multiEdit) return;
|
||||
|
||||
var menuItem = (MAMenuItem) target;
|
||||
var menuItem = (ModularAvatarMenuItem) target;
|
||||
if (menuItem.Control.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
|
||||
{
|
||||
GUILayout.Space(EditorStyles.label.lineHeight);
|
||||
@ -92,7 +93,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
: menuItem.gameObject;
|
||||
foreach (Transform t in source.transform)
|
||||
{
|
||||
var child = t.GetComponent<MAMenuItem>();
|
||||
var child = t.GetComponent<ModularAvatarMenuItem>();
|
||||
if (child == null) continue;
|
||||
|
||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||
@ -100,7 +101,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
GUILayout.BeginHorizontal();
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
EditorGUILayout.ObjectField(new GUIContent(), child, typeof(MAMenuItem), true,
|
||||
EditorGUILayout.ObjectField(new GUIContent(), child, typeof(ModularAvatarMenuItem),
|
||||
true,
|
||||
GUILayout.ExpandWidth(true));
|
||||
}
|
||||
|
||||
@ -125,7 +127,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
};
|
||||
|
||||
var childSO = new SerializedObject(child);
|
||||
var childControl = childSO.FindProperty(nameof(MAMenuItem.Control));
|
||||
var childControl = childSO.FindProperty(nameof(ModularAvatarMenuItem.Control));
|
||||
DrawControlSettings(childControl, name, commitName);
|
||||
childSO.ApplyModifiedProperties();
|
||||
|
||||
|
@ -44,15 +44,15 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
itemObj.transform.localRotation = Quaternion.identity;
|
||||
itemObj.transform.localScale = Vector3.one;
|
||||
|
||||
var menuItem = itemObj.AddComponent<MAMenuItem>();
|
||||
var menuItem = itemObj.AddComponent<ModularAvatarMenuItem>();
|
||||
menuItem.Control = sourceControl;
|
||||
|
||||
if (menuItem.Control.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
|
||||
{
|
||||
if (convertedMenus.TryGetValue(sourceControl.subMenu, out var otherSource))
|
||||
{
|
||||
menuItem.MenuSource = SubmenuSource.OtherMenuItem;
|
||||
menuItem.menuSource_otherSource = otherSource;
|
||||
menuItem.MenuSource = SubmenuSource.Children;
|
||||
menuItem.menuSource_otherObjectChildren = otherSource.gameObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 435b9162d1fd4050b9ced045ab20af27
|
||||
timeCreated: 1676977199
|
@ -0,0 +1,375 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
NodeKey = nodeKey;
|
||||
}
|
||||
}
|
||||
|
||||
internal class VirtualControl : VRCExpressionsMenu.Control
|
||||
{
|
||||
/// <summary>
|
||||
/// VirtualControls do not reference real VRCExpressionsMenu objects, but rather virtual MenuNodes.
|
||||
/// </summary>
|
||||
internal MenuNode 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The VirtualMenu class tracks a fully realized shadow menu. Notably, this is _not_ converted to unity
|
||||
* ScriptableObjects, making it easier to discard it when we need to update it.
|
||||
*/
|
||||
internal class VirtualMenu
|
||||
{
|
||||
private readonly object RootMenuKey;
|
||||
|
||||
/// <summary>
|
||||
/// Indexes which menu installers are contributing to which VRCExpressionMenu assets.
|
||||
/// </summary>
|
||||
private Dictionary<object, List<ModularAvatarMenuInstaller>> _targetMenuToInstaller
|
||||
= new Dictionary<object, List<ModularAvatarMenuInstaller>>();
|
||||
|
||||
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>();
|
||||
|
||||
// TODO: immutable?
|
||||
public Dictionary<object, MenuNode> ResolvedMenu => _resolvedMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the VirtualMenu.
|
||||
/// </summary>
|
||||
/// <param name="rootMenu">The root VRCExpressionsMenu to import</param>
|
||||
internal VirtualMenu(VRCExpressionsMenu rootMenu)
|
||||
{
|
||||
if (rootMenu != null)
|
||||
{
|
||||
RootMenuKey = rootMenu;
|
||||
ImportMenu(rootMenu);
|
||||
}
|
||||
else
|
||||
{
|
||||
RootMenuKey = RootMenu.Instance;
|
||||
_menuNodeMap[RootMenu.Instance] = new MenuNode(RootMenu.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
/// </summary>
|
||||
/// <param name="installer"></param>
|
||||
internal void RegisterMenuInstaller(ModularAvatarMenuInstaller installer)
|
||||
{
|
||||
// initial validation
|
||||
if (installer.menuToAppend == null && installer.GetComponent<MenuSource>() == null) return;
|
||||
|
||||
var target = installer.installTargetMenu ? (object) installer.installTargetMenu : RootMenuKey;
|
||||
if (!_targetMenuToInstaller.TryGetValue(target, out var targets))
|
||||
{
|
||||
targets = new List<ModularAvatarMenuInstaller>();
|
||||
_targetMenuToInstaller[target] = targets;
|
||||
}
|
||||
|
||||
targets.Add(installer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an install target with this virtual menu. As with menu installers, processing is delayed.
|
||||
/// </summary>
|
||||
/// <param name="target"></param>
|
||||
internal void RegisterMenuInstallTarget(ModularAvatarMenuInstallTarget target)
|
||||
{
|
||||
if (target.installer == null) return;
|
||||
if (!_installerToTargetComponent.TryGetValue(target.installer, out var targets))
|
||||
{
|
||||
targets = new List<ModularAvatarMenuInstallTarget>();
|
||||
_installerToTargetComponent[target.installer] = targets;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Freezes the menu, fully resolving all members of all menus.
|
||||
/// </summary>
|
||||
internal void FreezeMenu()
|
||||
{
|
||||
ResolveNode(RootMenuKey);
|
||||
}
|
||||
|
||||
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.External:
|
||||
{
|
||||
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,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70cfde8e889a4057b14153c7021e16c8
|
||||
timeCreated: 1676977210
|
@ -0,0 +1,67 @@
|
||||
using System.Collections.Generic;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
/// <summary>
|
||||
/// The menu install target includes the controls of the target menu installer at the point of reference.
|
||||
/// Notably, this can include multiple controls.
|
||||
///
|
||||
/// One tricky aspect of this feature is that we need to disambiguate when a menu installer also cites a target menu.
|
||||
/// Generally, if an installer is targeted by any menu install target (even if - especially if - disabled), we
|
||||
/// ignore its install target configuration entirely.
|
||||
///
|
||||
/// 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
|
||||
{
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fad1419b52a42ae89b0df52eb861e47
|
||||
timeCreated: 1676976513
|
@ -22,17 +22,15 @@ namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
External,
|
||||
Children,
|
||||
MenuInstaller,
|
||||
OtherMenuItem,
|
||||
}
|
||||
|
||||
public class MAMenuItem : MenuSource
|
||||
[AddComponentMenu("Modular Avatar/MA Menu Item")]
|
||||
public class ModularAvatarMenuItem : MenuSource
|
||||
{
|
||||
public VRCExpressionsMenu.Control Control;
|
||||
public SubmenuSource MenuSource;
|
||||
|
||||
public ModularAvatarMenuInstaller menuSource_installer;
|
||||
public MenuSource menuSource_otherSource;
|
||||
public GameObject menuSource_otherObjectChildren;
|
||||
|
||||
internal override VRCExpressionsMenu.Control[] GenerateMenu()
|
||||
@ -75,6 +73,7 @@ namespace nadena.dev.modular_avatar.core
|
||||
|
||||
break;
|
||||
}
|
||||
/*
|
||||
case SubmenuSource.MenuInstaller:
|
||||
controls = menuSource_installer.installTargetMenu?.controls?.ToList();
|
||||
break;
|
||||
@ -95,6 +94,7 @@ namespace nadena.dev.modular_avatar.core
|
||||
_recursing = false;
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
if (controls == null)
|
Loading…
x
Reference in New Issue
Block a user