From d212dabc271df006f38148bb1cfbbc5bcf02a752 Mon Sep 17 00:00:00 2001 From: bd_ Date: Tue, 21 Feb 2023 19:47:10 +0900 Subject: [PATCH] feat: add the MAMenuItem component --- .../Editor/AvatarProcessor.cs | 3 + .../Editor/BuildContext.cs | 39 +++++ .../ErrorReporting/ComponentValidation.cs | 2 +- .../Editor/Inspector/MAMenuItemInspector.cs | 141 ++++++++++++++++++ .../Inspector/MAMenuItemInspector.cs.meta | 3 + .../Editor/Inspector/MenuInstallerEditor.cs | 116 +++++++++----- .../Editor/MenuExtractor.cs | 76 ++++++++++ .../Editor/MenuExtractor.cs.meta | 3 + .../Editor/MenuInstallHook.cs | 48 +----- .../Editor/ReifyMenuPass.cs | 32 ++++ .../Editor/ReifyMenuPass.cs.meta | 3 + .../Runtime/MAMenuItem.cs | 130 ++++++++++++++++ .../Runtime/MAMenuItem.cs.meta | 3 + 13 files changed, 519 insertions(+), 80 deletions(-) create mode 100644 Packages/nadena.dev.modular-avatar/Editor/Inspector/MAMenuItemInspector.cs create mode 100644 Packages/nadena.dev.modular-avatar/Editor/Inspector/MAMenuItemInspector.cs.meta create mode 100644 Packages/nadena.dev.modular-avatar/Editor/MenuExtractor.cs create mode 100644 Packages/nadena.dev.modular-avatar/Editor/MenuExtractor.cs.meta create mode 100644 Packages/nadena.dev.modular-avatar/Editor/ReifyMenuPass.cs create mode 100644 Packages/nadena.dev.modular-avatar/Editor/ReifyMenuPass.cs.meta create mode 100644 Packages/nadena.dev.modular-avatar/Runtime/MAMenuItem.cs create mode 100644 Packages/nadena.dev.modular-avatar/Runtime/MAMenuItem.cs.meta diff --git a/Packages/nadena.dev.modular-avatar/Editor/AvatarProcessor.cs b/Packages/nadena.dev.modular-avatar/Editor/AvatarProcessor.cs index af45db47..9600375f 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/AvatarProcessor.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/AvatarProcessor.cs @@ -172,6 +172,7 @@ namespace nadena.dev.modular_avatar.core.editor var context = new BuildContext(vrcAvatarDescriptor); + new ReifyMenuPass().OnPreprocessAvatar(vrcAvatarDescriptor, context); new RenameParametersHook().OnPreprocessAvatar(avatarGameObject, context); new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject, context); context.AnimationDatabase.Bootstrap(vrcAvatarDescriptor); @@ -212,6 +213,8 @@ namespace nadena.dev.modular_avatar.core.editor ErrorReportUI.MaybeOpenErrorReportUI(); AssetDatabase.SaveAssets(); + + Resources.UnloadUnusedAssets(); } } } diff --git a/Packages/nadena.dev.modular-avatar/Editor/BuildContext.cs b/Packages/nadena.dev.modular-avatar/Editor/BuildContext.cs index b3c46283..19bd479c 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/BuildContext.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/BuildContext.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using UnityEditor; using UnityEditor.Animations; using UnityEngine; using VRC.SDK3.Avatars.Components; +using VRC.SDK3.Avatars.ScriptableObjects; using Object = UnityEngine.Object; namespace nadena.dev.modular_avatar.core.editor @@ -13,6 +15,10 @@ namespace nadena.dev.modular_avatar.core.editor internal readonly AnimationDatabase AnimationDatabase = new AnimationDatabase(); internal readonly AnimatorController AssetContainer; + internal readonly Dictionary ClonedMenus + = new Dictionary(); + + public BuildContext(VRCAvatarDescriptor avatarDescriptor) { AvatarDescriptor = avatarDescriptor; @@ -72,5 +78,38 @@ namespace nadena.dev.modular_avatar.core.editor merger.AddOverrideController("", overrideController, null); return merger.Finish(); } + + public VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu) + { + if (menu == null) return null; + if (ClonedMenus.TryGetValue(menu, out var newMenu)) return newMenu; + newMenu = Object.Instantiate(menu); + this.SaveAsset(newMenu); + ClonedMenus[menu] = newMenu; + + foreach (var control in newMenu.controls) + { + if (Util.ValidateExpressionMenuIcon(control.icon) != Util.ValidateExpressionMenuIconResult.Success) + control.icon = null; + + for (int i = 0; i < control.labels.Length; i++) + { + var label = control.labels[i]; + var labelResult = Util.ValidateExpressionMenuIcon(label.icon); + if (labelResult != Util.ValidateExpressionMenuIconResult.Success) + { + label.icon = null; + control.labels[i] = label; + } + } + + if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu) + { + control.subMenu = CloneMenu(control.subMenu); + } + } + + return newMenu; + } } } \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/ErrorReporting/ComponentValidation.cs b/Packages/nadena.dev.modular-avatar/Editor/ErrorReporting/ComponentValidation.cs index 17376f2a..9b098396 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/ErrorReporting/ComponentValidation.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/ErrorReporting/ComponentValidation.cs @@ -137,7 +137,7 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting private static List CheckInternal(ModularAvatarMenuInstaller mi) { // TODO - check that target menu is in the avatar - if (mi.menuToAppend == null) + if (mi.menuToAppend == null && mi.GetComponent() == null) { return new List() { diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MAMenuItemInspector.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MAMenuItemInspector.cs new file mode 100644 index 00000000..975c5722 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MAMenuItemInspector.cs @@ -0,0 +1,141 @@ +using System; +using UnityEditor; +using UnityEngine; +using VRC.SDK3.Avatars.ScriptableObjects; + +namespace nadena.dev.modular_avatar.core.editor +{ + [CustomEditor(typeof(MAMenuItem))] + internal class MAMenuItemInspector : MAEditorBase + { + private SerializedProperty prop_submenu_source; + private SerializedProperty prop_control; + private SerializedProperty prop_otherObjChildren; + + 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)); + } + + private void DrawControlSettings(SerializedProperty control, string name = null, + Action commitName = null) + { + if (name != null) + { + EditorGUI.BeginChangeCheck(); + var targetGameObject = ((MAMenuItem) target).gameObject; + var newName = EditorGUILayout.TextField("Name", targetGameObject.name); + if (EditorGUI.EndChangeCheck() && commitName != null) + { + commitName(newName); + } + } + + var prop_type = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.type)); + var prop_parameter = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter)) + .FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter.name)); + var prop_value = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.value)); + + EditorGUILayout.PropertyField(prop_type); + EditorGUILayout.PropertyField(prop_parameter, new GUIContent("Parameter")); + EditorGUILayout.PropertyField(prop_value); + } + + protected override void OnInnerInspectorGUI() + { + bool multiEdit = targets.Length != 1; + string name = null; + Action commitName = null; + if (!multiEdit) + { + EditorGUI.BeginChangeCheck(); + var targetGameObject = ((MAMenuItem) target).gameObject; + name = targetGameObject.name; + commitName = newName => + { + Undo.RecordObject(targetGameObject, "Rename object"); + targetGameObject.name = newName; + }; + } + + serializedObject.Update(); + + DrawControlSettings(prop_control, name, commitName); + + serializedObject.ApplyModifiedProperties(); + + if (multiEdit) return; + + var menuItem = (MAMenuItem) target; + if (menuItem.Control.type == VRCExpressionsMenu.Control.ControlType.SubMenu) + { + GUILayout.Space(EditorStyles.label.lineHeight); + EditorGUILayout.LabelField("Sub Menu", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(prop_submenu_source); + + if (prop_submenu_source.enumValueIndex == (int) SubmenuSource.Children) + { + EditorGUILayout.PropertyField(prop_otherObjChildren); + } + + serializedObject.ApplyModifiedProperties(); + + switch (menuItem.MenuSource) + { + default: break; + case SubmenuSource.Children: + { + var source = menuItem.menuSource_otherObjectChildren != null + ? menuItem.menuSource_otherObjectChildren + : menuItem.gameObject; + foreach (Transform t in source.transform) + { + var child = t.GetComponent(); + if (child == null) continue; + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + GUILayout.BeginHorizontal(); + using (new EditorGUI.DisabledScope(true)) + { + EditorGUILayout.ObjectField(new GUIContent(), child, typeof(MAMenuItem), true, + GUILayout.ExpandWidth(true)); + } + + GUILayout.Space(20); + GUILayout.Label("Enabled", GUILayout.Width(50)); + var childObject = t.gameObject; + EditorGUI.BeginChangeCheck(); + var active = GUILayout.Toggle(childObject.activeSelf, new GUIContent(), + GUILayout.Width(EditorGUIUtility.singleLineHeight)); + if (EditorGUI.EndChangeCheck()) + { + childObject.SetActive(active); + } + + GUILayout.EndHorizontal(); + + name = t.gameObject.name; + commitName = newName => + { + Undo.RecordObject(t.gameObject, "Rename object"); + t.gameObject.name = newName; + }; + + var childSO = new SerializedObject(child); + var childControl = childSO.FindProperty(nameof(MAMenuItem.Control)); + DrawControlSettings(childControl, name, commitName); + childSO.ApplyModifiedProperties(); + + EditorGUILayout.EndVertical(); + } + + break; + } + } + } + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MAMenuItemInspector.cs.meta b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MAMenuItemInspector.cs.meta new file mode 100644 index 00000000..e4426ed4 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MAMenuItemInspector.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5b674c72186c4e6884b0cd05098f11b6 +timeCreated: 1676791017 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs index 13310e65..563389cc 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs @@ -51,6 +51,7 @@ namespace nadena.dev.modular_avatar.core.editor _menuToAppend = _installer.menuToAppend; } } + protected override void OnInnerInspectorGUI() { SetupMenuEditor(); @@ -107,6 +108,7 @@ namespace nadena.dev.modular_avatar.core.editor if (targets.Length == 1) { + /* TODO _menuFoldout = EditorGUILayout.Foldout(_menuFoldout, G("menuinstall.showcontents")); if (_menuFoldout) { @@ -119,30 +121,66 @@ namespace nadena.dev.modular_avatar.core.editor EditorGUI.indentLevel--; } + */ } - _devFoldout = EditorGUILayout.Foldout(_devFoldout, G("menuinstall.devoptions")); - if (_devFoldout) + bool inconsistentSources = false; + MenuSource menuSource = null; + bool first = true; + foreach (var target in targets) { - SerializedProperty menuToAppendProperty = serializedObject.FindProperty(nameof(ModularAvatarMenuInstaller.menuToAppend)); - switch (ValidateExpressionMenuIcon((VRCExpressionsMenu)menuToAppendProperty.objectReferenceValue)) + var component = (ModularAvatarMenuInstaller) target; + var componentSource = component.GetComponent(); + if (componentSource != null) { - case ValidateExpressionMenuIconResult.Success: - break; - case ValidateExpressionMenuIconResult.TooLarge: - EditorGUILayout.HelpBox(S("menuinstall.menu_icon_too_large"), MessageType.Error); - break; - case ValidateExpressionMenuIconResult.Uncompressed: - EditorGUILayout.HelpBox(S("menuinstall.menu_icon_uncompressed"), MessageType.Error); - break; - default: - throw new ArgumentOutOfRangeException(); + if (menuSource == null && first) + { + menuSource = componentSource; + } + else + { + inconsistentSources = true; + } + } + } + + if (menuSource != null) + { + // TODO localize + EditorGUILayout.HelpBox("Menu contents provided by " + menuSource.GetType() + " component", + MessageType.Info); + } + + if (!inconsistentSources) + { + _devFoldout = EditorGUILayout.Foldout(_devFoldout, G("menuinstall.devoptions")); + if (_devFoldout) + { + SerializedProperty menuToAppendProperty = + serializedObject.FindProperty(nameof(ModularAvatarMenuInstaller.menuToAppend)); + if (!menuToAppendProperty.hasMultipleDifferentValues) + { + switch (ValidateExpressionMenuIcon( + (VRCExpressionsMenu) menuToAppendProperty.objectReferenceValue)) + { + case ValidateExpressionMenuIconResult.Success: + break; + case ValidateExpressionMenuIconResult.TooLarge: + EditorGUILayout.HelpBox(S("menuinstall.menu_icon_too_large"), MessageType.Error); + break; + case ValidateExpressionMenuIconResult.Uncompressed: + EditorGUILayout.HelpBox(S("menuinstall.menu_icon_uncompressed"), MessageType.Error); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + EditorGUI.indentLevel++; + EditorGUILayout.PropertyField( + menuToAppendProperty, new GUIContent(G("menuinstall.srcmenu"))); + EditorGUI.indentLevel--; } - - EditorGUI.indentLevel++; - EditorGUILayout.PropertyField( - menuToAppendProperty, new GUIContent(G("menuinstall.srcmenu"))); - EditorGUI.indentLevel--; } serializedObject.ApplyModifiedProperties(); @@ -203,35 +241,37 @@ namespace nadena.dev.modular_avatar.core.editor } } - private void FindMenuInstallers() + private void FindMenuInstallers() { - if (targets.Length > 1) + if (targets.Length > 1) { _menuInstallersMap = null; return; } _menuInstallersMap = new Dictionary>(); - var avatar = RuntimeUtil.FindAvatarInParents(((Component)target).transform); + var avatar = RuntimeUtil.FindAvatarInParents(((Component) target).transform); if (avatar == null) return; var menuInstallers = avatar.GetComponentsInChildren(true) .Where(menuInstaller => menuInstaller.enabled && menuInstaller.menuToAppend != null); - foreach (ModularAvatarMenuInstaller menuInstaller in menuInstallers) + foreach (ModularAvatarMenuInstaller menuInstaller in menuInstallers) { if (menuInstaller == target) continue; var visitedMenus = new HashSet(); var queue = new Queue(); queue.Enqueue(menuInstaller.menuToAppend); - while (queue.Count > 0) + while (queue.Count > 0) { VRCExpressionsMenu parent = queue.Dequeue(); - var controls = parent.controls.Where(control => control.type == VRCExpressionsMenu.Control.ControlType.SubMenu && control.subMenu != null); - foreach (VRCExpressionsMenu.Control control in controls) + var controls = parent.controls.Where(control => + control.type == VRCExpressionsMenu.Control.ControlType.SubMenu && control.subMenu != null); + foreach (VRCExpressionsMenu.Control control in controls) { // Do not filter in LINQ to avoid closure allocation if (visitedMenus.Contains(control.subMenu)) continue; - if (!_menuInstallersMap.TryGetValue(control.subMenu, out List fromInstallers)) + if (!_menuInstallersMap.TryGetValue(control.subMenu, + out List fromInstallers)) { fromInstallers = new List(); _menuInstallersMap[control.subMenu] = fromInstallers; @@ -245,22 +285,24 @@ namespace nadena.dev.modular_avatar.core.editor } } - private bool IsMenuReachable(VRCAvatarDescriptor avatar, VRCExpressionsMenu menu, HashSet visitedInstaller = null) + private bool IsMenuReachable(VRCAvatarDescriptor avatar, VRCExpressionsMenu menu, + HashSet visitedInstaller = null) { if (_avatarMenus == null || _avatarMenus.Contains(menu)) return true; if (_menuInstallersMap == null) return true; - if (visitedInstaller == null) visitedInstaller = new HashSet { (ModularAvatarMenuInstaller)target }; + if (visitedInstaller == null) + visitedInstaller = new HashSet {(ModularAvatarMenuInstaller) target}; if (!_menuInstallersMap.TryGetValue(menu, out List installers)) return false; - foreach (ModularAvatarMenuInstaller installer in installers) + foreach (ModularAvatarMenuInstaller installer in installers) { // Root is always reachable if installTargetMenu is null if (installer.installTargetMenu == null) return true; // Even in a circular structure, it may be possible to reach root by another path. if (visitedInstaller.Contains(installer)) continue; visitedInstaller.Add(installer); - if (IsMenuReachable(avatar, installer.installTargetMenu, visitedInstaller)) + if (IsMenuReachable(avatar, installer.installTargetMenu, visitedInstaller)) { return true; } @@ -269,14 +311,16 @@ namespace nadena.dev.modular_avatar.core.editor return false; } - private static ValidateExpressionMenuIconResult ValidateExpressionMenuIcon(VRCExpressionsMenu menu, HashSet visitedMenus = null) + private static ValidateExpressionMenuIconResult ValidateExpressionMenuIcon(VRCExpressionsMenu menu, + HashSet visitedMenus = null) { if (menu == null) return ValidateExpressionMenuIconResult.Success; if (visitedMenus == null) visitedMenus = new HashSet(); if (visitedMenus.Contains(menu)) return ValidateExpressionMenuIconResult.Success; visitedMenus.Add(menu); - - foreach (VRCExpressionsMenu.Control control in menu.controls) { + + foreach (VRCExpressionsMenu.Control control in menu.controls) + { // Control ValidateExpressionMenuIconResult result = Util.ValidateExpressionMenuIcon(control.icon); if (result != ValidateExpressionMenuIconResult.Success) return result; @@ -293,12 +337,12 @@ namespace nadena.dev.modular_avatar.core.editor // SubMenu if (control.type != VRCExpressionsMenu.Control.ControlType.SubMenu) continue; - ValidateExpressionMenuIconResult subMenuResult = ValidateExpressionMenuIcon(control.subMenu, visitedMenus); + ValidateExpressionMenuIconResult subMenuResult = + ValidateExpressionMenuIcon(control.subMenu, visitedMenus); if (subMenuResult != ValidateExpressionMenuIconResult.Success) return subMenuResult; } return ValidateExpressionMenuIconResult.Success; } - } } \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/MenuExtractor.cs b/Packages/nadena.dev.modular-avatar/Editor/MenuExtractor.cs new file mode 100644 index 00000000..39e04b08 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/MenuExtractor.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using VRC.SDK3.Avatars.Components; +using VRC.SDK3.Avatars.ScriptableObjects; + +namespace nadena.dev.modular_avatar.core.editor +{ + internal class MenuExtractor + { + private const int PRIORITY = 49; + + [MenuItem("GameObject/[Modular Avatar] Extract menu", false, PRIORITY)] + static void ExtractMenu(MenuCommand menuCommand) + { + if (!(menuCommand.context is GameObject gameObj)) return; + var avatar = gameObj.GetComponent(); + if (avatar == null || avatar.expressionsMenu == null) return; + + VRCExpressionsMenu.Control fakeControl = new VRCExpressionsMenu.Control() + { + subMenu = avatar.expressionsMenu, + type = VRCExpressionsMenu.Control.ControlType.SubMenu, + name = "Avatar Menu" + }; + var rootMenu = ConvertSubmenu(gameObj, fakeControl, new Dictionary()); + Undo.RecordObject(avatar, "Convert menu"); + avatar.expressionsMenu = null; + + rootMenu.gameObject.AddComponent(); + } + + private static MenuSource ConvertSubmenu( + GameObject parentObj, + VRCExpressionsMenu.Control sourceControl, + Dictionary convertedMenus + ) + { + var itemObj = new GameObject(); + itemObj.name = string.IsNullOrEmpty(sourceControl.name) ? " " : sourceControl.name; + Undo.RegisterCreatedObjectUndo(itemObj, "Convert menu"); + itemObj.transform.SetParent(parentObj.transform); + itemObj.transform.localPosition = Vector3.zero; + itemObj.transform.localRotation = Quaternion.identity; + itemObj.transform.localScale = Vector3.one; + + var menuItem = itemObj.AddComponent(); + 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; + } + else + { + convertedMenus[sourceControl.subMenu] = menuItem; + + menuItem.MenuSource = SubmenuSource.Children; + + if (sourceControl.subMenu.controls != null) + { + foreach (var childControl in sourceControl.subMenu.controls) + { + ConvertSubmenu(itemObj, childControl, convertedMenus); + } + } + } + } + + return menuItem; + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/MenuExtractor.cs.meta b/Packages/nadena.dev.modular-avatar/Editor/MenuExtractor.cs.meta new file mode 100644 index 00000000..c8fa22d3 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/MenuExtractor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: de7ea831512b4d9c9e92985ab6fd5f17 +timeCreated: 1676896326 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/MenuInstallHook.cs b/Packages/nadena.dev.modular-avatar/Editor/MenuInstallHook.cs index 6a60ebf1..2e8a4692 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/MenuInstallHook.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/MenuInstallHook.cs @@ -20,9 +20,6 @@ namespace nadena.dev.modular_avatar.core.editor private BuildContext _context; - private Dictionary _clonedMenus; - - private VRCExpressionsMenu _rootMenu; private MenuTree _menuTree; @@ -38,8 +35,6 @@ namespace nadena.dev.modular_avatar.core.editor .ToArray(); if (menuInstallers.Length == 0) return; - - _clonedMenus = new Dictionary(); _visitedInstallerStack = new Stack(); VRCAvatarDescriptor avatar = avatarRoot.GetComponent(); @@ -49,14 +44,14 @@ namespace nadena.dev.modular_avatar.core.editor var menu = ScriptableObject.CreateInstance(); _context.SaveAsset(menu); avatar.expressionsMenu = menu; - _clonedMenus[menu] = menu; + context.ClonedMenus[menu] = menu; } _rootMenu = avatar.expressionsMenu; _menuTree = new MenuTree(avatar); _menuTree.TraverseAvatarMenu(); - avatar.expressionsMenu = CloneMenu(avatar.expressionsMenu); + avatar.expressionsMenu = _context.CloneMenu(avatar.expressionsMenu); foreach (ModularAvatarMenuInstaller installer in menuInstallers) { @@ -84,10 +79,10 @@ namespace nadena.dev.modular_avatar.core.editor } if (installer.installTargetMenu == null || installer.menuToAppend == null) return; - if (!_clonedMenus.TryGetValue(installTarget, out var targetMenu)) return; + if (!_context.ClonedMenus.TryGetValue(installTarget, out var targetMenu)) return; // Clone before appending to sanitize menu icons - targetMenu.controls.AddRange(CloneMenu(installer.menuToAppend).controls); + targetMenu.controls.AddRange(_context.CloneMenu(installer.menuToAppend).controls); SplitMenu(installer, targetMenu); @@ -128,42 +123,9 @@ namespace nadena.dev.modular_avatar.core.editor labels = Array.Empty() }); - _clonedMenus[installer.installTargetMenu] = newMenu; + _context.ClonedMenus[installer.installTargetMenu] = newMenu; targetMenu = newMenu; } } - - private VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu) - { - if (menu == null) return null; - if (_clonedMenus.TryGetValue(menu, out var newMenu)) return newMenu; - newMenu = Object.Instantiate(menu); - _context.SaveAsset(newMenu); - _clonedMenus[menu] = newMenu; - - foreach (var control in newMenu.controls) - { - if (Util.ValidateExpressionMenuIcon(control.icon) != Util.ValidateExpressionMenuIconResult.Success) - control.icon = null; - - for (int i = 0; i < control.labels.Length; i++) - { - var label = control.labels[i]; - var labelResult = Util.ValidateExpressionMenuIcon(label.icon); - if (labelResult != Util.ValidateExpressionMenuIconResult.Success) - { - label.icon = null; - control.labels[i] = label; - } - } - - if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu) - { - control.subMenu = CloneMenu(control.subMenu); - } - } - - return newMenu; - } } } \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/ReifyMenuPass.cs b/Packages/nadena.dev.modular-avatar/Editor/ReifyMenuPass.cs new file mode 100644 index 00000000..44cd4f5a --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/ReifyMenuPass.cs @@ -0,0 +1,32 @@ +using System.Linq; +using nadena.dev.modular_avatar.editor.ErrorReporting; +using UnityEngine; +using VRC.SDK3.Avatars.Components; +using VRC.SDK3.Avatars.ScriptableObjects; + +namespace nadena.dev.modular_avatar.core.editor +{ + internal class ReifyMenuPass + { + public void OnPreprocessAvatar(VRCAvatarDescriptor root, BuildContext context) + { + foreach (ModularAvatarMenuInstaller installer in + root.GetComponentsInChildren(true)) + { + BuildReport.ReportingObject(installer, () => ReifyMenu(context, installer)); + } + } + + private void ReifyMenu(BuildContext context, ModularAvatarMenuInstaller installer) + { + var source = installer.GetComponent(); + if (source == null) return; + + var controls = source.GenerateMenu(); + var menu = ScriptableObject.CreateInstance(); + menu.controls = controls.ToList(); + + installer.menuToAppend = context.CloneMenu(menu); + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/ReifyMenuPass.cs.meta b/Packages/nadena.dev.modular-avatar/Editor/ReifyMenuPass.cs.meta new file mode 100644 index 00000000..182b2e97 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/ReifyMenuPass.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e50c6e0b2ad64fc0b1b00cb45117f023 +timeCreated: 1676895255 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Runtime/MAMenuItem.cs b/Packages/nadena.dev.modular-avatar/Runtime/MAMenuItem.cs new file mode 100644 index 00000000..a583320f --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Runtime/MAMenuItem.cs @@ -0,0 +1,130 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using VRC.SDK3.Avatars.ScriptableObjects; + +namespace nadena.dev.modular_avatar.core +{ + [DisallowMultipleComponent] + public abstract class MenuSource : AvatarTagComponent + { + /** + * Generates the menu items for this menu source object. Submenus are not required to be persisted as assets; + * this will be handled by the caller if necessary. + * + * Note that this method might be called outside of a build context (e.g. from custom inspectors). + */ + internal abstract VRCExpressionsMenu.Control[] GenerateMenu(); + } + + + public enum SubmenuSource + { + External, + Children, + MenuInstaller, + OtherMenuItem, + } + + public class MAMenuItem : 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() + { + switch (Control.type) + { + case VRCExpressionsMenu.Control.ControlType.SubMenu: + return GenerateSubmenu(); + default: + return new[] + {Control}; + } + } + + private bool _recursing = false; + private VRCExpressionsMenu _cachedMenu; + + private VRCExpressionsMenu.Control[] GenerateSubmenu() + { + List controls = null; + switch (MenuSource) + { + case SubmenuSource.External: + controls = Control.subMenu?.controls?.ToList(); + break; + case SubmenuSource.Children: + { + var menuRoot = menuSource_otherObjectChildren == null + ? gameObject + : menuSource_otherObjectChildren; + controls = new List(); + foreach (Transform child in menuRoot.transform) + { + var menuSource = child.GetComponent(); + if (menuSource != null && child.gameObject.activeSelf && menuSource.enabled) + { + controls.AddRange(menuSource.GenerateMenu()); + } + } + + break; + } + case SubmenuSource.MenuInstaller: + controls = menuSource_installer.installTargetMenu?.controls?.ToList(); + break; + case SubmenuSource.OtherMenuItem: + if (_recursing || menuSource_otherSource == null) + { + return new VRCExpressionsMenu.Control[] { }; + } + else + { + _recursing = true; + try + { + return menuSource_otherSource.GenerateMenu(); + } + finally + { + _recursing = false; + } + } + } + + if (controls == null) + { + return new VRCExpressionsMenu.Control[] { }; + } + + if (_cachedMenu == null) _cachedMenu = ScriptableObject.CreateInstance(); + _cachedMenu.controls = controls; + + var control = CloneControl(Control); + control.name = gameObject.name; + control.subMenu = _cachedMenu; + + return new[] {control}; + } + + private static VRCExpressionsMenu.Control CloneControl(VRCExpressionsMenu.Control control) + { + return new VRCExpressionsMenu.Control() + { + type = control.type, + parameter = control.parameter, + labels = control.labels.ToArray(), + subParameters = control.subParameters.ToArray(), + icon = control.icon, + name = control.name, + value = control.value, + subMenu = control.subMenu + }; + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Runtime/MAMenuItem.cs.meta b/Packages/nadena.dev.modular-avatar/Runtime/MAMenuItem.cs.meta new file mode 100644 index 00000000..ae5a5a16 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Runtime/MAMenuItem.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3b29d45007c5493d926d2cd45a489529 +timeCreated: 1676787152 \ No newline at end of file