Code/SboxDirector/Sandbox/AdaptiveMusicDirectorComponent.cs

A component that hosts an optional local/authoritative adaptive music director and runtime. It creates and manages MusicDirectorCore, MusicRuntime, and a runner, queues gameplay music events, updates the runner on fixed update when authoritative, and exposes save/load state hooks and abstract methods for catalog, world capture, and applying audio actions.

#nullable enable annotations
using Sandbox;

namespace SboxDirector;

/// <summary>Optional local/authoritative s&amp;box bridge for adaptive music. Networked games may replicate MusicDecisionBatch and run MusicRuntime on each client instead.</summary>
[Category("Gameplay")]
[Title("Adaptive Music Director")]
[Icon("graphic_eq")]
public abstract class AdaptiveMusicDirectorComponent : Component
{
    [Property] public bool MusicEnabled { get; set; } = true;
    [Property] public int Seed { get; set; } = 1;
    [Property] public string RuntimeParticipantId { get; set; } = "local";

    public MusicDirectorCore? MusicDirector { get; private set; }
    public MusicRuntime? MusicRuntime { get; private set; }
    public MusicHostRunResult? LastMusicResult { get; private set; }

    readonly List<MusicGameplayEvent> _pendingEvents = new();
    MusicDirectorHostRunner? _runner;
    long _nextEventSequence = 1;

    protected override void OnStart() => InitializeMusicDirector();
    protected override void OnFixedUpdate()
    {
        if (!MusicEnabled || _runner is null || !HasMusicAuthority) return;
        LastMusicResult = _runner.Update(CaptureMusicWorld(), _pendingEvents.ToArray()); _pendingEvents.Clear();
    }

    public void InitializeMusicDirector()
    {
        var catalog = CreateMusicCatalog(); MusicDirector = new MusicDirectorCore(CreateMusicConfiguration()); MusicRuntime = new MusicRuntime(RuntimeParticipantId, catalog, configuration: CreateMusicRuntimeConfiguration());
        _runner = new(MusicDirector, MusicRuntime, new ComponentAudioSink(this)); OnMusicDirectorInitialized(MusicDirector, MusicRuntime);
    }

    public long QueueMusicEvent(MusicTriggerKind kind, string participantId = "", string targetId = "", float fadeSeconds = 0, string emitterId = "", DirectorVector? position = null, IReadOnlyList<string>? arguments = null)
    {
        var sequence = _nextEventSequence++; _pendingEvents.Add(new(sequence, kind, participantId, targetId, fadeSeconds, emitterId, position) { Arguments = arguments ?? Array.Empty<string>() }); return sequence;
    }

    public string SaveMusicDirectorState() => MusicDirector is null ? throw new InvalidOperationException("Music Director has not initialized.") : MusicStateCodec.DirectorToJson(MusicDirector.CaptureState());
    public string SaveMusicRuntimeState() => MusicRuntime is null ? throw new InvalidOperationException("Music runtime has not initialized.") : MusicStateCodec.RuntimeToJson(MusicRuntime.CaptureState());
    public void LoadMusicState(string directorJson, string runtimeJson)
    {
        var directorState = MusicStateCodec.DirectorFromJson(directorJson); var runtimeState = MusicStateCodec.RuntimeFromJson(runtimeJson); var catalog = CreateMusicCatalog();
        var director = new MusicDirectorCore(CreateMusicConfiguration(), directorState); var runtime = new MusicRuntime(RuntimeParticipantId, catalog, runtimeState, CreateMusicRuntimeConfiguration());
        MusicDirector = director; MusicRuntime = runtime; _runner = new(director, runtime, new ComponentAudioSink(this)); _nextEventSequence = Math.Max(1, directorState.LastGameplayEventSequence + 1); _pendingEvents.Clear(); OnMusicDirectorInitialized(director, runtime);
    }

    protected virtual MusicDirectorConfiguration CreateMusicConfiguration() => RecoveredMusicDirectorDefaults.CreateConfiguration(seed: Seed);
    protected virtual MusicRuntimeConfiguration CreateMusicRuntimeConfiguration() => new();
    protected virtual bool HasMusicAuthority => !IsProxy;
    protected virtual void OnMusicDirectorInitialized(MusicDirectorCore director, MusicRuntime runtime) { }
    protected abstract IMusicEventCatalog CreateMusicCatalog();
    protected abstract MusicWorldSnapshot CaptureMusicWorld();
    protected abstract void ExecuteMusicAction(MusicRuntimeAction action);

    sealed class ComponentAudioSink : IMusicAudioSink
    {
        readonly AdaptiveMusicDirectorComponent _component;
        public ComponentAudioSink(AdaptiveMusicDirectorComponent component) { _component = component; }
        public void Apply(MusicRuntimeAction action) => _component.ExecuteMusicAction(action);
    }
}