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 } }; 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; } if ( node.TryGetPropertyValue( JsonKeys.Snapshot, out var snapshotNode ) && this is INetworkSnapshot sw ) { var data = snapshotNode.Deserialize(); var reader = ByteStream.CreateReader( data ); sw.ReadSnapshot( ref reader ); reader.Dispose(); } jsonData = node; InitializeComponent(); Enabled = (bool)(jsonData[JsonKeys.Enabled] ?? true); } 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(); } /// /// Deserialize this component as per but update and property /// references immediately instead of having them deferred. /// 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(); 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( 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 ); } /// /// Json Keys used for serialization and deserialization of Components. /// Kept here so they are easier to change, and we are less susceptible to typos. /// 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"; } }