mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-05-10 13:19:01 +08:00
feat: add the MAMenuItem component
This commit is contained in:
parent
d385eb8800
commit
d212dabc27
@ -172,6 +172,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
var context = new BuildContext(vrcAvatarDescriptor);
|
var context = new BuildContext(vrcAvatarDescriptor);
|
||||||
|
|
||||||
|
new ReifyMenuPass().OnPreprocessAvatar(vrcAvatarDescriptor, context);
|
||||||
new RenameParametersHook().OnPreprocessAvatar(avatarGameObject, context);
|
new RenameParametersHook().OnPreprocessAvatar(avatarGameObject, context);
|
||||||
new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject, context);
|
new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject, context);
|
||||||
context.AnimationDatabase.Bootstrap(vrcAvatarDescriptor);
|
context.AnimationDatabase.Bootstrap(vrcAvatarDescriptor);
|
||||||
@ -212,6 +213,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
ErrorReportUI.MaybeOpenErrorReportUI();
|
ErrorReportUI.MaybeOpenErrorReportUI();
|
||||||
|
|
||||||
AssetDatabase.SaveAssets();
|
AssetDatabase.SaveAssets();
|
||||||
|
|
||||||
|
Resources.UnloadUnusedAssets();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEditor.Animations;
|
using UnityEditor.Animations;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using VRC.SDK3.Avatars.Components;
|
using VRC.SDK3.Avatars.Components;
|
||||||
|
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||||
using Object = UnityEngine.Object;
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core.editor
|
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 AnimationDatabase AnimationDatabase = new AnimationDatabase();
|
||||||
internal readonly AnimatorController AssetContainer;
|
internal readonly AnimatorController AssetContainer;
|
||||||
|
|
||||||
|
internal readonly Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> ClonedMenus
|
||||||
|
= new Dictionary<VRCExpressionsMenu, VRCExpressionsMenu>();
|
||||||
|
|
||||||
|
|
||||||
public BuildContext(VRCAvatarDescriptor avatarDescriptor)
|
public BuildContext(VRCAvatarDescriptor avatarDescriptor)
|
||||||
{
|
{
|
||||||
AvatarDescriptor = avatarDescriptor;
|
AvatarDescriptor = avatarDescriptor;
|
||||||
@ -72,5 +78,38 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
merger.AddOverrideController("", overrideController, null);
|
merger.AddOverrideController("", overrideController, null);
|
||||||
return merger.Finish();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -137,7 +137,7 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting
|
|||||||
private static List<ErrorLog> CheckInternal(ModularAvatarMenuInstaller mi)
|
private static List<ErrorLog> CheckInternal(ModularAvatarMenuInstaller mi)
|
||||||
{
|
{
|
||||||
// TODO - check that target menu is in the avatar
|
// TODO - check that target menu is in the avatar
|
||||||
if (mi.menuToAppend == null)
|
if (mi.menuToAppend == null && mi.GetComponent<MenuSource>() == null)
|
||||||
{
|
{
|
||||||
return new List<ErrorLog>()
|
return new List<ErrorLog>()
|
||||||
{
|
{
|
||||||
|
@ -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<string> 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<string> 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<MAMenuItem>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5b674c72186c4e6884b0cd05098f11b6
|
||||||
|
timeCreated: 1676791017
|
@ -51,6 +51,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
_menuToAppend = _installer.menuToAppend;
|
_menuToAppend = _installer.menuToAppend;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnInnerInspectorGUI()
|
protected override void OnInnerInspectorGUI()
|
||||||
{
|
{
|
||||||
SetupMenuEditor();
|
SetupMenuEditor();
|
||||||
@ -107,6 +108,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
if (targets.Length == 1)
|
if (targets.Length == 1)
|
||||||
{
|
{
|
||||||
|
/* TODO
|
||||||
_menuFoldout = EditorGUILayout.Foldout(_menuFoldout, G("menuinstall.showcontents"));
|
_menuFoldout = EditorGUILayout.Foldout(_menuFoldout, G("menuinstall.showcontents"));
|
||||||
if (_menuFoldout)
|
if (_menuFoldout)
|
||||||
{
|
{
|
||||||
@ -119,30 +121,66 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
EditorGUI.indentLevel--;
|
EditorGUI.indentLevel--;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
_devFoldout = EditorGUILayout.Foldout(_devFoldout, G("menuinstall.devoptions"));
|
bool inconsistentSources = false;
|
||||||
if (_devFoldout)
|
MenuSource menuSource = null;
|
||||||
|
bool first = true;
|
||||||
|
foreach (var target in targets)
|
||||||
{
|
{
|
||||||
SerializedProperty menuToAppendProperty = serializedObject.FindProperty(nameof(ModularAvatarMenuInstaller.menuToAppend));
|
var component = (ModularAvatarMenuInstaller) target;
|
||||||
switch (ValidateExpressionMenuIcon((VRCExpressionsMenu)menuToAppendProperty.objectReferenceValue))
|
var componentSource = component.GetComponent<MenuSource>();
|
||||||
|
if (componentSource != null)
|
||||||
{
|
{
|
||||||
case ValidateExpressionMenuIconResult.Success:
|
if (menuSource == null && first)
|
||||||
break;
|
{
|
||||||
case ValidateExpressionMenuIconResult.TooLarge:
|
menuSource = componentSource;
|
||||||
EditorGUILayout.HelpBox(S("menuinstall.menu_icon_too_large"), MessageType.Error);
|
}
|
||||||
break;
|
else
|
||||||
case ValidateExpressionMenuIconResult.Uncompressed:
|
{
|
||||||
EditorGUILayout.HelpBox(S("menuinstall.menu_icon_uncompressed"), MessageType.Error);
|
inconsistentSources = true;
|
||||||
break;
|
}
|
||||||
default:
|
}
|
||||||
throw new ArgumentOutOfRangeException();
|
}
|
||||||
|
|
||||||
|
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();
|
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;
|
_menuInstallersMap = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_menuInstallersMap = new Dictionary<VRCExpressionsMenu, List<ModularAvatarMenuInstaller>>();
|
_menuInstallersMap = new Dictionary<VRCExpressionsMenu, List<ModularAvatarMenuInstaller>>();
|
||||||
var avatar = RuntimeUtil.FindAvatarInParents(((Component)target).transform);
|
var avatar = RuntimeUtil.FindAvatarInParents(((Component) target).transform);
|
||||||
if (avatar == null) return;
|
if (avatar == null) return;
|
||||||
var menuInstallers = avatar.GetComponentsInChildren<ModularAvatarMenuInstaller>(true)
|
var menuInstallers = avatar.GetComponentsInChildren<ModularAvatarMenuInstaller>(true)
|
||||||
.Where(menuInstaller => menuInstaller.enabled && menuInstaller.menuToAppend != null);
|
.Where(menuInstaller => menuInstaller.enabled && menuInstaller.menuToAppend != null);
|
||||||
foreach (ModularAvatarMenuInstaller menuInstaller in menuInstallers)
|
foreach (ModularAvatarMenuInstaller menuInstaller in menuInstallers)
|
||||||
{
|
{
|
||||||
if (menuInstaller == target) continue;
|
if (menuInstaller == target) continue;
|
||||||
var visitedMenus = new HashSet<VRCExpressionsMenu>();
|
var visitedMenus = new HashSet<VRCExpressionsMenu>();
|
||||||
var queue = new Queue<VRCExpressionsMenu>();
|
var queue = new Queue<VRCExpressionsMenu>();
|
||||||
queue.Enqueue(menuInstaller.menuToAppend);
|
queue.Enqueue(menuInstaller.menuToAppend);
|
||||||
|
|
||||||
while (queue.Count > 0)
|
while (queue.Count > 0)
|
||||||
{
|
{
|
||||||
VRCExpressionsMenu parent = queue.Dequeue();
|
VRCExpressionsMenu parent = queue.Dequeue();
|
||||||
var controls = parent.controls.Where(control => control.type == VRCExpressionsMenu.Control.ControlType.SubMenu && control.subMenu != null);
|
var controls = parent.controls.Where(control =>
|
||||||
foreach (VRCExpressionsMenu.Control control in controls)
|
control.type == VRCExpressionsMenu.Control.ControlType.SubMenu && control.subMenu != null);
|
||||||
|
foreach (VRCExpressionsMenu.Control control in controls)
|
||||||
{
|
{
|
||||||
// Do not filter in LINQ to avoid closure allocation
|
// Do not filter in LINQ to avoid closure allocation
|
||||||
if (visitedMenus.Contains(control.subMenu)) continue;
|
if (visitedMenus.Contains(control.subMenu)) continue;
|
||||||
if (!_menuInstallersMap.TryGetValue(control.subMenu, out List<ModularAvatarMenuInstaller> fromInstallers))
|
if (!_menuInstallersMap.TryGetValue(control.subMenu,
|
||||||
|
out List<ModularAvatarMenuInstaller> fromInstallers))
|
||||||
{
|
{
|
||||||
fromInstallers = new List<ModularAvatarMenuInstaller>();
|
fromInstallers = new List<ModularAvatarMenuInstaller>();
|
||||||
_menuInstallersMap[control.subMenu] = fromInstallers;
|
_menuInstallersMap[control.subMenu] = fromInstallers;
|
||||||
@ -245,22 +285,24 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsMenuReachable(VRCAvatarDescriptor avatar, VRCExpressionsMenu menu, HashSet<ModularAvatarMenuInstaller> visitedInstaller = null)
|
private bool IsMenuReachable(VRCAvatarDescriptor avatar, VRCExpressionsMenu menu,
|
||||||
|
HashSet<ModularAvatarMenuInstaller> visitedInstaller = null)
|
||||||
{
|
{
|
||||||
if (_avatarMenus == null || _avatarMenus.Contains(menu)) return true;
|
if (_avatarMenus == null || _avatarMenus.Contains(menu)) return true;
|
||||||
|
|
||||||
if (_menuInstallersMap == null) return true;
|
if (_menuInstallersMap == null) return true;
|
||||||
if (visitedInstaller == null) visitedInstaller = new HashSet<ModularAvatarMenuInstaller> { (ModularAvatarMenuInstaller)target };
|
if (visitedInstaller == null)
|
||||||
|
visitedInstaller = new HashSet<ModularAvatarMenuInstaller> {(ModularAvatarMenuInstaller) target};
|
||||||
|
|
||||||
if (!_menuInstallersMap.TryGetValue(menu, out List<ModularAvatarMenuInstaller> installers)) return false;
|
if (!_menuInstallersMap.TryGetValue(menu, out List<ModularAvatarMenuInstaller> installers)) return false;
|
||||||
foreach (ModularAvatarMenuInstaller installer in installers)
|
foreach (ModularAvatarMenuInstaller installer in installers)
|
||||||
{
|
{
|
||||||
// Root is always reachable if installTargetMenu is null
|
// Root is always reachable if installTargetMenu is null
|
||||||
if (installer.installTargetMenu == null) return true;
|
if (installer.installTargetMenu == null) return true;
|
||||||
// Even in a circular structure, it may be possible to reach root by another path.
|
// Even in a circular structure, it may be possible to reach root by another path.
|
||||||
if (visitedInstaller.Contains(installer)) continue;
|
if (visitedInstaller.Contains(installer)) continue;
|
||||||
visitedInstaller.Add(installer);
|
visitedInstaller.Add(installer);
|
||||||
if (IsMenuReachable(avatar, installer.installTargetMenu, visitedInstaller))
|
if (IsMenuReachable(avatar, installer.installTargetMenu, visitedInstaller))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -269,14 +311,16 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ValidateExpressionMenuIconResult ValidateExpressionMenuIcon(VRCExpressionsMenu menu, HashSet<VRCExpressionsMenu> visitedMenus = null)
|
private static ValidateExpressionMenuIconResult ValidateExpressionMenuIcon(VRCExpressionsMenu menu,
|
||||||
|
HashSet<VRCExpressionsMenu> visitedMenus = null)
|
||||||
{
|
{
|
||||||
if (menu == null) return ValidateExpressionMenuIconResult.Success;
|
if (menu == null) return ValidateExpressionMenuIconResult.Success;
|
||||||
if (visitedMenus == null) visitedMenus = new HashSet<VRCExpressionsMenu>();
|
if (visitedMenus == null) visitedMenus = new HashSet<VRCExpressionsMenu>();
|
||||||
if (visitedMenus.Contains(menu)) return ValidateExpressionMenuIconResult.Success;
|
if (visitedMenus.Contains(menu)) return ValidateExpressionMenuIconResult.Success;
|
||||||
visitedMenus.Add(menu);
|
visitedMenus.Add(menu);
|
||||||
|
|
||||||
foreach (VRCExpressionsMenu.Control control in menu.controls) {
|
foreach (VRCExpressionsMenu.Control control in menu.controls)
|
||||||
|
{
|
||||||
// Control
|
// Control
|
||||||
ValidateExpressionMenuIconResult result = Util.ValidateExpressionMenuIcon(control.icon);
|
ValidateExpressionMenuIconResult result = Util.ValidateExpressionMenuIcon(control.icon);
|
||||||
if (result != ValidateExpressionMenuIconResult.Success) return result;
|
if (result != ValidateExpressionMenuIconResult.Success) return result;
|
||||||
@ -293,12 +337,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
// SubMenu
|
// SubMenu
|
||||||
if (control.type != VRCExpressionsMenu.Control.ControlType.SubMenu) continue;
|
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;
|
if (subMenuResult != ValidateExpressionMenuIconResult.Success) return subMenuResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ValidateExpressionMenuIconResult.Success;
|
return ValidateExpressionMenuIconResult.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
76
Packages/nadena.dev.modular-avatar/Editor/MenuExtractor.cs
Normal file
76
Packages/nadena.dev.modular-avatar/Editor/MenuExtractor.cs
Normal file
@ -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<VRCAvatarDescriptor>();
|
||||||
|
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<VRCExpressionsMenu, MenuSource>());
|
||||||
|
Undo.RecordObject(avatar, "Convert menu");
|
||||||
|
avatar.expressionsMenu = null;
|
||||||
|
|
||||||
|
rootMenu.gameObject.AddComponent<ModularAvatarMenuInstaller>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MenuSource ConvertSubmenu(
|
||||||
|
GameObject parentObj,
|
||||||
|
VRCExpressionsMenu.Control sourceControl,
|
||||||
|
Dictionary<VRCExpressionsMenu, MenuSource> 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<MAMenuItem>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: de7ea831512b4d9c9e92985ab6fd5f17
|
||||||
|
timeCreated: 1676896326
|
@ -20,9 +20,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
private BuildContext _context;
|
private BuildContext _context;
|
||||||
|
|
||||||
private Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> _clonedMenus;
|
|
||||||
|
|
||||||
|
|
||||||
private VRCExpressionsMenu _rootMenu;
|
private VRCExpressionsMenu _rootMenu;
|
||||||
|
|
||||||
private MenuTree _menuTree;
|
private MenuTree _menuTree;
|
||||||
@ -38,8 +35,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
if (menuInstallers.Length == 0) return;
|
if (menuInstallers.Length == 0) return;
|
||||||
|
|
||||||
|
|
||||||
_clonedMenus = new Dictionary<VRCExpressionsMenu, VRCExpressionsMenu>();
|
|
||||||
_visitedInstallerStack = new Stack<ModularAvatarMenuInstaller>();
|
_visitedInstallerStack = new Stack<ModularAvatarMenuInstaller>();
|
||||||
|
|
||||||
VRCAvatarDescriptor avatar = avatarRoot.GetComponent<VRCAvatarDescriptor>();
|
VRCAvatarDescriptor avatar = avatarRoot.GetComponent<VRCAvatarDescriptor>();
|
||||||
@ -49,14 +44,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
var menu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
var menu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
||||||
_context.SaveAsset(menu);
|
_context.SaveAsset(menu);
|
||||||
avatar.expressionsMenu = menu;
|
avatar.expressionsMenu = menu;
|
||||||
_clonedMenus[menu] = menu;
|
context.ClonedMenus[menu] = menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
_rootMenu = avatar.expressionsMenu;
|
_rootMenu = avatar.expressionsMenu;
|
||||||
_menuTree = new MenuTree(avatar);
|
_menuTree = new MenuTree(avatar);
|
||||||
_menuTree.TraverseAvatarMenu();
|
_menuTree.TraverseAvatarMenu();
|
||||||
|
|
||||||
avatar.expressionsMenu = CloneMenu(avatar.expressionsMenu);
|
avatar.expressionsMenu = _context.CloneMenu(avatar.expressionsMenu);
|
||||||
|
|
||||||
foreach (ModularAvatarMenuInstaller installer in menuInstallers)
|
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 (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
|
// 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);
|
SplitMenu(installer, targetMenu);
|
||||||
|
|
||||||
@ -128,42 +123,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
labels = Array.Empty<VRCExpressionsMenu.Control.Label>()
|
labels = Array.Empty<VRCExpressionsMenu.Control.Label>()
|
||||||
});
|
});
|
||||||
|
|
||||||
_clonedMenus[installer.installTargetMenu] = newMenu;
|
_context.ClonedMenus[installer.installTargetMenu] = newMenu;
|
||||||
targetMenu = 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
32
Packages/nadena.dev.modular-avatar/Editor/ReifyMenuPass.cs
Normal file
32
Packages/nadena.dev.modular-avatar/Editor/ReifyMenuPass.cs
Normal file
@ -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<ModularAvatarMenuInstaller>(true))
|
||||||
|
{
|
||||||
|
BuildReport.ReportingObject(installer, () => ReifyMenu(context, installer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReifyMenu(BuildContext context, ModularAvatarMenuInstaller installer)
|
||||||
|
{
|
||||||
|
var source = installer.GetComponent<MenuSource>();
|
||||||
|
if (source == null) return;
|
||||||
|
|
||||||
|
var controls = source.GenerateMenu();
|
||||||
|
var menu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
||||||
|
menu.controls = controls.ToList();
|
||||||
|
|
||||||
|
installer.menuToAppend = context.CloneMenu(menu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e50c6e0b2ad64fc0b1b00cb45117f023
|
||||||
|
timeCreated: 1676895255
|
130
Packages/nadena.dev.modular-avatar/Runtime/MAMenuItem.cs
Normal file
130
Packages/nadena.dev.modular-avatar/Runtime/MAMenuItem.cs
Normal file
@ -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<VRCExpressionsMenu.Control> 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<VRCExpressionsMenu.Control>();
|
||||||
|
foreach (Transform child in menuRoot.transform)
|
||||||
|
{
|
||||||
|
var menuSource = child.GetComponent<MenuSource>();
|
||||||
|
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<VRCExpressionsMenu>();
|
||||||
|
_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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3b29d45007c5493d926d2cd45a489529
|
||||||
|
timeCreated: 1676787152
|
Loading…
x
Reference in New Issue
Block a user