Bring in AAC!

project ive been working on for a while, just felt confident enough to move it here
This commit is contained in:
Lillith Rose 2025-12-09 21:40:28 -05:00
parent e608e2a56b
commit 1d7052a258
209 changed files with 1561 additions and 74738 deletions

8
AAC/AACCore.meta Normal file
View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: da7e8c9875c7937418ca83b4ee80e8bc
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
AAC/AACCore/Editor.meta Normal file
View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4d93bb3c8aebb5a4980d314c9c09afb7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,300 @@
using System.Linq;
using System.Diagnostics;
using AnimatorAsCode.V1;
using gay.lilyy.aaccore;
using nadena.dev.ndmf;
using AnimatorAsCode.V1.ModularAvatar;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Dynamics.Constraint.Components;
using System.Collections.Generic;
using gay.lilyy.EditorNotes;
[assembly: ExportsPlugin(typeof(AACPlugin))]
namespace gay.lilyy.aaccore
{
public class AACAssets
{
public AacFlBase aac;
public AacFlController fx;
public AacFlClip emptyClip;
public BuildContext ctx;
public AACRoot root;
public MaAc modularAvatar;
public bool experimentalEnabled;
public bool isPC;
}
public static class AACUtils
{
public static GameObject FindChildRecursive(GameObject parent, string name)
{
return FindChildRecursive(parent.transform, name)?.gameObject;
}
public static Transform FindChildRecursive(Transform parent, string name)
{
Transform childTransform = parent.GetComponentsInChildren<Transform>()
.FirstOrDefault(t => t.gameObject.name == name);
return childTransform;
}
public static AacFlClip CreateConstraintWeightClip(AACAssets 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(AACAssets assets, int frames) {
if (frames == 0) return assets.emptyClip;
var emptyGo = new GameObject();
emptyGo.name = "_EmptyClipInstant";
emptyGo.transform.SetParent(assets.ctx.AvatarRootTransform);
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(AACAssets assets, float seconds) {
if (seconds == 0) return assets.emptyClip;
var emptyGo = new GameObject();
emptyGo.name = "_EmptyClipInstant";
emptyGo.transform.SetParent(assets.ctx.AvatarRootTransform);
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<SkinnedMeshRenderer> FindWithBlendshape(AACAssets assets, string shapeName) {
var list = new List<SkinnedMeshRenderer>();
foreach (var smr in assets.ctx.AvatarRootObject.GetComponentsInChildren<SkinnedMeshRenderer>(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<T> FindNamedComponents<T>(Transform parent, string name) where T : Component {
var list = new List<T>();
foreach (var component in parent.GetComponentsInChildren<T>(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<T> FindNamedComponents<T>(AACAssets assets, string name) where T : Component {
return FindNamedComponents<T>(assets.ctx.AvatarRootTransform, name);
}
public static List<Transform> FindTransformWithNote(Transform parent, string tag) {
var notes = parent.GetComponentsInChildren<EditorNote>(true);
var matched = new List<Transform>();
foreach (var note in notes) {
if (note.valueType != EditorNote.ValueType.String) continue;
if (note.stringValue != "aactag") continue;
if (note.note == tag) {
matched.Add(note.transform);
}
}
return matched;
}
}
public class AACPlugin : Plugin<AACPlugin>
{
public override string QualifiedName => "gay.lilyy.aaccore.plugin";
public override string DisplayName => "AACCore";
private const string SystemName = "AACCore";
private const bool UseWriteDefaults = true;
protected override void Configure()
{
InPhase(BuildPhase.PlatformInit).BeforePlugin("nadena.dev.modular-avatar").Run($"Platform Init {DisplayName}", (ctx) => RunPhase(ctx, BuildPhase.PlatformInit));
InPhase(BuildPhase.Resolving).BeforePlugin("nadena.dev.modular-avatar").Run($"Resolving {DisplayName}", (ctx) => RunPhase(ctx, BuildPhase.Resolving));
InPhase(BuildPhase.Generating).BeforePlugin("nadena.dev.modular-avatar").Run($"Generating {DisplayName}", (ctx) => RunPhase(ctx, BuildPhase.Generating));
InPhase(BuildPhase.Transforming).BeforePlugin("nadena.dev.modular-avatar").Run($"Transforming {DisplayName}", (ctx) => RunPhase(ctx, BuildPhase.Transforming));
InPhase(BuildPhase.Optimizing).BeforePlugin("nadena.dev.modular-avatar").Run($"Optimizing {DisplayName}", (ctx) => RunPhase(ctx, BuildPhase.Optimizing));
InPhase(BuildPhase.PlatformFinish).BeforePlugin("nadena.dev.modular-avatar").Run($"Platform Finish {DisplayName}", (ctx) => RunPhase(ctx, BuildPhase.PlatformFinish));
}
private void RunPhase(BuildContext ctx, BuildPhase phase)
{
var allLayerGroups = LayerGroup.Instances.ToList();
var layerGroups = allLayerGroups
.Where(lg => lg.SystemName != "template" && lg.buildPhase == phase)
.ToList();
// Start overall timer
var overallStopwatch = Stopwatch.StartNew();
V5AACLogger.LogInfo("Starting Lillith V5 AAC generation...");
var root = ctx.AvatarRootObject.GetComponent<AACRoot>();
if (root == null)
{
V5AACLogger.LogInfo("No LillithV5AACRoot component found. Skipping.");
return;
}
// Skip if the root GameObject or the component itself is disabled
if (!root.enabled || !root.gameObject.activeInHierarchy)
{
V5AACLogger.LogInfo($"LillithV5AACRoot on GameObject '{root.name}' is disabled or inactive. Skipping.");
return;
}
AACAssets assets = new();
assets.ctx = ctx;
assets.root = root;
assets.modularAvatar = MaAc.Create(new GameObject(SystemName)
{
transform = { parent = ctx.AvatarRootTransform }
});
assets.isPC = EditorUserBuildSettings.activeBuildTarget == BuildTarget.StandaloneWindows
|| EditorUserBuildSettings.activeBuildTarget == BuildTarget.StandaloneWindows64;
// Time AAC initialization
var initStopwatch = Stopwatch.StartNew();
V5AACLogger.LogInfo("Initializing Animator As Code...");
assets.aac = AacV1.Create(new AacConfiguration
{
SystemName = SystemName,
AnimatorRoot = ctx.AvatarRootTransform,
DefaultValueRoot = ctx.AvatarRootTransform,
AssetKey = GUID.Generate().ToString(),
AssetContainer = ctx.AssetContainer,
ContainerMode = AacConfiguration.Container.OnlyWhenPersistenceRequired,
AssetContainerProvider = new NDMFContainerProvider(ctx),
DefaultsProvider = new AacDefaultsProvider(UseWriteDefaults)
});
assets.emptyClip = assets.aac.NewClip();
assets.fx = assets.aac.NewAnimatorController();
initStopwatch.Stop();
V5AACLogger.LogInfo($"AAC initialization completed in {initStopwatch.ElapsedMilliseconds}ms");
// Process layer groups with individual timing
// Filter out the template LayerGroup (by SystemName)
V5AACLogger.LogInfo($"Processing {layerGroups.Count} layer groups...");
var totalLayerGroupTime = 0L;
var processedCount = 0;
var skippedCount = 0;
if (root.experimentalPlayMode && Application.isPlaying)
{
assets.experimentalEnabled = true;
}
if (root.experimentalUpload && !Application.isPlaying)
{
assets.experimentalEnabled = true;
}
foreach (var layerGroup in layerGroups)
{
var layerStopwatch = Stopwatch.StartNew();
bool skipped = false;
if (!layerGroup.enabled)
{
V5AACLogger.LogWarning($"Skipping layer group: {layerGroup.DisplayName} (disabled)");
skipped = true;
}
else if (layerGroup.experimental && !assets.experimentalEnabled)
{
V5AACLogger.LogWarning($"Skipping layer group: {layerGroup.DisplayName} (Experimental)");
skipped = true;
}
else if (!layerGroup.IsApplicable(assets))
{
if (layerGroup.shouldWarnIfNotApplicable)
V5AACLogger.LogWarning($"Skipping layer group: {layerGroup.DisplayName} (Not applicable)");
skipped = true;
}
if (!skipped)
{
V5AACLogger.LogInfo($"Running layer group: {layerGroup.DisplayName}");
layerGroup.Run(assets);
processedCount++;
}
else
{
skippedCount++;
}
layerStopwatch.Stop();
totalLayerGroupTime += layerStopwatch.ElapsedMilliseconds;
V5AACLogger.LogInfo($"Layer group '{layerGroup.DisplayName}' completed in {layerStopwatch.ElapsedMilliseconds}ms");
}
// Time Modular Avatar setup
var maStopwatch = Stopwatch.StartNew();
V5AACLogger.LogInfo("Creating Modular Avatar merge animator...");
assets.modularAvatar.NewMergeAnimator(assets.fx.AnimatorController, VRCAvatarDescriptor.AnimLayerType.FX);
maStopwatch.Stop();
V5AACLogger.LogInfo($"Modular Avatar setup completed in {maStopwatch.ElapsedMilliseconds}ms");
// Final timing summary
overallStopwatch.Stop();
V5AACLogger.LogSuccess($"Lillith V5 AAC generation completed successfully!");
V5AACLogger.LogInfo($"=== TIMING SUMMARY ===");
V5AACLogger.LogInfo($"Total time: {overallStopwatch.ElapsedMilliseconds}ms ({overallStopwatch.Elapsed.TotalSeconds:F2}s)");
V5AACLogger.LogInfo($"AAC initialization: {initStopwatch.ElapsedMilliseconds}ms");
V5AACLogger.LogInfo($"Layer groups: {totalLayerGroupTime}ms (processed: {processedCount}, skipped: {skippedCount})");
V5AACLogger.LogInfo($"Modular Avatar: {maStopwatch.ElapsedMilliseconds}ms");
V5AACLogger.LogInfo($"Average per layer group: {(processedCount > 0 ? totalLayerGroupTime / processedCount : 0)}ms");
}
}
internal class NDMFContainerProvider : IAacAssetContainerProvider
{
private readonly BuildContext _ctx;
public NDMFContainerProvider(BuildContext ctx) => _ctx = ctx;
public void SaveAsPersistenceRequired(Object objectToAdd) => _ctx.AssetSaver.SaveAsset(objectToAdd);
public void SaveAsRegular(Object objectToAdd) { } // Let NDMF crawl our assets when it finishes
public void ClearPreviousAssets() { } // ClearPreviousAssets is never used in non-destructive contexts
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 130146287c9f763418ba1890a11f5633
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,27 @@
{
"name": "AACCoreEditor",
"rootNamespace": "",
"references": [
"GUID:165b54f9f25c92d48859a4f1a962cac0",
"GUID:5718fb738711cd34ea54e9553040911d",
"GUID:b906909fcc54f634db50f2cad0f988d9",
"GUID:d689052aa981bf8459346a530f6e6678",
"GUID:71d9dcc7d30ab1c45866d01afa59b6cf",
"GUID:62ced99b048af7f4d8dfe4bed8373d76",
"GUID:901e56b065a857d4483a77f8cae73588",
"GUID:04a7e5cf006503242b1db329a84d694d",
"GUID:95124d49b8c897e4286f0bf6c6e57f4d",
"GUID:a65a5779a3702144986d83fca255f5da"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8d0a4403a05276c4087dd785a3c7818d
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,31 @@
using System.Collections.Generic;
using nadena.dev.ndmf;
namespace gay.lilyy.aaccore {
public abstract class LayerGroup
{
public virtual bool enabled { get { return true; } }
public virtual bool experimental { get { return false; } }
private V5AACLayerGroupLogger _logger;
protected V5AACLayerGroupLogger Logger => _logger ??= new V5AACLayerGroupLogger(SystemName);
private static readonly List<LayerGroup> instances = new();
protected LayerGroup() => instances.Add(this);
public static IEnumerable<LayerGroup> Instances => instances;
public virtual bool shouldWarnIfNotApplicable => true;
public virtual BuildPhase buildPhase => BuildPhase.Generating;
public virtual bool IsApplicable(AACAssets assets) {
return true;
}
public abstract string DisplayName { get; }
public abstract string SystemName { get; }
public abstract void Run(AACAssets assets);
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b62c5c48d33ff344184e9d2af6229e52
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,68 @@
using System.Collections.Generic;
using gay.lilyy.logger;
using UnityEngine;
namespace gay.lilyy.aaccore {
public class V5AACLogger : BaseLogger
{
private static V5AACLogger _instance;
public static V5AACLogger Instance => _instance ??= new V5AACLogger();
protected override string SystemName => "AACCore";
protected override Dictionary<LogLevel, string> LogColors => new()
{
{ LogLevel.Info, "#87CEEB" }, // Sky Blue
{ LogLevel.Warning, "#FFA500" }, // Orange
{ LogLevel.Error, "#FF6B6B" }, // Red
{ LogLevel.Success, "#90EE90" }, // Light Green
{ LogLevel.Debug, "#DDA0DD" } // Plum
};
protected override Dictionary<LogLevel, string> LogPrefixes => new()
{
{ LogLevel.Info, "" },
{ LogLevel.Warning, "⚠️" },
{ LogLevel.Error, "❌" },
{ LogLevel.Success, "✅" },
{ LogLevel.Debug, "🐛" }
};
// Static convenience methods for easy access
public static void LogInfo(string message, Object context = null) => Instance.Info(message, context);
public static void LogWarning(string message, Object context = null) => Instance.Warning(message, context);
public static void LogError(string message, Object context = null) => Instance.Error(message, context);
public static void LogSuccess(string message, Object context = null) => Instance.Success(message, context);
public static void LogDebug(string message, Object context = null) => Instance.Debug(message, context);
}
public class V5AACLayerGroupLogger : BaseLogger
{
private readonly string _layerName;
public V5AACLayerGroupLogger(string layerName)
{
_layerName = layerName;
}
protected override string SystemName => $"AACCore [{_layerName}] ";
protected override Dictionary<LogLevel, string> LogColors => new()
{
{ LogLevel.Info, "#87CEEB" },
{ LogLevel.Warning, "#FFA500" },
{ LogLevel.Error, "#FF6B6B" },
{ LogLevel.Success, "#90EE90" },
{ LogLevel.Debug, "#DDA0DD" }
};
protected override Dictionary<LogLevel, string> LogPrefixes => new();
// Convenience methods for easy access
public void LogInfo(string message, Object context = null) => Info(message, context);
public void LogWarning(string message, Object context = null) => Warning(message, context);
public void LogError(string message, Object context = null) => Error(message, context);
public void LogSuccess(string message, Object context = null) => Success(message, context);
public void LogDebug(string message, Object context = null) => Debug(message, context);
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b4fd8b997bc1c544e83e5ed5014361c4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

8
AAC/AACCore/Runtime.meta Normal file
View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7613e1079336b844dbd75b28c6cf9050
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,16 @@
{
"name": "AACCoreRuntime",
"rootNamespace": "",
"references": [
"GUID:5718fb738711cd34ea54e9553040911d"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 165b54f9f25c92d48859a4f1a962cac0
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,50 @@
using UnityEditor;
using UnityEngine;
using VRC.SDKBase;
namespace gay.lilyy.aaccore {
/// <summary>
/// Adding this to the avatar root will build the FX layer.
/// </summary>
public class AACRoot : MonoBehaviour, IEditorOnly {
public bool experimentalPlayMode = true;
public bool experimentalUpload = false;
// unity needs this to show the enable/disable box
void Start(){}
#if UNITY_EDITOR
[CustomEditor(typeof(AACRoot))]
private class Editor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
// Draw the default message
EditorGUILayout.HelpBox(
"Adding this to the avatar root will build the FX layer.",
MessageType.Info
);
// Draw the 'Enable Experimental Layers' toggle
var component = (AACRoot)target;
EditorGUI.BeginChangeCheck();
bool newValPlay = EditorGUILayout.Toggle("Enable Experimental Layers in Play Mode", component.experimentalPlayMode);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(component, "Toggle Experimental Layers in Play Mode");
component.experimentalPlayMode = newValPlay;
EditorUtility.SetDirty(component);
}
EditorGUI.BeginChangeCheck();
bool newValUpload = EditorGUILayout.Toggle("Enable Experimental Layers in Uploads", component.experimentalUpload);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(component, "Toggle Experimental Layers in Uploads");
component.experimentalPlayMode = newValUpload;
EditorUtility.SetDirty(component);
}
}
}
#endif
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3a0c8f4a9a1d73e4ab34270b10988813
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 02ed31c0bed0fd045b53f2a08b792152
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,23 @@
using gay.lilyy.aaccore;
using UnityEditor;
namespace gay.lilyy.avatarname.version.aac {
[InitializeOnLoad]
public class Template : LayerGroup
{
// remove when ready for uploaded
public override bool experimental => true;
private static readonly Template _instance = new();
static Template() {}
public override string DisplayName => "Template";
public override string SystemName => "template";
public override void Run(AACAssets assets)
{
Logger.LogInfo("LayerGroup Template.Run() Called!");
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a63a02520572be74b9866f7c32915518
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,25 @@
{
"name": "TemplateAAC",
"rootNamespace": "",
"references": [
"GUID:8d0a4403a05276c4087dd785a3c7818d",
"GUID:95124d49b8c897e4286f0bf6c6e57f4d",
"GUID:5718fb738711cd34ea54e9553040911d",
"GUID:b906909fcc54f634db50f2cad0f988d9",
"GUID:d689052aa981bf8459346a530f6e6678",
"GUID:71d9dcc7d30ab1c45866d01afa59b6cf",
"GUID:04a7e5cf006503242b1db329a84d694d",
"GUID:62ced99b048af7f4d8dfe4bed8373d76"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b13df479673c65d4785814fc8079202f
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
AAC/AACShared.meta Normal file
View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 980779dd23e254847be872de3d0796f1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fbd24b1ba03c78f4bbb7279612566919
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,27 @@
{
"name": "AACSharedLayers",
"rootNamespace": "",
"references": [
"GUID:8d0a4403a05276c4087dd785a3c7818d",
"GUID:95124d49b8c897e4286f0bf6c6e57f4d",
"GUID:5718fb738711cd34ea54e9553040911d",
"GUID:b906909fcc54f634db50f2cad0f988d9",
"GUID:d689052aa981bf8459346a530f6e6678",
"GUID:71d9dcc7d30ab1c45866d01afa59b6cf",
"GUID:04a7e5cf006503242b1db329a84d694d",
"GUID:62ced99b048af7f4d8dfe4bed8373d76",
"GUID:e73da13578f7b4d4fa785d6f8fe72ba3",
"GUID:fc900867c0f47cd49b6e2ae4ef907300"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 82ac5caf0663be04faa61ac94362fd55
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

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

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2c428d7f31c05e0479683c9b1f17a05d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,156 @@
using System.Collections.Generic;
using System.Linq;
using gay.lilyy.aaccore;
using gay.lilyy.aacshared.runtimecomponents;
using UnityEditor;
using UnityEngine;
namespace gay.lilyy.aacshared.layers {
[InitializeOnLoad]
public class Floater : LayerGroup
{
// remove when ready for uploaded
public override bool experimental => false;
public override bool shouldWarnIfNotApplicable => false;
private static readonly Floater _instance = new();
static Floater() {}
public override string DisplayName => "Floater";
public override string SystemName => "floater";
private static FloaterDefinition[] getDefinitions(AACAssets assets)
{
return assets.ctx.AvatarRootObject.GetComponentsInChildren<FloaterDefinition>();
}
public override bool IsApplicable(AACAssets assets)
{
return getDefinitions(assets).Length > 0;
}
private void createLayer(AACAssets assets, MultiTransformDefinition multiDef)
{
var layer = assets.fx.NewLayer($"Floater {multiDef.Definition.name} and {multiDef.Transforms.Count - 1} more");
if (!new[] { "x", "y", "z" }.Contains(multiDef.Definition.spinAxis.ToLower()))
{
Logger.LogWarning($"Floater system: Invalid spin axis '{multiDef.Definition.spinAxis}' in definition '{multiDef.Definition.name}'. Must be one of 'x', 'y', or 'z'. Skipping.");
return;
}
var state = layer.NewState("Floater").WithAnimation(assets.aac.NewClip().Animating(action =>
{
Logger.LogInfo($"Floater system: Creating animation for definition '{multiDef.Definition.name}' with {multiDef.Transforms.Count} transforms");
foreach (var t in multiDef.Transforms)
{
if (multiDef.Definition.floatHeight != 0f)
{
action.Animates(t, "m_LocalPosition.y").WithSecondsUnit(unit =>
{
unit
.Easing(0, t.transform.localPosition.y);
unit
.Easing(multiDef.Definition.loopInSeconds / 2f, t.transform.localPosition.y + multiDef.Definition.floatHeight)
.Easing(multiDef.Definition.loopInSeconds, t.transform.localPosition.y);
});
}
if (multiDef.Definition.spinCycles > 0)
{
string axis = multiDef.Definition.spinAxis.ToLower();
string prop = $"m_LocalEulerAngles.{axis}";
float segment = multiDef.Definition.loopInSeconds / multiDef.Definition.spinCycles;
for (int cycle = 0; cycle < multiDef.Definition.spinCycles; cycle++)
{
action.Animates(t, prop).WithSecondsUnit(unit =>
{
float baseAngle = 0f; // ignore quaternion components entirely
float startTime = cycle * segment;
float endTime = (cycle + 1) * segment;
float startAngle = baseAngle + 360f * cycle;
float endAngle = baseAngle + 360f * (cycle + 1);
Logger.LogDebug($"Floater system: Animating {t.name} {prop} on cycle {cycle} from {startAngle} to {endAngle} between {startTime}s and {endTime}s");
unit
.Linear(startTime, startAngle)
.Linear(endTime, endAngle);
Logger.LogDebug($"unit.Linear({startTime}, {startAngle}).Linear({endTime}, {endAngle})");
});
}
// Fill OTHER Euler axes with constant values
foreach (var other in new[] { "x", "y", "z" })
{
if (other == axis) continue;
float current =
other == "x" ? t.transform.localEulerAngles.x :
other == "y" ? t.transform.localEulerAngles.y :
t.transform.localEulerAngles.z;
Logger.LogDebug($"Floater system: Setting constant {t.name} {other} to {current}");
action.Animates(t, $"m_LocalEulerAngles.{other}")
.WithSecondsUnit(unit =>
{
unit
.Linear(0f, current)
.Linear(multiDef.Definition.loopInSeconds, current);
});
}
}
}
}).Looping());
if (multiDef.Definition.toggleParamName != "")
{
var param = layer.BoolParameter(multiDef.Definition.toggleParamName);
var offState = layer.NewState("Floater Off").WithAnimation(assets.emptyClip);
layer.WithDefaultState(offState);
offState.TransitionsTo(state).When(param.IsTrue());
state.TransitionsTo(offState).When(param.IsFalse());
}
}
private struct MultiTransformDefinition
{
public List<Transform> Transforms { get; set; }
public FloaterDefinition Definition { get; set; }
}
public override void Run(AACAssets assets)
{
var definitions = getDefinitions(assets);
Logger.LogDebug($"Floater system: Found {definitions.Length} floater definitions");
var matchedDefinitions = new Dictionary<int, MultiTransformDefinition>();
foreach (var def in definitions)
{
if (!def.enabled) continue;
if (!def.enableOnPC && assets.isPC) continue;
if (!def.enableOnMobile && !assets.isPC) continue;
Logger.LogInfo($"Floater system: Found definition '{def.name}'");
var hash = def.GetHashCode();
if (!matchedDefinitions.ContainsKey(hash))
{
matchedDefinitions[hash] = new MultiTransformDefinition
{
Definition = def,
Transforms = new List<Transform>()
};
} else
{
Logger.LogInfo($"Floater system: Adding additional transform to definition '{def.name}'");
}
matchedDefinitions[hash].Transforms.Add(def.transform);
}
foreach (var multiDef in matchedDefinitions.Values)
{
createLayer(assets, multiDef);
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8c1340bf8df44f24cabe4db762796c21
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,187 @@
using UnityEditor;
using UnityEngine;
using AnimatorAsCode.V1;
using System.Collections.Generic;
using gay.lilyy.aaccore;
namespace gay.lilyy.aacshared.layers
{
/**
<summary>
ported from v5
</summary>
*/
[InitializeOnLoad]
public class LanternFlicker : LayerGroup
{
private static readonly LanternFlicker _instance = new();
static LanternFlicker() { }
public override string DisplayName => "Lantern Flicker";
public override string SystemName => "lantern_flicker";
public override bool shouldWarnIfNotApplicable => false;
private Transform GetLantern(AACAssets assets)
{
return AACUtils.FindChildRecursive(assets.ctx.AvatarRootTransform, "Lantern Floater");
}
public override bool IsApplicable(AACAssets assets)
{
var lamp = GetLantern(assets);
if (lamp == null)
{
Logger.LogWarning("Lamp Light could not be found!");
return false;
}
var light = lamp.GetComponentInChildren<Light>();
return light != null;
}
private void CreateFlickerLayer(AACAssets assets)
{
var lamp = GetLantern(assets);
var light = lamp.GetComponentInChildren<Light>();
var layer = assets.fx.NewLayer("Lantern Flicker");
var flickerParam = layer.BoolParameter("LanternFlicker");
var off = layer.NewState("Off").WithAnimation(
assets.aac.NewClip().Animating(anim =>
{
if (light != null) {
anim.Animates(light, "m_Enabled").WithFrameCountUnit(sec =>
{
sec.Constant(0, 0);
});
anim.Animates(light, "m_Intensity").WithFrameCountUnit(sec =>
{
sec.Constant(0, light.intensity);
});
}
})
);
layer.WithDefaultState(off);
var on = layer.NewState("Flicker").WithAnimation(
assets.aac.NewClip().Animating(anim =>
{
AnimateLantern(assets, anim, lamp);
}).Looping()
);
off.TransitionsTo(on).When(flickerParam.IsEqualTo(true));
on.TransitionsTo(off).When(flickerParam.IsEqualTo(false));
}
private void CreateBrightnessLayer(AACAssets assets)
{
var lamp = GetLantern(assets);
var light = lamp.GetComponentInChildren<Light>();
if (light == null) return;
var layer = assets.fx.NewLayer("Lantern Brightness");
var brightnessParam = layer.FloatParameter("LanternBrightness");
var state = layer.NewState("Brightness").WithAnimation(
assets.aac.NewClip().Animating(anim =>
{
anim.Animates(light, "m_Enabled").WithUnit(AacFlUnit.Frames, sec =>
{
sec.Linear(0, 0);
sec.Linear(1, 1);
});
anim.AnimatesColor(light, "m_Color").WithUnit(AacFlUnit.Frames, sec =>
{
sec.Linear(0, light.color * 0);
sec.Linear(100, light.color);
});
})
).WithMotionTime(brightnessParam);
}
private void AnimateLantern(AACAssets assets, AacFlEditClip anim, Transform lantern)
{
var renderer = lantern.GetComponent<MeshRenderer>();
var light = lantern.GetComponentInChildren<Light>();
if (renderer == null)
{
Logger.LogWarning("AnimateLantern: Missing renderer on lantern");
return;
}
var rng = new System.Random("lillithlanternrandomseed".GetHashCode());
var keyframes = new List<(float time, double value)>();
float time = 0;
while (time < 60)
{
double value = rng.NextDouble();
keyframes.Add((time, value));
time += (float)(rng.NextDouble() * (0.3 - 0.05) + 0.05);
}
if (keyframes.Count == 0)
{
Logger.LogWarning("AnimateLantern: No keyframes generated");
}
if (light != null)
{
anim.Animates(light, "m_Intensity").WithSecondsUnit(sec =>
{
foreach (var (t, v) in keyframes)
{
sec.Easing(t, (float)v * light.intensity);
}
});
}
if (assets.isPC) {
Logger.LogInfo("Using PC material for lantern flicker");
Color initialColor = renderer.sharedMaterial.GetColor("_Emission_Color");
// ValueFactory Orbits
var prop = "material._Emission_Color";
void animateChannel(string suffix, float initial)
{
anim.Animates(renderer, $"{prop}.{suffix}").WithSecondsUnit(sec =>
{
foreach (var (t, v) in keyframes)
{
sec.Easing(t, initial * (float)v);
}
});
}
animateChannel("x", initialColor.r);
animateChannel("y", initialColor.g);
animateChannel("z", initialColor.b);
animateChannel("w", initialColor.a);
}
else
{
// Toon Standard
Logger.LogInfo("Using Non-PC material for lantern flicker");
// float initialStrength = renderer.sharedMaterial.GetFloat("_EmissionStrength");
// this still isnt running before vqt for some cursed reason so im just gonna hardcode this
var initialStrength = 2;
anim.Animates(renderer, "material._EmissionStrength").WithSecondsUnit(sec =>
{
foreach (var (t, v) in keyframes)
{
sec.Easing(t, initialStrength * (float)v);
}
});
}
}
public override void Run(AACAssets assets)
{
CreateFlickerLayer(assets);
CreateBrightnessLayer(assets);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b23933d2d4383264e8a63812c8580be2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,114 @@
using System.Collections.Generic;
using AnimatorAsCode.V1;
using AnimatorAsCode.V1.VRC;
using gay.lilyy.aaccore;
using gay.lilyy.aacshared.runtimecomponents;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.ScriptableObjects;
namespace gay.lilyy.aacshared.layers
{
[InitializeOnLoad]
public class ParameterPreset : LayerGroup
{
private static readonly ParameterPreset _instance = new();
static ParameterPreset() { }
public override string DisplayName => "Parameter Preset";
public override string SystemName => "parameter_preset";
public override bool shouldWarnIfNotApplicable => false;
private static ParameterPresetDefinition[] getDefinitions(AACAssets assets)
{
return assets.ctx.AvatarRootObject.GetComponents<ParameterPresetDefinition>();
}
public override bool IsApplicable(AACAssets assets)
{
return getDefinitions(assets).Length > 0;
}
private void RunGroup(AACAssets assets, List<ParameterPresetDefinition> presets)
{
if (presets.Count == 0) return; // If there are none in the avatar, skip this entirely.
var layer = assets.fx.NewLayer("Presets " + presets[0].Group);
var presetParam = layer.IntParameter("Preset " + presets[0].Group);
Dictionary<string, AacFlParameter<int>> intParams = new();
Dictionary<string, AacFlParameter<float>> floatParams = new();
Dictionary<string, AacFlParameter<bool>> boolParams = new();
var idle = layer.NewState("Idle").WithAnimation(assets.emptyClip);
layer.WithDefaultState(idle);
var presetIndex = 0;
foreach (var preset in presets)
{
presetIndex++;
var state = layer.NewState("Preset " + preset.Name)
.WithAnimation(assets.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));
}
}
public override void Run(AACAssets assets)
{
var presets = getDefinitions(assets);
Dictionary<string, List<ParameterPresetDefinition>> groupedPresets = new();
foreach (var preset in presets)
{
if (!groupedPresets.ContainsKey(preset.Group))
{
groupedPresets[preset.Group] = new List<ParameterPresetDefinition>();
}
groupedPresets[preset.Group].Add(preset);
}
foreach (var group in groupedPresets)
{
RunGroup(assets, group.Value);
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 35b03c4eb985a2a489054b0738690d57
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

9
AAC/AACShared/README.md Normal file
View file

@ -0,0 +1,9 @@
# AACShared
A group of AACCore layers that arent avatar specific
lillith if u dont make all of these togglable / configurable im going to commit
im so fr
also MAKE SURE ALL LAYERS HAVE `public override bool shouldWarnIfNotApplicable => false;`

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6b5e9937d32bd674fb0909528112acc2
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 71318e6f58f43d24b911579de721d22b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,16 @@
{
"name": "AACSharedRuntime",
"rootNamespace": "",
"references": [
"GUID:3456780c4fb2d324ab9c633d6f1b0ddb"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e73da13578f7b4d4fa785d6f8fe72ba3
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,36 @@
using System;
using UnityEngine;
using VRC.SDKBase;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace gay.lilyy.aacshared.runtimecomponents
{
public class ChildToggleDefinition : MonoBehaviour, IEditorOnly {
// unity needs this to show the enable/disable box
void Start(){}
public string MenuPath = "";
}
#if UNITY_EDITOR
[CustomEditor(typeof(ChildToggleDefinition))]
public class ChildToggleDefinitionInspector : Editor
{
public override void OnInspectorGUI()
{
EditorGUILayout.HelpBox(
"Any direct children of this object will have a toggle in the menu",
MessageType.Info
);
EditorGUILayout.Space();
// Draw the default inspector to show the 'enable' field
DrawDefaultInspector();
}
}
#endif
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: be675dd25b2a8854687c05c2ab33af0c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,35 @@
using System;
using UnityEngine;
using VRC.SDKBase;
namespace gay.lilyy.aacshared.runtimecomponents
{
public class FloaterDefinition: MonoBehaviour, IEditorOnly {
// unity needs this to show the enable/disable box
void Start(){}
public float floatHeight = 0;
[Tooltip("Time in seconds for one full spin cycle")]
public float loopInSeconds = 10f;
[Tooltip("Number of times object spins in one loop")]
public int spinCycles = 2;
[Tooltip("Axis to spin around (x, y, or z)")]
public string spinAxis = "z";
public bool enableOnPC = true;
[Tooltip("Quest, Pico, Android/iOS Mobile, etc.")]
public bool enableOnMobile = true;
[Tooltip("If set, this parameter will toggle the floater on/off")]
public string toggleParamName = "";
/** <summary>
Compares two FloaterDefinitions for equality based on their properties.
Used for chunking similar floaters into one animation layer
</summary> */
public override int GetHashCode()
{
return HashCode.Combine(floatHeight, loopInSeconds, spinCycles, enableOnPC, enableOnMobile, toggleParamName);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3b3b5f65ee33db24bbb5f0042bec1f87
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,224 @@
using System.Collections.Generic;
using UnityEngine;
using VRC.SDK3.Avatars.ScriptableObjects;
using VRC.SDKBase;
#if UNITY_EDITOR
using UnityEditor;
using System.Linq;
using VRC.SDK3.Avatars.Components;
#endif
namespace gay.lilyy.aacshared.runtimecomponents
{
[System.Serializable]
public class PresetParameter
{
public string name;
public VRCExpressionParameters.ValueType valueType;
public float setTo;
public bool shouldChange = false;
}
[AddComponentMenu("LillithRosePup/Parameter Preset")]
public class ParameterPresetDefinition : MonoBehaviour, IEditorOnly
{
public string Name = "New Preset";
public string Group = "Default";
[HideInInspector] // custom inspector
public List<PresetParameter> Parameters;
// unity needs this to show the enable/disable box
void Start() { }
}
#if UNITY_EDITOR
[CustomEditor(typeof(ParameterPresetDefinition))]
public class ParameterPresetDefinitionInspector : Editor
{
private ParameterPresetDefinition preset;
private bool parametersLoaded = false;
private void OnEnable()
{
preset = (ParameterPresetDefinition)target;
LoadMissingParameters();
}
private void LoadMissingParameters()
{
VRCAvatarDescriptor aviDesc = preset.gameObject.GetComponent<VRCAvatarDescriptor>();
if (aviDesc == null || aviDesc.expressionParameters == null || parametersLoaded)
return;
// Initialize Parameters list if it's null
if (preset.Parameters == null)
preset.Parameters = new List<PresetParameter>();
// 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<string>(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;
}
private int GetPresetIndex()
{
VRCAvatarDescriptor aviDesc = preset.gameObject.GetComponent<VRCAvatarDescriptor>();
if (aviDesc == null)
return -1;
var presets = aviDesc.GetComponentsInChildren<ParameterPresetDefinition>().ToList().Where(p => p.Group == preset.Group).ToList();
for (int i = 0; i < presets.Count; i++)
{
if (presets[i] == preset)
{
return i + 1; // 1-based index matching ParameterPreset.cs
}
}
return -1;
}
public override void OnInspectorGUI()
{
// Ensure parameters are loaded when inspector is drawn
LoadMissingParameters();
// Draw the default inspector
DrawDefaultInspector();
// Display preset index
int presetIndex = GetPresetIndex();
if (presetIndex > 0)
{
EditorGUILayout.Space();
EditorGUILayout.HelpBox($"Preset Index: {presetIndex}\n(This preset will be activated when the 'Preset {preset.Group}' parameter equals {presetIndex})", MessageType.Info);
}
if (GUILayout.Button("Copy Parameter Name")) {
GUIUtility.systemCopyBuffer = "Preset " + preset.Group;
EditorGUILayout.LabelField("Parameter Name Copied to Clipboard", EditorStyles.boldLabel);
}
// 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);
}
}
}
}
#endif
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d6a5ecef749799144819cc07612056ef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: