#if MA_VRCSDK3_AVATARS using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using nadena.dev.modular_avatar.core.menu; 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.menu { /// /// Sentinel object to represent the avatar root menu (for avatars which don't have a root menu) /// internal sealed class RootMenu : MenuSource { public static readonly RootMenu Instance = new RootMenu(); private RootMenu() { } public void Visit(NodeContext context) { // we initialize the root node manually throw new NotImplementedException(); } } class NodeContextImpl : NodeContext { [CanBeNull] internal delegate VirtualMenuNode NodeForDelegate(object menu, Action postprocessor); private readonly ImmutableDictionary> _menuToInstallerMap; private readonly ImmutableDictionary> _postProcessControls = ImmutableDictionary>.Empty; private readonly VirtualMenuNode _node; private readonly NodeForDelegate _nodeFor; private readonly Action _visitedMenu; private readonly HashSet _visited = new HashSet(); private Action _currentPostprocessor; internal ImmutableHashSet Visited => _visited.ToImmutableHashSet(); private class PostprocessorContext : IDisposable { private NodeContextImpl _context; private Action _priorPostprocessor; public PostprocessorContext(NodeContextImpl context, Action postprocessor) { this._context = context; this._priorPostprocessor = context._currentPostprocessor; context._currentPostprocessor = postprocessor ?? context._currentPostprocessor; } public void Dispose() { _context._currentPostprocessor = _priorPostprocessor; } } public NodeContextImpl( VirtualMenuNode node, NodeForDelegate nodeFor, ImmutableDictionary> menuToInstallerMap, ImmutableDictionary> postProcessControls, Action visitedMenu, Action postprocessor ) { _node = node; _nodeFor = nodeFor; _menuToInstallerMap = menuToInstallerMap; _postProcessControls = postProcessControls; _visitedMenu = visitedMenu; _currentPostprocessor = postprocessor; } public void PushMenuContents(VRCExpressionsMenu expMenu) { if (expMenu == null) return; if (_visited.Contains(expMenu)) return; _visited.Add(expMenu); _visitedMenu(expMenu); try { foreach (var control in expMenu.controls) { PushControl(control); } if (_menuToInstallerMap.TryGetValue(expMenu, out var installers)) { foreach (var installer in installers) { using (new PostprocessorContext(this, null)) { PushNode(installer); } } } } finally { // We can visit the same expMenu multiple times, with different visit contexts (owing to having // different source installers, with different postprocessing configurations). _visited.Remove(expMenu); } } public void PushNode(MenuSource source) { if (source == null) return; if (_visited.Contains(source)) return; _visited.Add(source); BuildReport.ReportingObject(source as UnityEngine.Object, () => source.Visit(this)); _visited.Remove(source); } public void PushNode(ModularAvatarMenuInstaller installer) { if (installer == null) return; if (_visited.Contains(installer)) return; _visited.Add(installer); BuildReport.ReportingObject(installer, () => { using (new PostprocessorContext(this, _postProcessControls.GetValueOrDefault(installer))) { var menuSourceComp = installer.GetComponent(); if (menuSourceComp != null) { PushNode(menuSourceComp); } else if (installer.menuToAppend != null) { PushMenuContents(installer.menuToAppend); } } }); _visited.Remove(installer); } public void PushControl(VRCExpressionsMenu.Control control) { // XXX: When we invoke NodeFor on the subMenu, we need to ensure we dedup considering the parameter context // of the source control. This is because the same subMenu can be used in multiple places, with different // parameter replacements. (FIXME) var virtualControl = new VirtualControl(control); if (control.subMenu != null) { virtualControl.SubmenuNode = NodeFor(control.subMenu); } _currentPostprocessor(virtualControl); PushControl(virtualControl); } public void PushControl(VirtualControl control) { _node.Controls.Add(control); } public VirtualMenuNode NodeFor(VRCExpressionsMenu menu) { if (menu == null) return null; return _nodeFor(menu, _currentPostprocessor); } public VirtualMenuNode NodeFor(MenuSource source) { if (source == null) return null; return _nodeFor(source, _currentPostprocessor); } } /** * The VirtualMenu class tracks a fully realized shadow menu. Notably, this is _not_ converted to unity * ScriptableObjects, making it easier to discard it when we need to update it. */ internal class VirtualMenu { private static readonly Action NoopPostprocessor = control => { }; internal readonly object RootMenuKey; private static long _cacheSeq = 0; internal static void InvalidateCaches() { _cacheSeq++; } static VirtualMenu() { RuntimeUtil.OnMenuInvalidate += InvalidateCaches; } internal static long CacheSequence => _cacheSeq; private readonly long _initialCacheSeq = _cacheSeq; internal bool IsOutdated => _initialCacheSeq != _cacheSeq; /// /// Indexes which menu installers are contributing to which VRCExpressionMenu assets. /// private Dictionary> _targetMenuToInstaller = new Dictionary>(); private Dictionary> _installerToTargetComponent = new Dictionary>(); private ImmutableDictionary> _postprocessControlsHooks = ImmutableDictionary>.Empty; private Dictionary _resolvedMenu = new Dictionary(); // TODO: immutable? public Dictionary ResolvedMenu => _resolvedMenu; public VirtualMenuNode RootMenuNode => ResolvedMenu[RootMenuKey]; private Queue _pendingGeneration = new Queue(); private HashSet _visitedMenus = new HashSet(); private ImmutableHashSet _visitedNodes = ImmutableHashSet.Empty; /// /// Initializes the VirtualMenu. /// /// The root VRCExpressionsMenu to import internal VirtualMenu( VRCExpressionsMenu rootMenu, BuildContext context = null ) { if (context != null) { _postprocessControlsHooks = context.PostProcessControls.ToImmutableDictionary(); } if (rootMenu != null) { RootMenuKey = (ValueTuple) (rootMenu, NoopPostprocessor); } else { RootMenuKey = RootMenu.Instance; } } internal static VirtualMenu ForAvatar( VRCAvatarDescriptor avatar, BuildContext context = null ) { var menu = new VirtualMenu(avatar.expressionsMenu, context); foreach (var installer in avatar.GetComponentsInChildren(true)) { menu.RegisterMenuInstaller(installer); } foreach (var target in avatar.GetComponentsInChildren(true)) { menu.RegisterMenuInstallTarget(target); } menu.FreezeMenu(); return menu; } internal IEnumerable GetInstallTargetsForInstaller( ModularAvatarMenuInstaller installer ) { if (_installerToTargetComponent.TryGetValue(installer, out var targets)) { return targets; } else { return Array.Empty(); } } /// /// Registers a menu installer with this virtual menu. Because we need the full set of components indexed to /// determine the effects of this menu installer, further processing is deferred until we freeze the menu. /// /// internal void RegisterMenuInstaller(ModularAvatarMenuInstaller installer) { // initial validation if (installer.menuToAppend == null && installer.GetComponent() == null) return; var target = installer.installTargetMenu ? (object) installer.installTargetMenu : RootMenuKey; if (!_targetMenuToInstaller.TryGetValue(target, out var targets)) { targets = new List(); _targetMenuToInstaller[target] = targets; } targets.Add(installer); } /// /// Registers an install target with this virtual menu. As with menu installers, processing is delayed. /// /// internal void RegisterMenuInstallTarget(ModularAvatarMenuInstallTarget target) { if (target.installer == null) return; if (!_installerToTargetComponent.TryGetValue(target.installer, out var targets)) { targets = new List(); _installerToTargetComponent[target.installer] = targets; } targets.Add(target); } /// /// Freezes the menu, fully resolving all members of all menus. /// internal void FreezeMenu() { ImmutableDictionary> menuToInstallerFiltered = _targetMenuToInstaller .Select(kvp => new KeyValuePair>( kvp.Key, kvp.Value.Where(i => !_installerToTargetComponent.ContainsKey(i)).ToImmutableList() )) .Where(kvp => !kvp.Value.IsEmpty) .ToImmutableDictionary(); var RootNode = new VirtualMenuNode(RootMenuKey); _resolvedMenu[RootMenuKey] = RootNode; var rootContext = new NodeContextImpl(RootNode, NodeFor, menuToInstallerFiltered, _postprocessControlsHooks, m => _visitedMenus.Add(m), NoopPostprocessor); if (RootMenuKey is ValueTuple tuple && tuple.Item1 is VRCExpressionsMenu menu) { foreach (var control in menu.controls) { rootContext.PushControl(control); } // Some menu installers may be bound to the root menu _asset_ directly. if (menuToInstallerFiltered.TryGetValue(menu, out var installers)) { foreach (var installer in installers) { rootContext.PushNode(installer); } } } // Untargeted installers are bound to the RootMenuKey, rather than the menu asset itself. if (menuToInstallerFiltered.TryGetValue(RootMenuKey, out var installers2)) { foreach (var installer in installers2) { rootContext.PushNode(installer); } } while (_pendingGeneration.Count > 0) { _pendingGeneration.Dequeue()(); } _visitedNodes = rootContext.Visited; VirtualMenuNode NodeFor(object key, Action postprocessContext) { var lookupKey = key; if (key is VRCExpressionsMenu) { lookupKey = (ValueTuple) (key, postprocessContext); } if (_resolvedMenu.TryGetValue(lookupKey, out var node)) return node; node = new VirtualMenuNode(lookupKey); _resolvedMenu[lookupKey] = node; _pendingGeneration.Enqueue(() => { BuildReport.ReportingObject(key as UnityEngine.Object, () => { var context = new NodeContextImpl(node, NodeFor, menuToInstallerFiltered, _postprocessControlsHooks, m => _visitedMenus.Add(m), postprocessContext); if (key is VRCExpressionsMenu expMenu) { context.PushMenuContents(expMenu); } else if (key is MenuSource source) { context.PushNode(source); } else { // TODO warning } }); }); return node; } } internal VRCExpressionsMenu SerializeMenu(Action SaveAsset) { Dictionary serializedMenus = new Dictionary(); return Serialize(RootMenuKey); VRCExpressionsMenu Serialize(object menuKey) { if (menuKey == null) return null; if (serializedMenus.TryGetValue(menuKey, out var menu)) return menu; if (!_resolvedMenu.TryGetValue(menuKey, out var node)) return null; menu = ScriptableObject.CreateInstance(); serializedMenus[menuKey] = menu; menu.controls = node.Controls.Select(c => { var control = new VRCExpressionsMenu.Control(); control.name = c.name; control.type = c.type; control.parameter = new VRCExpressionsMenu.Control.Parameter() {name = c.parameter.name}; control.value = c.value; control.icon = c.icon; control.style = c.style; control.labels = c.labels.ToArray(); control.subParameters = c.subParameters.Select(p => new VRCExpressionsMenu.Control.Parameter() { name = p.name }).ToArray(); control.subMenu = Serialize(c.SubmenuNode?.NodeKey); return control; }).ToList(); SaveAsset(menu); return menu; } } public bool ContainsMenu(VRCExpressionsMenu menu) { return _visitedMenus.Contains(menu); } public bool ContainsNode(ModularAvatarMenuItem item) { return _visitedNodes.Contains(item); } } } #endif