diff --git a/Music Player/Editor/MusicPlayer.cs b/Music Player/Editor/MusicPlayer.cs index f7b08d4..00b0e73 100644 --- a/Music Player/Editor/MusicPlayer.cs +++ b/Music Player/Editor/MusicPlayer.cs @@ -12,11 +12,25 @@ 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"; @@ -35,48 +49,31 @@ namespace gay.lilyy.MusicPlayer.Editor if (songs.Length == 0) return; // If there are none in the avatar, skip this entirely. var songGo = songs[0].gameObject; - List usedIndexes = new List(); - + 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."); } - if (usedIndexes.Contains(song.TrackNumber)) - { - throw new System.Exception($"${song.Name} is using a duplicate track number: ${song.TrackNumber}"); - } + trackNumber++; + song.TrackNumber = 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 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. @@ -106,18 +103,75 @@ namespace gay.lilyy.MusicPlayer.Editor // 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); + var songIndexParam = CreatePlayerLayer(controller, modularAvatar, songs, audioSource); + var volumeParam = CreateVolumeLayer(aac, controller, modularAvatar, audioSource); + + CreateMenu(songIndexParam, volumeParam, modularAvatar, songs); return controller; } - private void CreatePlayerLayer(AacFlController controller, MaAc modularAvatar, MusicPlayerSong[] songs, AudioSource audioSource) + 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("Player"); - var songIndexParam = playerLayer.IntParameter("SongIndex"); + var playerLayer = controller.NewLayer(LayerNames.Player); + var songIndexParam = playerLayer.IntParameter(ParameterNames.SongIndex); modularAvatar.NewParameter(songIndexParam).NotSaved(); - var isLocal = playerLayer.BoolParameter("IsLocal"); + var isLocal = playerLayer.Av3().IsLocal; var nonLocalState = playerLayer.NewState("NonLocal"); var idleState = playerLayer.NewState("Idle"); @@ -125,6 +179,8 @@ namespace gay.lilyy.MusicPlayer.Editor 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) { @@ -142,21 +198,24 @@ namespace gay.lilyy.MusicPlayer.Editor audio.StartsPlayingOnEnter(); }); } + return songIndexParam; } - 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); + 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, 1); + seconds.Linear(1, 0.3f); }); }); var state = songVolumeLayer.NewState("Volume"); state.WithAnimation(clip).WithMotionTime(volumeParam); + + return volumeParam; } } diff --git a/Music Player/Editor/MusicPlayerEditor.asmdef b/Music Player/Editor/MusicPlayerEditor.asmdef index 49a09ce..b49cf07 100644 --- a/Music Player/Editor/MusicPlayerEditor.asmdef +++ b/Music Player/Editor/MusicPlayerEditor.asmdef @@ -9,7 +9,8 @@ "AnimatorAsCode.V1.VRChat", "AnimatorAsCode.V1.ModularAvatar", "nadena.dev.ndmf", - "nadena.dev.ndmf.vrchat" + "nadena.dev.ndmf.vrchat", + "nadena.dev.modular-avatar.core" ], "includePlatforms": [ "Editor" diff --git a/Music Player/Runtime/MusicPlayerSong.cs b/Music Player/Runtime/MusicPlayerSong.cs index c58ea9e..8e18bbe 100644 --- a/Music Player/Runtime/MusicPlayerSong.cs +++ b/Music Player/Runtime/MusicPlayerSong.cs @@ -1,4 +1,5 @@ +using System; using System.Collections.Generic; using UnityEngine; using VRC.SDK3.Avatars.ScriptableObjects; @@ -8,7 +9,10 @@ namespace gay.lilyy.MusicPlayer [RequireComponent(typeof(AudioSource))] public class MusicPlayerSong : MonoBehaviour, IEditorOnly { public string Name = "Song Name"; - public int TrackNumber = 1; + [HideInInspector] + public int TrackNumber = -1; public AudioClip song; + public Texture2D icon; + public string Folder; } } \ No newline at end of file