music player makes menu now

This commit is contained in:
Lillith Rose 2025-08-31 13:13:24 -04:00
parent d30d357f38
commit d6fd187cea
3 changed files with 102 additions and 38 deletions

View file

@ -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<MusicPlayerPlugin>
{
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<int> usedIndexes = new List<int>();
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<AudioSource>();
// 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<int> trackNumbers = new HashSet<int>(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<ModularAvatarMenuItem>();
menuItem.label = "Music";
menuItem.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
menuItem.MenuSource = SubmenuSource.Children;
menuGo.AddComponent<ModularAvatarMenuInstaller>();
// Dictionary to store folder hierarchy
var folderHierarchy = new Dictionary<string, GameObject>();
foreach (var song in songs)
{
var playerLayer = controller.NewLayer("Player");
var songIndexParam = playerLayer.IntParameter("SongIndex");
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<ModularAvatarMenuItem>();
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.BoolParameter("IsLocal");
var isLocal = playerLayer.Av3().IsLocal;
var nonLocalState = playerLayer.NewState("NonLocal");
var idleState = playerLayer.NewState("Idle");
@ -126,6 +180,8 @@ namespace gay.lilyy.MusicPlayer.Editor
// 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}");
@ -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;
}
}

View file

@ -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"

View file

@ -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;
}
}