SharedVRCStuff/MenuStyling/Editor/MenuStylingPlugin.cs

117 lines
4.3 KiB
C#

using System.Collections.Generic;
using System.Text.RegularExpressions;
using gay.lilyy.MenuStyling;
using nadena.dev.ndmf;
using nadena.dev.ndmf.vrchat;
using UnityEngine;
using VRC.SDK3.Avatars.ScriptableObjects;
using static VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionsMenu;
[assembly: ExportsPlugin(typeof(MenuStylingPlugin))]
namespace gay.lilyy.MenuStyling
{
public class MenuStylingPlugin : Plugin<MenuStylingPlugin>
{
public override string DisplayName => "MenuStyling";
public override string QualifiedName => "gay.lilyy.MenuStyling";
protected override void Configure()
{
InPhase(BuildPhase.Optimizing).AfterPlugin("gay.lilyy.MenuIconRemover").Run("SetMenuStyling", ctx =>
{
var obj = ctx.AvatarRootObject.GetComponent<MenuStylingConfig>();
if (obj != null)
{
var descriptor = ctx.VRChatAvatarDescriptor();
var processedMenus = new HashSet<VRCExpressionsMenu>();
descriptor.expressionsMenu = DuplicateMenu(descriptor.expressionsMenu, processedMenus);
var walkProcessedMenus = new HashSet<VRCExpressionsMenu>();
WalkMenu(obj, descriptor.expressionsMenu, walkProcessedMenus);
Object.DestroyImmediate(obj);
}
});
}
private VRCExpressionsMenu DuplicateMenu(VRCExpressionsMenu menu, HashSet<VRCExpressionsMenu> processedMenus)
{
if (menu == null) return null;
// Prevent circular references - if we've already processed this menu, return null to break the cycle
if (processedMenus.Contains(menu))
{
return null;
}
processedMenus.Add(menu);
var newMenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
newMenu.controls = new List<Control>();
foreach (var control in menu.controls)
{
var newControl = new Control
{
name = control.name,
type = control.type,
icon = control.icon,
parameter = control.parameter,
subMenu = DuplicateMenu(control.subMenu, processedMenus),
value = control.value,
style = control.style,
labels = control.labels,
subParameters = control.subParameters
};
newMenu.controls.Add(newControl);
}
return newMenu;
}
private string StripTextMeshProTags(string text)
{
if (string.IsNullOrEmpty(text)) return text;
// Remove all TextMeshPro/rich text tags
// Pattern matches: <tag>, </tag>, <tag=value>, <tag="value">, <tag='value'>
text = Regex.Replace(text, @"<[^>]+>", "");
return text;
}
private void WalkMenu(MenuStylingConfig config, VRCExpressionsMenu menu, HashSet<VRCExpressionsMenu> processedMenus)
{
if (menu == null) return;
// Prevent circular references
if (processedMenus.Contains(menu))
{
return;
}
processedMenus.Add(menu);
foreach (var child in menu.controls)
{
if (child.name != " " && child.name != "" && child.name != null) {
// Strip all TextMeshPro styling tags first if enabled
if (config.RemovePreexistingStyling)
{
child.name = StripTextMeshProTags(child.name);
}
if (!child.name.StartsWith(config.Prefix))
{
child.name = config.Prefix + child.name;
}
if (!child.name.EndsWith(config.Suffix))
{
child.name += config.Suffix;
}
}
if (child.type == Control.ControlType.SubMenu && child.subMenu != null)
{
WalkMenu(config, child.subMenu, processedMenus);
}
}
}
}
}