#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