231 lines
10 KiB
C#
231 lines
10 KiB
C#
#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<MusicPlayerPlugin>
|
|
{
|
|
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<MusicPlayerSong>();
|
|
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<AudioSource>();
|
|
|
|
|
|
|
|
// 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<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)
|
|
{
|
|
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.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<AudioClip> { 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
|