mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-05-24 06:46:26 -04:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
401 lines
12 KiB
C#
401 lines
12 KiB
C#
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using System.Text.Json.Serialization;
|
|
using Sandbox.MovieMaker;
|
|
using Sandbox.MovieMaker.Compiled;
|
|
|
|
namespace Editor.MovieMaker;
|
|
|
|
#nullable enable
|
|
|
|
[JsonConverter( typeof(MovieProjectConverter) )]
|
|
partial class MovieProject : IJsonPopulator
|
|
{
|
|
internal sealed record Model(
|
|
int SampleRate, MovieTime? Duration, VideoExportConfig? ExportConfig,
|
|
ImmutableDictionary<Guid, ProjectTrackModel> Tracks,
|
|
ImmutableDictionary<Guid, ProjectSourceClip.Model>? Sources,
|
|
ImmutableHashSet<MovieResource>? References );
|
|
|
|
public JsonNode Serialize() => JsonSerializer.SerializeToNode( Snapshot(), EditorJsonOptions )!;
|
|
|
|
internal Model Snapshot()
|
|
{
|
|
using var scope = MovieSerializationContext.Push();
|
|
|
|
return new Model(
|
|
SampleRate, Duration, ExportConfig,
|
|
Tracks.ToImmutableDictionary( x => x.Id, x => x.Serialize( EditorJsonOptions ) ),
|
|
scope.SourceClips.ToImmutableDictionary( x => x.Id, x => x.Serialize() ),
|
|
Tracks.OfType<ProjectSequenceTrack>().SelectMany( x => x.References ).ToImmutableHashSet() );
|
|
}
|
|
|
|
internal void Restore( Model model )
|
|
{
|
|
using var scope = MovieSerializationContext.Push();
|
|
|
|
var config = ProjectSettings.Get<MovieMakerConfig>( Session.ConfigFileName );
|
|
|
|
SampleRate = model.SampleRate;
|
|
ExportConfig = model.ExportConfig;
|
|
|
|
foreach ( var (id, sourceModel) in model.Sources ?? ImmutableDictionary<Guid, ProjectSourceClip.Model>.Empty )
|
|
{
|
|
scope.RegisterSourceClip( new ProjectSourceClip( id, sourceModel.Clip, sourceModel.Metadata ) );
|
|
}
|
|
|
|
_rootTrackList.Clear();
|
|
_trackDict.Clear();
|
|
_trackList.Clear();
|
|
_tracksChanged = true;
|
|
|
|
var addedTracks = new Dictionary<Guid, IProjectTrack?>();
|
|
|
|
foreach ( var (id, trackModel) in model.Tracks )
|
|
{
|
|
try
|
|
{
|
|
switch ( trackModel )
|
|
{
|
|
case ProjectReferenceTrackModel refModel:
|
|
addedTracks[id] = IProjectReferenceTrack.Create( this, id, refModel.Name, refModel.TargetType );
|
|
break;
|
|
|
|
case ProjectPropertyTrackModel propertyModel:
|
|
addedTracks[id] =
|
|
IProjectPropertyTrack.Create( this, id, propertyModel.Name, propertyModel.TargetType );
|
|
break;
|
|
|
|
case ProjectSequenceTrackModel sequenceModel:
|
|
addedTracks[id] = new ProjectSequenceTrack( this, id, sequenceModel.Name );
|
|
break;
|
|
|
|
default:
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
catch ( Exception ex )
|
|
{
|
|
Log.Warning( ex, $"Exception when deserializing track \"{trackModel.Name}\"" );
|
|
|
|
addedTracks[id] = null;
|
|
}
|
|
}
|
|
|
|
foreach ( var (id, trackModel) in model.Tracks )
|
|
{
|
|
if ( addedTracks[id] is not { } addedTrack ) continue;
|
|
|
|
if ( trackModel.ParentId is { } parentId )
|
|
{
|
|
if ( addedTracks[parentId] is not { } parentTrack ) continue;
|
|
|
|
AddTrackInternal( addedTrack, parentTrack );
|
|
}
|
|
else
|
|
{
|
|
AddTrackInternal( addedTrack, null );
|
|
}
|
|
}
|
|
|
|
foreach ( var (id, trackModel) in model.Tracks )
|
|
{
|
|
var addedTrack = addedTracks[id];
|
|
|
|
addedTrack?.Deserialize( trackModel, EditorJsonOptions );
|
|
}
|
|
}
|
|
|
|
public void Deserialize( JsonNode node ) =>
|
|
Restore( node.Deserialize<Model>( EditorJsonOptions )! );
|
|
}
|
|
|
|
file sealed class MovieProjectConverter : JsonConverter<MovieProject>
|
|
{
|
|
public override void Write( Utf8JsonWriter writer, MovieProject value, JsonSerializerOptions options )
|
|
{
|
|
JsonSerializer.Serialize( value.Serialize(), options );
|
|
}
|
|
|
|
public override MovieProject? Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options )
|
|
{
|
|
var project = new MovieProject();
|
|
var node = JsonSerializer.Deserialize<JsonNode>( ref reader, options )!;
|
|
|
|
project.Deserialize( node );
|
|
|
|
return project;
|
|
}
|
|
}
|
|
|
|
file sealed class MovieSerializationContext : IDisposable
|
|
{
|
|
public static MovieSerializationContext Push()
|
|
{
|
|
return Current = new MovieSerializationContext( Current );
|
|
}
|
|
|
|
[field: ThreadStatic]
|
|
public static MovieSerializationContext? Current { get; private set; }
|
|
|
|
private readonly Dictionary<IPropertySignal, int> _signalsToId = new();
|
|
private readonly Dictionary<int, IPropertySignal> _signalsFromId = new();
|
|
|
|
private readonly Dictionary<Guid, ProjectSourceClip> _sources = new();
|
|
|
|
private readonly MovieSerializationContext? _parent;
|
|
|
|
public IEnumerable<ProjectSourceClip> SourceClips => _sources.Values.OrderBy( x => x.Id );
|
|
|
|
private MovieSerializationContext( MovieSerializationContext? parent )
|
|
{
|
|
_parent = parent;
|
|
}
|
|
|
|
public void ResetSignals()
|
|
{
|
|
_signalsToId.Clear();
|
|
_signalsFromId.Clear();
|
|
}
|
|
|
|
public bool TryRegisterSignal( IPropertySignal signal, out int id )
|
|
{
|
|
if ( _signalsToId.TryGetValue( signal, out id ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_signalsToId[signal] = id = _signalsToId.Count + 1;
|
|
|
|
return true;
|
|
}
|
|
|
|
public void RegisterSignal( int id, IPropertySignal signal ) => _signalsFromId[id] = signal;
|
|
public IPropertySignal GetSignal( int id ) => _signalsFromId[id];
|
|
public ProjectSourceClip GetSourceClip( Guid id ) => _sources[id];
|
|
|
|
public void Dispose()
|
|
{
|
|
if ( Current != this ) throw new InvalidOperationException();
|
|
|
|
Current = _parent;
|
|
}
|
|
|
|
public void RegisterSourceClip( ProjectSourceClip value )
|
|
{
|
|
_sources[value.Id] = value;
|
|
}
|
|
}
|
|
|
|
[JsonPolymorphic]
|
|
[JsonDerivedType( typeof( ProjectReferenceTrackModel ), "Reference" )]
|
|
[JsonDerivedType( typeof( ProjectPropertyTrackModel ), "Property" )]
|
|
[JsonDerivedType( typeof( ProjectSequenceTrackModel ), "Sequence" )]
|
|
public abstract record ProjectTrackModel( string Name, Type TargetType,
|
|
[property: JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )] Guid? ParentId );
|
|
|
|
file sealed record ProjectReferenceTrackModel( string Name, Type TargetType, Guid? ParentId, Guid? ReferenceId )
|
|
: ProjectTrackModel( Name, TargetType, ParentId );
|
|
|
|
file sealed record ProjectPropertyTrackModel( string Name, Type TargetType, Guid? ParentId,
|
|
[property: JsonPropertyOrder( 100 ), JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )] JsonArray? Blocks )
|
|
: ProjectTrackModel( Name, TargetType, ParentId );
|
|
|
|
file sealed record ProjectSequenceTrackModel( string Name, Guid? ParentId, ImmutableArray<ProjectSequenceBlockModel> Blocks )
|
|
: ProjectTrackModel( Name, typeof( MovieResource ), ParentId );
|
|
|
|
file sealed record ProjectSequenceBlockModel(
|
|
MovieTimeRange TimeRange,
|
|
MovieTransform Transform,
|
|
MovieResource Resource );
|
|
|
|
partial interface IProjectTrack
|
|
{
|
|
ProjectTrackModel Serialize( JsonSerializerOptions options );
|
|
void Deserialize( ProjectTrackModel model, JsonSerializerOptions options );
|
|
}
|
|
|
|
partial class ProjectTrack<T>
|
|
{
|
|
public abstract ProjectTrackModel Serialize( JsonSerializerOptions options );
|
|
public abstract void Deserialize( ProjectTrackModel model, JsonSerializerOptions options );
|
|
}
|
|
|
|
partial class ProjectReferenceTrack<T>
|
|
{
|
|
public override ProjectTrackModel Serialize( JsonSerializerOptions options )
|
|
{
|
|
return new ProjectReferenceTrackModel( Name, TargetType, Parent?.Id, ReferenceId );
|
|
}
|
|
|
|
public override void Deserialize( ProjectTrackModel model, JsonSerializerOptions options )
|
|
{
|
|
if ( model is not ProjectReferenceTrackModel refModel ) return;
|
|
|
|
ReferenceId = refModel.ReferenceId;
|
|
}
|
|
}
|
|
|
|
partial class ProjectPropertyTrack<T>
|
|
{
|
|
public override ProjectTrackModel Serialize( JsonSerializerOptions options )
|
|
{
|
|
MovieSerializationContext.Current?.ResetSignals();
|
|
|
|
return new ProjectPropertyTrackModel( Name, TargetType, Parent?.Id,
|
|
Blocks.Count != 0
|
|
? JsonSerializer.SerializeToNode( Blocks, EditorJsonOptions )!.AsArray()
|
|
: null );
|
|
}
|
|
|
|
public override void Deserialize( ProjectTrackModel model, JsonSerializerOptions options )
|
|
{
|
|
if ( model is not ProjectPropertyTrackModel propertyModel ) return;
|
|
|
|
MovieSerializationContext.Current?.ResetSignals();
|
|
|
|
_blocks.Clear();
|
|
_blocksChanged = true;
|
|
|
|
if ( propertyModel.Blocks?.Deserialize<ImmutableArray<PropertyBlock<T>>>( options ) is not { } blocks )
|
|
{
|
|
return;
|
|
}
|
|
|
|
_blocks.AddRange( blocks );
|
|
}
|
|
}
|
|
|
|
partial class ProjectSequenceTrack
|
|
{
|
|
public override ProjectTrackModel Serialize( JsonSerializerOptions options )
|
|
{
|
|
return new ProjectSequenceTrackModel( Name, Parent?.Id, [..Blocks.Select( x => new ProjectSequenceBlockModel( x.TimeRange, x.Transform, x.Resource ) )] );
|
|
}
|
|
|
|
public override void Deserialize( ProjectTrackModel model, JsonSerializerOptions options )
|
|
{
|
|
if ( model is not ProjectSequenceTrackModel sequenceModel ) return;
|
|
|
|
_tracksInvalid = true;
|
|
|
|
_blocks.Clear();
|
|
|
|
if ( sequenceModel.Blocks is { IsDefaultOrEmpty: false } blocks )
|
|
{
|
|
_blocks.AddRange( blocks.Select( x => new ProjectSequenceBlock( x.TimeRange, x.Transform, x.Resource ) ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class JsonDiscriminatorAttribute( string value ) : Attribute
|
|
{
|
|
public string Value { get; } = value;
|
|
}
|
|
|
|
[JsonConverter( typeof(PropertySignalConverterFactory) )]
|
|
partial record PropertySignal<T>;
|
|
|
|
file class PropertySignalConverterFactory : JsonConverterFactory
|
|
{
|
|
public override bool CanConvert( Type typeToConvert ) =>
|
|
typeToConvert.IsConstructedGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(PropertySignal<>);
|
|
|
|
public override JsonConverter CreateConverter( Type typeToConvert, JsonSerializerOptions options )
|
|
{
|
|
var valueType = typeToConvert.GetGenericArguments()[0];
|
|
var converterType = typeof(PropertySignalConverter<>).MakeGenericType( valueType );
|
|
|
|
return (JsonConverter)Activator.CreateInstance( converterType )!;
|
|
}
|
|
}
|
|
|
|
file class PropertySignalConverter<T> : JsonConverter<PropertySignal<T>>
|
|
{
|
|
[SkipHotload]
|
|
private static ImmutableDictionary<string, Type>? _discriminatorLookup;
|
|
|
|
private static string GetDiscriminator( Type type )
|
|
{
|
|
return type.GetCustomAttribute<JsonDiscriminatorAttribute>()?.Value ?? type.Name;
|
|
}
|
|
|
|
private static Type GetType( string discriminator )
|
|
{
|
|
_discriminatorLookup ??= EditorTypeLibrary.GetTypesWithAttribute<JsonDiscriminatorAttribute>()
|
|
.Where( x => !x.Type.IsAbstract && x.Type.IsGenericType )
|
|
.Select( x => (Name: x.Attribute.Value, Type: x.Type.TargetType.MakeGenericType( typeof(T) )) )
|
|
.ToImmutableDictionary( x => x.Name, x => x.Type );
|
|
|
|
return _discriminatorLookup[discriminator];
|
|
}
|
|
|
|
public override void Write( Utf8JsonWriter writer, PropertySignal<T> value, JsonSerializerOptions options )
|
|
{
|
|
var context = MovieSerializationContext.Current!;
|
|
|
|
if ( !context.TryRegisterSignal( value, out var id ) )
|
|
{
|
|
writer.WriteNumberValue( id );
|
|
return;
|
|
}
|
|
|
|
var type = value.GetType();
|
|
var node = JsonSerializer.SerializeToNode( value, type, options )!.AsObject();
|
|
|
|
node.Insert( 0, "$id", id );
|
|
node.Insert( 1, "$type", GetDiscriminator( type ) );
|
|
|
|
JsonSerializer.Serialize( writer, node, options );
|
|
}
|
|
|
|
public override PropertySignal<T>? Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options )
|
|
{
|
|
if ( reader.TokenType == JsonTokenType.Number )
|
|
{
|
|
var refId = JsonSerializer.Deserialize<int>( ref reader, options );
|
|
|
|
return (PropertySignal<T>)MovieSerializationContext.Current!.GetSignal( refId );
|
|
}
|
|
|
|
var node = JsonSerializer.Deserialize<JsonObject>( ref reader, options )!;
|
|
var id = node["$id"]!.GetValue<int>();
|
|
var discriminator = node["$type"]!.GetValue<string>();
|
|
var type = GetType( discriminator );
|
|
|
|
var signal = (PropertySignal<T>)node.Deserialize( type, options )!;
|
|
|
|
MovieSerializationContext.Current!.RegisterSignal( id, signal );
|
|
|
|
return signal;
|
|
}
|
|
}
|
|
|
|
[JsonConverter( typeof(ProjectSourceClipConverter) )]
|
|
partial record ProjectSourceClip
|
|
{
|
|
public record Model( MovieClip Clip, [property: JsonPropertyOrder( -1 )] JsonObject? Metadata );
|
|
|
|
public Model Serialize() => new ( Clip, Metadata );
|
|
}
|
|
|
|
file class ProjectSourceClipConverter : JsonConverter<ProjectSourceClip>
|
|
{
|
|
public override void Write( Utf8JsonWriter writer, ProjectSourceClip value, JsonSerializerOptions options )
|
|
{
|
|
MovieSerializationContext.Current?.RegisterSourceClip( value );
|
|
|
|
writer.WriteStringValue( value.Id );
|
|
}
|
|
|
|
public override ProjectSourceClip? Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options )
|
|
{
|
|
var id = JsonSerializer.Deserialize<Guid>( ref reader, options );
|
|
|
|
return MovieSerializationContext.Current!.GetSourceClip( id );
|
|
}
|
|
}
|