Editor/ShaderGraphPlus/ShaderGraphPlus.Serialize.cs
using ShaderGraphPlus.Nodes;
using System.Text.Json.Nodes;
namespace ShaderGraphPlus;
public interface ISGPJsonUpgradeable
{
[Hide]
public int Version { get; }
}
partial class ShaderGraphPlus
{
/// <summary>
/// Json Keys used for serialization and deserialization.
/// </summary>
internal static class JsonKeys
{
internal const string Version = "__version";
internal const string Class = "_class";
internal const string NodeArray = "nodes";
internal const string ParameterArray = "parameters";
}
internal static JsonSerializerOptions SerializerOptions( bool indented = false )
{
var options = new JsonSerializerOptions
{
WriteIndented = indented,
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
ReadCommentHandling = JsonCommentHandling.Skip,
};
options.Converters.Add( new JsonStringEnumConverter( null, true ) );
return options;
}
public string Serialize()
{
var doc = new JsonObject();
var options = SerializerOptions( true );
SerializeObject( this, doc, options );
SerializeNodes( Nodes, doc, options );
SerializeParameters( Parameters, doc, options );
doc.Add( JsonKeys.Version, JsonSerializer.SerializeToNode( Version, options ) );
return doc.ToJsonString( options );
}
public void Deserialize( string json, string subgraphPath = null, string fileName = "" )
{
using var doc = JsonDocument.Parse( json );
var root = doc.RootElement;
var options = SerializerOptions();
var fileVersion = GetGraphVersion( root );
if ( HandleGraphUpgrades( fileVersion, Json.ParseToJsonObject( json ), options, out JsonElement upgradedElement ) )
{
root = upgradedElement;
}
DeserializeObject( this, root, options );
DeserializeParameters( root, options );
DeserializeNodes( root, options, subgraphPath, fileVersion );
}
private bool HandleGraphUpgrades( int fileVersion, JsonObject json, JsonSerializerOptions options, out JsonElement upgradedElement )
{
upgradedElement = default;
if ( fileVersion >= Version )
return false;
ShaderGraphPlusJsonUpgrader.Upgrade( fileVersion, json, typeof( ShaderGraphPlus ) );
upgradedElement = JsonSerializer.Deserialize<JsonElement>( json.ToJsonString(), options );
return true;
}
public IEnumerable<BaseNodePlus> DeserializeNodes( string json, bool useCurrentVersion = false )
{
using var doc = JsonDocument.Parse( json, new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip } );
var root = doc.RootElement;
var fileVersion = GetGraphVersion( root, useCurrentVersion );
return DeserializeNodes( root, SerializerOptions(), null, fileVersion );
}
private static void DeserializeObject( object obj, JsonElement doc, JsonSerializerOptions options )
{
var type = obj.GetType();
var properties = type.GetProperties( BindingFlags.Instance | BindingFlags.Public )
.Where( x => x.GetSetMethod() != null );
// start deserilzing each property of the current type we are deserialzing. Also handle
// any property that needs upgrading.
foreach ( var jsonProperty in doc.EnumerateObject() )
{
var propertyInfo = properties.FirstOrDefault( x =>
{
var propName = x.Name;
if ( x.GetCustomAttribute<JsonPropertyNameAttribute>() is JsonPropertyNameAttribute jpna )
propName = jpna.Name;
return string.Equals( propName, jsonProperty.Name, StringComparison.OrdinalIgnoreCase );
} );
if ( propertyInfo == null )
continue;
if ( propertyInfo.CanWrite == false )
continue;
if ( propertyInfo.IsDefined( typeof( JsonIgnoreAttribute ) ) )
continue;
// Handle any types that use the ISGPJsonUpgradeable interface
if ( typeof( ISGPJsonUpgradeable ).IsAssignableFrom( propertyInfo.PropertyType ) )
{
var propertyTypeInstance = EditorTypeLibrary.Create( propertyInfo.PropertyType.Name, propertyInfo.PropertyType );
int oldVersionNumber = 0;
// if we have a valid version then set oldVersionNumber otherwise just use a version of 0.
if ( jsonProperty.Value.TryGetProperty( JsonKeys.Version, out var versionElement ) )
{
oldVersionNumber = versionElement.GetInt32();
}
else
{
SGPLogger.Warning( $"Failed to get property \"{type}\" upgradeable version. defaulting to \"0\"", ConCommands.VerboseJsonUpgrader );
}
// Dont even bother upgrading if we dont need to.
if ( propertyTypeInstance is ISGPJsonUpgradeable upgradeable && oldVersionNumber < upgradeable.Version )
{
var upgradedElement = UpgradeJsonUpgradeable( oldVersionNumber, upgradeable, propertyInfo.PropertyType, jsonProperty, options );
propertyInfo.SetValue( obj, JsonSerializer.Deserialize( upgradedElement.GetRawText(), propertyInfo.PropertyType, options ) );
// Continue to the next jsonProperty :)
continue;
}
}
propertyInfo.SetValue( obj, JsonSerializer.Deserialize( jsonProperty.Value.GetRawText(), propertyInfo.PropertyType, options ) );
}
}
private IEnumerable<BaseNodePlus> DeserializeNodes( JsonElement doc, JsonSerializerOptions options, string subgraphPath = null, int graphFileVersion = -1 )
{
var nodes = new Dictionary<string, BaseNodePlus>();
var identifiers = _nodes.Count > 0 ? new Dictionary<string, string>() : null;
var connections = new List<(IPlugIn Plug, NodeInput Value)>();
var arrayProperty = doc.GetProperty( JsonKeys.NodeArray );
foreach ( var element in arrayProperty.EnumerateArray() )
{
var typeName = element.GetProperty( JsonKeys.Class ).GetString();
var typeDesc = EditorTypeLibrary.GetType<BaseNodePlus>( typeName );
var type = new ClassNodeType( typeDesc );
BaseNodePlus node;
if ( typeDesc is null )
{
SGPLogger.Error( $"Missing Node : \"{typeName}\"" );
var missingNode = new MissingNode( typeName, element );
node = missingNode;
DeserializeObject( node, element, options );
nodes.Add( node.Identifier, node );
AddNode( node );
}
else
{
node = EditorTypeLibrary.Create<BaseNodePlus>( typeName );
DeserializeObject( node, element, options );
if ( identifiers != null && _nodes.ContainsKey( node.Identifier ) )
{
identifiers.Add( node.Identifier, node.NewIdentifier() );
}
// Early case to hook up the graph to node early so that the connections are actually loaded and dont break on load.
if ( node is EnumFeatureSwitchNode || node is SubgraphOutput )
{
node.Graph = this;
}
if ( node is BaseNodePlus.IInitializeNode initializeableNode )
{
initializeableNode.InitializeNode();
}
if ( node is SubgraphNode subgraphNode )
{
if ( !Editor.FileSystem.Content.FileExists( subgraphNode.SubgraphPath ) )
{
var missingNode = new MissingNode( typeName, element );
node = missingNode;
DeserializeObject( node, element, options );
}
else
{
subgraphNode.OnNodeCreated();
}
}
foreach ( var input in node.Inputs )
{
if ( !element.TryGetProperty( input.Identifier, out var connectedElem ) )
continue;
var connected = connectedElem
.Deserialize<NodeInput?>();
if ( connected is { IsValid: true } )
{
var connection = connected.Value;
if ( !string.IsNullOrEmpty( subgraphPath ) )
{
connection = new()
{
Identifier = connection.Identifier,
Output = connection.Output,
Subgraph = subgraphPath
};
}
connections.Add( (input, connection) );
}
}
nodes.Add( node.Identifier, node );
AddNode( node );
}
}
foreach ( var (input, value) in connections )
{
var outputIdent = identifiers?.TryGetValue( value.Identifier, out var newIdent ) ?? false
? newIdent : value.Identifier;
if ( nodes.TryGetValue( outputIdent, out var node ) )
{
var output = node.Outputs.FirstOrDefault( x => x.Identifier == value.Output );
if ( output is null )
{
// Check for Aliases
foreach ( var op in node.Outputs )
{
if ( op is not BasePlugOut plugOut ) continue;
var aliasAttr = plugOut.Info.Property?.GetCustomAttribute<AliasAttribute>();
if ( aliasAttr is not null && aliasAttr.Value.Contains( value.Output ) )
{
output = plugOut;
break;
}
}
}
input.ConnectedOutput = output;
}
}
return nodes.Values;
}
public IEnumerable<BlackboardParameter> DeserializeParameters( string json )
{
using var doc = JsonDocument.Parse( json, new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip } );
var root = doc.RootElement;
return DeserializeParameters( root, SerializerOptions() );
}
private IEnumerable<BlackboardParameter> DeserializeParameters( JsonElement doc, JsonSerializerOptions options )
{
var parameters = new Dictionary<string, BlackboardParameter>();
if ( doc.TryGetProperty( JsonKeys.ParameterArray, out var arrayProperty ) )
{
foreach ( var element in arrayProperty.EnumerateArray() )
{
var typeName = element.GetProperty( JsonKeys.Class ).GetString();
var typeDesc = EditorTypeLibrary.GetType<BlackboardParameter>( typeName );
var type = new ClassBlackboardParameterType( typeDesc );
BlackboardParameter parameter;
if ( typeDesc != null )
{
parameter = EditorTypeLibrary.Create<BlackboardParameter>( typeName );
DeserializeObject( parameter, element, options );
if ( string.IsNullOrWhiteSpace( parameter.Name ) )
{
var name = $"{(IsSubgraph ? "SubgraphInput" : "MaterialParameter")}";
var id = name;
int count = 0;
while ( parameters.ContainsKey( id ) )
{
id = $"{name}_{count++}";
}
parameter.Name = id;
}
parameters.Add( parameter.Name, parameter );
AddParameter( parameter );
}
}
}
return parameters.Values;
}
public string SerializeNodes()
{
return SerializeNodes( Nodes );
}
public string UndoStackSerialize()
{
var doc = new JsonObject();
var options = SerializerOptions();
doc = SerializeNodes( Nodes, doc );
return SerializeParameters( Parameters, doc ).ToJsonString( options );
}
public string SerializeNodes( IEnumerable<BaseNodePlus> nodes )
{
var doc = new JsonObject();
var options = SerializerOptions();
SerializeNodes( nodes, doc, options );
return doc.ToJsonString( options );
}
public JsonObject SerializeNodes( IEnumerable<BaseNodePlus> nodes, JsonObject doc )
{
var options = SerializerOptions();
SerializeNodes( nodes, doc, options );
return doc;
}
private static void SerializeObject( object obj, JsonObject doc, JsonSerializerOptions options, Dictionary<string, string> identifiers = null )
{
var type = obj.GetType();
var properties = type.GetProperties( BindingFlags.Instance | BindingFlags.Public )
.Where( x => x.GetSetMethod() != null );
foreach ( var property in properties )
{
if ( !property.CanRead )
continue;
if ( property.PropertyType == typeof( NodeInput ) )
continue;
if ( property.IsDefined( typeof( JsonIgnoreAttribute ) ) )
continue;
var propertyName = property.Name;
if ( property.GetCustomAttribute<JsonPropertyNameAttribute>() is { } jpna )
propertyName = jpna.Name;
var propertyValue = property.GetValue( obj );
if ( propertyName == "Identifier" && propertyValue is string identifier )
{
if ( identifiers.TryGetValue( identifier, out var newIdentifier ) )
{
propertyValue = newIdentifier;
}
}
if ( propertyName != "Version" )
{
doc.Add( propertyName, JsonSerializer.SerializeToNode( propertyValue, options ) );
}
}
if ( obj is IGraphNode node )
{
foreach ( var input in node.Inputs )
{
if ( input.ConnectedOutput is not { } output )
continue;
doc.Add( input.Identifier, JsonSerializer.SerializeToNode( new NodeInput
{
Identifier = identifiers?.TryGetValue( output.Node.Identifier, out var newIdent ) ?? false ? newIdent : output.Node.Identifier,
Output = output.Identifier,
} ) );
}
}
}
private static void SerializeNodes( IEnumerable<BaseNodePlus> nodes, JsonObject doc, JsonSerializerOptions options )
{
var identifiers = new Dictionary<string, string>();
foreach ( var node in nodes )
{
identifiers.Add( node.Identifier, $"{identifiers.Count}" );
}
var nodeArray = new JsonArray();
foreach ( var node in nodes )
{
if ( node is DummyNode )
continue;
var type = node.GetType();
var nodeObject = new JsonObject { { JsonKeys.Class, type.Name } };
SerializeObject( node, nodeObject, options, identifiers );
nodeArray.Add( nodeObject );
}
doc.Add( JsonKeys.NodeArray, nodeArray );
}
public string SerializeParameters()
{
return SerializeParameters( Parameters );
}
private string SerializeParameters( IEnumerable<BlackboardParameter> parameters )
{
var doc = new JsonObject();
var options = SerializerOptions();
SerializeParameters( parameters, doc, options );
return doc.ToJsonString( options );
}
private JsonObject SerializeParameters( IEnumerable<BlackboardParameter> parameters, JsonObject doc )
{
var options = SerializerOptions();
SerializeParameters( parameters, doc, options );
return doc;
}
private static void SerializeParameters( IEnumerable<BlackboardParameter> parameters, JsonObject doc, JsonSerializerOptions options )
{
var parameterArray = new JsonArray();
foreach ( var parameter in parameters )
{
var type = parameter.GetType();
var parameterObject = new JsonObject { { JsonKeys.Class, type.Name } };
SerializeObject( parameter, parameterObject, options );
parameterArray.Add( parameterObject );
}
doc.Add( JsonKeys.ParameterArray, parameterArray );
}
}