#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; [assembly: ExportsPlugin(typeof(MusicPlayerPlugin))] namespace gay.lilyy.MusicPlayer.Editor { 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; List usedIndexes = new List(); foreach (var song in songs) { if (song.gameObject != songGo) { throw new System.Exception("All MusicPlayerSong components must be on the same GameObject."); } if (usedIndexes.Contains(song.TrackNumber)) { throw new System.Exception($"${song.Name} is using a duplicate track number: ${song.TrackNumber}"); } } var audioSource = songGo.GetComponent(); // INSERT_YOUR_CODE // Ensure all track numbers from 1 to max are present int minTrackNumber = songs.Min(song => song.TrackNumber); if (minTrackNumber < 1) { throw new System.Exception("Track numbers are 1 and up"); } int maxTrackNumber = songs.Max(song => song.TrackNumber); HashSet trackNumbers = new HashSet(songs.Select(song => song.TrackNumber)); for (int i = 1; i <= maxTrackNumber; i++) { if (!trackNumbers.Contains(i)) { throw new System.Exception($"Missing track number: {i}. All track numbers from 1 to {maxTrackNumber} must be filled."); } } // Initialize Animator As Code. var modularAvatar = MaAc.Create(new GameObject(SystemName) { transform = { parent = ctx.AvatarRootTransform } }); var controller = CreateController(ctx, modularAvatar, songs, audioSource); // 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(); CreatePlayerLayer(controller, modularAvatar, songs, audioSource); CreateVolumeLayer(aac, controller, modularAvatar, audioSource); return controller; } private void CreatePlayerLayer(AacFlController controller, MaAc modularAvatar, MusicPlayerSong[] songs, AudioSource audioSource) { var playerLayer = controller.NewLayer("Player"); var songIndexParam = playerLayer.IntParameter("SongIndex"); modularAvatar.NewParameter(songIndexParam).NotSaved(); var isLocal = playerLayer.BoolParameter("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(); }); } } private void CreateVolumeLayer(AacFlBase aac, AacFlController controller, MaAc modularAvatar, AudioSource audioSource) { var songVolumeLayer = controller.NewLayer("Volume"); var volumeParam = songVolumeLayer.FloatParameter("volume"); modularAvatar.NewParameter(volumeParam).WithDefaultValue(0.01f); var clip = aac.NewClip().Animating((action) => { var animate = action.Animates(audioSource, "m_Volume"); animate.WithSecondsUnit((seconds) => { seconds.Linear(0, 0); seconds.Linear(1, 1); }); }); var state = songVolumeLayer.NewState("Volume"); state.WithAnimation(clip).WithMotionTime(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