A game framework written with osu! in mind.
at master 194 lines 8.9 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using osu.Framework.Utils; 5using System; 6using System.Collections.Concurrent; 7using System.Reflection.Emit; 8using osu.Framework.Extensions.TypeExtensions; 9using System.Reflection; 10using System.Diagnostics; 11 12namespace osu.Framework.Graphics.Transforms 13{ 14 /// <summary> 15 /// A transform which operates on arbitrary fields or properties of a given target. 16 /// </summary> 17 /// <typeparam name="TValue">The type of the field or property to operate upon.</typeparam> 18 /// <typeparam name="TEasing">The type of easing.</typeparam> 19 /// <typeparam name="T">The type of the target to operate upon.</typeparam> 20 internal class TransformCustom<TValue, TEasing, T> : Transform<TValue, TEasing, T> 21 where T : class, ITransformable 22 where TEasing : IEasingFunction 23 { 24 public override string TargetGrouping => targetGrouping ?? TargetMember; 25 26 private readonly string targetGrouping; 27 28 private delegate TValue ReadFunc(T transformable); 29 30 private delegate void WriteFunc(T transformable, TValue value); 31 32 private class Accessor 33 { 34 public ReadFunc Read; 35 public WriteFunc Write; 36 } 37 38 private static readonly ConcurrentDictionary<string, Accessor> accessors = new ConcurrentDictionary<string, Accessor>(); 39 40 private static ReadFunc createFieldGetter(FieldInfo field) 41 { 42 if (!RuntimeInfo.SupportsJIT) return transformable => (TValue)field.GetValue(transformable); 43 44 string methodName = $"{typeof(T).ReadableName()}.{field.Name}.get_{Guid.NewGuid():N}"; 45 DynamicMethod setterMethod = new DynamicMethod(methodName, typeof(TValue), new[] { typeof(T) }, true); 46 ILGenerator gen = setterMethod.GetILGenerator(); 47 gen.Emit(OpCodes.Ldarg_0); 48 gen.Emit(OpCodes.Ldfld, field); 49 gen.Emit(OpCodes.Ret); 50 return (ReadFunc)setterMethod.CreateDelegate(typeof(ReadFunc)); 51 } 52 53 private static WriteFunc createFieldSetter(FieldInfo field) 54 { 55 if (!RuntimeInfo.SupportsJIT) return (transformable, value) => field.SetValue(transformable, value); 56 57 string methodName = $"{typeof(T).ReadableName()}.{field.Name}.set_{Guid.NewGuid():N}"; 58 DynamicMethod setterMethod = new DynamicMethod(methodName, null, new[] { typeof(T), typeof(TValue) }, true); 59 ILGenerator gen = setterMethod.GetILGenerator(); 60 gen.Emit(OpCodes.Ldarg_0); 61 gen.Emit(OpCodes.Ldarg_1); 62 gen.Emit(OpCodes.Stfld, field); 63 gen.Emit(OpCodes.Ret); 64 return (WriteFunc)setterMethod.CreateDelegate(typeof(WriteFunc)); 65 } 66 67 private static ReadFunc createPropertyGetter(MethodInfo getter) 68 { 69 if (!RuntimeInfo.SupportsJIT) return transformable => (TValue)getter.Invoke(transformable, Array.Empty<object>()); 70 71 return (ReadFunc)getter.CreateDelegate(typeof(ReadFunc)); 72 } 73 74 private static WriteFunc createPropertySetter(MethodInfo setter) 75 { 76 if (!RuntimeInfo.SupportsJIT) return (transformable, value) => setter.Invoke(transformable, new object[] { value }); 77 78 return (WriteFunc)setter.CreateDelegate(typeof(WriteFunc)); 79 } 80 81 private static Accessor findAccessor(Type type, string propertyOrFieldName) 82 { 83 PropertyInfo property = type.GetProperty(propertyOrFieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 84 85 if (property != null) 86 { 87 if (property.PropertyType != typeof(TValue)) 88 { 89 throw new InvalidOperationException( 90 $"Cannot create {nameof(TransformCustom<TValue, T>)} for property {type.ReadableName()}.{propertyOrFieldName} " + 91 $"since its type should be {typeof(TValue).ReadableName()}, but is {property.PropertyType.ReadableName()}."); 92 } 93 94 var getter = property.GetGetMethod(true); 95 var setter = property.GetSetMethod(true); 96 97 if (getter == null || setter == null) 98 { 99 throw new InvalidOperationException( 100 $"Cannot create {nameof(TransformCustom<TValue, T>)} for property {type.ReadableName()}.{propertyOrFieldName} " + 101 "since it needs to have both a getter and a setter."); 102 } 103 104 if (getter.IsStatic || setter.IsStatic) 105 { 106 throw new NotSupportedException( 107 $"Cannot create {nameof(TransformCustom<TValue, T>)} for property {type.ReadableName()}.{propertyOrFieldName} because static fields are not supported."); 108 } 109 110 return new Accessor 111 { 112 Read = createPropertyGetter(getter), 113 Write = createPropertySetter(setter), 114 }; 115 } 116 117 FieldInfo field = type.GetField(propertyOrFieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); 118 119 if (field != null) 120 { 121 if (field.FieldType != typeof(TValue)) 122 { 123 throw new InvalidOperationException( 124 $"Cannot create {nameof(TransformCustom<TValue, T>)} for field {type.ReadableName()}.{propertyOrFieldName} " + 125 $"since its type should be {typeof(TValue).ReadableName()}, but is {field.FieldType.ReadableName()}."); 126 } 127 128 if (field.IsStatic) 129 { 130 throw new NotSupportedException( 131 $"Cannot create {nameof(TransformCustom<TValue, T>)} for field {type.ReadableName()}.{propertyOrFieldName} because static fields are not supported."); 132 } 133 134 return new Accessor 135 { 136 Read = createFieldGetter(field), 137 Write = createFieldSetter(field), 138 }; 139 } 140 141 if (type.BaseType == null) 142 throw new InvalidOperationException($"Cannot create {nameof(TransformCustom<TValue, T>)} for non-existent property or field {typeof(T).ReadableName()}.{propertyOrFieldName}."); 143 144 // Private members aren't visible unless we check the base type explicitly, so let's try our luck. 145 return findAccessor(type.BaseType, propertyOrFieldName); 146 } 147 148 private static Accessor getAccessor(string propertyOrFieldName) => accessors.GetOrAdd(propertyOrFieldName, key => findAccessor(typeof(T), key)); 149 150 private readonly Accessor accessor; 151 152 /// <summary> 153 /// Creates a new instance operating on a property or field of <typeparamref name="T"/>. The property or field is 154 /// denoted by its name, passed as <paramref name="propertyOrFieldName"/>. 155 /// By default, an interpolation method "ValueAt" from <see cref="Interpolation"/> with suitable signature is 156 /// picked for interpolating between <see cref="Transform{TValue}.StartValue"/> and 157 /// <see cref="Transform{TValue}.EndValue"/> according to <see cref="Transform.StartTime"/>, 158 /// <see cref="Transform.EndTime"/>, and a current time. 159 /// </summary> 160 /// <param name="propertyOrFieldName">The property or field name to be operated upon.</param> 161 /// <param name="grouping">An optional grouping, for a case where the target property can potentially conflict with others.</param> 162 public TransformCustom(string propertyOrFieldName, string grouping = null) 163 { 164 TargetMember = propertyOrFieldName; 165 targetGrouping = grouping; 166 167 accessor = getAccessor(propertyOrFieldName); 168 Trace.Assert(accessor.Read != null && accessor.Write != null, $"Failed to populate {nameof(accessor)}."); 169 } 170 171 private TValue valueAt(double time) 172 { 173 if (time < StartTime) return StartValue; 174 if (time >= EndTime) return EndValue; 175 176 return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing); 177 } 178 179 public override string TargetMember { get; } 180 181 protected override void Apply(T d, double time) => accessor.Write(d, valueAt(time)); 182 183 protected override void ReadIntoStartValue(T d) => StartValue = accessor.Read(d); 184 } 185 186 internal class TransformCustom<TValue, T> : TransformCustom<TValue, DefaultEasingFunction, T> 187 where T : class, ITransformable 188 { 189 public TransformCustom(string propertyOrFieldName) 190 : base(propertyOrFieldName) 191 { 192 } 193 } 194}