diff --git a/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/InternalParamTestMenu.asset b/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/InternalParamTestMenu.asset new file mode 100644 index 00000000..a57130bd --- /dev/null +++ b/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/InternalParamTestMenu.asset @@ -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: [] diff --git a/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/InternalParamTestMenu.asset.meta b/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/InternalParamTestMenu.asset.meta new file mode 100644 index 00000000..131a0bdc --- /dev/null +++ b/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/InternalParamTestMenu.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 09a6ec3c61c7db840b3cd958099c5798 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/InternalParameterTest.prefab b/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/InternalParameterTest.prefab new file mode 100644 index 00000000..5ad839b8 --- /dev/null +++ b/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/InternalParameterTest.prefab @@ -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} diff --git a/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/InternalParameterTest.prefab.meta b/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/InternalParameterTest.prefab.meta new file mode 100644 index 00000000..b08694ce --- /dev/null +++ b/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/InternalParameterTest.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 15cecd8ca5178eb40953a581734e61f6 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/VirtualMenuTests.cs b/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/VirtualMenuTests.cs index 91153fd0..ae1f49d7 100644 --- a/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/VirtualMenuTests.cs +++ b/Assets/_ModularAvatar/EditModeTests/VirtualMenuTests/VirtualMenuTests.cs @@ -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) 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(); + 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()); + virtualMenu.FreezeMenu(); + Assert.AreEqual(2, virtualMenu.RootMenuNode.Controls.Count); + } + + [Test] + public void remapParams_isAppliedSeparatelyForEachDedup() + { + var menu_a = Create(); + 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(); + 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().parameters = new List() + { + 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().parameters = new List() + { + new ParameterConfig() + { + nameOrPrefix = "p", + remapTo = "b", + } + }; + + var buildContext = new BuildContext(av_root.GetComponent()); + new RenameParametersHook().OnPreprocessAvatar(av_root, buildContext); + + var virtualMenu = VirtualMenu.ForAvatar(av_root.GetComponent(), 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()); + new RenameParametersHook().OnPreprocessAvatar(root, buildContext); + var virtualMenu = VirtualMenu.ForAvatar(root.GetComponent(), 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 tuple && ReferenceEquals(tuple.Item1, asset) + ).Value; + } + + internal static VRCExpressionsMenu SourceMenu(this VirtualMenuNode node) + { + if (node.NodeKey is ValueTuple tuple && tuple.Item1 is VRCExpressionsMenu menu) + { + return menu; + } + + return null; + } + } } \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/Menu/MenuPreviewGUI.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Menu/MenuPreviewGUI.cs index 9fc0b23d..e0fe7aa2 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Inspector/Menu/MenuPreviewGUI.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Menu/MenuPreviewGUI.cs @@ -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 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); } diff --git a/Packages/nadena.dev.modular-avatar/Editor/Menu/VirtualMenu.cs b/Packages/nadena.dev.modular-avatar/Editor/Menu/VirtualMenu.cs index eb16b802..addb3afb 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Menu/VirtualMenu.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/Menu/VirtualMenu.cs @@ -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(); - 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(); + 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 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) (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 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 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) (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) { diff --git a/Packages/nadena.dev.modular-avatar/Runtime/Menu/VirtualMenuAPI.cs b/Packages/nadena.dev.modular-avatar/Runtime/Menu/VirtualMenuAPI.cs index 52180939..e0a992cd 100644 --- a/Packages/nadena.dev.modular-avatar/Runtime/Menu/VirtualMenuAPI.cs +++ b/Packages/nadena.dev.modular-avatar/Runtime/Menu/VirtualMenuAPI.cs @@ -106,7 +106,7 @@ namespace nadena.dev.modular_avatar.core.menu /// installer invocations. /// /// - void PushNode(VRCExpressionsMenu expMenu); + void PushMenuContents(VRCExpressionsMenu expMenu); /// /// Pushes the contents of this menu source onto the current menu node.