Editor/ShaderGraphPlus/ShaderGraphPlus.V10Upgrade.cs

Editor upgrade handler for ShaderGraphPlus v10. Adds logic to migrate serialized shader graph JSON: reads old parameter and node arrays, deserializes BlackboardParameter types, groups parameters into CategoryData based on UI group info, reorders parameters and writes new ParameterArray and CategoryDataArray entries.

File Access
using System.Text.Json.Nodes;

namespace ShaderGraphPlus;

file struct ParameterEntry : IValid
{
	public BlackboardParameter Parameter { get; internal set; }

	public CategoryData CategoryData { get; internal set; }

	public int OrderInRoot { get; internal set; }

	public bool Grouped { get; internal set; }

	public bool IsValid => Parameter != null;

	public ParameterEntry( BlackboardParameter parameter, int orderInRoot, bool grouped )
	{
		Parameter = parameter;
		OrderInRoot = orderInRoot;
		Grouped = grouped;
	}

	public ParameterEntry( CategoryData categoryData, int orderInRoot, bool grouped )
	{
		Parameter = null;

		CategoryData = categoryData;
		OrderInRoot = orderInRoot;
		Grouped = grouped;
	}
}

public partial class ShaderGraphPlus
{
	[SGPJsonUpgrader( typeof( ShaderGraphPlus ), 10 )]
	internal static void Upgrader_v10( JsonObject obj )
	{
		var categories = new List<CategoryData>();
		var updatedParameters = new List<ParameterEntry>();
		var registeredGroupNames = new List<string>();

		static int GetGroupOrder( JsonNode node, string uiKeyName )
		{
			if ( node[uiKeyName] is not JsonNode uiNode )
				return -1;

			if ( uiNode["Priority"] is JsonValue priorityValue )
			{
				return priorityValue.GetValue<int>();
			}

			return -1;
		}

		static UIGroup GetUIGroup( JsonNode node, string uiKeyName )
		{
			if ( node[uiKeyName] is not JsonNode uiNode )
				return default;

			if ( uiNode["PrimaryGroup"] is JsonNode primaryGroupNode )
			{
				return JsonSerializer.Deserialize<UIGroup>( primaryGroupNode, SerializerOptions() );
			}

			return default;
		}

		void AddToGroup( BlackboardParameter parameter, int parameterPriorityInGroup, UIGroup primaryGroup )
		{
			if ( !string.IsNullOrWhiteSpace( primaryGroup.Name ) )
			{
				if ( !registeredGroupNames.Contains( primaryGroup.Name ) )
				{
					var newCategoryData = new CategoryData
					{
						Name = $"{primaryGroup.Name} Group",
						Priority = primaryGroup.Priority
					};
					newCategoryData.ParameterReferences.Add( parameter.Identifier );

					if ( parameter is IGroupableBlackboardParameter groupableParameter )
					{
						groupableParameter.GroupReference = newCategoryData.Identifier;
					}

					if ( !categories.Contains( newCategoryData ) )
					{
						categories.Add( newCategoryData );
					}

					registeredGroupNames.Add( primaryGroup.Name );
				}
				else
				{
					var existingEntry = categories.FirstOrDefault( x => x.Name == $"{primaryGroup.Name} Group" );

					if ( existingEntry != null )
					{
						if ( !existingEntry.ParameterReferences.Contains( parameter.Identifier ) )
						{
							existingEntry.ParameterReferences.Add( parameter.Identifier );
						}

						categories[categories.IndexOf( existingEntry )] = existingEntry;

						if ( parameter is IGroupableBlackboardParameter groupableParameter )
						{
							groupableParameter.GroupReference = existingEntry.Identifier;
						}
					}
				}

				if ( !updatedParameters.Any( x => x.Parameter.Name == parameter.Name ) )
				{
					updatedParameters.Add( new ParameterEntry( parameter, 0, true ) );
				}
			}
			else
			{
				if ( !updatedParameters.Any( x => x.Parameter.Name == parameter.Name ) )
				{
					// Shove all non-grouped parameters after the grouped parameters.
					updatedParameters.Add( new ParameterEntry( parameter, updatedParameters.Count(), false ) );
				}
			}
		}

		if ( obj[JsonKeys.ParameterArray] is not JsonArray oldParameterArray )
			throw new Exception( $"Cannot find jsonArray \'{JsonKeys.ParameterArray}\'" );

		if ( obj[JsonKeys.NodeArray] is not JsonArray oldNodeArray )
			throw new Exception( $"Cannot find jsonArray \'{JsonKeys.NodeArray}\'" );

		var isSubgraph = CheckIfSubgraph( obj );

		//
		// Upgrade Parameters
		//
		var newParameterArray = new JsonArray();

		foreach ( var jsonNode in oldParameterArray )
		{
			if ( jsonNode[JsonKeys.Class] is not JsonValue classValue )
				continue;

			var typeName = classValue.GetValue<string>();
			var typeDesc = EditorTypeLibrary.GetType<BlackboardParameter>( typeName );
			var type = new ClassBlackboardParameterType( typeDesc );

			var newParameterObj = jsonNode.DeepClone().AsObject();

			BlackboardParameter parameter = null;

			if ( typeDesc != null )
			{
				parameter = EditorTypeLibrary.Create<BlackboardParameter>( typeName );
				DeserializeObject( parameter, JsonSerializer.Deserialize<JsonElement>( jsonNode ), SerializerOptions() );

				if ( !typeDesc.TargetType.IsAssignableTo( typeof( IBlackboardSubgraphParameter ) ) && typeDesc.TargetType.IsAssignableTo( typeof( IGroupableBlackboardParameter ) ) )
				{
					var parameterPriorityInGroup = 0;
					var primaryGroup = new UIGroup();

					if ( typeDesc.TargetType.IsAssignableTo( typeof( IBlackboardMaterialParameter ) ) )
					{
						parameterPriorityInGroup = GetGroupOrder( jsonNode, "UI" );
						primaryGroup = GetUIGroup( jsonNode, "UI" );
					}
					else if ( typeDesc.TargetType.IsAssignableTo( typeof( BlackboardTextureMaterialParameter ) ) )
					{
						parameterPriorityInGroup = GetGroupOrder( jsonNode, "Value" );
						primaryGroup = GetUIGroup( jsonNode, "Value" );
					}

					if ( parameterPriorityInGroup == -1 )
					{
						parameterPriorityInGroup = 0;
					}

					AddToGroup( parameter, parameterPriorityInGroup, primaryGroup );
				}
				else
				{
					var portOrder = 0;
					if ( jsonNode["PortOrder"] is JsonValue portOrderValue )
					{
						portOrder = portOrderValue.GetValue<int>();
					}

					updatedParameters.Add( new ParameterEntry( parameter, portOrder, false ) );
				}
			}
		}

		foreach ( var parameter in updatedParameters.OrderBy( x => x.OrderInRoot ) )
		{
			var parameterType = parameter.Parameter.GetType();
			var parameterObject = new JsonObject { { JsonKeys.Class, parameterType.Name } };

			SerializeObject( parameter.Parameter, parameterObject, SerializerOptions() );
			newParameterArray.Add( parameterObject );
		}

		obj.Remove( JsonKeys.ParameterArray );
		obj.Add( JsonKeys.ParameterArray, newParameterArray );

		var newCategoryDataArray = new JsonArray();

		// Sort the categories
		var sortedCategories = categories.OrderBy( x => x.Priority ).ToList();

		for ( int i = 0; i < sortedCategories.Count; i++ )
		{
			sortedCategories[i].Priority = i;
		}

		foreach ( var category in sortedCategories )
		{
			var type = category.GetType();
			var categoryDataObject = new JsonObject { { JsonKeys.Class, type.Name } };

			SerializeObject( category, categoryDataObject, SerializerOptions() );

			newCategoryDataArray.Add( categoryDataObject );
		}

		obj.Add( JsonKeys.CategoryDataArray, newCategoryDataArray );
	}
}