A game framework written with osu! in mind.
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}