mirror of
https://github.com/Facepunch/sbox-public.git
synced 2025-12-23 22:48:07 -05:00
359 lines
9.2 KiB
C#
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";
|
|
}
|
|
|
|
}
|