using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
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
{
public static readonly RootMenu Instance = new RootMenu();
private RootMenu()
{
}
}
///
/// A MenuNode represents a single VRCExpressionsMenu, prior to overflow splitting. MenuNodes form a directed graph,
/// which may contain cycles, and may include contributions from multiple MenuInstallers, or from the base avatar
/// menu.
///
internal class MenuNode
{
internal List Controls = new List();
///
/// The primary (serialized) object that contributed to this menu; if we want to add more items to it, we look
/// here. This can currently be either a VRCExpressionsMenu, a MAMenuItem, or a RootMenu.
///
internal readonly object NodeKey;
internal MenuNode(object nodeKey)
{
NodeKey = nodeKey;
}
}
internal class VirtualControl : VRCExpressionsMenu.Control
{
///
/// VirtualControls do not reference real VRCExpressionsMenu objects, but rather virtual MenuNodes.
///
internal MenuNode SubmenuNode;
internal VirtualControl(VRCExpressionsMenu.Control control)
{
this.name = control.name;
this.type = control.type;
this.parameter = new Parameter() {name = control?.parameter?.name};
this.value = control.value;
this.icon = control.icon;
this.style = control.style;
this.subMenu = null;
this.subParameters = control.subParameters?.Select(p => new VRCExpressionsMenu.Control.Parameter()
{
name = p.name
})?.ToArray();
this.labels = control.labels?.ToArray();
}
}
/**
* 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
{
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>();
///
/// Maps from either VRCEXpressionsMenu objects or MenuItems to menu nodes. The ROOT_MENU here is a special
/// object used to mark contributors to the avatar root menu.
///
private Dictionary _menuNodeMap = new Dictionary();
private Dictionary _resolvedMenu = new Dictionary();
// TODO: immutable?
public Dictionary ResolvedMenu => _resolvedMenu;
public MenuNode RootMenuNode => ResolvedMenu[RootMenuKey];
///
/// Initializes the VirtualMenu.
///
/// The root VRCExpressionsMenu to import
internal VirtualMenu(VRCExpressionsMenu rootMenu)
{
if (rootMenu != null)
{
RootMenuKey = rootMenu;
ImportMenu(rootMenu);
}
else
{
RootMenuKey = RootMenu.Instance;
_menuNodeMap[RootMenu.Instance] = new MenuNode(RootMenu.Instance);
}
}
internal static VirtualMenu ForAvatar(VRCAvatarDescriptor avatar)
{
var menu = new VirtualMenu(avatar.expressionsMenu);
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();
}
}
private MenuNode ImportMenu(VRCExpressionsMenu menu, object menuKey = null)
{
if (menuKey == null) menuKey = menu;
if (_menuNodeMap.TryGetValue(menuKey, out var subMenuNode)) return subMenuNode;
var node = new MenuNode(menuKey);
_menuNodeMap[menuKey] = node;
foreach (var control in menu.controls)
{
var virtualControl = new VirtualControl(control);
if (control.subMenu != null)
{
virtualControl.SubmenuNode = ImportMenu(control.subMenu);
}
node.Controls.Add(virtualControl);
}
return node;
}
///
/// 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()
{
ResolveNode(RootMenuKey);
}
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;
}
}
private HashSet _sourceTrace = null;
private MenuNode ResolveNode(object nodeKey)
{
if (_resolvedMenu.TryGetValue(nodeKey, out var node)) return node;
if (nodeKey is ModularAvatarMenuItem item)
{
return ResolveSubmenuItem(item);
}
if (nodeKey is VRCExpressionsMenu menu)
{
ImportMenu(menu);
}
if (_menuNodeMap.TryGetValue(nodeKey, out node))
{
_resolvedMenu[nodeKey] = node;
}
else
{
node = new MenuNode(nodeKey);
_menuNodeMap[nodeKey] = node;
_resolvedMenu[nodeKey] = node;
}
// Find any menu installers which target this node, and recursively include them.
// Note that we're also recursing through MenuNodes, and should not consider the objects visited on
// different submenus when cutting off cycles.
var priorTrace = _sourceTrace;
_sourceTrace = new HashSet();
try
{
// We use a stack here to maintain the expected order of elements. Consider if we have three menu
// installers as follows:
// A -> root
// B -> root
// C -> A
// We'll first push [B, A], then visit A. At this point we'll push C back on the stack, so we visit
// [A, C, B] in the end.
Stack installers = new Stack();
if (_targetMenuToInstaller.TryGetValue(nodeKey, out var rootInstallers))
{
foreach (var i in rootInstallers.Select(x => x).Reverse())
{
if (_installerToTargetComponent.ContainsKey(i)) continue;
installers.Push(i);
}
}
while (installers.Count > 0)
{
var next = installers.Pop();
if (_sourceTrace.Contains(next)) continue;
_sourceTrace.Add(next);
BuildReport.ReportingObject(next, () => ResolveInstaller(node, next, installers));
}
// Resolve any submenus
foreach (var virtualControl in node.Controls)
{
if (virtualControl.SubmenuNode != null)
{
virtualControl.SubmenuNode = ResolveNode(virtualControl.SubmenuNode.NodeKey);
}
}
}
finally
{
_sourceTrace = priorTrace;
}
return node;
}
private MenuNode ResolveSubmenuItem(ModularAvatarMenuItem item)
{
return BuildReport.ReportingObject(item, () =>
{
MenuNode node = new MenuNode(item);
_resolvedMenu[item] = node;
switch (item.MenuSource)
{
case SubmenuSource.MenuAsset:
{
if (item.Control.subMenu != null)
{
node.Controls = ResolveNode(item.Control.subMenu).Controls;
}
break;
}
case SubmenuSource.Children:
{
var transformRoot = item.menuSource_otherObjectChildren != null
? item.menuSource_otherObjectChildren.transform
: item.transform;
foreach (Transform child in transformRoot)
{
if (!child.gameObject.activeSelf) continue;
var source = child.GetComponent();
if (source == null) continue;
if (source is ModularAvatarMenuItem subItem)
{
var control = new VirtualControl(subItem.Control);
if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
{
control.SubmenuNode = ResolveNode(subItem);
}
control.name = subItem.gameObject.name;
node.Controls.Add(control);
}
else if (source is ModularAvatarMenuInstallTarget target && target.installer != null)
{
ResolveInstaller(node, target.installer, new Stack());
}
else
{
// TODO validation
}
}
break;
}
default:
// TODO validation
break;
}
return node;
});
}
private void ResolveInstaller(MenuNode node, ModularAvatarMenuInstaller installer,
Stack installers)
{
if (installer == null || !installer.enabled) return;
var menuSource = installer.GetComponent();
if (menuSource == null)
{
var expMenu = installer.menuToAppend;
if (expMenu == null) return;
var controls = expMenu.controls;
if (controls == null) return;
foreach (var control in controls)
{
var virtualControl = new VirtualControl(control);
if (control.subMenu != null)
{
virtualControl.SubmenuNode = ResolveNode(control.subMenu);
}
node.Controls.Add(virtualControl);
}
if (_targetMenuToInstaller.TryGetValue(expMenu, out var subInstallers))
{
foreach (var subInstaller in subInstallers.Select(x => x).Reverse())
{
if (_installerToTargetComponent.ContainsKey(subInstaller)) continue;
installers.Push(subInstaller);
}
}
}
else if (menuSource is ModularAvatarMenuInstallTarget target)
{
if (target.installer != null)
{
installers.Push(target.installer);
}
}
else if (menuSource is ModularAvatarMenuItem item)
{
var virtualControl = new VirtualControl(item.Control);
virtualControl.name = item.gameObject.name;
node.Controls.Add(virtualControl);
if (virtualControl.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
{
virtualControl.SubmenuNode = ResolveNode(item);
}
}
else
{
BuildReport.Log(ReportLevel.Error, "virtual_menu.unknown_source_type",
strings: new object[] {menuSource.GetType().ToString()});
}
}
}
}