yum-slop/YOTS

an optimized toggle system for vrchat

git clone https://git.yummers.dev/yum-slop/YOTS

yumImplement object sets feature161ccba

master
67.6 KiB1605 linesraw
1#if UNITY_EDITOR
2
3using System;
4using System.Collections.Generic;
5using System.IO;
6using System.Linq;
7using System.Text.RegularExpressions;
8using UnityEngine;
9using UnityEditor;
10using UnityEditor.Animations;
11using VRC.SDK3.Avatars.Components;
12using VRC.SDK3.Avatars.ScriptableObjects;
13
14namespace YOTS
15{
16  [System.Serializable]
17  public class ToggleSpec {
18    // The name of the toggle. This is shown in the menu.
19    [SerializeField]
20    public string name;
21
22    // The type of toggle.
23    // Accepted values:
24    //  "toggle" - A boolean toggle. Creates a boolean sync param.
25    //  "radial" - A radial puppet. Creates a float sync param.
26    [SerializeField]
27    public string type = "toggle";
28
29    // The name of the parameter to use.
30    // If not specified, the name will be generated from the menuPath and name.
31    [SerializeField]
32    public string parameterName;
33
34    // Dependencies are toggles that will be evaluated before this one. If
35    // you have two toggles which animate the same thing, one must depend
36    // on the other.
37    [SerializeField]
38    public List<string> dependencies = new List<string>();
39
40    // The name of meshes to toggle.
41    // For example, "Body" or "Shirt".
42    [SerializeField]
43    public List<string> meshToggles = new List<string>();
44
45    // The name of meshes to *disable* when the toggle is turned on.
46    // For example, if you want to hide certain meshes when this toggle is active.
47    [SerializeField]
48    public List<string> inverseMeshToggles = new List<string>();
49
50    // Blendshapes to animate.
51    [SerializeField]
52    public List<BlendShapeSpec> blendShapes = new List<BlendShapeSpec>();
53
54    // Material properties to animate.
55    [SerializeField]
56    public List<ShaderToggleSpec> shaderToggles = new List<ShaderToggleSpec>();
57
58    // External animations to use.
59    [SerializeField]
60    public List<ExternalAnimationSpec> externalAnimations = new List<ExternalAnimationSpec>();
61
62    // Where to put the toggle in the menu. Defaults to the top-level menu.
63    // For example, if you put "Clothes" here, it'll be placed under /Clothes.
64    [SerializeField]
65    public string menuPath = "/";
66
67    // The default value of the toggle. Range from 0-1.
68    // For example, if you want a gimmick to start toggled off, set this to
69    // 0.0f.
70    [SerializeField]
71    public float defaultValue = 1.0f;
72
73    // Fully drive the "off" animation when the parameter is below this threshold.
74    [SerializeField]
75    public float offThreshold = 0.0f;
76
77    // Fully drive the "on" animation when the parameter is above this threshold.
78    [SerializeField]
79    public float onThreshold = 1.0f;
80
81    // Default value for blendshapes when the toggle is off. Range from 0-100.
82    // Individual blendshapes can override this with their own offValue.
83    [SerializeField]
84    public float offValue = 0.0f;
85
86    // Default value for blendshapes when the toggle is on. Range from 0-100.
87    // Individual blendshapes can override this with their own onValue.
88    [SerializeField]
89    public float onValue = 100.0f;
90
91    // Whether the corresponding VRChat parameter is synced.
92    [SerializeField]
93    public bool synced = true;
94
95    // Whether the corresponding VRChat parameter is saved.
96    [SerializeField]
97    public bool saved = true;
98
99    // If true, no menu entry will be created for this toggle.
100    // The parameter will still be created and can be controlled by other means
101    // - for example, via contacts.
102    [SerializeField]
103    public bool disableMenuEntry = false;
104
105    // If true, it's as if this ToggleSpec doesn't exist.
106    [SerializeField]
107    public bool disabled = false;
108
109    // Parent constraint weights to animate
110    [SerializeField]
111    public List<ParentConstraintWeight> parentConstraintWeights = new List<ParentConstraintWeight>();
112
113    // Get the effective parameter name, generating one if not specified
114    public string GetParameterName() {
115      // Use explicit parameter name if provided
116      if (!string.IsNullOrEmpty(parameterName)) {
117        return parameterName;
118      }
119
120      // Otherwise, generate one based on menu structure
121      if (disableMenuEntry) {
122        return name;
123      }
124
125      return menuPath.TrimEnd('/') + "/" + name;
126    }
127  }
128
129  [System.Serializable]
130  public class BlendShapeSpec {
131    // The name of the blend shape to apply.
132    // For example, "Chest_Hide" or "Boobs+".
133    [SerializeField]
134    public string blendShape;
135
136    public List<string> blendShapes = new List<string>();
137
138    // The path to the mesh renderer to apply the blend shape to.
139    // For example, "Body" or "Shirt".
140    [SerializeField]
141    public string path;
142
143    [SerializeField]
144    public List<string> paths = new List<string>();
145
146    // Names of object sets to use as paths.
147    [SerializeField]
148    public List<string> sets = new List<string>();
149
150    // The value of the blendshape when the toggle is off. Range from 0-100.
151    [SerializeField]
152    public float offValue = 0.0f;
153
154    // The value of the blendshape when the toggle is on. Range from 0-100.
155    [SerializeField]
156    public float onValue = 100.0f;
157  }
158
159  [System.Serializable]
160  public class ShaderToggleSpec {
161    [SerializeField]
162    public string materialProperty;
163
164    [SerializeField]
165    public string path = "";
166
167    [SerializeField]
168    public List<string> paths = new List<string>();
169
170    // Names of object sets to use as paths.
171    [SerializeField]
172    public List<string> sets = new List<string>();
173
174    [SerializeField]
175    public float offValue = 0.0f;
176
177    [SerializeField]
178    public float onValue = 1.0f;
179
180    [SerializeField]
181    public string rendererType = "SkinnedMeshRenderer"; // Can be "SkinnedMeshRenderer" or "MeshRenderer"
182  }
183
184  [System.Serializable]
185  public class ParentConstraintWeight {
186    [SerializeField]
187    public string path = "";
188
189    [SerializeField]
190    public float offValue = 0.0f;
191
192    [SerializeField]
193    public float onValue = 1.0f;
194  }
195
196  [System.Serializable]
197  public class ObjectSet {
198    [SerializeField]
199    public string name;
200
201    [SerializeField]
202    public List<string> objects = new List<string>();
203  }
204
205  [System.Serializable]
206  public class AnimatorConfigFile {
207    [SerializeField]
208    public List<ObjectSet> objectSets = new List<ObjectSet>();
209
210    [SerializeField]
211    public List<ToggleSpec> toggles = new List<ToggleSpec>();
212
213    [SerializeField]
214    public string api_version;
215  }
216
217  [System.Serializable]
218  public class GeneratedAnimationsConfig {
219    public List<GeneratedAnimationClipConfig> animations =
220      new List<GeneratedAnimationClipConfig>();
221  }
222
223  [System.Serializable]
224  public class GeneratedAnimationClipConfig {
225    public string name;
226    public List<GeneratedMeshToggle> meshToggles =
227      new List<GeneratedMeshToggle>();
228    public List<GeneratedBlendShape> blendShapes =
229      new List<GeneratedBlendShape>();
230    public List<GeneratedShaderToggle> shaderToggles =
231      new List<GeneratedShaderToggle>();
232    public List<GeneratedParentConstraint> parentConstraintWeights = new List<GeneratedParentConstraint>();
233  }
234
235  [System.Serializable]
236  public class GeneratedMeshToggle {
237    public string path;
238    public float value;
239  }
240
241  [System.Serializable]
242  public class GeneratedBlendShape {
243    public string path;
244    public string blendShape;
245    public float value;
246  }
247
248  // Add new class for generated shader toggles
249  [System.Serializable]
250  public class GeneratedShaderToggle {
251    public string path;
252    public string materialProperty;
253    public float value;
254    public string rendererType = "SkinnedMeshRenderer"; // Default to SkinnedMeshRenderer for backward compatibility
255  }
256
257  // Add new class for generated parent constraints
258  [System.Serializable]
259  public class GeneratedParentConstraint {
260    public string path;
261    public float value;
262  }
263
264  [System.Serializable]
265  public class ExternalAnimationSpec {
266    // Path to the "on" animation clip asset (e.g., "Assets/MyAnims/External_On.anim")
267    [SerializeField]
268    public string onClipPath;
269    // Path to the "off" animation clip asset (e.g., "Assets/MyAnims/External_Off.anim")
270    [SerializeField]
271    public string offClipPath;
272    [SerializeField]
273    public bool mirror = false;
274  }
275
276  // These classes describe the generated JSON output for the animator configuration.
277  [System.Serializable]
278  public class GeneratedAnimatorConfig {
279    public List<AnimatorParameterSetting> parameters = new List<AnimatorParameterSetting>();
280    public List<AnimatorLayer> layers = new List<AnimatorLayer>();
281    public List<GeneratedAnimationClipConfig> animations =
282      new List<GeneratedAnimationClipConfig>();
283  }
284
285  [System.Serializable]
286  public class AnimatorLayer {
287    public string name;
288    public AnimatorDirectBlendTree directBlendTree =
289      new AnimatorDirectBlendTree();
290  }
291
292  [System.Serializable]
293  public class AnimatorDirectBlendTree {
294    public List<AnimatorDirectBlendTreeEntry> entries =
295      new List<AnimatorDirectBlendTreeEntry>();
296  }
297
298  [System.Serializable]
299  public class AnimatorDirectBlendTreeEntry {
300    public string name;       // animation name
301    public string parameter;  // parameter driving the animation
302    public float offThreshold = 0.0f;  // threshold for off animation
303    public float onThreshold = 1.0f;   // threshold for on animation
304  }
305
306  // Add these new classes at the namespace level
307  [System.Serializable]
308  public class VRCMenuConfig {
309    public string menuName = "YOTS";
310    public List<VRCMenuItemConfig> items = new List<VRCMenuItemConfig>();
311  }
312
313  [System.Serializable]
314  public class VRCMenuItemConfig {
315    public string name;
316    public string parameter;
317    public Texture2D icon;
318  }
319
320  [System.Serializable]
321  public class AnimatorParameterSetting {
322    public string name;
323    public float defaultValue;
324    public float offThreshold = 0.0f;
325    public float onThreshold = 1.0f;
326  }
327
328  public class YOTSCore {
329    private static Dictionary<string, AnimationClip> animationClips = new Dictionary<string, AnimationClip>();
330
331    private static string GetMeshToggleAttributeId(string path) {
332      return "MeshToggle:" + path;
333    }
334
335    private static string GetBlendShapeAttributeId(string path, string blendShape) {
336      return "BlendShape:" + path + "/" + blendShape;
337    }
338
339    private static string GetShaderToggleAttributeId(string path, string materialProperty) {
340      return "ShaderToggle:" + path + "/" + materialProperty;
341    }
342
343    private static string GetParentConstraintAttributeId(string path) {
344      return "ParentConstraint:" + path;
345    }
346
347    public static AnimatorController GenerateAnimator(string configJson,
348        VRCExpressionParameters vrcParams, VRCExpressionsMenu vrcMenu) {
349      Debug.Log("=== Starting Animator Generation Process ===");
350
351      if (string.IsNullOrEmpty(configJson)) {
352        throw new ArgumentException("No config JSON provided.");
353      }
354
355      AnimatorConfigFile config;
356      config = JsonUtility.FromJson<AnimatorConfigFile>(configJson);
357      if (config == null) {
358        throw new ArgumentException("JSON config is empty or invalid");
359      }
360
361      // Remove disabled specs upon ingest so that the usual checks apply.
362      int n_removed = config.toggles.RemoveAll(spec => spec.disabled);
363      Debug.Log($"Removed {n_removed} disabled toggles");
364
365      if (config.toggles == null) {
366        throw new ArgumentException("No toggleSpecs found in configuration");
367      }
368      Debug.Log($"Configuration loaded. Found {config.toggles.Count} toggles.");
369
370      // Create abstract representation of the animator structure.
371      GeneratedAnimatorConfig genAnimatorConfig = GenerateNaiveAnimatorConfig(config.toggles);
372      genAnimatorConfig = ApplyIndependentFixToAnimatorConfig(genAnimatorConfig);
373      genAnimatorConfig = RemoveOffAnimationsFromOverrideLayers(genAnimatorConfig);
374      genAnimatorConfig = RemoveUnusedAnimations(genAnimatorConfig);
375
376      animationClips.Clear();
377      Debug.Log("--- Preparing Final Animation Clips ---");
378
379      // Create lookup from animation name to toggle spec
380      Dictionary<string, ToggleSpec> animNameToToggleSpec = new Dictionary<string, ToggleSpec>();
381      foreach (var toggle in config.toggles) {
382        string paramName = toggle.GetParameterName();
383        string animName = paramName;
384        if (config.toggles.Count(t => t.GetParameterName() == paramName) > 1) {
385          animName = paramName + "_" + toggle.name;
386        }
387        animNameToToggleSpec[animName] = toggle;
388      }
389
390      // Iterate through the FINAL animation configurations after potential renaming/splitting
391      foreach (var finalAnimConfig in genAnimatorConfig.animations) {
392        string finalClipName = finalAnimConfig.name;
393
394        // Determine the original base animation name from the final clip name
395        string baseAnimName = finalClipName;
396        string[] suffixes = { "_Independent_On", "_Independent_Off", "_Dependent_On", "_Dependent_Off", "_On", "_Off" };
397        foreach(var suffix in suffixes) {
398          if (baseAnimName.EndsWith(suffix)) {
399            baseAnimName = baseAnimName.Substring(0, baseAnimName.Length - suffix.Length);
400            break;
401          }
402        }
403
404        if (!animNameToToggleSpec.TryGetValue(baseAnimName, out ToggleSpec originalToggleSpec)) {
405           Debug.LogError($"Could not find original ToggleSpec for animation name '{baseAnimName}' derived from animation clip '{finalClipName}'. Skipping clip.");
406           continue;
407        }
408
409        bool usesExternal = originalToggleSpec.externalAnimations != null && originalToggleSpec.externalAnimations.Count > 0;
410
411        if (usesExternal) {
412          var externalSpec = originalToggleSpec.externalAnimations[0];
413          string sourceClipPath = null;
414          bool isOffClip = finalClipName.EndsWith("_Off") || finalClipName.EndsWith("_Independent_Off") || finalClipName.EndsWith("_Dependent_Off");
415
416          sourceClipPath = isOffClip ? externalSpec.offClipPath : externalSpec.onClipPath;
417
418          if (string.IsNullOrEmpty(sourceClipPath)) {
419            Debug.LogError($"Toggle '{originalToggleSpec.name}' (Param: '{originalToggleSpec.GetParameterName()}'): External clip path is missing for '{finalClipName}'. Skipping clip.");
420            continue;
421          }
422
423          AnimationClip sourceClip = AssetDatabase.LoadAssetAtPath<AnimationClip>(sourceClipPath);
424          if (sourceClip == null) {
425              Debug.LogError($"Toggle '{originalToggleSpec.name}' (Param: '{originalToggleSpec.GetParameterName()}'): Failed to load source external animation clip '{finalClipName}' at path: {sourceClipPath}. Skipping clip.");
426              continue;
427          }
428
429          AnimationClip clipToUse = null;
430
431          if (externalSpec.mirror) {
432             // Generate mirrored clip in memory
433             Debug.Log($"Generating in-memory mirrored clip for '{sourceClip.name}' used by '{finalClipName}'");
434             try {
435               clipToUse = MirrorAnimationClipInMemory(sourceClip);
436               if (clipToUse == null) {
437                  Debug.LogError($"Failed to generate in-memory mirrored clip for '{sourceClip.name}'. Using source clip instead.");
438                  clipToUse = sourceClip; // Fallback to source
439               } else {
440                  Debug.Log($"Successfully generated in-memory mirrored clip for '{sourceClip.name}'");
441               }
442             } catch (Exception e) {
443                Debug.LogError($"Error generating in-memory mirrored clip for '{sourceClip.name}': {e.Message}. Using source clip instead.");
444                clipToUse = sourceClip; // Fallback to source on error
445             }
446          } else {
447             // Not mirrored, use the loaded source clip directly
448             clipToUse = sourceClip;
449             Debug.Log($"Using external clip '{finalClipName}' for toggle '{originalToggleSpec.name}' from path: {sourceClipPath}");
450          }
451
452          if (clipToUse != null) {
453             // Important: Assign a unique name to the in-memory clip instance if it was mirrored,
454             // otherwise Unity might get confused if multiple states reference the same in-memory clip object.
455             // We use the finalClipName which should be unique within the context of this generator run.
456             clipToUse.name = finalClipName;
457             animationClips[finalClipName] = clipToUse;
458          }
459
460        } else {
461          // Generate internal clip using the potentially modified GeneratedAnimationClipConfig
462          AnimationClip internalClip = CreateAnimationClipFromConfig(finalAnimConfig); // Pass the final config
463          // Ensure the internal clip also has a unique name matching its key
464          internalClip.name = finalClipName;
465          animationClips[finalClipName] = internalClip;
466          Debug.Log($"Generated internal clip '{finalClipName}' for toggle '{originalToggleSpec.name}'");
467        }
468      }
469      Debug.Log("--- Finished Preparing Final Animation Clips ---");
470
471      // Create actual assets.
472      GenerateVRChatAssets(config.toggles, vrcParams, vrcMenu);
473      AnimatorController controller = GenerateAnimatorController(genAnimatorConfig); // Pass the final config
474
475      Debug.Log("=== Animator Generation Process Complete ===");
476      return controller;
477    }
478
479    private static void CreateAnimationClips(GeneratedAnimationsConfig animationsConfig) {
480      foreach (var clipConfig in animationsConfig.animations) {
481        AnimationClip newClip = new AnimationClip();
482        newClip.name = clipConfig.name;
483
484        // Apply mesh toggles
485        foreach (var meshToggle in clipConfig.meshToggles) {
486          AnimationCurve curve = new AnimationCurve(new Keyframe(0, meshToggle.value));
487          EditorCurveBinding binding = new EditorCurveBinding();
488          binding.path = meshToggle.path;
489          binding.type = typeof(GameObject);
490          binding.propertyName = "m_IsActive";
491          AnimationUtility.SetEditorCurve(newClip, binding, curve);
492        }
493
494        // Apply blend shapes
495        foreach (var blendShape in clipConfig.blendShapes) {
496          AnimationCurve curve = AnimationCurve.Constant(0, 0, blendShape.value);
497          EditorCurveBinding binding = new EditorCurveBinding();
498          binding.path = blendShape.path;
499          binding.type = typeof(SkinnedMeshRenderer);
500          binding.propertyName = "blendShape." + blendShape.blendShape;
501          AnimationUtility.SetEditorCurve(newClip, binding, curve);
502        }
503
504        // Apply shader toggles
505        foreach (var shaderToggle in clipConfig.shaderToggles) {
506          AnimationCurve curve = AnimationCurve.Constant(0, 0, shaderToggle.value);
507          EditorCurveBinding binding = new EditorCurveBinding();
508          binding.path = shaderToggle.path;
509          
510          // Use the specified renderer type
511          if (shaderToggle.rendererType == "MeshRenderer") {
512            binding.type = typeof(MeshRenderer);
513          } else {
514            binding.type = typeof(SkinnedMeshRenderer); // Default or when explicitly specified
515          }
516          
517          binding.propertyName = $"material.{shaderToggle.materialProperty}";
518          AnimationUtility.SetEditorCurve(newClip, binding, curve);
519        }
520
521        // Apply parent constraint weights
522        foreach (var parentConstraint in clipConfig.parentConstraintWeights) {
523          AnimationCurve curve = AnimationCurve.Constant(0, 0, parentConstraint.value);
524          EditorCurveBinding binding = new EditorCurveBinding();
525          binding.path = parentConstraint.path;
526          binding.type = typeof(UnityEngine.Animations.ParentConstraint);
527          binding.propertyName = "m_Weight";
528          AnimationUtility.SetEditorCurve(newClip, binding, curve);
529        }
530
531        // Store in memory
532        animationClips[clipConfig.name] = newClip;
533        Debug.Log("Created animation clip " + clipConfig.name);
534      }
535    }
536
537    private static AnimatorController GenerateAnimatorController(GeneratedAnimatorConfig animatorConfig) {
538      AnimatorController controller = new AnimatorController();
539
540      // Add weight parameter used to ensure that the blendtrees always
541      // run. All layers use this. Documented on vrc.school:
542      //   http://vrc.school/docs/Other/DBT-Combining#ed504c95853f4924adeffb6b125234ad
543      List<AnimatorControllerParameter> parameters_list = new List<AnimatorControllerParameter>();
544      var yots_weight = new AnimatorControllerParameter();
545      yots_weight.name = "YOTS_Weight";
546      yots_weight.type = AnimatorControllerParameterType.Float;
547      yots_weight.defaultFloat = 1.0f;
548      parameters_list.Add(yots_weight);
549      // Add all other parameters
550      foreach (var param in animatorConfig.parameters) {
551        var p = new AnimatorControllerParameter();
552        p.name = param.name;
553        // Note: Parameter type is always Float, even for toggles, because blend trees use floats.
554        // The VRCExpressionParameters handle the Bool/Float distinction for the menu.
555        p.type = AnimatorControllerParameterType.Float;
556        p.defaultFloat = param.defaultValue;
557        parameters_list.Add(p);
558      }
559      controller.parameters = parameters_list.ToArray();
560
561      // Add base layer. This is structured as a wide direct blendtree
562      // (DBT) comprised of blendtrees animating pairs of On/Off
563      // animations.
564      var baseLayerConfig = animatorConfig.layers[0];
565      var baseStateMachine = new AnimatorStateMachine();
566      baseStateMachine.name = "YOTS_BaseLayer_SM";
567
568      var rootBlendTree = new BlendTree();
569      rootBlendTree.name = "YOTS_BaseLayer_RootBlendTree";
570      rootBlendTree.blendType = BlendTreeType.Direct;
571
572      // Group animations by their base name (without _On/_Off suffix) to pair them
573      var animationPairs = new Dictionary<string, List<AnimatorDirectBlendTreeEntry>>();
574      foreach (var entry in baseLayerConfig.directBlendTree.entries) {
575        string baseName = entry.name;
576        if (baseName.EndsWith("_On"))
577          baseName = baseName.Substring(0, baseName.Length - "_On".Length);
578        else if (baseName.EndsWith("_Off"))
579          baseName = baseName.Substring(0, baseName.Length - "_Off".Length);
580        
581        if (!animationPairs.ContainsKey(baseName))
582          animationPairs[baseName] = new List<AnimatorDirectBlendTreeEntry>();
583        animationPairs[baseName].Add(entry);
584      }
585
586      // Create a blend tree for each animation pair
587      foreach (var pair in animationPairs) {
588        var animations = pair.Value;
589        if (animations.Count == 0) continue;
590        
591        // Get thresholds from the first animation (they should all be the same for this pair)
592        var firstAnim = animations[0];
593        var param = firstAnim.parameter;
594        float offThreshold = firstAnim.offThreshold;
595        float onThreshold = firstAnim.onThreshold;
596
597        // Create a blendtree controlled by this toggle's parameter.
598        var paramBlendTree = new BlendTree();
599        paramBlendTree.name = $"YOTS_BlendTree_{pair.Key}";
600        paramBlendTree.blendType = BlendTreeType.Simple1D;
601        paramBlendTree.blendParameter = param;
602        
603        // Handle inverted thresholds (e.g., offThreshold=0.5, onThreshold=0.0)
604        float minThreshold = Mathf.Min(offThreshold, onThreshold);
605        float maxThreshold = Mathf.Max(offThreshold, onThreshold);
606        paramBlendTree.minThreshold = minThreshold;
607        paramBlendTree.maxThreshold = maxThreshold;
608        paramBlendTree.useAutomaticThresholds = false;
609
610        var children = new List<ChildMotion>();
611        
612        // Build list of animations with their thresholds
613        var animsWithThresholds = new List<(AnimatorDirectBlendTreeEntry entry, float threshold)>();
614        foreach (var animation in animations) {
615          float threshold = animation.name.EndsWith("_On") ? onThreshold : offThreshold;
616          animsWithThresholds.Add((animation, threshold));
617        }
618        
619        // Sort by threshold value (ascending) to ensure Unity interpolates correctly
620        foreach (var (animation, threshold) in animsWithThresholds.OrderBy(a => a.threshold)) {
621          Debug.Log($"Adding child motion for: {animation.name} at threshold {threshold}");
622          if (!animationClips.TryGetValue(animation.name, out AnimationClip clip)) {
623            throw new InvalidOperationException($"Animation clip not found in memory: {animation.name}");
624          }
625
626          children.Add(new ChildMotion{
627            motion = clip,
628            timeScale = 1f,
629            threshold = threshold
630          });
631        }
632        paramBlendTree.children = children.ToArray();
633
634        // Add that blendtree to the parent direct blendtree (DBT)
635        // controlled by YOTS_Weight. That YOTS_Weight parameter is
636        // always set to 1, so the child blendtree always runs.
637        rootBlendTree.children = rootBlendTree.children.Append(
638            new ChildMotion{
639              motion = paramBlendTree,
640              timeScale = 1f,
641              directBlendParameter = "YOTS_Weight"
642            }).ToArray();
643      }
644
645      var baseState = baseStateMachine.AddState("YOTS_BaseLayer_State");
646      baseState.motion = rootBlendTree;
647      baseState.writeDefaultValues = true;
648      baseStateMachine.defaultState = baseState;
649
650      controller.AddLayer(new AnimatorControllerLayer{
651        name = "YOTS_BaseLayer",
652        defaultWeight = 1.0f,
653        stateMachine = baseStateMachine
654      });
655
656      // Add override layers. These are DBTs of On animations (no Off
657      // animations).
658      for (int i = 1; i < animatorConfig.layers.Count; i++) {
659        var layerConfig = animatorConfig.layers[i];
660        string layerName = $"YOTS_OverrideLayer{(i-1).ToString("00")}";
661
662        var stateMachine = new AnimatorStateMachine();
663        stateMachine.name = layerName + "_SM";
664
665        var blendTree = new BlendTree();
666        blendTree.name = layerName + "_BlendTree";
667        blendTree.blendType = BlendTreeType.Direct;
668
669        foreach (var entry in layerConfig.directBlendTree.entries) {
670          if (!animationClips.TryGetValue(entry.name, out AnimationClip clip)) {
671            throw new InvalidOperationException($"Animation clip not found in memory: {entry.name}");
672          }
673
674          blendTree.children = blendTree.children.Append(new ChildMotion{
675            motion = clip,
676            timeScale = 1f,
677            directBlendParameter = entry.parameter
678          }).ToArray();
679        }
680
681        var state = stateMachine.AddState(layerName + "_State");
682        state.motion = blendTree;
683        state.writeDefaultValues = true;
684        stateMachine.defaultState = state;
685
686        controller.AddLayer(new AnimatorControllerLayer{
687          name = layerName,
688          defaultWeight = 1.0f,
689          stateMachine = stateMachine
690        });
691
692        Debug.Log($"Added override layer: {layerName}");
693      }
694
695      return controller;
696    }
697
698    private static Dictionary<string, int> TopologicalSortToggles(List<ToggleSpec> toggleSpecs) {
699      // Group toggles by parameter name to handle shared parameters
700      var togglesByParam = toggleSpecs
701        .GroupBy(t => t.GetParameterName())
702        .ToDictionary(g => g.Key, g => g.ToList());
703      
704      // Get mapping from toggle parameter name to children
705      Dictionary<string, HashSet<string>> graph = new Dictionary<string, HashSet<string>>();
706      Dictionary<string, HashSet<string>> dependencyNames = new Dictionary<string, HashSet<string>>();
707      
708      foreach (var paramGroup in togglesByParam) {
709        string paramName = paramGroup.Key;
710        if (!graph.ContainsKey(paramName))
711          graph[paramName] = new HashSet<string>();
712        if (!dependencyNames.ContainsKey(paramName))
713          dependencyNames[paramName] = new HashSet<string>();
714          
715        // Collect all dependencies from all toggles that share this parameter
716        foreach (var toggle in paramGroup.Value) {
717          foreach (var dep in toggle.dependencies) {
718            // Find the toggle with this dependency name
719            var depToggle = toggleSpecs.FirstOrDefault(t => t.name == dep);
720            if (depToggle == null) {
721              throw new ArgumentException($"Toggle '{toggle.name}' has dependency '{dep}' that doesn't exist");
722            }
723            string depParamName = depToggle.GetParameterName();
724            if (!graph.ContainsKey(depParamName))
725              graph[depParamName] = new HashSet<string>();
726            graph[depParamName].Add(paramName);
727            dependencyNames[paramName].Add(dep);
728          }
729        }
730      }
731
732      Dictionary<string, int> inDegree = new Dictionary<string, int>();
733      foreach (var paramGroup in togglesByParam) {
734        string paramName = paramGroup.Key;
735        inDegree[paramName] = dependencyNames[paramName].Count;
736      }
737
738      Dictionary<string, int> depths = new Dictionary<string, int>();
739      Queue<string> queue = new Queue<string>();
740
741      // Identify start nodes
742      foreach (var pair in inDegree) {
743        if (pair.Value == 0) {
744          queue.Enqueue(pair.Key);
745          depths[pair.Key] = 0;
746        }
747      }
748
749      int processedNodes = 0;
750      while (queue.Count > 0) {
751        // Pop start nodes one by one.
752        string current = queue.Dequeue();
753        processedNodes++;
754        int currentDepth = depths[current];
755        // Enqueue children and set their depth to cur depth + 1.
756        foreach (var child in graph[current]) {
757          inDegree[child]--;
758          if (inDegree[child] == 0) {
759            queue.Enqueue(child);
760            depths[child] = currentDepth + 1;
761          }
762        }
763      }
764
765      // Check if all unique parameter names were processed
766      if (processedNodes != togglesByParam.Count) {
767        var unprocessedParams = togglesByParam.Keys
768          .Where(p => !depths.ContainsKey(p))
769          .ToList();
770        
771        // Collect all toggle names that are part of the cycle
772        var cycleNodes = new List<string>();
773        foreach (var param in unprocessedParams) {
774          cycleNodes.AddRange(togglesByParam[param].Select(t => t.name));
775        }
776        
777        // Provide detailed error message
778        if (cycleNodes.Count == 0) {
779          // This should never happen.
780          throw new ArgumentException($"Dependency cycle detected but couldn't identify specific nodes. Unprocessed parameters: {string.Join(", ", unprocessedParams)}");
781        } else {
782          throw new ArgumentException($"Dependency cycle detected in toggle specifications. Nodes involved: {string.Join(", ", cycleNodes)}");
783        }
784      }
785
786      return depths;
787    }
788
789    private static GeneratedAnimatorConfig GenerateNaiveAnimatorConfig(List<ToggleSpec> toggleSpecs) {
790      GeneratedAnimatorConfig genAnimatorConfig = new GeneratedAnimatorConfig();
791      // Sort toggles into layers
792      Dictionary<string, int> depths = TopologicalSortToggles(toggleSpecs);
793      var togglesByDepth = toggleSpecs
794        .GroupBy(t => depths[t.GetParameterName()])
795        .OrderBy(g => g.Key)
796        .ToList();
797      // Add layers
798      for (int i = 0; i < togglesByDepth.Count; i++) {
799        var depthGroup = togglesByDepth[i];
800        AnimatorLayer layer = new AnimatorLayer();
801        layer.name = i == 0 ? "YOTS_BaseLayer" : $"YOTS_OverrideLayer{(i - 1).ToString("00")}";
802        foreach (var toggle in depthGroup) {
803          string paramName = toggle.GetParameterName();
804          if (!genAnimatorConfig.parameters.Any(p => p.name == paramName))
805            // Populate thresholds when adding parameter settings
806            genAnimatorConfig.parameters.Add(new AnimatorParameterSetting{
807              name = paramName,
808              defaultValue = toggle.defaultValue,
809              offThreshold = toggle.offThreshold,
810              onThreshold = toggle.onThreshold
811            });
812
813          // Use a unique name for animations when toggles share parameters
814          string animName = paramName;
815          if (toggleSpecs.Count(t => t.GetParameterName() == paramName) > 1) {
816            // Make animation names unique by including toggle name
817            animName = paramName + "_" + toggle.name;
818          }
819
820          layer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry{
821            name = animName + "_On",
822            parameter = paramName,
823            offThreshold = toggle.offThreshold,
824            onThreshold = toggle.onThreshold
825          });
826
827          layer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry{
828            name = animName + "_Off",
829            parameter = paramName,
830            offThreshold = toggle.offThreshold,
831            onThreshold = toggle.onThreshold
832          });
833        }
834        genAnimatorConfig.layers.Add(layer);
835      }
836      // Add animations
837      GeneratedAnimationsConfig animConfig = GenerateAnimationConfig(toggleSpecs);
838      genAnimatorConfig.animations = animConfig.animations;
839      return genAnimatorConfig;
840    }
841
842    private static GeneratedAnimationsConfig GenerateAnimationConfig(List<ToggleSpec> toggleSpecs) {
843      // This function is now only used to populate the initial GeneratedAnimatorConfig.
844      // The actual clip creation or loading happens later in GenerateAnimator.
845      GeneratedAnimationsConfig genAnimConfig = new GeneratedAnimationsConfig();
846      foreach (var toggle in toggleSpecs) {
847        string paramName = toggle.GetParameterName();
848        
849        // Use a unique name for animations when toggles share parameters
850        string animName = paramName;
851        if (toggleSpecs.Count(t => t.GetParameterName() == paramName) > 1) {
852          // Make animation names unique by including toggle name
853          animName = paramName + "_" + toggle.name;
854        }
855
856        // We still create dummy entries here so ApplyIndependentFixToAnimatorConfig etc. have something to work with.
857        // The *content* of these might not be used if external clips are provided.
858        var (onConfig, offConfig) = GenerateSingleToggleAnimationConfigs(toggle, animName);
859        genAnimConfig.animations.Add(onConfig);
860        genAnimConfig.animations.Add(offConfig);
861      }
862      return genAnimConfig;
863    }
864
865    private static (GeneratedAnimationClipConfig onConfig, GeneratedAnimationClipConfig offConfig)
866        GenerateSingleToggleAnimationConfigs(ToggleSpec toggle, string animName) {
867      string paramName = toggle.GetParameterName();
868
869      GeneratedAnimationClipConfig onAnim = new GeneratedAnimationClipConfig();
870      onAnim.name = animName + "_On";
871      if (toggle.meshToggles != null) {
872        foreach (var mesh in toggle.meshToggles) {
873          onAnim.meshToggles.Add(new GeneratedMeshToggle { path = mesh, value = 1.0f });
874        }
875      }
876      if (toggle.inverseMeshToggles != null) {
877        foreach (var mesh in toggle.inverseMeshToggles) {
878          onAnim.meshToggles.Add(new GeneratedMeshToggle { path = mesh, value = 0.0f });
879        }
880      }
881      if (toggle.blendShapes != null) {
882        foreach (var bs in toggle.blendShapes) {
883          // Validate that either path or paths is specified
884          if (string.IsNullOrEmpty(bs.path) && (bs.paths == null || bs.paths.Count == 0)) {
885            throw new ArgumentException($"Blend shape in '{toggle.name}' must specify either 'path' or 'paths'");
886          }
887          // Validate that either blendShape or blendShapes is specified
888          if (string.IsNullOrEmpty(bs.blendShape) && (bs.blendShapes == null || bs.blendShapes.Count == 0)) {
889            throw new ArgumentException($"Blend shape in '{toggle.name}' must specify either 'blendShape' or 'blendShapes'");
890          }
891
892          // Use toggle's onValue if blendshape is using the default (100.0f)
893          float effectiveOnValue = (bs.onValue == 100.0f) ? toggle.onValue : bs.onValue;
894
895          // Collect all blendshape names
896          List<string> allBlendShapes = new List<string>();
897          if (!string.IsNullOrEmpty(bs.blendShape)) {
898            allBlendShapes.Add(bs.blendShape);
899          }
900          if (bs.blendShapes != null) {
901            allBlendShapes.AddRange(bs.blendShapes);
902          }
903
904          foreach (var blendShapeName in allBlendShapes) {
905            // Handle single path
906            if (!string.IsNullOrEmpty(bs.path)) {
907              onAnim.blendShapes.Add(new GeneratedBlendShape{
908                path = bs.path,
909                blendShape = blendShapeName,
910                value = effectiveOnValue
911              });
912            }
913
914            // Handle multiple paths
915            if (bs.paths != null) {
916              foreach (var path in bs.paths) {
917                onAnim.blendShapes.Add(new GeneratedBlendShape{
918                  path = path,
919                  blendShape = blendShapeName,
920                  value = effectiveOnValue
921                });
922              }
923            }
924          }
925        }
926      }
927      if (toggle.shaderToggles != null) {
928        foreach (var st in toggle.shaderToggles) {
929          if (string.IsNullOrEmpty(st.path) && (st.paths == null || st.paths.Count == 0)) {
930            throw new ArgumentException($"Shader toggle in '{toggle.name}' must specify either 'path' or 'paths'");
931          }
932          if (!string.IsNullOrEmpty(st.path)) {
933            onAnim.shaderToggles.Add(new GeneratedShaderToggle {
934              path = st.path, materialProperty = st.materialProperty, value = st.onValue, rendererType = st.rendererType
935            });
936          }
937          if (st.paths != null) {
938            foreach (var path in st.paths) {
939              onAnim.shaderToggles.Add(new GeneratedShaderToggle {
940                path = path, materialProperty = st.materialProperty, value = st.onValue, rendererType = st.rendererType
941              });
942            }
943          }
944        }
945      }
946      if (toggle.parentConstraintWeights != null) {
947        foreach (var pc in toggle.parentConstraintWeights) {
948          onAnim.parentConstraintWeights.Add(new GeneratedParentConstraint { path = pc.path, value = pc.onValue });
949        }
950      }
951
952      GeneratedAnimationClipConfig offAnim = new GeneratedAnimationClipConfig();
953      offAnim.name = animName + "_Off";
954      if (toggle.meshToggles != null) {
955        foreach (var mesh in toggle.meshToggles) {
956          offAnim.meshToggles.Add(new GeneratedMeshToggle { path = mesh, value = 0.0f });
957        }
958      }
959      if (toggle.inverseMeshToggles != null) {
960        foreach (var mesh in toggle.inverseMeshToggles) {
961          offAnim.meshToggles.Add(new GeneratedMeshToggle { path = mesh, value = 1.0f });
962        }
963      }
964      if (toggle.blendShapes != null) {
965        foreach (var bs in toggle.blendShapes) {
966          // Validate that either path or paths is specified
967          if (string.IsNullOrEmpty(bs.path) && (bs.paths == null || bs.paths.Count == 0)) {
968            throw new ArgumentException($"Blend shape in '{toggle.name}' must specify either 'path' or 'paths'");
969          }
970          // Validate that either blendShape or blendShapes is specified
971          if (string.IsNullOrEmpty(bs.blendShape) && (bs.blendShapes == null || bs.blendShapes.Count == 0)) {
972            throw new ArgumentException($"Blend shape in '{toggle.name}' must specify either 'blendShape' or 'blendShapes'");
973          }
974
975          // Use toggle's offValue if blendshape is using the default (0.0f)
976          float effectiveOffValue = (bs.offValue == 0.0f) ? toggle.offValue : bs.offValue;
977
978          // Collect all blendshape names
979          List<string> allBlendShapes = new List<string>();
980          if (!string.IsNullOrEmpty(bs.blendShape)) {
981            allBlendShapes.Add(bs.blendShape);
982          }
983          if (bs.blendShapes != null) {
984            allBlendShapes.AddRange(bs.blendShapes);
985          }
986
987          foreach (var blendShapeName in allBlendShapes) {
988            // Handle single path
989            if (!string.IsNullOrEmpty(bs.path)) {
990              offAnim.blendShapes.Add(new GeneratedBlendShape{
991                path = bs.path,
992                blendShape = blendShapeName,
993                value = effectiveOffValue
994              });
995            }
996
997            // Handle multiple paths
998            if (bs.paths != null) {
999              foreach (var path in bs.paths) {
1000                offAnim.blendShapes.Add(new GeneratedBlendShape{
1001                  path = path,
1002                  blendShape = blendShapeName,
1003                  value = effectiveOffValue
1004                });
1005              }
1006            }
1007          }
1008        }
1009      }
1010      if (toggle.shaderToggles != null) {
1011        foreach (var st in toggle.shaderToggles) {
1012           if (string.IsNullOrEmpty(st.path) && (st.paths == null || st.paths.Count == 0)) {
1013            throw new ArgumentException($"Shader toggle in '{toggle.name}' must specify either 'path' or 'paths'");
1014          }
1015          if (!string.IsNullOrEmpty(st.path)) {
1016            offAnim.shaderToggles.Add(new GeneratedShaderToggle {
1017              path = st.path, materialProperty = st.materialProperty, value = st.offValue, rendererType = st.rendererType
1018            });
1019          }
1020          if (st.paths != null) {
1021            foreach (var path in st.paths) {
1022              offAnim.shaderToggles.Add(new GeneratedShaderToggle {
1023                path = path, materialProperty = st.materialProperty, value = st.offValue, rendererType = st.rendererType
1024              });
1025            }
1026          }
1027        }
1028      }
1029      if (toggle.parentConstraintWeights != null) {
1030        foreach (var pc in toggle.parentConstraintWeights) {
1031          offAnim.parentConstraintWeights.Add(new GeneratedParentConstraint { path = pc.path, value = pc.offValue });
1032        }
1033      }
1034
1035      return (onAnim, offAnim);
1036    }
1037
1038    private static AnimationClip CreateAnimationClipFromConfig(GeneratedAnimationClipConfig clipConfig) {
1039      AnimationClip newClip = new AnimationClip();
1040      newClip.name = clipConfig.name;
1041
1042      // Apply mesh toggles
1043      foreach (var meshToggle in clipConfig.meshToggles) {
1044        AnimationCurve curve = new AnimationCurve(new Keyframe(0, meshToggle.value));
1045        EditorCurveBinding binding = EditorCurveBinding.FloatCurve(meshToggle.path, typeof(GameObject), "m_IsActive");
1046        AnimationUtility.SetEditorCurve(newClip, binding, curve);
1047      }
1048
1049      // Apply blend shapes
1050      foreach (var blendShape in clipConfig.blendShapes) {
1051        AnimationCurve curve = AnimationCurve.Constant(0, 0, blendShape.value);
1052        EditorCurveBinding binding = EditorCurveBinding.FloatCurve(blendShape.path, typeof(SkinnedMeshRenderer), "blendShape." + blendShape.blendShape);
1053        AnimationUtility.SetEditorCurve(newClip, binding, curve);
1054      }
1055
1056      // Apply shader toggles
1057      foreach (var shaderToggle in clipConfig.shaderToggles) {
1058        AnimationCurve curve = AnimationCurve.Constant(0, 0, shaderToggle.value);
1059        Type rendererType = shaderToggle.rendererType == "MeshRenderer" ? typeof(MeshRenderer) : typeof(SkinnedMeshRenderer);
1060        EditorCurveBinding binding = EditorCurveBinding.FloatCurve(shaderToggle.path, rendererType, $"material.{shaderToggle.materialProperty}");
1061        AnimationUtility.SetEditorCurve(newClip, binding, curve);
1062      }
1063
1064      // Apply parent constraint weights
1065      foreach (var parentConstraint in clipConfig.parentConstraintWeights) {
1066        AnimationCurve curve = AnimationCurve.Constant(0, 0, parentConstraint.value);
1067        EditorCurveBinding binding = EditorCurveBinding.FloatCurve(parentConstraint.path, typeof(UnityEngine.Animations.ParentConstraint), "m_Weight");
1068        AnimationUtility.SetEditorCurve(newClip, binding, curve);
1069      }
1070
1071      return newClip;
1072    }
1073
1074    private static GeneratedAnimatorConfig ApplyIndependentFixToAnimatorConfig(GeneratedAnimatorConfig genAnimatorConfig) {
1075      // TODO meshToggles do not implement offValue/onValue at the JSON level,
1076      // so this is redundant.
1077      float GetOffValueForMesh(string path, List<GeneratedMeshToggle> offList) {
1078        var offToggle = offList?.FirstOrDefault(mt => mt.path == path);
1079        return offToggle != null ? offToggle.value : 0.0f;
1080      }
1081
1082      float GetOffValueForBlend(string path, string blendShapeName, List<GeneratedBlendShape> offList) {
1083        var offBlend = offList?.FirstOrDefault(bs => bs.path == path && bs.blendShape == blendShapeName);
1084        return offBlend != null ? offBlend.value : 0.0f;
1085      }
1086
1087      float GetOffValueForShader(string path, string materialProperty, List<GeneratedShaderToggle> offList) {
1088        var offShader = offList?.FirstOrDefault(st => st.path == path && st.materialProperty == materialProperty);
1089        return offShader != null ? offShader.value : 0.0f;
1090      }
1091
1092      float GetOffValueForParentConstraint(string path, List<GeneratedParentConstraint> offList) {
1093        var offPC = offList?.FirstOrDefault(pc => pc.path == path);
1094        return offPC != null ? offPC.value : 0.0f;
1095      }
1096
1097      // Create mapping from toggle name -> (on animation, off animation)
1098      Dictionary<string, (GeneratedAnimationClipConfig on, GeneratedAnimationClipConfig off)> toggleAnimations =
1099        new Dictionary<string, (GeneratedAnimationClipConfig, GeneratedAnimationClipConfig)>();
1100      foreach (var anim in genAnimatorConfig.animations) {
1101        if (anim.name.EndsWith("_On")) {
1102          string toggleName = anim.name.Substring(0, anim.name.LastIndexOf("_On"));
1103          if (!toggleAnimations.ContainsKey(toggleName))
1104            toggleAnimations[toggleName] = (null, null);
1105          var pair = toggleAnimations[toggleName];
1106          pair.on = anim;
1107          toggleAnimations[toggleName] = pair;
1108        }
1109        else if (anim.name.EndsWith("_Off")) {
1110          string toggleName = anim.name.Substring(0, anim.name.LastIndexOf("_Off"));
1111          if (!toggleAnimations.ContainsKey(toggleName))
1112            toggleAnimations[toggleName] = (null, null);
1113          var pair = toggleAnimations[toggleName];
1114          pair.off = anim;
1115          toggleAnimations[toggleName] = pair;
1116        }
1117      }
1118
1119      Dictionary<string, int> toggleToLayerIndex = new Dictionary<string, int>();
1120      for (int i = 0; i < genAnimatorConfig.layers.Count; i++) {
1121        var layer = genAnimatorConfig.layers[i];
1122        foreach (var entry in layer.directBlendTree.entries) {
1123          string entryName = entry.name;
1124          string toggleName = entryName;
1125          if (toggleName.EndsWith("_On"))
1126            toggleName = toggleName.Substring(0, toggleName.Length - "_On".Length);
1127          else if (toggleName.EndsWith("_Off"))
1128            toggleName = toggleName.Substring(0, toggleName.Length - "_Off".Length);
1129          if (!toggleToLayerIndex.ContainsKey(toggleName))
1130            toggleToLayerIndex[toggleName] = i;
1131        }
1132      }
1133
1134      // Mapping from attribute touched by animation to the set of toggles
1135      // which affect it.
1136      Dictionary<string, HashSet<string>> attributeToToggles = new Dictionary<string, HashSet<string>>();
1137      foreach (var kvp in toggleAnimations) {
1138        string toggleName = kvp.Key;
1139        var pair = kvp.Value;
1140        if (pair.on == null) continue;
1141
1142        HashSet<string> attributes = new HashSet<string>();
1143        if (pair.on.meshToggles != null) {
1144          foreach (var mt in pair.on.meshToggles) {
1145            string attr = GetMeshToggleAttributeId(mt.path);
1146            attributes.Add(attr);
1147          }
1148        }
1149        if (pair.on.blendShapes != null) {
1150          foreach (var bs in pair.on.blendShapes) {
1151            string attr = GetBlendShapeAttributeId(bs.path, bs.blendShape);
1152            attributes.Add(attr);
1153          }
1154        }
1155        if (pair.on.shaderToggles != null) {
1156          foreach (var st in pair.on.shaderToggles) {
1157            string attr = GetShaderToggleAttributeId(st.path, st.materialProperty);
1158            attributes.Add(attr);
1159          }
1160        }
1161        // Add parent constraint attributes
1162        if (pair.on.parentConstraintWeights != null) {
1163          foreach (var pc in pair.on.parentConstraintWeights) {
1164            string attr = GetParentConstraintAttributeId(pc.path);
1165            attributes.Add(attr);
1166          }
1167        }
1168        foreach (var attr in attributes) {
1169          if (!attributeToToggles.TryGetValue(attr, out var set)) {
1170            set = new HashSet<string>();
1171            attributeToToggles[attr] = set;
1172          }
1173          set.Add(toggleName);
1174        }
1175      }
1176
1177      // TODO assert that all toggles affecting the same attribute are on
1178      // different layers.
1179
1180      List<GeneratedAnimationClipConfig> newAnimations = new List<GeneratedAnimationClipConfig>();
1181
1182      AnimatorLayer baseLayer = genAnimatorConfig.layers.FirstOrDefault(l => l.name == "BaseLayer");
1183      if (baseLayer == null && genAnimatorConfig.layers.Count > 0)
1184        baseLayer = genAnimatorConfig.layers[0];
1185
1186      foreach (var kvp in toggleAnimations) {
1187        string toggleName = kvp.Key;
1188        var pair = kvp.Value;
1189        int layerIndex = toggleToLayerIndex[toggleName];
1190
1191        if (layerIndex == 0) {
1192          newAnimations.Add(pair.on);
1193          newAnimations.Add(pair.off);
1194          continue;
1195        }
1196
1197        // Work out which of the animation's mesh toggles are overrides and
1198        // which are independent.
1199        List<GeneratedMeshToggle> independentMesh = new List<GeneratedMeshToggle>();
1200        List<GeneratedMeshToggle> dependentMesh = new List<GeneratedMeshToggle>();
1201        if (pair.on.meshToggles != null) {
1202          foreach (var mt in pair.on.meshToggles) {
1203            string attr = GetMeshToggleAttributeId(mt.path);
1204            if (attributeToToggles[attr].Count == 1)
1205              independentMesh.Add(mt);
1206            else
1207              dependentMesh.Add(mt);
1208          }
1209        }
1210
1211        // Work out which of the animation's blendshapes are overrides and
1212        // which are independent.
1213        List<GeneratedBlendShape> independentBlend = new List<GeneratedBlendShape>();
1214        List<GeneratedBlendShape> dependentBlend = new List<GeneratedBlendShape>();
1215        if (pair.on.blendShapes != null) {
1216          foreach (var bs in pair.on.blendShapes) {
1217            string attr = GetBlendShapeAttributeId(bs.path, bs.blendShape);
1218            if (attributeToToggles[attr].Count == 1)
1219              independentBlend.Add(bs);
1220            else
1221              dependentBlend.Add(bs);
1222          }
1223        }
1224
1225        // Work out which of the animation's shader toggles are overrides and which are independent
1226        List<GeneratedShaderToggle> independentShader = new List<GeneratedShaderToggle>();
1227        List<GeneratedShaderToggle> dependentShader = new List<GeneratedShaderToggle>();
1228        if (pair.on.shaderToggles != null) {
1229          foreach (var st in pair.on.shaderToggles) {
1230            string attr = GetShaderToggleAttributeId(st.path, st.materialProperty);
1231            if (attributeToToggles[attr].Count == 1)
1232              independentShader.Add(st);
1233            else
1234              dependentShader.Add(st);
1235          }
1236        }
1237
1238        // Handle parent constraints the same way as other animated properties
1239        List<GeneratedParentConstraint> independentParentConstraint = new List<GeneratedParentConstraint>();
1240        List<GeneratedParentConstraint> dependentParentConstraint = new List<GeneratedParentConstraint>();
1241        if (pair.on.parentConstraintWeights != null) {
1242          foreach (var pc in pair.on.parentConstraintWeights) {
1243            string attr = GetParentConstraintAttributeId(pc.path);
1244            if (attributeToToggles[attr].Count == 1)
1245              independentParentConstraint.Add(pc);
1246            else
1247              dependentParentConstraint.Add(pc);
1248          }
1249        }
1250
1251        bool hasIndependent = (independentMesh.Count > 0 || independentBlend.Count > 0 || 
1252                              independentShader.Count > 0 || independentParentConstraint.Count > 0);
1253        bool hasDependent = (dependentMesh.Count > 0 || dependentBlend.Count > 0 || 
1254                            dependentShader.Count > 0 || dependentParentConstraint.Count > 0);
1255
1256        if (hasIndependent && hasDependent) {
1257          GeneratedAnimationClipConfig dependentOn = new GeneratedAnimationClipConfig();
1258          dependentOn.name = toggleName + "_Dependent_On";
1259          dependentOn.meshToggles = dependentMesh;
1260          dependentOn.blendShapes = dependentBlend;
1261          dependentOn.shaderToggles = dependentShader;
1262          dependentOn.parentConstraintWeights = dependentParentConstraint;
1263
1264          GeneratedAnimationClipConfig dependentOff = new GeneratedAnimationClipConfig();
1265          dependentOff.name = toggleName + "_Dependent_Off";
1266          dependentOff.meshToggles = dependentMesh
1267            .Select(mt => new GeneratedMeshToggle{
1268              path = mt.path,
1269              value = GetOffValueForMesh(mt.path, pair.off.meshToggles)
1270            })
1271          .ToList();
1272          dependentOff.blendShapes = dependentBlend
1273            .Select(bs => new GeneratedBlendShape{
1274              path = bs.path,
1275              blendShape = bs.blendShape,
1276              value = GetOffValueForBlend(bs.path, bs.blendShape, pair.off.blendShapes)
1277            })
1278          .ToList();
1279          dependentOff.shaderToggles = dependentShader
1280            .Select(st => new GeneratedShaderToggle {
1281              path = st.path,
1282              materialProperty = st.materialProperty,
1283              value = GetOffValueForShader(st.path, st.materialProperty, pair.off.shaderToggles),
1284              rendererType = st.rendererType
1285            })
1286          .ToList();
1287          dependentOff.parentConstraintWeights = dependentParentConstraint
1288            .Select(pc => new GeneratedParentConstraint {
1289              path = pc.path,
1290              value = GetOffValueForParentConstraint(pc.path, pair.off.parentConstraintWeights)
1291            })
1292          .ToList();
1293
1294          GeneratedAnimationClipConfig independentOn = new GeneratedAnimationClipConfig();
1295          independentOn.name = toggleName + "_Independent_On";
1296          independentOn.meshToggles = independentMesh;
1297          independentOn.blendShapes = independentBlend;
1298          independentOn.shaderToggles = independentShader;
1299          independentOn.parentConstraintWeights = independentParentConstraint;
1300
1301          GeneratedAnimationClipConfig independentOff = new GeneratedAnimationClipConfig();
1302          independentOff.name = toggleName + "_Independent_Off";
1303          independentOff.meshToggles = independentMesh
1304            .Select(mt => new GeneratedMeshToggle{
1305              path = mt.path,
1306              value = GetOffValueForMesh(mt.path, pair.off.meshToggles)
1307            })
1308          .ToList();
1309          independentOff.blendShapes = independentBlend
1310            .Select(bs => new GeneratedBlendShape{
1311              path = bs.path,
1312              blendShape = bs.blendShape,
1313              value = GetOffValueForBlend(bs.path, bs.blendShape, pair.off.blendShapes)
1314            })
1315          .ToList();
1316          independentOff.shaderToggles = independentShader
1317            .Select(st => new GeneratedShaderToggle {
1318              path = st.path,
1319              materialProperty = st.materialProperty,
1320              value = GetOffValueForShader(st.path, st.materialProperty, pair.off.shaderToggles),
1321              rendererType = st.rendererType
1322            })
1323          .ToList();
1324          independentOff.parentConstraintWeights = independentParentConstraint
1325            .Select(pc => new GeneratedParentConstraint {
1326              path = pc.path,
1327              value = GetOffValueForParentConstraint(pc.path, pair.off.parentConstraintWeights)
1328            })
1329          .ToList();
1330
1331          newAnimations.Add(dependentOn);
1332          newAnimations.Add(dependentOff);
1333          newAnimations.Add(independentOn);
1334          newAnimations.Add(independentOff);
1335
1336          AnimatorLayer overrideLayer = genAnimatorConfig.layers[layerIndex];
1337          foreach (var entry in overrideLayer.directBlendTree.entries) {
1338            if (entry.name.StartsWith(toggleName) &&
1339                (entry.name.EndsWith("_On") || entry.name.EndsWith("_Off"))) {
1340              entry.name = entry.name.EndsWith("_On") ? toggleName + "_Dependent_On" : toggleName + "_Dependent_Off";
1341            }
1342          }
1343
1344          if (baseLayer != null) {
1345            baseLayer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry{
1346              name = toggleName + "_Independent_On",
1347              parameter = toggleName
1348            });
1349            baseLayer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry{
1350              name = toggleName + "_Independent_Off",
1351              parameter = toggleName
1352            });
1353          }
1354        } else if (hasIndependent) {
1355          GeneratedAnimationClipConfig independentOn = new GeneratedAnimationClipConfig();
1356          independentOn.name = toggleName + "_Independent_On";
1357          independentOn.meshToggles = pair.on.meshToggles;
1358          independentOn.blendShapes = pair.on.blendShapes;
1359          independentOn.shaderToggles = pair.on.shaderToggles;
1360          independentOn.parentConstraintWeights = pair.on.parentConstraintWeights;
1361          GeneratedAnimationClipConfig independentOff = new GeneratedAnimationClipConfig();
1362          independentOff.name = toggleName + "_Independent_Off";
1363          independentOff.meshToggles = pair.off.meshToggles;
1364          independentOff.blendShapes = pair.off.blendShapes;
1365          independentOff.shaderToggles = pair.off.shaderToggles;
1366          independentOff.parentConstraintWeights = pair.off.parentConstraintWeights;
1367
1368          newAnimations.Add(independentOn);
1369          newAnimations.Add(independentOff);
1370
1371          AnimatorLayer overrideLayer = genAnimatorConfig.layers[layerIndex];
1372          overrideLayer.directBlendTree.entries.RemoveAll(e => e.name.StartsWith(toggleName));
1373          if (baseLayer != null) {
1374            baseLayer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry{
1375              name = toggleName + "_Independent_On",
1376              parameter = toggleName
1377            });
1378            baseLayer.directBlendTree.entries.Add(new AnimatorDirectBlendTreeEntry{
1379              name = toggleName + "_Independent_Off",
1380              parameter = toggleName
1381            });
1382          }
1383        } else if (hasDependent) {
1384          GeneratedAnimationClipConfig dependentOn = new GeneratedAnimationClipConfig();
1385          dependentOn.name = toggleName + "_Dependent_On";
1386          dependentOn.meshToggles = pair.on.meshToggles;
1387          dependentOn.blendShapes = pair.on.blendShapes;
1388          dependentOn.shaderToggles = pair.on.shaderToggles;
1389          dependentOn.parentConstraintWeights = pair.on.parentConstraintWeights;
1390          GeneratedAnimationClipConfig dependentOff = new GeneratedAnimationClipConfig();
1391          dependentOff.name = toggleName + "_Dependent_Off";
1392          dependentOff.meshToggles = pair.off.meshToggles;
1393          dependentOff.blendShapes = pair.off.blendShapes;
1394          dependentOff.shaderToggles = pair.off.shaderToggles;
1395          dependentOff.parentConstraintWeights = pair.off.parentConstraintWeights;
1396
1397          newAnimations.Add(dependentOn);
1398          newAnimations.Add(dependentOff);
1399
1400          AnimatorLayer overrideLayer = genAnimatorConfig.layers[layerIndex];
1401          foreach (var entry in overrideLayer.directBlendTree.entries) {
1402            if (entry.name.StartsWith(toggleName) &&
1403                (entry.name.EndsWith("_On") || entry.name.EndsWith("_Off"))) {
1404              entry.name = entry.name.EndsWith("_On") ? toggleName + "_Dependent_On" : toggleName + "_Dependent_Off";
1405            }
1406          }
1407        } else {
1408          throw new ArgumentException($"Toggle {toggleName} seems to have no animations.");
1409        }
1410      }
1411
1412      genAnimatorConfig.animations = newAnimations;
1413      return genAnimatorConfig;
1414    }
1415
1416    private static GeneratedAnimatorConfig
1417        RemoveOffAnimationsFromOverrideLayers(GeneratedAnimatorConfig config) {
1418      for (int i = 1; i < config.layers.Count; i++) {
1419        var layer = config.layers[i];
1420        layer.directBlendTree.entries.RemoveAll(entry => entry.name.EndsWith("_Off"));
1421      }
1422      return config;
1423    }
1424
1425    private static GeneratedAnimatorConfig
1426        RemoveUnusedAnimations(GeneratedAnimatorConfig config) {
1427      HashSet<string> referencedAnimations = new HashSet<string>();
1428      foreach (var layer in config.layers) {
1429        foreach (var entry in layer.directBlendTree.entries)
1430          referencedAnimations.Add(entry.name);
1431      }
1432
1433      config.animations = config.animations
1434        .Where(anim => referencedAnimations.Contains(anim.name))
1435        .ToList();
1436
1437      return config;
1438    }
1439
1440    private static VRCExpressionsMenu GetOrCreateSubmenu(
1441        VRCExpressionsMenu parentMenu,
1442        string submenuName) {
1443      // Check if submenu already exists
1444      foreach (var control in parentMenu.controls) {
1445        if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu &&
1446            control.name == submenuName && control.subMenu != null) {
1447          return control.subMenu;
1448        }
1449      }
1450
1451      // Create new submenu
1452      var newSubmenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
1453      newSubmenu.name = submenuName;
1454      newSubmenu.controls = new List<VRCExpressionsMenu.Control>();
1455
1456      var newControl = new VRCExpressionsMenu.Control{
1457        name = submenuName,
1458        type = VRCExpressionsMenu.Control.ControlType.SubMenu,
1459        subMenu = newSubmenu
1460      };
1461      parentMenu.controls.Add(newControl);
1462
1463      return newSubmenu;
1464    }
1465
1466    private static void GenerateVRChatAssets(
1467        List<ToggleSpec> toggleSpecs,
1468        VRCExpressionParameters vrcParams,
1469        VRCExpressionsMenu vrcMenu
1470        ) {
1471      var uniqueToggles = toggleSpecs
1472        .Where(t => t.GetParameterName() != "YOTS_Weight")
1473        .GroupBy(t => t.GetParameterName())
1474        .Select(g => g.First())
1475        .ToList();
1476
1477      // Update parameters
1478      var paramList = new List<VRCExpressionParameters.Parameter>();
1479      paramList.AddRange(vrcParams.parameters.Where(p => !uniqueToggles.Any(t => t.GetParameterName() == p.name)));
1480      foreach (var toggle in uniqueToggles) {
1481        string paramName = toggle.GetParameterName();
1482        paramList.Add(new VRCExpressionParameters.Parameter{
1483          name = paramName,
1484          valueType = toggle.type == "radial" ? VRCExpressionParameters.ValueType.Float : VRCExpressionParameters.ValueType.Bool,
1485          defaultValue = toggle.defaultValue,
1486          saved = toggle.saved,
1487          networkSynced = toggle.synced
1488        });
1489      }
1490      vrcParams.parameters = paramList.ToArray();
1491
1492      // Add toggles to menu (skipping those with disableMenuEntry=true)
1493      foreach (var toggle in toggleSpecs) {
1494        // Skip creating menu entries for toggles with disableMenuEntry=true
1495        if (toggle.disableMenuEntry)
1496          continue;
1497
1498        VRCExpressionsMenu currentMenu = vrcMenu;
1499
1500        // Navigate or create menu path if specified
1501        if (!string.IsNullOrEmpty(toggle.menuPath)) {
1502          string trimmedPath = toggle.menuPath.Trim('/');
1503          if (!string.IsNullOrEmpty(trimmedPath)) {
1504            var sections = trimmedPath.Split('/');
1505            foreach (var section in sections) {
1506              currentMenu = GetOrCreateSubmenu(currentMenu, section);
1507            }
1508          }
1509        }
1510
1511        // Add toggle control - use toggle.name for display but paramName for the parameter
1512        string paramName = toggle.GetParameterName();
1513        if (toggle.type == "radial") {
1514          currentMenu.controls.Add(new VRCExpressionsMenu.Control{
1515            name = toggle.name,
1516            type = VRCExpressionsMenu.Control.ControlType.RadialPuppet,
1517            subParameters = new VRCExpressionsMenu.Control.Parameter[]{
1518              new VRCExpressionsMenu.Control.Parameter { name = paramName }
1519            }
1520          });
1521        } else {
1522          currentMenu.controls.Add(new VRCExpressionsMenu.Control{
1523            name = toggle.name,
1524            type = VRCExpressionsMenu.Control.ControlType.Toggle,
1525            parameter = new VRCExpressionsMenu.Control.Parameter { name = paramName },
1526            value = 1f
1527          });
1528        }
1529      }
1530    }
1531
1532    private static AnimationClip MirrorAnimationClipInMemory(AnimationClip sourceClip) {
1533        if (sourceClip == null) {
1534            Debug.LogError("Cannot mirror a null AnimationClip.");
1535            return null;
1536        }
1537
1538        // Create a new clip instance in memory
1539        AnimationClip mirroredClip = new AnimationClip();
1540        // Set a base name; the calling code will set a more specific final name
1541        mirroredClip.name = sourceClip.name + "_Mirrored_InMemory";
1542
1543        EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(sourceClip);
1544
1545        foreach (var binding in bindings) {
1546            // Curves are value types (structs), copying them is fine.
1547            AnimationCurve curve = AnimationUtility.GetEditorCurve(sourceClip, binding);
1548            if (curve == null) continue;
1549
1550            EditorCurveBinding mirroredBinding = binding; // Start with original
1551
1552            // 1. Mirror Path
1553            string mirroredPath = binding.path;
1554            mirroredPath = Regex.Replace(mirroredPath, @"\bLeft\b", "TEMP_RIGHT_MARKER");
1555            mirroredPath = Regex.Replace(mirroredPath, @"\bRight\b", "Left");
1556            mirroredPath = mirroredPath.Replace("TEMP_RIGHT_MARKER", "Right");
1557
1558            mirroredPath = Regex.Replace(mirroredPath, @"\.L\b", ".TEMP_R_MARKER");
1559            mirroredPath = Regex.Replace(mirroredPath, @"\.R\b", ".L");
1560            mirroredPath = mirroredPath.Replace(".TEMP_R_MARKER", ".R");
1561
1562            mirroredPath = Regex.Replace(mirroredPath, @"_L\b", "_TEMP_R_MARKER");
1563            mirroredPath = Regex.Replace(mirroredPath, @"_R\b", "_L");
1564            mirroredPath = mirroredPath.Replace("_TEMP_R_MARKER", "_R");
1565
1566            mirroredBinding.path = mirroredPath;
1567
1568            // 2. Mirror Property Name
1569            string mirroredPropertyName = binding.propertyName;
1570            mirroredPropertyName = Regex.Replace(mirroredPropertyName, @"\bLeft\b", "TEMP_RIGHT_MARKER");
1571            mirroredPropertyName = Regex.Replace(mirroredPropertyName, @"\bRight\b", "Left");
1572            mirroredPropertyName = mirroredPropertyName.Replace("TEMP_RIGHT_MARKER", "Right");
1573            mirroredBinding.propertyName = mirroredPropertyName;
1574
1575            Debug.Log($"Saw binding: {binding.path} // {binding.propertyName}");
1576
1577            // 3. Mirror Curve Values
1578            bool valueNeedsNegating = false;
1579            if (binding.propertyName == "m_LocalPosition.x") valueNeedsNegating = true;
1580            if (binding.propertyName == "m_LocalRotation.y" || binding.propertyName == "m_LocalRotation.z") valueNeedsNegating = true;
1581            if (binding.propertyName == "localEulerAnglesRaw.y" || binding.propertyName == "localEulerAnglesRaw.z") valueNeedsNegating = true;
1582            if (binding.propertyName == "m_LocalScale.x") valueNeedsNegating = true;
1583
1584            if (valueNeedsNegating) {
1585                Keyframe[] keys = curve.keys;
1586                for (int i = 0; i < keys.Length; i++) {
1587                    keys[i].value *= -1f;
1588                    keys[i].inTangent *= -1f;
1589                    keys[i].outTangent *= -1f;
1590                }
1591                // Create a new curve with modified keys, as AnimationCurve is a class but behaves like a value type here.
1592                curve = new AnimationCurve(keys);
1593            }
1594
1595            // Set the potentially modified curve on the mirrored clip
1596            AnimationUtility.SetEditorCurve(mirroredClip, mirroredBinding, curve);
1597        }
1598
1599        // Return the clip object without saving it
1600        return mirroredClip;
1601    }
1602  }
1603}
1604
1605#endif  // UNITY_EDITOR