Code/Compiled/Serialization.cs
using System;
using System.Collections.Immutable;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace Sandbox.MovieMaker.Compiled;
#nullable enable
[JsonConverter( typeof( ClipConverter ) )]
partial class MovieClip;
file sealed class ClipConverter : JsonConverter<MovieClip>
{
public override MovieClip Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options )
{
return JsonSerializer.Deserialize<ClipModel>( ref reader, options )!.Deserialize( options );
}
public override void Write( Utf8JsonWriter writer, MovieClip value, JsonSerializerOptions options )
{
var childDict = value.Tracks
.Where( x => x.Parent is not null )
.GroupBy( x => x.Parent! )
.ToImmutableDictionary( x => x.Key, x => x.ToImmutableArray() );
JsonSerializer.Serialize( writer, new ClipModel( value, childDict, options ), options );
}
}
[method: JsonConstructor]
file sealed record ClipModel(
[property: JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )]
ImmutableArray<TrackModel>? Tracks )
{
public ClipModel( MovieClip clip, ImmutableDictionary<ICompiledTrack, ImmutableArray<ICompiledTrack>> childDict, JsonSerializerOptions? options )
: this( clip.Tracks is { Length: > 0 }
? clip.Tracks.Where( x => x.Parent is null ).Select( x => new TrackModel( x, childDict, options ) ).ToImmutableArray()
: null )
{
}
public MovieClip Deserialize( JsonSerializerOptions? options )
{
return Tracks is { Length: > 0 } rootTracks
? MovieClip.FromTracks( rootTracks.SelectMany( x => x.Deserialize( null, options ) ) )
: MovieClip.Empty;
}
}
file enum TrackKind
{
Reference,
Action,
Property
}
[method: JsonConstructor]
file sealed record TrackModel( TrackKind Kind, string Name, Type Type,
[property: JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )] Guid? Id,
[property: JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )] Guid? ReferenceId,
[property: JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )] ImmutableArray<TrackModel>? Children,
[property: JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )] ImmutableArray<JsonObject>? Blocks )
{
public TrackModel( ICompiledTrack track, ImmutableDictionary<ICompiledTrack, ImmutableArray<ICompiledTrack>> childDict, JsonSerializerOptions? options )
: this( GetKind( track ), track.Name, track.TargetType, (track as IReferenceTrack)?.Id, (track as IReferenceTrack)?.ReferenceId,
childDict.TryGetValue( track, out var children )
? children.Select( x => new TrackModel( x, childDict, options ) ).ToImmutableArray()
: null,
track is ICompiledBlockTrack { Blocks.Count: > 0 } blockTrack
? blockTrack.Blocks.Select( x => SerializeBlock( x, options ) ).ToImmutableArray()
: null )
{
}
public IEnumerable<ICompiledTrack> Deserialize( ICompiledTrack? parent, JsonSerializerOptions? options )
{
var track = Kind switch
{
TrackKind.Reference when Type == typeof(GameObject) => new CompiledReferenceTrack<GameObject>(
Id ?? Guid.NewGuid(), Name, (CompiledReferenceTrack<GameObject>?)parent, ReferenceId ),
TrackKind.Reference => TypeLibrary.GetType( typeof( CompiledReferenceTrack<> ) ).CreateGeneric<ICompiledReferenceTrack>( [Type],
[Id ?? Guid.NewGuid(), Type.Name, (CompiledReferenceTrack<GameObject>?)parent, ReferenceId] ),
TrackKind.Action => new CompiledActionTrack( Name, Type, parent!, ImmutableArray<CompiledActionBlock>.Empty ),
TrackKind.Property => DeserializeHelper.Get( Type ).DeserializePropertyTrack( this, parent!, options ),
_ => throw new NotImplementedException()
};
return Children is { IsDefaultOrEmpty: false } children
? [track, ..children.SelectMany( x => x.Deserialize( track, options ) )]
: [track];
}
private static TrackKind GetKind( ICompiledTrack track )
{
return track switch
{
IReferenceTrack => TrackKind.Reference,
IActionTrack => TrackKind.Action,
IPropertyTrack => TrackKind.Property,
_ => throw new NotImplementedException()
};
}
private static JsonObject SerializeBlock( ICompiledBlock block, JsonSerializerOptions? options ) =>
JsonSerializer.SerializeToNode( block, block.GetType(), options )!.AsObject();
}
file abstract class DeserializeHelper
{
[SkipHotload]
private static Dictionary<Type, DeserializeHelper> Cache { get; } = new();
public static DeserializeHelper Get( Type type )
{
if ( Cache.TryGetValue( type, out var cached ) ) return cached;
return Cache[type] = TypeLibrary.GetType( typeof(DeserializeHelper<>) )
.CreateGeneric<DeserializeHelper>( [type] );
}
public abstract ICompiledTrack DeserializePropertyTrack( TrackModel model, ICompiledTrack parent, JsonSerializerOptions? options );
}
file sealed class DeserializeHelper<T> : DeserializeHelper
{
public override ICompiledTrack DeserializePropertyTrack( TrackModel model, ICompiledTrack parent, JsonSerializerOptions? options )
{
return new CompiledPropertyTrack<T>( model.Name, parent,
model.Blocks?
.Select( x => DeserializePropertyBlock( x, options ) )
.ToImmutableArray()
?? ImmutableArray<ICompiledPropertyBlock<T>>.Empty );
}
private static ICompiledPropertyBlock<T> DeserializePropertyBlock( JsonObject node, JsonSerializerOptions? options )
{
var hasSamples = node[nameof( CompiledSampleBlock<object>.Samples )] is not null;
return hasSamples
? node.Deserialize<CompiledSampleBlock<T>>( options )!
: node.Deserialize<CompiledConstantBlock<T>>( options )!;
}
}
[JsonConverter( typeof(CompiledSampleBlockConverterFactory) )]
partial record CompiledSampleBlock<T>;
file sealed class CompiledSampleBlockConverterFactory : JsonConverterFactory
{
public override bool CanConvert( Type typeToConvert ) =>
TypeLibrary.GetType( typeToConvert )?.TargetType == typeof(CompiledSampleBlock<>);
public override JsonConverter? CreateConverter( Type typeToConvert, JsonSerializerOptions options )
{
var valueType = TypeLibrary.GetGenericArguments( typeToConvert )[0];
try
{
return TypeLibrary.GetType( typeof(CompressedSampleBlockConverter<>) )
.CreateGeneric<JsonConverter>( [valueType] );
}
catch
{
return JsonSerializerOptions.Default.GetConverter( typeof(int) );
}
}
}
file sealed class CompressedSampleBlockConverter<T> : JsonConverter<CompiledSampleBlock<T>>
where T : unmanaged
{
private sealed record Model( MovieTimeRange TimeRange,
[property: JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingDefault )] MovieTime Offset,
int SampleRate, JsonNode Samples );
public override void Write( Utf8JsonWriter writer, CompiledSampleBlock<T> value, JsonSerializerOptions options )
{
var stream = ByteStream.Create( 16 * value.Samples.Length + 4 );
stream.WriteArray( value.Samples.AsSpan() );
var compressed = stream.Compress();
var base64 = Convert.ToBase64String( compressed.ToArray() );
var model = new Model( value.TimeRange, value.Offset, value.SampleRate, base64 );
JsonSerializer.Serialize( writer, model, options );
}
public override CompiledSampleBlock<T> Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options )
{
var model = JsonSerializer.Deserialize<Model>( ref reader, options )!;
ImmutableArray<T> samples;
if ( model.Samples is JsonArray sampleArray )
{
samples = sampleArray.Deserialize<ImmutableArray<T>>( options );
}
else if ( model.Samples.GetValue<string>() is { } base64 )
{
var compressed = ByteStream.CreateReader( Convert.FromBase64String( base64 ) );
var stream = compressed.Decompress();
samples = stream.ReadArray<T>( 0x10_0000 ).ToImmutableArray();
}
else
{
throw new Exception( "Expected array or compressed sample string." );
}
return new CompiledSampleBlock<T>( model.TimeRange, model.Offset, model.SampleRate, samples );
}
}