fix: issues with menu generation (#371)

* chore: adding unit tests for #366 and #326

* fix: duplicate submenu controls not generated for multiple installers

When multiple installers referenced the same expressions menu asset,
only one submenu control would be generated.

* fix: submenus incorrectly deduping across different postprocessing contexts

Fixes: #366, #326

* fix: postprocess context not being inherited into submenus (#326)

This caused issues where parameter mappings were not being applied to
submenus.
This commit is contained in:
bd_ 2023-08-04 21:44:41 +09:00
parent 333d4e8a95
commit 3044969454
8 changed files with 700 additions and 45 deletions

View File

@ -0,0 +1,25 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: -340790334, guid: 67cc4cb7839cd3741b63733d5adf0442, type: 3}
m_Name: InternalParamTestMenu
m_EditorClassIdentifier:
controls:
- name: item
icon: {fileID: 0}
type: 102
parameter:
name: x
value: 1
style: 0
subMenu: {fileID: 0}
subParameters: []
labels: []

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 09a6ec3c61c7db840b3cd958099c5798
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,450 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &2860324038786842778
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2860324038786842773}
- component: {fileID: 2860324038786842772}
- component: {fileID: 1761937879440307670}
- component: {fileID: 3202484316900348122}
- component: {fileID: 2669963860607714920}
- component: {fileID: 5085570563562387984}
m_Layer: 0
m_Name: InternalParameterTest
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &2860324038786842773
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2860324038786842778}
m_LocalRotation: {x: 0.07681167, y: 0.62035006, z: 0.20221491, w: 0.7539065}
m_LocalPosition: {x: -0.0534066, y: 0.9950551, z: 0.14142857}
m_LocalScale: {x: 1.098901, y: 1.0989009, z: 1.098901}
m_Children:
- {fileID: 8910535925068494434}
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &2860324038786842772
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2860324038786842778}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 71a96d4ea0c344f39e277d82035bf9bd, type: 3}
m_Name:
m_EditorClassIdentifier:
parameters:
- nameOrPrefix: x
remapTo:
internalParameter: 1
isPrefix: 0
syncType: 3
localOnly: 0
defaultValue: 0
saved: 1
--- !u!114 &1761937879440307670
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2860324038786842778}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3b29d45007c5493d926d2cd45a489529, type: 3}
m_Name:
m_EditorClassIdentifier:
Control:
name:
icon: {fileID: 0}
type: 103
parameter:
name:
value: 1
style: 0
subMenu: {fileID: 11400000, guid: 09a6ec3c61c7db840b3cd958099c5798, type: 2}
subParameters: []
labels: []
MenuSource: 0
menuSource_otherObjectChildren: {fileID: 0}
isSynced: 1
isSaved: 1
--- !u!114 &3202484316900348122
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2860324038786842778}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7ef83cb0c23d4d7c9d41021e544a1978, type: 3}
m_Name:
m_EditorClassIdentifier:
menuToAppend: {fileID: 11400000, guid: 09a6ec3c61c7db840b3cd958099c5798, type: 2}
installTargetMenu: {fileID: 0}
--- !u!114 &2669963860607714920
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2860324038786842778}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 542108242, guid: 67cc4cb7839cd3741b63733d5adf0442, type: 3}
m_Name:
m_EditorClassIdentifier:
Name:
ViewPosition: {x: 0, y: 1.6, z: 0.2}
Animations: 0
ScaleIPD: 1
lipSync: 0
lipSyncJawBone: {fileID: 0}
lipSyncJawClosed: {x: 0, y: 0, z: 0, w: 1}
lipSyncJawOpen: {x: 0, y: 0, z: 0, w: 1}
VisemeSkinnedMesh: {fileID: 0}
MouthOpenBlendShapeName: Facial_Blends.Jaw_Down
VisemeBlendShapes: []
unityVersion:
portraitCameraPositionOffset: {x: 0, y: 0, z: 0}
portraitCameraRotationOffset: {x: 0, y: 1, z: 0, w: -0.00000004371139}
networkIDs: []
customExpressions: 0
expressionsMenu: {fileID: 0}
expressionParameters: {fileID: 0}
enableEyeLook: 0
customEyeLookSettings:
eyeMovement:
confidence: 0.5
excitement: 0.5
leftEye: {fileID: 0}
rightEye: {fileID: 0}
eyesLookingStraight:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyesLookingUp:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyesLookingDown:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyesLookingLeft:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyesLookingRight:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidType: 0
upperLeftEyelid: {fileID: 0}
upperRightEyelid: {fileID: 0}
lowerLeftEyelid: {fileID: 0}
lowerRightEyelid: {fileID: 0}
eyelidsDefault:
upper:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
lower:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidsClosed:
upper:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
lower:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidsLookingUp:
upper:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
lower:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidsLookingDown:
upper:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
lower:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidsSkinnedMesh: {fileID: 0}
eyelidsBlendshapes:
customizeAnimationLayers: 0
baseAnimationLayers:
- isEnabled: 0
type: 0
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 2
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 3
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 4
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 5
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
specialAnimationLayers:
- isEnabled: 0
type: 6
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 7
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 8
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
AnimationPreset: {fileID: 0}
animationHashSet: []
autoFootsteps: 1
autoLocomotion: 1
collider_head:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_torso:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_footR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_footL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_handR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_handL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerIndexL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerMiddleL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerRingL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerLittleL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerIndexR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerMiddleR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerRingR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerLittleR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
--- !u!114 &5085570563562387984
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2860324038786842778}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: -1427037861, guid: 4ecd63eff847044b68db9453ce219299, type: 3}
m_Name:
m_EditorClassIdentifier:
launchedFromSDKPipeline: 0
completedSDKPipeline: 0
blueprintId:
contentType: 0
assetBundleUnityVersion:
fallbackStatus: 0
--- !u!1 &7624042284818776979
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8910535925068494434}
- component: {fileID: 2708762870899574912}
- component: {fileID: 216941358938488416}
m_Layer: 0
m_Name: Menu
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &8910535925068494434
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7624042284818776979}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children: []
m_Father: {fileID: 2860324038786842773}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &2708762870899574912
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7624042284818776979}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3b29d45007c5493d926d2cd45a489529, type: 3}
m_Name:
m_EditorClassIdentifier:
Control:
name:
icon: {fileID: 0}
type: 103
parameter:
name:
value: 1
style: 0
subMenu: {fileID: 0}
subParameters: []
labels: []
MenuSource: 0
menuSource_otherObjectChildren: {fileID: 0}
isSynced: 1
isSaved: 1
--- !u!114 &216941358938488416
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7624042284818776979}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7ef83cb0c23d4d7c9d41021e544a1978, type: 3}
m_Name:
m_EditorClassIdentifier:
menuToAppend: {fileID: 0}
installTargetMenu: {fileID: 0}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 15cecd8ca5178eb40953a581734e61f6
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,12 +1,18 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.core;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.modular_avatar.core.editor.menu;
using nadena.dev.modular_avatar.core.menu;
using NUnit.Framework;
using UnityEditor;
using UnityEditor.VersionControl;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;
using Object = UnityEngine.Object;
using Random = UnityEngine.Random;
namespace modular_avatar_tests.VirtualMenuTests
{
@ -73,9 +79,9 @@ namespace modular_avatar_tests.VirtualMenuTests
virtualMenu.FreezeMenu();
Assert.AreEqual(1, virtualMenu.ResolvedMenu.Count);
var root = virtualMenu.ResolvedMenu[rootMenu];
var root = virtualMenu.NodeForMenuAsset(rootMenu);
Assert.AreEqual(2, root.Controls.Count);
Assert.AreSame(rootMenu, root.NodeKey);
Assert.AreSame(rootMenu, root.SourceMenu());
AssertControlEquals(rootMenu.controls[0], root.Controls[0]);
AssertControlEquals(rootMenu.controls[1], root.Controls[1]);
}
@ -106,22 +112,22 @@ namespace modular_avatar_tests.VirtualMenuTests
virtualMenu.FreezeMenu();
Assert.AreEqual(3, virtualMenu.ResolvedMenu.Count);
var rootNode = virtualMenu.ResolvedMenu[rootMenu];
var sub1Node = virtualMenu.ResolvedMenu[sub1];
var sub2Node = virtualMenu.ResolvedMenu[sub2];
var rootNode = virtualMenu.ResolvedMenu[virtualMenu.RootMenuKey];
var sub1Node = virtualMenu.NodeForMenuAsset(sub1);
var sub2Node = virtualMenu.NodeForMenuAsset(sub2);
Assert.AreEqual(1, rootNode.Controls.Count);
Assert.AreSame(rootMenu, rootNode.NodeKey);
Assert.AreSame(virtualMenu.RootMenuKey, 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(sub1, sub1Node.SourceMenu());
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(sub2, sub2Node.SourceMenu());
Assert.AreSame(rootNode, sub2Node.Controls[0].SubmenuNode);
Assert.IsNull(sub2Node.Controls[0].subMenu);
}
@ -243,10 +249,10 @@ namespace modular_avatar_tests.VirtualMenuTests
Assert.AreEqual(2, virtualMenu.ResolvedMenu.Count);
var rootMenu = virtualMenu.ResolvedMenu[RootMenu.Instance];
var subMenu = virtualMenu.ResolvedMenu[menu_b];
var subMenu = virtualMenu.NodeForMenuAsset(menu_b);
Assert.AreSame(subMenu, rootMenu.Controls[0].SubmenuNode);
Assert.AreSame(RootMenu.Instance, rootMenu.NodeKey);
Assert.AreSame(menu_b, subMenu.NodeKey);
Assert.AreSame(menu_b, ((ValueTuple<object, object>) subMenu.NodeKey).Item1);
Assert.AreEqual(1, subMenu.Controls.Count);
AssertControlEquals(menu_b.controls[0], subMenu.Controls[0]);
}
@ -324,8 +330,7 @@ namespace modular_avatar_tests.VirtualMenuTests
virtualMenu.FreezeMenu();
Assert.AreEqual(1, virtualMenu.ResolvedMenu.Count);
var rootMenu = virtualMenu.ResolvedMenu[menu_a];
var menu_a_node = virtualMenu.ResolvedMenu[menu_a];
var rootMenu = virtualMenu.NodeForMenuAsset(menu_a);
Assert.AreEqual(3, rootMenu.Controls.Count);
}
@ -557,10 +562,113 @@ namespace modular_avatar_tests.VirtualMenuTests
virtualMenu.FreezeMenu();
var root = virtualMenu.ResolvedMenu[menu_a];
var root = virtualMenu.NodeForMenuAsset(menu_a);
Assert.AreEqual(1, root.Controls.Count);
}
[Test]
public void multipleMenuAssets_areInstalledMultipleTimes()
{
var menu_a = Create<VRCExpressionsMenu>();
menu_a.controls.Add(new VRCExpressionsMenu.Control()
{
name = "control",
parameter = new VRCExpressionsMenu.Control.Parameter()
{
name = "p"
},
type = VRCExpressionsMenu.Control.ControlType.Toggle
});
var av_root = CreateRoot("avatar");
var node_a = CreateInstaller("menu_a");
node_a.transform.SetParent(av_root.transform);
node_a.menuToAppend = menu_a;
var node_b = CreateInstaller("menu_b");
node_b.transform.SetParent(av_root.transform);
node_b.menuToAppend = menu_a;
var virtualMenu = VirtualMenu.ForAvatar(av_root.GetComponent<VRCAvatarDescriptor>());
virtualMenu.FreezeMenu();
Assert.AreEqual(2, virtualMenu.RootMenuNode.Controls.Count);
}
[Test]
public void remapParams_isAppliedSeparatelyForEachDedup()
{
var menu_a = Create<VRCExpressionsMenu>();
menu_a.controls.Add(new VRCExpressionsMenu.Control()
{
name = "control",
parameter = new VRCExpressionsMenu.Control.Parameter()
{
name = "p"
},
type = VRCExpressionsMenu.Control.ControlType.Toggle
});
var menu_outer = Create<VRCExpressionsMenu>();
menu_outer.controls.Add(new VRCExpressionsMenu.Control()
{
name = "control",
type = VRCExpressionsMenu.Control.ControlType.SubMenu,
subMenu = menu_a
});
var av_root = CreateRoot("avatar");
var node_a = CreateInstaller("menu_a");
node_a.transform.SetParent(av_root.transform);
node_a.menuToAppend = menu_outer;
node_a.gameObject.AddComponent<ModularAvatarParameters>().parameters = new List<ParameterConfig>()
{
new ParameterConfig()
{
nameOrPrefix = "p",
remapTo = "a",
}
};
var node_b = CreateInstaller("menu_b");
node_b.transform.SetParent(av_root.transform);
node_b.menuToAppend = menu_outer;
node_b.gameObject.AddComponent<ModularAvatarParameters>().parameters = new List<ParameterConfig>()
{
new ParameterConfig()
{
nameOrPrefix = "p",
remapTo = "b",
}
};
var buildContext = new BuildContext(av_root.GetComponent<VRCAvatarDescriptor>());
new RenameParametersHook().OnPreprocessAvatar(av_root, buildContext);
var virtualMenu = VirtualMenu.ForAvatar(av_root.GetComponent<VRCAvatarDescriptor>(), buildContext);
virtualMenu.FreezeMenu();
Assert.IsTrue(virtualMenu.RootMenuNode.Controls.Any(c =>
c.SubmenuNode.Controls[0].parameter.name == "a"
));
Assert.IsTrue(virtualMenu.RootMenuNode.Controls.Any(c =>
c.SubmenuNode.Controls[0].parameter.name == "b"
));
}
[Test]
public void internalParameterTest()
{
var root = CreatePrefab("InternalParameterTest.prefab");
BuildContext buildContext = new BuildContext(root.GetComponent<VRCAvatarDescriptor>());
new RenameParametersHook().OnPreprocessAvatar(root, buildContext);
var virtualMenu = VirtualMenu.ForAvatar(root.GetComponent<VRCAvatarDescriptor>(), buildContext);
Assert.AreNotEqual("x", virtualMenu.RootMenuNode.Controls[0]
.SubmenuNode.Controls[0].parameter.name);
}
ModularAvatarMenuInstaller CreateInstaller(string name)
{
GameObject obj = new GameObject();
@ -640,4 +748,24 @@ namespace modular_avatar_tests.VirtualMenuTests
Assert.AreEqual(expected.style, actual.style);
}
}
internal static class TestHelpers
{
internal static VirtualMenuNode NodeForMenuAsset(this VirtualMenu menu, VRCExpressionsMenu asset)
{
return menu.ResolvedMenu.FirstOrDefault(
kvp => kvp.Key is ValueTuple<object, object> tuple && ReferenceEquals(tuple.Item1, asset)
).Value;
}
internal static VRCExpressionsMenu SourceMenu(this VirtualMenuNode node)
{
if (node.NodeKey is ValueTuple<object, object> tuple && tuple.Item1 is VRCExpressionsMenu menu)
{
return menu;
}
return null;
}
}
}

View File

@ -107,7 +107,7 @@ namespace nadena.dev.modular_avatar.core.editor
public void DoGUI(VRCExpressionsMenu menu, GameObject parameterReference = null)
{
new VisitorContext(this).PushNode(menu);
new VisitorContext(this).PushMenuContents(menu);
}
private void PushGuiNode(object key, Func<Action> guiBuilder)
@ -162,7 +162,7 @@ namespace nadena.dev.modular_avatar.core.editor
});
}
public void PushNode(VRCExpressionsMenu expMenu)
public void PushMenuContents(VRCExpressionsMenu expMenu)
{
PushMenu(expMenu, null);
}

View File

@ -87,28 +87,37 @@ namespace nadena.dev.modular_avatar.core.editor.menu
_currentPostprocessor = postprocessor;
}
public void PushNode(VRCExpressionsMenu expMenu)
public void PushMenuContents(VRCExpressionsMenu expMenu)
{
if (expMenu == null) return;
if (_visited.Contains(expMenu)) return;
_visited.Add(expMenu);
_visitedMenu(expMenu);
foreach (var control in expMenu.controls)
try
{
PushControl(control);
}
if (_menuToInstallerMap.TryGetValue(expMenu, out var installers))
{
foreach (var installer in installers)
foreach (var control in expMenu.controls)
{
using (new PostprocessorContext(this, null))
PushControl(control);
}
if (_menuToInstallerMap.TryGetValue(expMenu, out var installers))
{
foreach (var installer in installers)
{
PushNode(installer);
using (new PostprocessorContext(this, null))
{
PushNode(installer);
}
}
}
}
finally
{
// We can visit the same expMenu multiple times, with different visit contexts (owing to having
// different source installers, with different postprocessing configurations).
_visited.Remove(expMenu);
}
}
public void PushNode(MenuSource source)
@ -118,6 +127,8 @@ namespace nadena.dev.modular_avatar.core.editor.menu
_visited.Add(source);
BuildReport.ReportingObject(source as UnityEngine.Object, () => source.Visit(this));
_visited.Remove(source);
}
public void PushNode(ModularAvatarMenuInstaller installer)
@ -128,26 +139,34 @@ namespace nadena.dev.modular_avatar.core.editor.menu
BuildReport.ReportingObject(installer, () =>
{
var menuSourceComp = installer.GetComponent<MenuSource>();
if (menuSourceComp != null)
using (new PostprocessorContext(this, _postProcessControls.GetValueOrDefault(installer)))
{
PushNode(menuSourceComp);
}
else if (installer.menuToAppend != null)
{
using (new PostprocessorContext(this, _postProcessControls.GetValueOrDefault(installer)))
var menuSourceComp = installer.GetComponent<MenuSource>();
if (menuSourceComp != null)
{
PushNode(installer.menuToAppend);
PushNode(menuSourceComp);
}
else if (installer.menuToAppend != null)
{
PushMenuContents(installer.menuToAppend);
}
}
});
_visited.Remove(installer);
}
public void PushControl(VRCExpressionsMenu.Control control)
{
// XXX: When we invoke NodeFor on the subMenu, we need to ensure we dedup considering the parameter context
// of the source control. This is because the same subMenu can be used in multiple places, with different
// parameter replacements. (FIXME)
var virtualControl = new VirtualControl(control);
virtualControl.SubmenuNode = NodeFor(control.subMenu);
if (control.subMenu != null)
{
virtualControl.SubmenuNode = NodeFor(control.subMenu);
}
_currentPostprocessor(virtualControl);
@ -178,6 +197,8 @@ namespace nadena.dev.modular_avatar.core.editor.menu
*/
internal class VirtualMenu
{
private static readonly Action<VRCExpressionsMenu.Control> NoopPostprocessor = control => { };
internal readonly object RootMenuKey;
private static long _cacheSeq = 0;
@ -236,7 +257,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
if (rootMenu != null)
{
RootMenuKey = rootMenu;
RootMenuKey = (ValueTuple<object, object>) (rootMenu, NoopPostprocessor);
}
else
{
@ -335,18 +356,28 @@ namespace nadena.dev.modular_avatar.core.editor.menu
var rootContext =
new NodeContextImpl(RootNode, NodeFor, menuToInstallerFiltered, _postprocessControlsHooks,
m => _visitedMenus.Add(m),
_control => { });
if (RootMenuKey is VRCExpressionsMenu menu)
NoopPostprocessor);
if (RootMenuKey is ValueTuple<object, object> tuple && tuple.Item1 is VRCExpressionsMenu menu)
{
foreach (var control in menu.controls)
{
rootContext.PushControl(control);
}
// Some menu installers may be bound to the root menu _asset_ directly.
if (menuToInstallerFiltered.TryGetValue(menu, out var installers))
{
foreach (var installer in installers)
{
rootContext.PushNode(installer);
}
}
}
if (menuToInstallerFiltered.TryGetValue(RootMenuKey, out var installers))
// Untargeted installers are bound to the RootMenuKey, rather than the menu asset itself.
if (menuToInstallerFiltered.TryGetValue(RootMenuKey, out var installers2))
{
foreach (var installer in installers)
foreach (var installer in installers2)
{
rootContext.PushNode(installer);
}
@ -361,9 +392,15 @@ namespace nadena.dev.modular_avatar.core.editor.menu
VirtualMenuNode NodeFor(object key, Action<VRCExpressionsMenu.Control> postprocessContext)
{
if (_resolvedMenu.TryGetValue(key, out var node)) return node;
node = new VirtualMenuNode(key);
_resolvedMenu[key] = node;
var lookupKey = key;
if (key is VRCExpressionsMenu)
{
lookupKey = (ValueTuple<object, object>) (key, postprocessContext);
}
if (_resolvedMenu.TryGetValue(lookupKey, out var node)) return node;
node = new VirtualMenuNode(lookupKey);
_resolvedMenu[lookupKey] = node;
_pendingGeneration.Enqueue(() =>
{
@ -375,7 +412,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
postprocessContext);
if (key is VRCExpressionsMenu expMenu)
{
context.PushNode(expMenu);
context.PushMenuContents(expMenu);
}
else if (key is MenuSource source)
{

View File

@ -106,7 +106,7 @@ namespace nadena.dev.modular_avatar.core.menu
/// installer invocations.
/// </summary>
/// <param name="expMenu"></param>
void PushNode(VRCExpressionsMenu expMenu);
void PushMenuContents(VRCExpressionsMenu expMenu);
/// <summary>
/// Pushes the contents of this menu source onto the current menu node.