using System.Collections.Immutable; using System.Reflection; using System.Text.Json.Serialization; namespace Sandbox; /// /// We cache results for some expensive reflection queries. /// This results in large performance improvements during various operations (Cloning, NetworkSpawn, Serilization...) /// internal static class ReflectionQueryCache { private static Dictionary _isTypeCloneableByCopy = new(); private static Dictionary _orderedMemberCache = new(); private static Dictionary _requiredComponentMemberCache = new(); public record SyncVarPropertyAndAttribute( PropertyInfo Property, SyncAttribute Attribute ); private static Dictionary _syncVarMemberCache = new(); /// /// Clears the type cache, called after HotLoad and after a game ended. /// Called from EditorUtilities.ClearCloneTypeCache and Game.Close /// public static void ClearTypeCache() { _isTypeCloneableByCopy.Clear(); _orderedMemberCache.Clear(); _requiredComponentMemberCache.Clear(); _syncVarMemberCache.Clear(); } /// /// Returns all properties and fields that should be (de)serialized. /// Also sorts the members for historic reasons. /// public static IEnumerable OrderedSerializableMembers( Type t ) { if ( _orderedMemberCache.TryGetValue( t, out var members ) ) { return members; } var type = Game.TypeLibrary.GetType( t ); if ( type is null ) { Log.Warning( $"TypeLibrary could not find {t}" ); return Array.Empty(); } // It's fucked that we need to order the members for cloning, but some games actually rely on that order. // See https://github.com/Facepunch/sbox/issues/1785 var fieldAndPropertyMembers = type.Members.Where( ShouldSerializeMember ).OrderBy( x => x.Name ).ToArray(); _orderedMemberCache[t] = fieldAndPropertyMembers; return fieldAndPropertyMembers; } private static bool ShouldSerializeMember( MemberDescription memberDesc ) { if ( memberDesc is not PropertyDescription && memberDesc is not FieldDescription ) return false; return memberDesc.HasAttribute() && !memberDesc.HasAttribute(); } /// /// Returns all properties that have a [RequireComponent] attribute. /// public static IEnumerable RequiredComponentMembers( Type t ) { if ( _requiredComponentMemberCache.TryGetValue( t, out var members ) ) { return members; } var type = Game.TypeLibrary.GetType( t ); if ( type is null ) { Log.Warning( $"TypeLibrary could not find {t}" ); return Array.Empty(); } var requiredComponentProps = type.Properties .Where( IsRequiredComponent ) .ToArray(); _requiredComponentMemberCache[t] = requiredComponentProps; return requiredComponentProps; } private static bool IsRequiredComponent( PropertyDescription prop ) { return prop.HasAttribute(); } /// /// Returns all properties that have a [Sync] attribute. /// public static IEnumerable SyncProperties( Type t ) { if ( _syncVarMemberCache.TryGetValue( t, out var members ) ) { return members; } var properties = new List(); var currentType = t; // Collect all properties from the type and its base types while ( currentType != null ) { var ourProperties = currentType.GetProperties( BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly ); properties.AddRange( ourProperties ); currentType = currentType.BaseType; } // Find properties with the [Sync] attribute and create records var syncProperties = properties .Select( prop => { var syncAttribute = prop.GetCustomAttributes( typeof( SyncAttribute ), inherit: true ).FirstOrDefault() as SyncAttribute; if ( syncAttribute != null ) { return new SyncVarPropertyAndAttribute( prop, syncAttribute ); } return null; } ) .Where( x => x != null ) .ToArray(); _syncVarMemberCache[t] = syncProperties; return syncProperties; } /// /// Determines if a type can be cloned by a simple copy. /// This recursively walks through all properties and fields of the type to determine if they are cloneable by copy. /// Since this is fairly expensive, we cache the results for each type. /// public static bool IsTypeCloneableByCopy( Type t ) { if ( _isTypeCloneableByCopy.TryGetValue( t, out var result ) ) { return result; } // Use a HashSet to track types being processed to avoid infinite recursion _processingTypesCache.Clear(); var isCloneableByCopy = IsTypeCloneableByCopyInternal( t, _processingTypesCache ); _isTypeCloneableByCopy[t] = isCloneableByCopy; return isCloneableByCopy; } // Avoid allocation -> cache this private static HashSet _processingTypesCache = new(); private static bool IsTypeCloneableByCopyInternal( Type t, HashSet processingTypes ) { if ( t.IsPrimitive || t.IsEnum || t == typeof( string ) ) { return true; } // Resource references can just be copied, if ( t.HasBaseType( "Sandbox.Resource" ) ) { return true; } // Immutable lists are safe to copy, if their containing type is safe to copy if ( IsImmutableList( t ) ) { return IsTypeCloneableByCopyInternal( t.GetGenericArguments()[0], processingTypes ); } // Other Ref types are not cloneable by copy if ( !t.IsValueType ) { return false; } if ( processingTypes.Contains( t ) ) { // If the type is already being processed, return to avoid infinite recursion return true; } processingTypes.Add( t ); // For value types check if all properties are cloneable by copy foreach ( var prop in t.GetProperties() ) { if ( ShouldSkipPropertyTypeCheck( prop ) ) { continue; } var isCloneable = IsTypeCloneableByCopyInternal( prop.PropertyType, processingTypes ); _isTypeCloneableByCopy[prop.PropertyType] = isCloneable; if ( !isCloneable ) { processingTypes.Remove( t ); return false; } } foreach ( var field in t.GetFields() ) { if ( ShouldSkipFieldTypeCheck( field ) ) { continue; } var isCloneable = IsTypeCloneableByCopyInternal( field.FieldType, processingTypes ); _isTypeCloneableByCopy[field.FieldType] = isCloneable; if ( !isCloneable ) { processingTypes.Remove( t ); return false; } } processingTypes.Remove( t ); return true; } private static bool ShouldSkipPropertyTypeCheck( PropertyInfo prop ) { var alwaysCheck = prop.HasAttribute( typeof( JsonIncludeAttribute ) ) || prop.HasAttribute( typeof( PropertyAttribute ) ); var ignoredByDefault = prop.HasAttribute( typeof( JsonIgnoreAttribute ) ) || !prop.CanWrite || prop.SetMethod is null || prop.SetMethod.IsPrivate || prop.SetMethod.IsVirtual || (prop.GetMethod is not null && (prop.GetMethod.IsStatic || prop.GetMethod.IsVirtual)); return !alwaysCheck && ignoredByDefault; } private static bool ShouldSkipFieldTypeCheck( FieldInfo field ) { var alwaysCheck = field.HasAttribute( typeof( JsonIncludeAttribute ) ) || field.HasAttribute( typeof( PropertyAttribute ) ); var ignoredByDefault = field.HasAttribute( typeof( JsonIgnoreAttribute ) ) || field.IsPrivate || field.IsStatic; return !alwaysCheck && ignoredByDefault; } private static bool IsImmutableList( Type t ) { if ( !t.IsGenericType ) return false; return t.GetGenericTypeDefinition() == typeof( ImmutableList<> ); } }