Code/Binder/Properties/Member.cs
using System;
using System.Numerics;
using System.Xml.Linq;
namespace Sandbox.MovieMaker.Properties;
#nullable enable
/// <summary>
/// Movie property that references a field or property contained in another <see cref="ITrackTarget"/>.
/// For example, a property in a <see cref="Component"/>.
/// </summary>
/// <typeparam name="T">Value type stored in the property.</typeparam>
file sealed record MemberProperty<T>( ITrackTarget Parent, MemberDescription Member )
: ITrackProperty<T>
{
public string Name => Member.Name;
/// <summary>
/// Default behaviour is to check if the parent is active. We need a special case for properties bound to
/// <see cref="GameObject.Enabled"/> or <see cref="Component.Enabled"/>, otherwise we'd never be able to record them
/// being false.
/// </summary>
public bool IsActive => Parent.IsActive || Name == nameof(GameObject.Enabled) && Parent is ITrackReference { IsBound: true };
public bool CanWrite => Member switch
{
PropertyDescription propDesc => propDesc.CanWrite,
FieldDescription fieldDesc => !fieldDesc.IsInitOnly,
_ => false
};
public T Value
{
// TODO: we can avoid boxing / reflection here when we're in engine code using System.Linq.Expressions
get => Parent.Value is { } target ? Member switch
{
PropertyDescription propDesc => (T)propDesc.GetValue( target ),
FieldDescription fieldDesc => (T)fieldDesc.GetValue( target ),
_ => throw new NotImplementedException()
} : default!;
set
{
if ( Parent.Value is not { } target )
{
return;
}
if ( !CanWrite ) return;
SetInternal( target, value );
if ( Parent is ITrackProperty { TargetType.IsValueType: true } parentMember )
{
parentMember.Value = target;
}
}
}
private void SetInternal( object target, object? value )
{
switch ( Member )
{
case PropertyDescription propDesc:
propDesc.SetValue( target, value );
return;
case FieldDescription fieldDesc:
fieldDesc.SetValue( target, value );
return;
default:
throw new NotImplementedException();
}
}
}
file sealed class MemberPropertyFactory : ITrackPropertyFactory
{
string ITrackPropertyFactory.CategoryName => "Members";
int ITrackPropertyFactory.Order => 0x4000_0000;
IEnumerable<string> ITrackPropertyFactory.GetPropertyNames( ITrackTarget parent )
{
if ( TypeLibrary.GetType( parent.TargetType ) is not { } typeDesc ) return Enumerable.Empty<string>();
if ( !CanMakeTrackFromProperties( typeDesc.TargetType ) ) return Enumerable.Empty<string>();
return typeDesc.Members
.Where( x => x is { IsPublic: true } and (FieldDescription or PropertyDescription) )
.Select( x => x.Name );
}
private MemberDescription? GetMember( ITrackTarget parent, string name )
{
if ( TypeLibrary.GetType( parent.TargetType ) is not { } typeDesc ) return null;
if ( !CanMakeTrackFromProperties( typeDesc.TargetType ) ) return null;
return typeDesc.Members
.Where( CanMakeTrackFromMember )
.FirstOrDefault( m => m.Name == name );
}
public Type? GetTargetType( ITrackTarget parent, string name )
{
return GetMember( parent, name ) switch
{
PropertyDescription propDesc => propDesc.PropertyType,
FieldDescription fieldDesc => fieldDesc.FieldType,
_ => null
};
}
public ITrackProperty<T> CreateProperty<T>( ITrackTarget parent, string name )
{
return new MemberProperty<T>( parent, GetMember( parent, name )! );
}
// TODO: Because Type.IsPrimitive isn't allowed
private static HashSet<Type> PrimitiveTypes { get; } = new()
{
typeof(bool),
typeof(byte),
typeof(sbyte),
typeof(char),
typeof(decimal),
typeof(double),
typeof(float),
typeof(int),
typeof(uint),
typeof(long),
typeof(ulong),
typeof(short),
typeof(ushort)
};
private static HashSet<Type> MathPrimitiveTypes { get; } = new()
{
typeof(Color),
typeof(Color32),
typeof(ColorHsv),
typeof(Vector2),
typeof(Vector3),
typeof(Vector4),
typeof(Vector2Int),
typeof(Vector3Int),
typeof(Angles),
typeof(Rotation)
};
private static HashSet<Type> AccessorTypes { get; } = new()
{
typeof(SkinnedModelRenderer.MorphAccessor),
typeof(SkinnedModelRenderer.ParameterAccessor),
typeof(SkinnedModelRenderer.SequenceAccessor)
};
private static bool CanMakeTrackFromProperties( Type type )
{
if ( type.IsAssignableTo( typeof(GameObject) ) ) return true;
if ( type.IsAssignableTo( typeof(Component) ) ) return true;
if ( PrimitiveTypes.Contains( type ) ) return false;
if ( MathPrimitiveTypes.Contains( type ) ) return type != typeof(Rotation);
// TODO: not hard-code these
if ( AccessorTypes.Contains( type ) ) return true;
return false;
}
private static bool CanMakeTrackFromMember( MemberDescription member )
{
Type valueType;
var canWrite = false;
switch ( member )
{
case FieldDescription { IsPublic: true } field:
valueType = field.FieldType;
canWrite = !field.IsInitOnly;
break;
case PropertyDescription { CanRead: true, IsGetMethodPublic: true, IsIndexer: false } property:
valueType = property.PropertyType;
canWrite = property is { CanWrite: true, IsSetMethodPublic: true };
break;
default:
return false;
}
if ( member.TypeDescription.TargetType.IsAssignableTo( typeof(Component) ) )
{
// if ( !member.HasAttribute( typeof(PropertyAttribute) ) ) return false;
}
if ( !canWrite )
{
// Allow readonly members only if they're a reference type,
// because we can modify its properties
if ( valueType.IsValueType ) return false;
// Filtering out scene object stuff to avoid the list getting cluttered
// TODO: should we support this kind of indirection?
if ( valueType == typeof(GameObject) ) return false;
if ( valueType.IsAssignableTo( typeof( Component ) ) ) return false;
return false;
}
return IsValidPropertyType( valueType );
}
private static bool IsValidPropertyType( Type type )
{
if ( PrimitiveTypes.Contains( type ) ) return true;
if ( MathPrimitiveTypes.Contains( type ) ) return true;
if ( TypeLibrary.GetType( type ) is null ) return false;
if ( type.IsAssignableTo( typeof(Component) ) ) return true;
if ( type.IsAssignableTo( typeof(Resource) ) ) return true;
if ( type == typeof(GameObject) ) return true;
if ( type == typeof(string) ) return true;
// For any other type not covered above,
// only support it if it has sub-properties we can control
return CanMakeTrackFromProperties( type );
}
}