#if UNITY_EDITOR using UnityEngine; using System.Collections.Generic; using VRC.SDK3.Avatars.ScriptableObjects; using nadena.dev.ndmf; using gay.lilyy.MusicPlayer.Editor; using AnimatorAsCode.V1; using VRC.SDK3.Avatars.Components; using AnimatorAsCode.V1.ModularAvatar; using UnityEditor; using AnimatorAsCode.V1.VRC; using NUnit.Framework.Constraints; using System.Linq; using System.Threading.Tasks; using nadena.dev.modular_avatar.core; [assembly: ExportsPlugin(typeof(MusicPlayerPlugin))] namespace gay.lilyy.MusicPlayer.Editor { public static class ParameterNames { public const string SongIndex = "Music_SongIndex"; public const string Volume = "Music_Volume"; public const string IsLocal = "Music_IsLocal"; } public static class LayerNames { public const string Volume = "Music_Volume"; public const string Player = "Music_Player"; } public class MusicPlayerPlugin : Plugin { public override string QualifiedName => "gay.lilyy.MusicPlayer"; public override string DisplayName => "Music Player"; private const string SystemName = "MusicPlayer"; 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 songs = ctx.AvatarRootTransform.GetComponentsInChildren(); if (songs.Length == 0) return; // If there are none in the avatar, skip this entirely. var songGo = songs[0].gameObject; int trackNumber = 0; foreach (var song in songs) { if (song.gameObject != songGo) { throw new System.Exception("All MusicPlayerSong components must be on the same GameObject."); } trackNumber++; song.TrackNumber = trackNumber; } var audioSource = songGo.GetComponent(); // Initialize Animator As Code. var modularAvatar = MaAc.Create(songGo); var controller = CreateController(ctx, modularAvatar, songs, audioSource); foreach (var song in songs) { UnityEngine.Object.DestroyImmediate(song); } // Create a new object in the scene. We will add Modular Avatar components inside it. // By creating a Modular Avatar Merge Animator component, // our animator controller will be added to the avatar's FX layer. modularAvatar.NewMergeAnimator(controller.AnimatorController, VRCAvatarDescriptor.AnimLayerType.FX); Debug.Log("Added music player"); } private AacFlController CreateController(BuildContext ctx, MaAc modularAvatar, MusicPlayerSong[] songs, AudioSource audioSource) { 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 controller = aac.NewAnimatorController(); var songIndexParam = CreatePlayerLayer(controller, modularAvatar, songs, audioSource); var volumeParam = CreateVolumeLayer(aac, controller, modularAvatar, audioSource); CreateMenu(songIndexParam, volumeParam, modularAvatar, songs); return controller; } private void CreateMenu(AacFlIntParameter songIndexParam, AacFlFloatParameter volumeParam, MaAc modularAvatar, MusicPlayerSong[] songs) { var menuGo = new GameObject("Menu"); menuGo.transform.SetParent(songs[0].transform); var menuItem = menuGo.AddComponent(); menuItem.label = "Music"; menuItem.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu; menuItem.MenuSource = SubmenuSource.Children; menuGo.AddComponent(); // Dictionary to store folder hierarchy var folderHierarchy = new Dictionary(); foreach (var song in songs) { GameObject currentParent = menuGo; // Create folder hierarchy if specified if (!string.IsNullOrEmpty(song.Folder)) { string[] folderPath = song.Folder.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); if (!folderHierarchy.ContainsKey(fullPath)) { var folderGo = new GameObject($"Folder_{folderName}"); folderGo.transform.SetParent(currentParent.transform); var folderMenuItem = folderGo.AddComponent(); folderMenuItem.label = folderName; folderMenuItem.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu; folderMenuItem.MenuSource = SubmenuSource.Children; if (song.icon != null) folderMenuItem.Control.icon = song.icon; folderHierarchy[fullPath] = folderGo; } currentParent = folderHierarchy[fullPath]; } } // Create the song toggle var go = new GameObject($"Song_{song.Name}"); go.transform.SetParent(currentParent.transform); var toggle = modularAvatar.EditMenuItem(go).ToggleSets(songIndexParam, song.TrackNumber).Name(song.Name); if (song.icon != null) toggle.WithIcon(song.icon); } var volumeGo = new GameObject("Volume"); volumeGo.transform.SetParent(menuGo.transform); modularAvatar.EditMenuItem(volumeGo).Radial(volumeParam).Name("Volume"); } private AacFlIntParameter CreatePlayerLayer(AacFlController controller, MaAc modularAvatar, MusicPlayerSong[] songs, AudioSource audioSource) { var playerLayer = controller.NewLayer(LayerNames.Player); var songIndexParam = playerLayer.IntParameter(ParameterNames.SongIndex); modularAvatar.NewParameter(songIndexParam).NotSaved(); var isLocal = playerLayer.Av3().IsLocal; var nonLocalState = playerLayer.NewState("NonLocal"); var idleState = playerLayer.NewState("Idle"); nonLocalState.TransitionsTo(idleState).When(isLocal.IsEqualTo(true)); // will never appear ingame, useful for testing idleState.TransitionsTo(nonLocalState).When(isLocal.IsEqualTo(false)); foreach (var song in songs) { var songState = playerLayer.NewState($"Song ${song.Name}"); songState.TransitionsTo(idleState).When(songIndexParam.IsNotEqualTo(song.TrackNumber)); // again, will never happen ingame. still useful songState.TransitionsTo(nonLocalState).When(isLocal.IsEqualTo(false)); idleState.TransitionsTo(songState).When(songIndexParam.IsEqualTo(song.TrackNumber)); songState.Audio(audioSource, (audio) => { audio.SelectsClip(0, new List { song.song }.ToArray() ); audio.StopsPlayingOnExit(); audio.StartsPlayingOnEnter(); }); } return songIndexParam; } private AacFlFloatParameter CreateVolumeLayer(AacFlBase aac, AacFlController controller, MaAc modularAvatar, AudioSource audioSource) { var songVolumeLayer = controller.NewLayer(LayerNames.Volume); var volumeParam = songVolumeLayer.FloatParameter(ParameterNames.Volume); modularAvatar.NewParameter(volumeParam).WithDefaultValue(0.2f); var clip = aac.NewClip().Animating((action) => { var animate = action.Animates(audioSource, "m_Volume"); animate.WithSecondsUnit((seconds) => { seconds.Linear(0, 0); seconds.Linear(1, 0.3f); }); }); var state = songVolumeLayer.NewState("Volume"); state.WithAnimation(clip).WithMotionTime(volumeParam); return volumeParam; } } 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