diff --git a/PresetGenerator.meta b/PresetGenerator.meta new file mode 100644 index 0000000..4a31411 --- /dev/null +++ b/PresetGenerator.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b6821c8a35123384ea2040a70b8544c5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/PresetGenerator/Editor.meta b/PresetGenerator/Editor.meta new file mode 100644 index 0000000..6449718 --- /dev/null +++ b/PresetGenerator/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6b73bb805dd5f4547b50c99d7eb519a0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/PresetGenerator/Editor/PresetGenerator.asmdef b/PresetGenerator/Editor/PresetGenerator.asmdef new file mode 100644 index 0000000..88760e5 --- /dev/null +++ b/PresetGenerator/Editor/PresetGenerator.asmdef @@ -0,0 +1,25 @@ +{ + "name": "PresetGenerator", + "rootNamespace": "", + "references": [ + "VRC.SDK3A", + "VRC.SDK3A.Editor", + "PresetGeneratorRuntime", + "AnimatorAsCode.V1", + "AnimatorAsCode.V1.VRChat", + "AnimatorAsCode.V1.ModularAvatar", + "nadena.dev.ndmf", + "nadena.dev.ndmf.vrchat" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/PresetGenerator/Editor/PresetGenerator.asmdef.meta b/PresetGenerator/Editor/PresetGenerator.asmdef.meta new file mode 100644 index 0000000..5017138 --- /dev/null +++ b/PresetGenerator/Editor/PresetGenerator.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: db37b7b64d44e2442ae3aed7b0759d8f +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/PresetGenerator/Editor/PresetGenerator.cs b/PresetGenerator/Editor/PresetGenerator.cs new file mode 100644 index 0000000..ff00454 --- /dev/null +++ b/PresetGenerator/Editor/PresetGenerator.cs @@ -0,0 +1,136 @@ +#if UNITY_EDITOR +using UnityEngine; +using System.Collections.Generic; +using System.IO; +using VRC.SDK3.Editor; +using VRC.SDK3.Avatars.ScriptableObjects; +using UnityEditor.Animations; +using System; +using VRC.SDKBase; +using nadena.dev.ndmf; +using gay.lilyy.PresetGenerator.Editor; +using AnimatorAsCode.V1; +using VRC.SDK3.Avatars.Components; +using AnimatorAsCode.V1.ModularAvatar; +using UnityEditor; +using AnimatorAsCode.V1.VRC; +using Mono.Cecil.Cil; +using NUnit.Framework; + +[assembly: ExportsPlugin(typeof(ParameterPresetPlugin))] + +namespace gay.lilyy.PresetGenerator.Editor +{ + public class ParameterPresetPlugin : Plugin + { + public override string QualifiedName => "gay.lilyy.presetgenerator"; + public override string DisplayName => "Preset Generator"; + + private const string SystemName = "PresetGenerator"; + private const bool UseWriteDefaults = true; + protected override void Configure() + { + InPhase(BuildPhase.Generating).Run($"Generate {DisplayName}", Generate); + } + private void Generate(BuildContext ctx) + { + // Find all components of type ExampleToggle in this avatar. + var presets = ctx.AvatarRootTransform.GetComponents(); + if (presets.Length == 0) return; // If there are none in the avatar, skip this entirely. + + // Initialize Animator As Code. + var aac = AacV1.Create(new AacConfiguration + { + SystemName = SystemName, + AnimatorRoot = ctx.AvatarRootTransform, + DefaultValueRoot = ctx.AvatarRootTransform, + AssetKey = GUID.Generate().ToString(), + AssetContainer = ctx.AssetContainer, + ContainerMode = AacConfiguration.Container.OnlyWhenPersistenceRequired, + // (For AAC 1.2.0 and above) The next line is recommended starting from NDMF 1.6.0. + // If you use a lower version of NDMF or if you don't use it, remove that line. + AssetContainerProvider = new NDMFContainerProvider(ctx), + // States will be created with Write Defaults set to ON or OFF based on whether UseWriteDefaults is true or false. + DefaultsProvider = new AacDefaultsProvider(UseWriteDefaults) + }); + + // Create a new animator controller. + // This will be merged with the rest of the playable layer at the end of this function. + var ctrl = aac.NewAnimatorController(); + var layer = ctrl.NewLayer("Presets"); + var presetParam = layer.IntParameter("Preset"); + Dictionary> intParams = new(); + Dictionary> floatParams = new(); + Dictionary> boolParams = new(); + var emptyClip = aac.NewClip(); + var idle = layer.NewState("Idle").WithAnimation(emptyClip); + + var presetIndex = 0; + foreach (var preset in presets) + { + presetIndex++; + var state = layer.NewState("Preset " + preset.Name) + .WithAnimation(emptyClip); + + foreach (var param in preset.Parameters) + { + if (!param.shouldChange) continue; + switch (param.valueType) + { + case VRCExpressionParameters.ValueType.Int: + { + if (!intParams.ContainsKey(param.name)) + { + intParams[param.name] = layer.IntParameter(param.name); + } + var layerParam = intParams[param.name]; + state.Drives(layerParam, (int)param.setTo); + break; + } + case VRCExpressionParameters.ValueType.Float: + { + if (!floatParams.ContainsKey(param.name)) + { + floatParams[param.name] = layer.FloatParameter(param.name); + } + var layerParam = floatParams[param.name]; + state.Drives(layerParam, param.setTo); + break; + } + case VRCExpressionParameters.ValueType.Bool: + { + if (!boolParams.ContainsKey(param.name)) + { + boolParams[param.name] = layer.BoolParameter(param.name); + } + var layerParam = boolParams[param.name]; + state.Drives(layerParam, param.setTo > 0.5f); + break; + } + } + } + + idle.TransitionsTo(state).When(presetParam.IsEqualTo(presetIndex)); + state.TransitionsTo(idle).When(presetParam.IsEqualTo(0)); + } + // Create a new object in the scene. We will add Modular Avatar components inside it. + var modularAvatar = MaAc.Create(new GameObject(SystemName) + { + transform = { parent = ctx.AvatarRootTransform } + }); + + // By creating a Modular Avatar Merge Animator component, + // our animator controller will be added to the avatar's FX layer. + modularAvatar.NewMergeAnimator(ctrl.AnimatorController, VRCAvatarDescriptor.AnimLayerType.FX); + } + } + internal class NDMFContainerProvider : IAacAssetContainerProvider + { + private readonly BuildContext _ctx; + public NDMFContainerProvider(BuildContext ctx) => _ctx = ctx; + public void SaveAsPersistenceRequired(UnityEngine.Object objectToAdd) => _ctx.AssetSaver.SaveAsset(objectToAdd); + public void SaveAsRegular(UnityEngine.Object objectToAdd) { } // Let NDMF crawl our assets when it finishes + public void ClearPreviousAssets() { } // ClearPreviousAssets is never used in non-destructive contexts + } +} +#endif diff --git a/PresetGenerator/Editor/PresetGenerator.cs.meta b/PresetGenerator/Editor/PresetGenerator.cs.meta new file mode 100644 index 0000000..5e6b864 --- /dev/null +++ b/PresetGenerator/Editor/PresetGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bee19258cf09a8b469dd38850dcb5138 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/PresetGenerator/Editor/PresetGeneratorInspector.cs b/PresetGenerator/Editor/PresetGeneratorInspector.cs new file mode 100644 index 0000000..f9ffc3c --- /dev/null +++ b/PresetGenerator/Editor/PresetGeneratorInspector.cs @@ -0,0 +1,168 @@ +using UnityEditor; +using UnityEngine; +using System.Collections.Generic; +using System.Linq; +using VRC.SDK3.Avatars.ScriptableObjects; +using VRC.SDK3.Avatars.Components; + +namespace gay.lilyy.PresetGenerator +{ + [CustomEditor(typeof(ParameterPreset))] + public class ParameterPresetInspector : UnityEditor.Editor + { + private ParameterPreset preset; + private bool parametersLoaded = false; + + private void OnEnable() + { + preset = (ParameterPreset)target; + LoadMissingParameters(); + } + + private void LoadMissingParameters() + { + VRCAvatarDescriptor aviDesc = preset.gameObject.GetComponent(); + if (aviDesc.expressionParameters == null || parametersLoaded) + return; + + // Initialize Parameters list if it's null + if (preset.Parameters == null) + preset.Parameters = new List(); + + // Get parameters from the Group's ParametersFile + var sourceParameters = aviDesc.expressionParameters.parameters; + if (sourceParameters == null) + return; + + // Create a dictionary of source parameters for quick lookup + var sourceParamDict = sourceParameters.ToDictionary(p => p.name, p => p); + + // Create a set of source parameter names + var sourceParameterNames = new HashSet(sourceParamDict.Keys); + + // Remove parameters that exist in preset but not in source + bool hasChanges = false; + preset.Parameters.RemoveAll(presetParam => + { + if (!sourceParameterNames.Contains(presetParam.name)) + { + hasChanges = true; + return true; // Remove this parameter + } + return false; // Keep this parameter + }); + + // Add missing parameters and update existing ones + foreach (var sourceParam in sourceParameters) + { + var existingParam = preset.Parameters.FirstOrDefault(p => p.name == sourceParam.name); + + if (existingParam == null) + { + // Add new parameter + var clonedParam = new PresetParameter + { + name = sourceParam.name, + valueType = sourceParam.valueType, + setTo = sourceParam.defaultValue, + shouldChange = false, + }; + + preset.Parameters.Add(clonedParam); + hasChanges = true; + } + else + { + // Update existing parameter to match source type + if (existingParam.valueType != sourceParam.valueType) + { + existingParam.valueType = sourceParam.valueType; + hasChanges = true; + } + } + } + + if (hasChanges) + { + EditorUtility.SetDirty(preset); + Debug.Log($"Updated parameters in preset '{preset.Name}' to match source parameters file"); + } + + parametersLoaded = true; + } + + public override void OnInspectorGUI() + { + // Ensure parameters are loaded when inspector is drawn + LoadMissingParameters(); + + // Draw the default inspector + DrawDefaultInspector(); + + // Custom parameter list rendering + if (preset.Parameters != null && preset.Parameters.Count > 0) + { + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Parameters", EditorStyles.boldLabel); + + for (int i = 0; i < preset.Parameters.Count; i++) + { + var param = preset.Parameters[i]; + EditorGUILayout.BeginHorizontal(); + + // Parameter name + EditorGUILayout.LabelField(param.name, GUILayout.Width(150)); + + // Parameter type (read-only) + EditorGUILayout.LabelField($"Type: {param.valueType}", GUILayout.Width(100)); + + // Should change toggle + param.shouldChange = EditorGUILayout.Toggle(param.shouldChange, GUILayout.Width(60)); + + // Set to value (only show if shouldChange is true) + if (param.shouldChange) + { + switch (param.valueType) + { + case VRCExpressionParameters.ValueType.Bool: + param.setTo = EditorGUILayout.Toggle(param.setTo > 0.5f) ? 1f : 0f; + break; + case VRCExpressionParameters.ValueType.Int: + param.setTo = EditorGUILayout.IntField((int)param.setTo); + break; + case VRCExpressionParameters.ValueType.Float: + param.setTo = EditorGUILayout.FloatField(param.setTo); + break; + } + } + else + { + EditorGUILayout.LabelField("(unchanged)", GUILayout.Width(80)); + } + + EditorGUILayout.EndHorizontal(); + } + } + + // Add a button to manually reload parameters + EditorGUILayout.Space(); + if (GUILayout.Button("Reload Parameters from Group")) + { + parametersLoaded = false; + LoadMissingParameters(); + } + + // Add a button to clear all parameters + if (GUILayout.Button("Clear All Parameters")) + { + if (EditorUtility.DisplayDialog("Clear Parameters", + "Are you sure you want to clear all parameters from this preset?", + "Yes", "No")) + { + preset.Parameters.Clear(); + EditorUtility.SetDirty(preset); + } + } + } + } +} \ No newline at end of file diff --git a/PresetGenerator/Editor/PresetGeneratorInspector.cs.meta b/PresetGenerator/Editor/PresetGeneratorInspector.cs.meta new file mode 100644 index 0000000..7a6ab7f --- /dev/null +++ b/PresetGenerator/Editor/PresetGeneratorInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 51ac4f0bd9fb2f74e94a980e6acd756f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/PresetGenerator/Runtime.meta b/PresetGenerator/Runtime.meta new file mode 100644 index 0000000..1cd03ac --- /dev/null +++ b/PresetGenerator/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1a56a58893574df45b8e4032a47fbc13 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/PresetGenerator/Runtime/ParameterPreset.cs b/PresetGenerator/Runtime/ParameterPreset.cs new file mode 100644 index 0000000..c128282 --- /dev/null +++ b/PresetGenerator/Runtime/ParameterPreset.cs @@ -0,0 +1,21 @@ + +using System.Collections.Generic; +using UnityEngine; +using VRC.SDK3.Avatars.ScriptableObjects; +using VRC.SDKBase; +namespace gay.lilyy.PresetGenerator +{ + [System.Serializable] + public class PresetParameter { + public string name; + public VRCExpressionParameters.ValueType valueType; + public float setTo; + public bool shouldChange = false; + } +public class ParameterPreset : MonoBehaviour, IEditorOnly { + public string Name = "New Preset"; + + [HideInInspector] // custom inspector + public List Parameters; + } +} \ No newline at end of file diff --git a/PresetGenerator/Runtime/ParameterPreset.cs.meta b/PresetGenerator/Runtime/ParameterPreset.cs.meta new file mode 100644 index 0000000..bf0fb3e --- /dev/null +++ b/PresetGenerator/Runtime/ParameterPreset.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 708f2c2e0c8172048b9c1f1b19705dc6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/PresetGenerator/Runtime/PresetGeneratorRuntime.asmdef b/PresetGenerator/Runtime/PresetGeneratorRuntime.asmdef new file mode 100644 index 0000000..3d790fd --- /dev/null +++ b/PresetGenerator/Runtime/PresetGeneratorRuntime.asmdef @@ -0,0 +1,16 @@ +{ + "name": "PresetGeneratorRuntime", + "rootNamespace": "", + "references": [ + "VRC.SDK3A" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/PresetGenerator/Runtime/PresetGeneratorRuntime.asmdef.meta b/PresetGenerator/Runtime/PresetGeneratorRuntime.asmdef.meta new file mode 100644 index 0000000..f8f5b28 --- /dev/null +++ b/PresetGenerator/Runtime/PresetGeneratorRuntime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d3fbd56666ff1ee4da8101d622364046 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: