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<> );
}
}