feat: add the MAMenuItem component

This commit is contained in:
bd_ 2023-02-21 19:47:10 +09:00
parent d385eb8800
commit d212dabc27
13 changed files with 519 additions and 80 deletions

View File

@ -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();
} }
} }
} }

View File

@ -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;
}
} }
} }

View File

@ -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>()
{ {

View File

@ -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;
}
}
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5b674c72186c4e6884b0cd05098f11b6
timeCreated: 1676791017

View File

@ -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();
} }
}
EditorGUI.indentLevel++; if (menuSource != null)
EditorGUILayout.PropertyField( {
menuToAppendProperty, new GUIContent(G("menuinstall.srcmenu"))); // TODO localize
EditorGUI.indentLevel--; 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--;
}
} }
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
@ -212,7 +250,7 @@ namespace nadena.dev.modular_avatar.core.editor
} }
_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);
@ -226,12 +264,14 @@ namespace nadena.dev.modular_avatar.core.editor
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 =>
control.type == VRCExpressionsMenu.Control.ControlType.SubMenu && control.subMenu != null);
foreach (VRCExpressionsMenu.Control control in controls) 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,12 +285,14 @@ 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)
@ -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;
} }
} }
} }

View 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;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: de7ea831512b4d9c9e92985ab6fd5f17
timeCreated: 1676896326

View File

@ -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;
}
} }
} }

View 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);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e50c6e0b2ad64fc0b1b00cb45117f023
timeCreated: 1676895255

View 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
};
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3b29d45007c5493d926d2cd45a489529
timeCreated: 1676787152