Files
sbox-public/engine/Sandbox.Engine/Scene/Components/Component.Serialize.cs
2025-12-22 08:31:55 +00:00

359 lines
9.2 KiB
C#

using Facepunch.ActionGraphs;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace Sandbox;
public abstract partial class Component : BytePack.ISerializer
{
public JsonNode Serialize( GameObject.SerializeOptions options = null )
{
var isSceneForNetwork = options?.SceneForNetwork ?? false;
var isSingleNetworkObject = options?.SingleNetworkObject ?? false;
var isNetworked = isSceneForNetwork || isSingleNetworkObject;
if ( isNetworked && Flags.Contains( ComponentFlags.NotNetworked ) )
return null;
if ( Flags.Contains( ComponentFlags.NotCloned ) )
return null;
if ( !isNetworked && Flags.Contains( ComponentFlags.NotSaved ) )
return null;
var t = Game.TypeLibrary.GetType( GetType() );
if ( t is null )
{
Log.Warning( $"TypeLibrary could not find {GetType()}" );
return null;
}
using var sceneScope = Scene.Push();
// Will omit serializing the target of embedded Action Graphs
using var targetScope = ActionGraph.PushTarget( InputDefinition.Target( typeof( GameObject ) ) );
var json = new JsonObject
{
{ JsonKeys.Type, t.SerializedName },
{ JsonKeys.Id, Id },
{ JsonKeys.Enabled, Enabled },
{ JsonKeys.Flags, (long)Flags }
};
if ( (isSceneForNetwork || isSingleNetworkObject) && this is INetworkSnapshot sw )
{
var writer = ByteStream.Create( 8 );
sw.WriteSnapshot( ref writer );
if ( writer.Length > 0 )
{
json[JsonKeys.Snapshot] = Json.ToNode( writer.ToArray() );
}
writer.Dispose();
}
if ( ComponentVersion != 0 ) json[JsonKeys.Version] = ComponentVersion;
foreach ( var member in ReflectionQueryCache.OrderedSerializableMembers( GetType() ) )
{
if ( member is FieldDescription field )
{
var value = field.GetValue( this );
try
{
json.Add( field.Name, Json.ToNode( value, field.FieldType ) );
}
catch ( System.Exception e )
{
Log.Warning( e, $"Error when serializing {this}.{field.Name} ({e.Message})\n{value}" );
}
}
else if ( member is PropertyDescription prop )
{
var value = prop.GetValue( this );
try
{
json.Add( prop.Name, Json.ToNode( value, prop.PropertyType ) );
}
catch ( System.Exception e )
{
Log.Warning( e, $"Error when serializing {this}.{prop.Name} ({e.Message})\n{value}" );
}
}
}
return json;
}
JsonObject jsonData;
public void Deserialize( JsonObject node )
{
var serializedVersion = (int)(node[JsonKeys.Version] ?? 0);
if ( serializedVersion < ComponentVersion )
{
//Log.Warning( $"{this} needs an API update, running upgraders" );
JsonUpgrader.Upgrade( serializedVersion, node, GetType() );
}
if ( node.TryGetPropertyValue( JsonKeys.Id, out var id ) )
{
Id = (Guid)id;
}
DeserializeFlags( node );
if ( node.TryGetPropertyValue( JsonKeys.Snapshot, out var snapshotNode ) && this is INetworkSnapshot sw )
{
var data = snapshotNode.Deserialize<byte[]>();
var reader = ByteStream.CreateReader( data );
sw.ReadSnapshot( ref reader );
reader.Dispose();
}
jsonData = node;
InitializeComponent();
Enabled = (bool)(jsonData[JsonKeys.Enabled] ?? true);
}
private void DeserializeFlags( JsonObject node )
{
if ( !node.TryGetPropertyValue( JsonKeys.Flags, out var inFlagNode ) )
return;
var inFlags = (ComponentFlags)(long)inFlagNode;
const ComponentFlags savedFlags = ComponentFlags.ShowAdvancedProperties;
Flags = (Flags & ~savedFlags) | (inFlags & savedFlags);
}
internal void PostDeserialize()
{
if ( jsonData is null )
return;
using var sceneScope = Scene.Push();
// Inject the host object into embedded Action Graphs
using var targetScope = ActionGraph.PushTarget( InputDefinition.Target( typeof( GameObject ), GameObject ) );
try
{
foreach ( var field in ReflectionQueryCache.OrderedSerializableMembers( GetType() ) )
{
// Skip fields that are not PRESENT in json data
// Those should stay code defined defaults.
if ( !jsonData.ContainsKey( field.Name ) )
{
continue;
}
// This also includes fields that are null
var v = jsonData[field.Name];
try
{
DeserializeProperty( field, v );
}
catch ( System.Exception e )
{
Log.Warning( e, $"Error when deserializing {this}.{field.Name} ({e.Message})\n{v}" );
}
}
}
finally
{
jsonData = null;
}
CheckRequireComponent();
}
/// <summary>
/// Deserialize this component as per <see cref="Deserialize"/> but update <see cref="GameObject"/> and <see cref="Component"/> property
/// references immediately instead of having them deferred.
/// </summary>
public void DeserializeImmediately( JsonObject node )
{
Deserialize( node );
PostDeserialize();
}
[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; }
private void StopTiming( MemberDescription member, Type type, FastTimer timer )
{
if ( DeserializeTimeWarnThreshold <= 0 ) return;
if ( Scene.IsLoading ) return;
var duration = timer.ElapsedMilliSeconds;
if ( duration > DeserializeTimeWarnThreshold )
{
Log.Warning( $"Deserializing {type.Name} {member.TypeDescription.Name}.{member.Name} took {duration:F1}ms!" );
}
}
internal void DeserializeProperty( MemberDescription member, JsonNode node )
{
var startTime = FastTimer.StartNew();
if ( member is PropertyDescription prop )
{
if ( !prop.IsSetMethodPublic )
{
// By getting the property from the declaring class, we can allow setting
// properties with a private setter.
var declaringType = Game.TypeLibrary.GetType( prop.MemberInfo.DeclaringType );
prop = declaringType.GetProperty( member.Name ) ?? prop;
}
if ( prop.PropertyType.IsAssignableTo( typeof( IJsonPopulator ) ) )
{
var value = prop.GetValue( this );
if ( value == null ) value = Activator.CreateInstance( prop.PropertyType );
if ( value is IJsonPopulator jsonConvert )
{
jsonConvert.Deserialize( node );
}
if ( prop.PropertyType.IsValueType )
{
prop.SetValue( this, value );
}
}
else
{
prop.SetValue( this, Json.FromNode( node, prop.PropertyType ) );
}
StopTiming( prop, prop.PropertyType, startTime );
return;
}
if ( member is FieldDescription field )
{
if ( !field.IsPublic )
{
// By getting the field from the declaring class, we can allow setting
// private fields.
var declaringType = Game.TypeLibrary.GetType( field.MemberInfo.DeclaringType );
field = declaringType.GetField( member.Name ) ?? field;
}
if ( field.FieldType.IsAssignableTo( typeof( IJsonPopulator ) ) )
{
var value = field.GetValue( this );
if ( value == null ) value = Activator.CreateInstance( field.FieldType );
if ( value is IJsonPopulator jsonConvert )
{
jsonConvert.Deserialize( node );
}
if ( field.FieldType.IsValueType )
{
field.SetValue( this, value );
}
}
else
{
field.SetValue( this, Json.FromNode( node, field.FieldType ) );
}
StopTiming( field, field.FieldType, startTime );
return;
}
}
static object BytePack.ISerializer.BytePackRead( ref ByteStream bs, Type targetType )
{
var id = bs.Read<Guid>();
if ( !Game.ActiveScene.IsValid() ) return default;
return Game.ActiveScene.Directory.FindComponentByGuid( id );
}
static void BytePack.ISerializer.BytePackWrite( object value, ref ByteStream bs )
{
if ( value is not Component component )
{
bs.Write( Guid.Empty );
return;
}
bs.Write( component.Id );
}
public static object JsonRead( ref Utf8JsonReader reader, Type targetType )
{
if ( reader.TokenType == JsonTokenType.StartObject )
{
var compRef = JsonSerializer.Deserialize<ComponentReference>( ref reader );
return compRef.Resolve( Game.ActiveScene, targetType, warn: true );
}
//
// Legacy way
//
if ( reader.TryGetGuid( out Guid guid ) )
{
if ( !Game.ActiveScene.IsValid() )
{
Log.Warning( "Tried to read component - but active scene was null!" );
return null;
}
var go = Game.ActiveScene.Directory.FindByGuid( guid );
if ( go is null )
throw new( $"GameObject {guid} was not found" );
var component = go.Components.Get( targetType, FindMode.EverythingInSelf );
if ( component is null )
throw new( $"Component {targetType} was not found on {go}" );
return component;
}
return null;
}
public static void JsonWrite( object value, Utf8JsonWriter writer )
{
if ( value is not Component component )
throw new NotImplementedException();
if ( !component.IsValid )
{
writer.WriteNullValue();
return;
}
JsonSerializer.Serialize( writer, ComponentReference.FromInstance( component ), Json.options );
}
/// <summary>
/// Json Keys used for serialization and deserialization of Components.
/// Kept here so they are easier to change, and we are less susceptible to typos.
/// </summary>
internal static class JsonKeys
{
internal const string Id = "__guid";
internal const string Flags = "Flags";
internal const string Version = "__version";
internal const string Type = "__type";
internal const string Snapshot = "__snapshot";
internal const string Enabled = "__enabled";
}
}