diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..c1f5974 --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 789a1325dbb0b3e48be672f6e2cf7d57 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Core.cs b/Editor/Core.cs new file mode 100644 index 0000000..1b9076a --- /dev/null +++ b/Editor/Core.cs @@ -0,0 +1,287 @@ +using System.Linq; +using System.Diagnostics; +using AnimatorAsCode.V1; +using UnityEditor; +using UnityEngine; +using VRC.SDK3.Avatars.Components; +using VRC.SDK3.Dynamics.Constraint.Components; +using System.Collections.Generic; +using UnityEditor.Animations; + + +namespace gay.lilyy.SoldAvatarBootstrap +{ + + + + public class AvatarAssets + { + public AacFlBase aac; + public GameObject root; + public AacFlController fx; + public AacFlClip emptyClip; + public bool experimentalEnabled; + public bool isPC; + } + + public static class AvatarUtils + { + public static GameObject FindChildRecursive(GameObject parent, string name) + { + return FindChildRecursive(parent.transform, name)?.gameObject; + } + public static Transform FindChildRecursive(Transform parent, string name) + { + return parent.GetComponentsInChildren(true).FirstOrDefault(t => t.gameObject.name == name); + } + public static Transform[] FindChildrenRecursive(Transform parent, string name) + { + return parent.GetComponentsInChildren(true).Where(t => t.gameObject.name == name).ToArray(); + } + + public static AacFlClip CreateConstraintWeightClip(AvatarAssets assets, VRCParentConstraint constraint, int index, int indexCount) + { + return assets.aac.NewClip() + .Animating(action => + { + action.Animates(constraint, $"Sources.source{index}.Weight").WithOneFrame(1f); + for (int i = 0; i < indexCount; i++) + { + if (i == index) continue; + action.Animates(constraint, $"Sources.source{i}.Weight").WithOneFrame(0f); + } + }); + } + + public static AacFlClip CreateEmptyClipWithFrames(AvatarAssets assets, int frames) + { + if (frames == 0) return assets.emptyClip; + var emptyGo = new GameObject(); + emptyGo.name = "_EmptyClipInstant"; + emptyGo.transform.SetParent(assets.root.transform); + var clip = assets.aac.NewClip().Animating(action => + { + action.Animates(emptyGo.transform, "m_LocalScale.z").WithFrameCountUnit(unit => + { + unit.Constant(0, 0); + unit.Constant(frames, 1); + }); + }); + Object.DestroyImmediate(emptyGo); + return clip; + } + + public static AacFlClip CreateEmptyClipWithSeconds(AvatarAssets assets, float seconds) + { + if (seconds == 0) return assets.emptyClip; + var emptyGo = new GameObject(); + emptyGo.name = "_EmptyClipInstant"; + emptyGo.transform.SetParent(assets.root.transform); + var clip = assets.aac.NewClip().Animating(action => + { + action.Animates(emptyGo.transform, "m_LocalScale.z").WithFrameCountUnit(unit => + { + unit.Constant(0, 0); + unit.Constant(seconds, 1); + }); + }); + Object.DestroyImmediate(emptyGo); + return clip; + } + + public static List FindWithBlendshape(AvatarAssets assets, string shapeName) + { + var list = new List(); + foreach (var smr in assets.root.GetComponentsInChildren(true)) + { + var mesh = smr.sharedMesh; + if (!mesh) continue; + + int count = mesh.blendShapeCount; + for (int i = 0; i < count; i++) + { + if (mesh.GetBlendShapeName(i) == shapeName) + { + list.Add(smr); + break; + } + } + } + return list; + } + + public static List FindNamedComponents(Transform parent, string name) where T : Component + { + var list = new List(); + foreach (var component in parent.GetComponentsInChildren(true)) + { + if (component.gameObject.name == name) + { + list.Add(component); + } + } + UnityEngine.Debug.Log($"Found {list.Count} {name} components"); + foreach (var component in list) + { + UnityEngine.Debug.Log($"Component: {component.gameObject.name}"); + } + return list; + } + + public static List FindNamedComponents(AvatarAssets assets, string name) where T : Component + { + return FindNamedComponents(assets.root.transform, name); + } + } + + + public class AvatarBootstrapCore + { + + private static AnimatorController GetController(AvatarDefinition definition) + { + string metaPath = definition.FXLayerPath + ".meta"; + string originalGuid = null; + if (System.IO.File.Exists(metaPath)) + { + foreach (var line in System.IO.File.ReadAllLines(metaPath)) + { + if (line.StartsWith("guid: ")) + { + originalGuid = line.Substring(6).Trim(); + break; + } + } + } + if (AssetDatabase.LoadAssetAtPath(definition.FXLayerPath) != null) + { + AssetDatabase.DeleteAsset(definition.FXLayerPath); + AssetDatabase.Refresh(); + } + var newController = AnimatorController.CreateAnimatorControllerAtPath(definition.FXLayerPath); + if (originalGuid != null && System.IO.File.Exists(metaPath)) + { + var lines = System.IO.File.ReadAllLines(metaPath); + for (int i = 0; i < lines.Length; ++i) + { + if (lines[i].StartsWith("guid: ")) + { + lines[i] = "guid: " + originalGuid; + } + } + System.IO.File.WriteAllLines(metaPath, lines); + AssetDatabase.Refresh(); + } + + return AssetDatabase.LoadAssetAtPath(definition.FXLayerPath); + } + public static void RunForCurrentAvatar(AvatarDefinition definition) + { + + var allLayerGroups = LayerGroup.Instances.ToList(); + var layerGroups = allLayerGroups + .Where(lg => lg.TargetDefinitions.Contains(definition)) + .ToList(); + + // Start overall timer + var overallStopwatch = Stopwatch.StartNew(); + + AvatarLogger.LogInfo("Starting SoldAvatarBootstrap generation for definition: " + definition.DisplayName); + GameObject root = null; + foreach (var candidate in GameObject.FindObjectsOfType(true)) + { + if (definition.IsApplicable(candidate.gameObject)) + { + AvatarLogger.LogInfo($"Found suitable avatar: {candidate.gameObject.name}"); + root = candidate.gameObject; + break; + } + } + if (root == null) + { + AvatarLogger.LogError($"No suitable avatar found for definition: {definition.DisplayName}"); + return; + } + + // Skip if the root GameObject or the component itself is disabled + if (!root.activeSelf || !root.gameObject.activeInHierarchy) + { + AvatarLogger.LogWarning($"Avatar root '{root.name}' is disabled or inactive. Skipping."); + return; + } + + AvatarAssets assets = new(); + assets.root = root; + + assets.isPC = EditorUserBuildSettings.activeBuildTarget == BuildTarget.StandaloneWindows + || EditorUserBuildSettings.activeBuildTarget == BuildTarget.StandaloneWindows64; + + + + // Time AAC initialization + var initStopwatch = Stopwatch.StartNew(); + AvatarLogger.LogInfo("Initializing Animator As Code..."); + assets.aac = AacV1.Create(new AacConfiguration + { + SystemName = definition.SystemName, + AnimatorRoot = root.transform, + DefaultValueRoot = root.transform, + AssetKey = GUID.Generate().ToString(), + AssetContainer = GetController(definition), + ContainerMode = AacConfiguration.Container.Everything, + DefaultsProvider = new AacDefaultsProvider(true) + }); + assets.emptyClip = assets.aac.NewClip(); + + assets.fx = assets.aac.NewAnimatorController(); + initStopwatch.Stop(); + AvatarLogger.LogInfo($"AAC initialization completed in {initStopwatch.ElapsedMilliseconds}ms"); + + // Process layer groups with individual timing + // Filter out the template LayerGroup (by SystemName) + // AACLogger.LogInfo($"Processing {layerGroups.Count} layer groups..."); + + AvatarLogger.LogInfo($"Processing {layerGroups.Count} layer groups..."); + var totalLayerGroupTime = 0L; + var processedCount = 0; + var skippedCount = 0; + + foreach (var layerGroup in layerGroups) + { + var layerStopwatch = Stopwatch.StartNew(); + + bool skipped = false; + + if (!layerGroup.enabled) + { + AvatarLogger.LogWarning($"Skipping layer group: {layerGroup.DisplayName} (disabled)"); + skipped = true; + } + + if (!skipped) + { + AvatarLogger.LogInfo($"Running layer group: {layerGroup.DisplayName}"); + layerGroup.Run(assets); + processedCount++; + } + else + { + skippedCount++; + } + + layerStopwatch.Stop(); + totalLayerGroupTime += layerStopwatch.ElapsedMilliseconds; + AvatarLogger.LogInfo($"Layer group '{layerGroup.DisplayName}' completed in {layerStopwatch.ElapsedMilliseconds}ms"); + } + + // Final timing summary + overallStopwatch.Stop(); + AvatarLogger.LogSuccess($"SoldAvatarBootstrap generation completed successfully!"); + AvatarLogger.LogInfo($"=== TIMING SUMMARY ==="); + AvatarLogger.LogInfo($"Total time: {overallStopwatch.ElapsedMilliseconds}ms ({overallStopwatch.Elapsed.TotalSeconds:F2}s)"); + AvatarLogger.LogInfo($"AAC initialization: {initStopwatch.ElapsedMilliseconds}ms"); + AvatarLogger.LogInfo($"Layer groups: {totalLayerGroupTime}ms (processed: {processedCount}, skipped: {skippedCount})"); + AvatarLogger.LogInfo($"Average per layer group: {(processedCount > 0 ? totalLayerGroupTime / processedCount : 0)}ms"); + } + } +} \ No newline at end of file diff --git a/Editor/Core.cs.meta b/Editor/Core.cs.meta new file mode 100644 index 0000000..ac7ffa8 --- /dev/null +++ b/Editor/Core.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 646700d2b124c3e4daf4532e7046f849 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Definition.cs b/Editor/Definition.cs new file mode 100644 index 0000000..c8892a1 --- /dev/null +++ b/Editor/Definition.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace gay.lilyy.SoldAvatarBootstrap +{ + public abstract class AvatarDefinition + { + private static readonly List instances = new(); + + protected AvatarDefinition() => instances.Add(this); + + public static IEnumerable Instances => instances; + + public abstract string DisplayName { get; } + public abstract string SystemName { get; } + public abstract string FXLayerPath { get; } + + public abstract bool IsApplicable(GameObject avatarRoot); + } +} \ No newline at end of file diff --git a/Editor/Definition.cs.meta b/Editor/Definition.cs.meta new file mode 100644 index 0000000..5568c67 --- /dev/null +++ b/Editor/Definition.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 04c641dd53bc5ac49b4e0c1cabab4a63 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/LayerGroup.cs b/Editor/LayerGroup.cs new file mode 100644 index 0000000..33446a5 --- /dev/null +++ b/Editor/LayerGroup.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace gay.lilyy.SoldAvatarBootstrap +{ + public abstract class LayerGroup + { + public virtual bool enabled { get { return true; } } + private AvatarLayerGroupLogger _logger; + + protected AvatarLayerGroupLogger Logger => _logger ??= new AvatarLayerGroupLogger(SystemName); + + private static readonly List instances = new(); + + protected LayerGroup() => instances.Add(this); + + public static IEnumerable Instances => instances; + + public abstract string DisplayName { get; } + public abstract string SystemName { get; } + public abstract AvatarDefinition[] TargetDefinitions { get; } + + public abstract void Run(AvatarAssets assets); + } +} \ No newline at end of file diff --git a/Editor/LayerGroup.cs.meta b/Editor/LayerGroup.cs.meta new file mode 100644 index 0000000..2d6c2d5 --- /dev/null +++ b/Editor/LayerGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4fcc5c3f7007f9244aebe2f88702d429 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Logger.cs b/Editor/Logger.cs new file mode 100644 index 0000000..4a65544 --- /dev/null +++ b/Editor/Logger.cs @@ -0,0 +1,92 @@ +using UnityEngine; + +namespace gay.lilyy.SoldAvatarBootstrap +{ + public static class AvatarLogger + { + private const string Prefix = "[SoldAvatarBootstrap]"; + + public static bool EnableDebug { get; set; } = false; + + public static void Log(string message) + { + Debug.Log(Format(message)); + } + + public static void LogDebug(string message) + { + if (!EnableDebug) return; + Debug.Log(Format("DEBUG: " + message)); + } + + public static void LogInfo(string message) + { + Debug.Log(Format(message)); + } + + public static void LogWarning(string message) + { + Debug.LogWarning(Format(message)); + } + + public static void LogError(string message) + { + Debug.LogError(Format(message)); + } + + public static void LogSuccess(string message) + { + Debug.Log(Format("✅ " + message)); + } + + private static string Format(string message) + { + return string.IsNullOrEmpty(message) ? Prefix : $"{Prefix} {message}"; + } + } + + public sealed class AvatarLayerGroupLogger + { + private readonly string _groupName; + + public AvatarLayerGroupLogger(string groupName) + { + _groupName = groupName; + } + + public void Log(string message) + { + AvatarLogger.Log(Format(message)); + } + + public void LogDebug(string message) + { + AvatarLogger.LogDebug(Format(message)); + } + + public void LogInfo(string message) + { + AvatarLogger.LogInfo(Format(message)); + } + + public void LogWarning(string message) + { + AvatarLogger.LogWarning(Format(message)); + } + + public void LogError(string message) + { + AvatarLogger.LogError(Format(message)); + } + + public void LogSuccess(string message) + { + AvatarLogger.LogSuccess(Format(message)); + } + + private string Format(string message) + { + return string.IsNullOrEmpty(message) ? $"[{_groupName}]" : $"[{_groupName}] {message}"; + } + } +} diff --git a/Editor/Logger.cs.meta b/Editor/Logger.cs.meta new file mode 100644 index 0000000..2d7c401 --- /dev/null +++ b/Editor/Logger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2ad5c99a61550aa4cbf010a7d834cb67 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/SoldAvatarBootstrap.Editor.asmdef b/Editor/SoldAvatarBootstrap.Editor.asmdef new file mode 100644 index 0000000..2017d74 --- /dev/null +++ b/Editor/SoldAvatarBootstrap.Editor.asmdef @@ -0,0 +1,21 @@ +{ + "name": "SoldAvatarBootstrap.Editor", + "rootNamespace": "", + "references": [ + "GUID:5718fb738711cd34ea54e9553040911d", + "GUID:b906909fcc54f634db50f2cad0f988d9", + "GUID:d689052aa981bf8459346a530f6e6678", + "GUID:71d9dcc7d30ab1c45866d01afa59b6cf" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Editor/SoldAvatarBootstrap.Editor.asmdef.meta b/Editor/SoldAvatarBootstrap.Editor.asmdef.meta new file mode 100644 index 0000000..b9fa136 --- /dev/null +++ b/Editor/SoldAvatarBootstrap.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 16dfbe6f38dd98d4aa89b7eaac50e6c8 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LICENSE.meta b/LICENSE.meta new file mode 100644 index 0000000..5d39798 --- /dev/null +++ b/LICENSE.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 545c229b037e15745b164b1dbcc4f885 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..a66919b --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e1d829a80fa35844abdc4eb785823b8d +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Template.meta b/Template.meta new file mode 100644 index 0000000..dfe9250 --- /dev/null +++ b/Template.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bd7f5cd94d343094780005eb00d6c66e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Template/Definition.cs b/Template/Definition.cs new file mode 100644 index 0000000..faa3e12 --- /dev/null +++ b/Template/Definition.cs @@ -0,0 +1,30 @@ +using UnityEditor; +using UnityEngine; + +namespace gay.lilyy.SoldAvatarBootstrap.Template +{ + [InitializeOnLoad] + public class TemplateDefinition : AvatarDefinition + { + public static readonly TemplateDefinition Instance = new(); + + static TemplateDefinition() { } + + public override string DisplayName => "Template"; + + public override string SystemName => "template"; + + public override string FXLayerPath => "Assets/_LillithRosePup/SoldAvatarBootstrap/Template/FX.controller"; + + public override bool IsApplicable(GameObject avatarRoot) + { + return true; + } + + [MenuItem("LillithRosePup/SoldAvatarBootstrap/Template")] + public static void Generate() + { + AvatarBootstrapCore.RunForCurrentAvatar(Instance); + } + } +} \ No newline at end of file diff --git a/Template/Definition.cs.meta b/Template/Definition.cs.meta new file mode 100644 index 0000000..6904e4a --- /dev/null +++ b/Template/Definition.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ea2d4a8738a3e0449830521d9f2ab669 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Template/Layer.cs b/Template/Layer.cs new file mode 100644 index 0000000..88daf05 --- /dev/null +++ b/Template/Layer.cs @@ -0,0 +1,21 @@ +using UnityEditor; + +namespace gay.lilyy.SoldAvatarBootstrap.Template +{ + [InitializeOnLoad] + public class TemplateLayer : LayerGroup + { + private static readonly TemplateLayer _instance = new(); + + static TemplateLayer() { } + + public override string DisplayName => "Template"; + + public override string SystemName => "template"; + public override AvatarDefinition[] TargetDefinitions => new[] { TemplateDefinition.Instance }; + public override void Run(AvatarAssets assets) + { + Logger.LogInfo("LayerGroup Template.Run() Called!"); + } + } +} \ No newline at end of file diff --git a/Template/Layer.cs.meta b/Template/Layer.cs.meta new file mode 100644 index 0000000..429bedb --- /dev/null +++ b/Template/Layer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8250cbd1c8378ab4592627fb1eb4b70d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Template/Template.asmdef b/Template/Template.asmdef new file mode 100644 index 0000000..0c8459a --- /dev/null +++ b/Template/Template.asmdef @@ -0,0 +1,18 @@ +{ + "name": "Template", + "rootNamespace": "", + "references": [ + "GUID:16dfbe6f38dd98d4aa89b7eaac50e6c8" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Template/Template.asmdef.meta b/Template/Template.asmdef.meta new file mode 100644 index 0000000..1841afd --- /dev/null +++ b/Template/Template.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ab920be56a4be1049a14744810032574 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: