Skip to content

Commit

Permalink
feat: add MA/NDMF support to improve compatibility with menu items
Browse files Browse the repository at this point in the history
  • Loading branch information
poi-vrc committed Mar 11, 2024
1 parent 96df571 commit 372a3a3
Show file tree
Hide file tree
Showing 13 changed files with 591 additions and 139 deletions.
189 changes: 189 additions & 0 deletions Editor/Detail/DK/DKMAMenuStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Copyright (c) 2024 chocopoi
*
* This file is part of DressingTools.
*
* DressingTools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* DressingTools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with DressingTools. If not, see <https://www.gnu.org/licenses/>.
*/

#if DK_MA && DK_VRCSDK3A
using System.Collections.Generic;
using Chocopoi.DressingFramework.Menu;
using Chocopoi.DressingFramework.Menu.VRChat;
using nadena.dev.modular_avatar.core;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;

namespace Chocopoi.DressingFramework.Detail.DK
{
/// <summary>
/// This might sound a bit confusing, but DKMAMenuStore is more associated to DK instead of MA/NDMF.
/// This store only appears during DK builds instead of NDMF builds. It will convert DK menus into
/// proper form and pass them to MA, in order to have good compatibility with MA menus, especially with
/// the Next Page button feature.
/// </summary>
internal class DKMAMenuStore : MenuStore
{
private readonly Context _ctx;
private readonly Dictionary<string, List<MenuItem>> _buffer;
private readonly HashSet<VRCExpressionsMenu> _clonedVrcMenus;

public DKMAMenuStore(Context ctx)
{
_ctx = ctx;
_buffer = new Dictionary<string, List<MenuItem>>();
_clonedVrcMenus = new HashSet<VRCExpressionsMenu>();
}

public override void Append(MenuItem menuItem, string path = null)
{
if (path == null)
{
path = "";
}
path = path.Trim();

if (!_buffer.TryGetValue(path, out var menuItems))
{
menuItems = _buffer[path] = new List<MenuItem>();
}
menuItems.Add(menuItem);
}

private static VRCExpressionsMenu.Control MakeSubMenuControl(string name, VRCExpressionsMenu subMenu)
{
return new VRCExpressionsMenu.Control()
{
name = name,
icon = null,
type = VRCExpressionsMenu.Control.ControlType.SubMenu,
parameter = new VRCExpressionsMenu.Control.Parameter() { name = "" },
style = VRCExpressionsMenu.Control.Style.Style1,
subMenu = subMenu,
subParameters = new VRCExpressionsMenu.Control.Parameter[0],
labels = new VRCExpressionsMenu.Control.Label[0]
};
}

private VRCExpressionsMenu MakeDownwardsMenuGroups(string[] paths, int index)
{
var menu = Object.Instantiate(VRCMenuUtils.GetDefaultExpressionsMenu());
_ctx.CreateUniqueAsset(menu, string.Join("_", paths, 0, index));
if (index < paths.Length)
{
var newMenuItem = MakeSubMenuControl(paths[index], MakeDownwardsMenuGroups(paths, index + 1));
menu.controls.Add(newMenuItem);
}
return menu;
}

private VRCExpressionsMenu FindInstallTarget(VRCExpressionsMenu parent, string[] paths, int index)
{
if (index >= paths.Length)
{
return parent;
}

foreach (var item in parent.controls)
{
if (item.name != paths[index])
{
continue;
}

if (item.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
{
if (item.subMenu == null)
{
var newVrcMenu = Object.Instantiate(VRCMenuUtils.GetDefaultExpressionsMenu());
_ctx.CreateUniqueAsset(newVrcMenu, string.Join("_", paths, 0, index + 1));
item.subMenu = newVrcMenu;

_clonedVrcMenus.Add(item.subMenu);
}
else if (!_clonedVrcMenus.Contains(item.subMenu))
{
var menuCopy = Object.Instantiate(item.subMenu);
_ctx.CreateUniqueAsset(menuCopy, string.Join("_", paths, 0, index + 1));
item.subMenu = menuCopy;

_clonedVrcMenus.Add(item.subMenu);
}

return FindInstallTarget(item.subMenu, paths, index + 1);
}
}

// if not found, we create empty menu groups recursively downwards
var newMenuItem = MakeSubMenuControl(paths[index], MakeDownwardsMenuGroups(paths, index + 1));
parent.controls.Add(newMenuItem);

// find again
return FindInstallTarget(parent, paths, index);
}

public override void Flush()
{
if (!_ctx.AvatarGameObject.TryGetComponent<VRCAvatarDescriptor>(out var avatarDesc))
{
// nothing to do if it's not a VRC avatar
return;
}

var dkMaRootObj = new GameObject("DKMA");
dkMaRootObj.transform.SetParent(_ctx.AvatarGameObject.transform);

foreach (var kvp in _buffer)
{
var path = kvp.Key;
var items = kvp.Value;

// find and create the install target and pass to MA
string menuObjName;
VRCExpressionsMenu installTarget;
if (string.IsNullOrEmpty(path))
{
menuObjName = "Root";
installTarget = avatarDesc.expressionsMenu;
}
else
{
var paths = path.Trim().Split('/');
menuObjName = string.Join("_", paths);
installTarget = FindInstallTarget(avatarDesc.expressionsMenu, paths, 0);
}

var menuObj = new GameObject(menuObjName);
menuObj.transform.SetParent(dkMaRootObj.transform);

// add installer
var maInstaller = menuObj.AddComponent<ModularAvatarMenuInstaller>();
maInstaller.installTargetMenu = installTarget;

// add menu group
var maGroup = menuObj.AddComponent<ModularAvatarMenuGroup>();
maGroup.targetObject = menuObj;

// add menu items
foreach (var item in items)
{
var maItemObj = new GameObject(item.Name);
maItemObj.transform.SetParent(maGroup.transform);

var maItem = maItemObj.AddComponent<ModularAvatarMenuItem>();
// TODO
}
}
}

internal override void OnEnable() { }

internal override void OnDisable() { }
}
}
#endif
11 changes: 11 additions & 0 deletions Editor/Detail/DK/DKMAMenuStore.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

122 changes: 120 additions & 2 deletions Editor/Detail/DK/DKMenuStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,131 @@
* You should have received a copy of the GNU General Public License along with DressingFramework. If not, see <https://www.gnu.org/licenses/>.
*/

using System.Collections.Generic;
using Chocopoi.DressingFramework.Menu;
using UnityEngine;
#if DK_VRCSDK3A
using Chocopoi.DressingFramework.Menu.VRChat;
using VRC.SDK3.Avatars.ScriptableObjects;
#endif

namespace Chocopoi.DressingFramework.Detail.DK
{
internal class DKMenuStore : MenuStore
{
private Context _ctx;
private readonly Dictionary<MenuItem, string> _buffer;
#if DK_VRCSDK3A
private readonly HashSet<VRCExpressionsMenu> _clonedVrcMenus;
#endif

public DKMenuStore(Context ctx) : base(ctx)
public DKMenuStore(Context ctx)
{
_ctx = ctx;
_buffer = new Dictionary<MenuItem, string>();
#if DK_VRCSDK3A
_clonedVrcMenus = new HashSet<VRCExpressionsMenu>();
#endif
}

public override void Append(MenuItem menuItem, string path = null)
{
if (path == null)
{
path = "";
}
path = path.Trim();

_buffer[menuItem] = path;
}

private IMenuRepository FindInstallTarget(IMenuRepository parent, string[] paths, int index)
{
if (index >= paths.Length)
{
return parent;
}

foreach (var item in parent)
{
if (item.Name != paths[index])
{
continue;
}

if (item is SubMenuItem subMenuItem)
{
if (subMenuItem.SubMenu == null)
{
subMenuItem.SubMenu = new MenuGroup();
}
return FindInstallTarget(subMenuItem.SubMenu, paths, index + 1);
}
#if DK_VRCSDK3A
else if (item is VRCSubMenuItem vrcSubMenuItem)
{
if (vrcSubMenuItem.SubMenu == null)
{
var newVrcMenu = Object.Instantiate(VRCMenuUtils.GetDefaultExpressionsMenu());
_ctx.CreateUniqueAsset(newVrcMenu, string.Join("_", paths, 0, index + 1));
vrcSubMenuItem.SubMenu = newVrcMenu;

_clonedVrcMenus.Add(vrcSubMenuItem.SubMenu);
}
else if (!_clonedVrcMenus.Contains(vrcSubMenuItem.SubMenu))
{
var menuCopy = Object.Instantiate(vrcSubMenuItem.SubMenu);
_ctx.CreateUniqueAsset(menuCopy, string.Join("_", paths, 0, index + 1));
vrcSubMenuItem.SubMenu = menuCopy;
_clonedVrcMenus.Add(vrcSubMenuItem.SubMenu);
}

return FindInstallTarget(new VRCMenuWrapper(vrcSubMenuItem.SubMenu, _ctx), paths, index + 1);
}
#endif
}

// if not found, we create empty menu groups recursively downwards
var newMenuItem = new SubMenuItem()
{
Name = paths[index],
Icon = null,
SubMenu = MakeDownwardsMenuGroups(paths, index + 1)
};
parent.Add(newMenuItem);

// find again, the menu group pointers above cannot be used after the CRUD operation
return FindInstallTarget(parent, paths, index);
}

private MenuGroup MakeDownwardsMenuGroups(string[] paths, int index)
{
var mg = new MenuGroup();
if (index < paths.Length)
{
var newMenuItem = new SubMenuItem()
{
Name = paths[index],
Icon = null,
SubMenu = MakeDownwardsMenuGroups(paths, index + 1)
};
mg.Add(newMenuItem);
}
return mg;
}

private void InstallMenuItem(IMenuRepository rootMenu, MenuItem item, string path)
{
var installTarget = rootMenu;
if (!string.IsNullOrEmpty(path))
{
var paths = path.Trim().Split('/');
installTarget = FindInstallTarget(rootMenu, paths, 0);
}
installTarget.Add(item);
}

public override IMenuRepository GetRootMenu()
private IMenuRepository GetRootMenu()
{
IMenuRepository rootMenu;
#if DK_VRCSDK3A
Expand All @@ -51,6 +159,16 @@ public override IMenuRepository GetRootMenu()
return rootMenu;
}

public override void Flush()
{
var rootMenu = GetRootMenu();
foreach (var kvp in _buffer)
{
InstallMenuItem(rootMenu, kvp.Key, kvp.Value);
}
_buffer.Clear();
}

internal override void OnDisable() { }

internal override void OnEnable() { }
Expand Down
8 changes: 8 additions & 0 deletions Editor/Detail/NDMF.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 372a3a3

Please sign in to comment.