using System.Collections.Generic; using System.Linq; using AnimatorAsCode.V1; using gay.lilyy.aaccore; using gay.lilyy.aacshared.runtimecomponents; using UnityEditor; using UnityEngine; using VRC.SDK3.Avatars.ScriptableObjects; using AnimatorAsCode.V1.ModularAvatar; using nadena.dev.modular_avatar.core; namespace gay.lilyy.aacshared.layers { [InitializeOnLoad] public class ChildToggle : LayerGroup { // remove when ready for uploaded public override bool experimental => false; public override bool shouldWarnIfNotApplicable => false; private static readonly ChildToggle _instance = new(); static ChildToggle() {} public override string DisplayName => "Child Toggle"; public override string SystemName => "childtoggle"; private static ChildToggleDefinition[] getDefinitions(AACAssets assets) { return assets.ctx.AvatarRootObject.GetComponentsInChildren(); } public override bool IsApplicable(AACAssets assets) { return getDefinitions(assets).Length > 0; } private AacFlBoolParameter createLayer(AACAssets assets, ChildToggleDefinition definition, Transform transform) { var layer = assets.fx.NewLayer($"Child toggle {definition.name} {transform.name}"); var toggle = layer.BoolParameter($"child_{definition.name}_{transform.name}"); var idle = layer.NewState("Idle").WithAnimation(assets.emptyClip); var flipped = layer.NewState("Flipped").WithAnimation(assets.aac.NewClip().Toggling(transform.gameObject, !transform.gameObject.activeSelf)); idle.TransitionsTo(flipped).When(toggle.IsEqualTo(true)); flipped.TransitionsTo(idle).When(toggle.IsEqualTo(false)); return toggle; } /** im gonna be so honest i got cursor to hack together my MusicPlayer's menu creator into this monstrosity. im so sorry. The AACAssets instance. The ChildToggleDefinition instance. A dictionary of child transforms and their corresponding toggle parameters. A shared dictionary of folder paths to GameObjects across all definitions. */ private GameObject FindOrCreateFolder(GameObject parent, string folderName, Dictionary sharedFolderHierarchy, string fullPath, Transform definitionTransform, HashSet allRootMenus) { // Check if folder already exists in shared hierarchy if (sharedFolderHierarchy.ContainsKey(fullPath)) { var existingFolder = sharedFolderHierarchy[fullPath]; // Verify it still exists and is accessible if (existingFolder != null) { return existingFolder; } else { // Remove stale entry sharedFolderHierarchy.Remove(fullPath); } } // Check if a folder with the same name already exists as a sibling in current parent foreach (Transform child in parent.transform) { var existingMenuItem = child.GetComponent(); if (existingMenuItem != null && existingMenuItem.label == folderName) { // Found existing folder, add it to shared hierarchy and return it sharedFolderHierarchy[fullPath] = child.gameObject; return child.gameObject; } } // Check all other root menus for a folder with the same name at the same level // This prevents duplicate folders when multiple definitions share the same MenuPath prefix foreach (var rootMenu in allRootMenus) { if (rootMenu == parent) continue; // Skip current menu // If we're at root level (parent is a Menu_*), check root level of other menus // If we're in a nested folder, check the same nesting level if (parent.name.StartsWith("Menu_") && rootMenu.name.StartsWith("Menu_")) { // Check root level folders foreach (Transform child in rootMenu.transform) { var existingMenuItem = child.GetComponent(); if (existingMenuItem != null && existingMenuItem.label == folderName) { // Found existing folder in another root menu, reuse it sharedFolderHierarchy[fullPath] = child.gameObject; return child.gameObject; } } } } // Create new folder var folderGo = new GameObject($"Folder_{folderName}"); folderGo.transform.SetParent(parent.transform); var folderMenuItem = folderGo.AddComponent(); folderMenuItem.label = folderName; folderMenuItem.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu; folderMenuItem.MenuSource = SubmenuSource.Children; sharedFolderHierarchy[fullPath] = folderGo; return folderGo; } private void CreateMenu(AACAssets assets, ChildToggleDefinition definition, Dictionary childParameters, Dictionary sharedFolderHierarchy, HashSet allRootMenus) { if (childParameters.Count == 0) return; var menuGo = new GameObject($"Menu_{definition.name}"); menuGo.transform.SetParent(definition.transform); var menuItem = menuGo.AddComponent(); menuItem.label = "Child Toggle"; menuItem.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu; menuItem.MenuSource = SubmenuSource.Children; allRootMenus.Add(menuGo); GameObject firstFolder = null; foreach (var kvp in childParameters) { var childTransform = kvp.Key; var toggleParam = kvp.Value; GameObject currentParent = menuGo; // Create folder hierarchy if MenuPath is specified if (!string.IsNullOrEmpty(definition.MenuPath)) { string[] folderPath = definition.MenuPath.Split('/'); // Create nested folder structure for (int i = 0; i < folderPath.Length; i++) { string folderName = folderPath[i]; string fullPath = string.Join("/", folderPath, 0, i + 1); currentParent = FindOrCreateFolder(currentParent, folderName, sharedFolderHierarchy, fullPath, definition.transform, allRootMenus); // Store the first folder for MenuInstaller if (firstFolder == null && i == 0) { firstFolder = currentParent; } } } // Create the toggle menu item var go = new GameObject($"Toggle_{childTransform.name}"); go.transform.SetParent(currentParent.transform); assets.modularAvatar.EditMenuItem(go).Toggle(toggleParam).Name(childTransform.name); } // Place MenuInstaller on the first folder if it exists, otherwise on root menu if (firstFolder != null) { // Only add MenuInstaller if it doesn't already exist if (firstFolder.GetComponent() == null) { firstFolder.AddComponent(); } } else { menuGo.AddComponent(); } } private void runDefinition(AACAssets assets, ChildToggleDefinition definition, Dictionary sharedFolderHierarchy, HashSet allRootMenus) { var childParameters = new Dictionary(); foreach (Transform child in definition.transform) { var toggleParam = createLayer(assets, definition, child); childParameters[child] = toggleParam; } CreateMenu(assets, definition, childParameters, sharedFolderHierarchy, allRootMenus); } public override void Run(AACAssets assets) { var definitions = getDefinitions(assets); Logger.LogDebug($"Child Toggle system: Found {definitions.Length} child toggle definitions"); // Shared folder hierarchy across all definitions to prevent duplicates var sharedFolderHierarchy = new Dictionary(); // Track all root menus to check for duplicate folders var allRootMenus = new HashSet(); foreach (var definition in definitions) { runDefinition(assets, definition, sharedFolderHierarchy, allRootMenus); } } } }