Code/Binder/Binder.cs
using System;
using System.Collections;
using System.Runtime.CompilerServices;
using Sandbox.MovieMaker.Properties;
namespace Sandbox.MovieMaker;
#nullable enable
/// <summary>
/// Controls which <see cref="ITrackTarget"/>s from a scene are controlled by which <see cref="ITrack"/> from a <see cref="IClip"/>.
/// Can be serialized to save which tracks are bound to which targets.
/// </summary>
public sealed partial class TrackBinder( Scene? scene = null ) : IEnumerable<KeyValuePair<Guid, IValid?>>
{
private readonly ConditionalWeakTable<ITrack, ITrackTarget> _cache = new();
/// <summary>
/// The scene this binder is targeting.
/// </summary>
public Scene Scene { get; } = scene ?? Game.ActiveScene ?? throw new Exception( "No active scene!" );
public void Add( IReferenceTrack track, IValid? target ) => Get( track ).Bind( target );
/// <summary>
/// Gets or creates a target that maps to the given <paramref name="track"/>.
/// The target might not be bound to anything in the scene yet, use <see cref="ITrackTarget.IsBound"/> to check.
/// </summary>
public ITrackTarget Get( ITrack track )
{
if ( _cache.TryGetValue( track, out var target ) )
{
return target;
}
// Recurse to get parent ITarget, if this track has a parent
var parent = track.Parent;
var parentTarget = parent is not null ? Get( parent ) : null;
// Create target for this track
target = CreateTarget( track, parentTarget );
// Update cache
_cache.AddOrUpdate( track, target );
return target;
}
/// <inheritdoc cref="Get(ITrack)"/>
public ITrackReference Get( IReferenceTrack track ) => (ITrackReference)Get( (ITrack)track );
/// <inheritdoc cref="Get(ITrack)"/>
public ITrackReference<T> Get<T>( IReferenceTrack<T> track ) where T : class, IValid => (ITrackReference<T>)Get( (ITrack)track );
/// <inheritdoc cref="Get(ITrack)"/>
public ITrackProperty Get( IPropertyTrack track ) => (ITrackProperty)Get( (ITrack)track );
/// <inheritdoc cref="Get(ITrack)"/>
public ITrackProperty<T> Get<T>( IPropertyTrack<T> track ) => (ITrackProperty<T>)Get( (ITrack)track );
private ITrackTarget CreateTarget( ITrack track, ITrackTarget? parent = null )
{
return track switch
{
// GameObject reference
IReferenceTrack refTrack when track.TargetType == typeof(GameObject) =>
new GameObjectReference( parent as ITrackReference<GameObject>, track.Name, this, refTrack.Id, refTrack.ReferenceId ),
// Component reference
IReferenceTrack refTrack =>
CreateComponentReference( parent as ITrackReference<GameObject>, track.TargetType, this, refTrack.Id ),
// Member property within another track
IPropertyTrack =>
TrackProperty.Create( parent ?? throw new ArgumentNullException( nameof(parent) ),
track.Name, track.TargetType ),
_ => throw new NotImplementedException()
};
}
public IEnumerable<T> GetComponents<T>( IClip clip )
where T : Component
{
return clip.Tracks
.OfType<IReferenceTrack<T>>()
.Select( x => Get( x ).Value )
.OfType<T>();
}
private static ConditionalWeakTable<Scene, TrackBinder> DefaultBinders { get; } = new();
/// <summary>
/// Gets the default binder for the active scene.
/// </summary>
public static TrackBinder Default
{
get
{
if ( Game.ActiveScene is not { } scene )
{
throw new InvalidOperationException( "No active scene!" );
}
if ( DefaultBinders.TryGetValue( scene, out var binder ) )
{
return binder;
}
DefaultBinders.Add( scene, binder = new TrackBinder( scene ) );
return binder;
}
}
public IEnumerator<KeyValuePair<Guid, IValid?>> GetEnumerator()
{
foreach ( var (guid, gameObject) in _gameObjectMap )
{
yield return new KeyValuePair<Guid, IValid?>( guid, gameObject );
}
foreach ( var (guid, component) in _componentMap )
{
yield return new KeyValuePair<Guid, IValid?>( guid, component );
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}