#if MA_VRCSDK3_AVATARS #region using System; using System.IO; using System.Linq; using nadena.dev.modular_avatar.ui; using UnityEditor; using UnityEngine; using VRC.SDK3.Avatars.Components; using VRC.SDK3.Avatars.ScriptableObjects; #endregion namespace nadena.dev.modular_avatar.core.editor { internal class MenuExtractor { [MenuItem(UnityMenuItems.GameObject_ExtractMenu, false, UnityMenuItems.GameObject_ExtractMenuOrder)] static void ExtractMenu(MenuCommand menuCommand) { if (!(menuCommand.context is GameObject gameObj)) return; var avatar = gameObj.GetComponent(); if (avatar == null || avatar.expressionsMenu == null || avatar.expressionsMenu.controls.Count == 0) return; var parent = ExtractSingleLayerMenu(avatar.expressionsMenu, gameObj, "Avatar Menu"); parent.AddComponent(); parent.AddComponent(); // The VRCSDK requires that an expressions menu asset be provided if any parameters are defined. // We can't just remove the asset, so we'll replace it with a dummy asset. However, to avoid users // accidentally overwriting files in Packages, we'll place this dummy asset next to where the original // asset was (or in the Assets root, if the original asset was in Packages). Undo.RecordObject(avatar, "Extract menu"); var assetPath = AssetDatabase.GetAssetPath(avatar.expressionsMenu); var dummyAssetPathBase = assetPath.Replace(".asset", " placeholder"); if (dummyAssetPathBase.StartsWith("Packages" + Path.DirectorySeparatorChar)) { var filename = Path.GetFileName(dummyAssetPathBase); dummyAssetPathBase = Path.Combine("Assets", filename); } // Check that a similarly-named file doesn't already exist int i = 0; do { var fullPath = dummyAssetPathBase + (i > 0 ? " " + i : "") + ".asset"; if (File.Exists(fullPath)) { var asset = AssetDatabase.LoadAssetAtPath(fullPath); if (asset != null && asset.controls.Count == 0) { avatar.expressionsMenu = asset; break; } } else if (!File.Exists(fullPath)) { var dummyAsset = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(dummyAsset, fullPath); avatar.expressionsMenu = dummyAsset; break; } i++; } while (true); EditorUtility.SetDirty(avatar); PrefabUtility.RecordPrefabInstancePropertyModifications(avatar); } /// /// Extracts a single expressions menu asset to Menu Item components. /// /// The menu to extract /// The parent object to use /// The name of a gameobject to place between the parent and menu item objects, /// or null to skip /// the direct parent of the generated menu items internal static GameObject ExtractSingleLayerMenu( VRCExpressionsMenu menu, GameObject parent, string containerName = null) { if (containerName != null) { var container = new GameObject(); container.name = containerName; container.transform.SetParent(parent.transform, false); parent = container; Undo.RegisterCreatedObjectUndo(container, "Convert menu"); } foreach (var control in menu.controls) { var itemObj = new GameObject(); itemObj.name = string.IsNullOrEmpty(control.name) ? " " : control.name; Undo.RegisterCreatedObjectUndo(itemObj, "Convert menu"); itemObj.transform.SetParent(parent.transform, false); var menuItem = itemObj.AddComponent(); ControlToMenuItem(menuItem, control); } return parent; } internal static void ControlToMenuItem(ModularAvatarMenuItem menuItem, VRCExpressionsMenu.Control control) { menuItem.Control = CloneControl(control); if (menuItem.Control.type == VRCExpressionsMenu.Control.ControlType.SubMenu) { menuItem.MenuSource = SubmenuSource.MenuAsset; } } internal static VRCExpressionsMenu.Control CloneControl(VRCExpressionsMenu.Control c) { var type = c.type != 0 ? c.type : VRCExpressionsMenu.Control.ControlType.Button; return new VRCExpressionsMenu.Control() { type = type, name = c.name, icon = c.icon, parameter = new VRCExpressionsMenu.Control.Parameter() { name = c.parameter?.name }, subMenu = c.subMenu, subParameters = c.subParameters?.Select(p => new VRCExpressionsMenu.Control.Parameter() { name = p?.name }) ?.ToArray() ?? Array.Empty(), labels = c.labels?.ToArray() ?? Array.Empty(), style = c.style, value = c.value, }; } } } #endif