172 lines
7.7 KiB
C#
172 lines
7.7 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;
|
|
|
|
[assembly: ExportsPlugin(typeof(MusicPlayerPlugin))]
|
|
|
|
namespace gay.lilyy.MusicPlayer.Editor
|
|
{
|
|
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;
|
|
List<int> usedIndexes = new List<int>();
|
|
|
|
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<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 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<AudioClip> { 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
|