diff --git a/engine/Sandbox.Engine/Scene/Components/Component.Serialize.cs b/engine/Sandbox.Engine/Scene/Components/Component.Serialize.cs index 46a9847f..e40730f4 100644 --- a/engine/Sandbox.Engine/Scene/Components/Component.Serialize.cs +++ b/engine/Sandbox.Engine/Scene/Components/Component.Serialize.cs @@ -1,6 +1,7 @@ using Facepunch.ActionGraphs; using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace Sandbox; @@ -63,7 +64,15 @@ public JsonNode Serialize( GameObject.SerializeOptions options = null ) var value = field.GetValue( this ); try { - json.Add( field.Name, Json.ToNode( value, field.FieldType ) ); + var serializerOptions = GetSerializerOptions( field, field.FieldType ); + if ( value is IJsonPopulator jss ) + { + json.Add( field.Name, jss.Serialize() ); + } + else + { + json.Add( field.Name, JsonSerializer.SerializeToNode( value, field.FieldType, serializerOptions ) ); + } } catch ( System.Exception e ) { @@ -75,7 +84,15 @@ public JsonNode Serialize( GameObject.SerializeOptions options = null ) var value = prop.GetValue( this ); try { - json.Add( prop.Name, Json.ToNode( value, prop.PropertyType ) ); + var serializerOptions = GetSerializerOptions( prop, prop.PropertyType ); + if ( value is IJsonPopulator jss ) + { + json.Add( prop.Name, jss.Serialize() ); + } + else + { + json.Add( prop.Name, JsonSerializer.SerializeToNode( value, prop.PropertyType, serializerOptions ) ); + } } catch ( System.Exception e ) { @@ -187,6 +204,51 @@ public void DeserializeImmediately( JsonObject node ) [ConVar( "serialization_warn_time", ConVarFlags.Protected, Help = "Warn if deserializing a component property takes longer than this number of milliseconds." )] internal static int DeserializeTimeWarnThreshold { get; set; } + /// + /// Gets JsonSerializerOptions with a custom converter if the member has a JsonConverterAttribute. + /// Otherwise returns the default options. + /// + private static JsonSerializerOptions GetSerializerOptions( MemberDescription member, Type targetType ) + { + var jsonConverterAttr = member.GetCustomAttribute(); + if ( jsonConverterAttr == null || jsonConverterAttr.ConverterType == null ) + { + return Json.options; + } + + // Create a new options instance based on the default options + var customOptions = new JsonSerializerOptions( Json.options ); + + // Create an instance of the converter type + try + { + var converterInstance = Activator.CreateInstance( jsonConverterAttr.ConverterType ); + + // Check if it's a JsonConverterFactory + if ( converterInstance is JsonConverterFactory factory ) + { + // For factories, we need to create the converter for the specific type + var converter = factory.CreateConverter( targetType, customOptions ); + if ( converter != null ) + { + customOptions.Converters.Insert( 0, converter ); + } + } + else if ( converterInstance is JsonConverter converter ) + { + // For regular converters, add them directly + customOptions.Converters.Insert( 0, converter ); + } + } + catch ( System.Exception e ) + { + Log.Warning( $"Failed to create instance of JsonConverter type {jsonConverterAttr.ConverterType}: {e.Message}" ); + return Json.options; + } + + return customOptions; + } + private void StopTiming( MemberDescription member, Type type, FastTimer timer ) { if ( DeserializeTimeWarnThreshold <= 0 ) return; @@ -231,7 +293,15 @@ internal void DeserializeProperty( MemberDescription member, JsonNode node ) } else { - prop.SetValue( this, Json.FromNode( node, prop.PropertyType ) ); + if ( node is null ) + { + prop.SetValue( this, default ); + } + else + { + var serializerOptions = GetSerializerOptions( prop, prop.PropertyType ); + prop.SetValue( this, node.Deserialize( prop.PropertyType, serializerOptions ) ); + } } StopTiming( prop, prop.PropertyType, startTime ); @@ -265,7 +335,15 @@ internal void DeserializeProperty( MemberDescription member, JsonNode node ) } else { - field.SetValue( this, Json.FromNode( node, field.FieldType ) ); + if ( node is null ) + { + field.SetValue( this, default ); + } + else + { + var serializerOptions = GetSerializerOptions( field, field.FieldType ); + field.SetValue( this, node.Deserialize( field.FieldType, serializerOptions ) ); + } } StopTiming( field, field.FieldType, startTime );