EventNamesAttribute.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. using System;
  3. using System.Reflection;
  4. #if UNITY_EDITOR
  5. using System.Collections;
  6. #endif
  7. namespace Animancer
  8. {
  9. /// <summary>[Editor-Conditional]
  10. /// Specifies a set of acceptable names for <see cref="AnimancerEvent"/>s
  11. /// so they can display a warning in the Inspector if an unexpected name is used.
  12. /// </summary>
  13. ///
  14. /// <remarks>
  15. /// Placing this attribute on a type applies it to all fields in that type.
  16. /// <para></para>
  17. /// <strong>Documentation:</strong>
  18. /// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer/usage#event-names">
  19. /// Event Names</see>
  20. /// <para></para>
  21. /// <strong>Example:</strong><code>
  22. /// [EventNames(...)]// Apply to all fields in this class.
  23. /// public class AttackState
  24. /// {
  25. /// [SerializeField]
  26. /// [EventNames(...)]// Apply to only this field.
  27. /// private ClipTransition _Action;
  28. /// }
  29. /// </code>
  30. /// See the constructors for examples of their usage.
  31. /// </remarks>
  32. ///
  33. /// https://kybernetik.com.au/animancer/api/Animancer/EventNamesAttribute
  34. ///
  35. [AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Struct, Inherited = true)]
  36. [System.Diagnostics.Conditional(Strings.UnityEditor)]
  37. public sealed class EventNamesAttribute : Attribute
  38. #if UNITY_EDITOR
  39. , IInitializable<MemberInfo>
  40. #endif
  41. {
  42. /************************************************************************************************************************/
  43. #if UNITY_EDITOR
  44. /// <summary>[Editor-Only] The names that can be used for events in the attributed field.</summary>
  45. public StringReference[] Names { get; private set; }
  46. /// <summary>[Editor-Only] Has the <see cref="Names"/> array been initialized?</summary>
  47. public bool HasNames
  48. => !Names.IsNullOrEmpty();
  49. #endif
  50. /************************************************************************************************************************/
  51. /// <summary>
  52. /// Creates a new <see cref="EventNamesAttribute"/>
  53. /// with <see cref="Names"/> from the attributed type or declaring type of the attributed member.</summary>
  54. ///
  55. /// <remarks>
  56. /// <strong>Example:</strong><code>
  57. /// [EventNames]// Use all StringReference fields in this class for any transitions in this class.
  58. /// public class AttackState
  59. /// {
  60. /// public static readonly StringReference HitStart = "Hit Start";
  61. /// public static readonly StringReference HitEnd = "Hit End";
  62. ///
  63. /// [SerializeField]
  64. /// [EventNames]// Use all StringReference fields in this class.
  65. /// private ClipTransition _Animation;
  66. ///
  67. /// protected virtual void Awake()
  68. /// {
  69. /// _Animation.Events.SetCallback(HitStart, OnHitStart);
  70. /// _Animation.Events.SetCallback(HitEnd, OnHitEnd);
  71. /// }
  72. ///
  73. /// private void OnHitStart() { }
  74. /// private void OnHitEnd() { }
  75. /// }
  76. /// </code></remarks>
  77. public EventNamesAttribute()
  78. {
  79. }
  80. /************************************************************************************************************************/
  81. /// <summary>Creates a new <see cref="EventNamesAttribute"/> containing the specified `names`.</summary>
  82. /// <remarks>
  83. /// <strong>Example:</strong><code>
  84. /// public class AttackState
  85. /// {
  86. /// [SerializeField]
  87. /// [EventNames("Hit Start", "Hit End")]
  88. /// private ClipTransition _Animation;
  89. ///
  90. /// protected virtual void Awake()
  91. /// {
  92. /// _Animation.Events.SetCallback("Hit Start", OnHitStart);
  93. /// _Animation.Events.SetCallback("Hit End", OnHitEnd);
  94. /// }
  95. ///
  96. /// private void OnHitStart() { }
  97. /// private void OnHitEnd() { }
  98. /// }
  99. /// </code></remarks>
  100. public EventNamesAttribute(params string[] names)
  101. {
  102. #if UNITY_EDITOR
  103. Names = StringReference.Get(names);
  104. #endif
  105. }
  106. /************************************************************************************************************************/
  107. /// <summary>Creates a new <see cref="EventNamesAttribute"/> with <see cref="Names"/> from the `type`.</summary>
  108. ///
  109. /// <remarks>
  110. /// If the `type` is an enum, all of its values will be used.
  111. /// <para></para>
  112. /// Otherwise the values of all static <see cref="string"/> and
  113. /// <see cref="StringReference"/> fields will be used.
  114. /// <para></para>
  115. /// <strong>Example:</strong><code>
  116. /// public class AttackState
  117. /// {
  118. /// public static readonly StringReference HitStart = "Hit Start";
  119. /// public static readonly StringReference HitEnd = "Hit End";
  120. ///
  121. /// [SerializeField]
  122. /// [EventNames(typeof(AttackState))]// Use all StringReference fields in this class.
  123. /// private ClipTransition _Animation;
  124. ///
  125. /// protected virtual void Awake()
  126. /// {
  127. /// _Animation.Events.SetCallback(HitStart, OnHitStart);
  128. /// _Animation.Events.SetCallback(HitEnd, OnHitEnd);
  129. /// }
  130. ///
  131. /// private void OnHitStart() { }
  132. /// private void OnHitEnd() { }
  133. /// }
  134. /// </code></remarks>
  135. ///
  136. /// <exception cref="ArgumentNullException"/>
  137. public EventNamesAttribute(Type type)
  138. {
  139. #if UNITY_EDITOR
  140. Initialize(type);
  141. #endif
  142. }
  143. /************************************************************************************************************************/
  144. /// <summary>
  145. /// Creates a new <see cref="EventNamesAttribute"/> with <see cref="Names"/> from a member in the `type`
  146. /// with the specified `name`.
  147. /// </summary>
  148. ///
  149. /// <remarks>
  150. /// The specified member must be static and can be a Field, Property, or Method.
  151. /// <para></para>
  152. /// The member type can be anything implementing <see cref="IEnumerable"/> (including arrays, lists, and
  153. /// coroutines).
  154. /// <para></para>
  155. /// <strong>Example:</strong><code>
  156. /// public class AttackState
  157. /// {
  158. /// public static readonly StringReference[] Events = { "Hit Start", "Hit End" };
  159. ///
  160. /// [SerializeField]
  161. /// [EventNames(typeof(AttackState), nameof(Events))]// Get the names from AttackState.Events.
  162. /// private ClipTransition _Animation;
  163. ///
  164. /// protected virtual void Awake()
  165. /// {
  166. /// _Animation.Events.SetCallback(Events[0], OnHitStart);
  167. /// _Animation.Events.SetCallback(Events[1], OnHitEnd);
  168. /// }
  169. ///
  170. /// private void OnHitStart() { }
  171. /// private void OnHitEnd() { }
  172. /// }
  173. /// </code></remarks>
  174. ///
  175. /// <exception cref="ArgumentNullException"/>
  176. /// <exception cref="ArgumentException">No member with the specified `name` exists in the `type`.</exception>
  177. ///
  178. public EventNamesAttribute(Type type, string name)
  179. {
  180. #if UNITY_EDITOR
  181. var obj = GetValue(type, name)
  182. ?? throw new ArgumentException(
  183. $"The collection retrieved from {type.GetNameCS()}.{name} is null");
  184. if (obj is not IEnumerable collection)
  185. throw new ArgumentException(
  186. $"The object retrieved from {type.GetNameCS()}.{name} is not an {nameof(IEnumerable)}");
  187. using (ListPool<StringReference>.Instance.Acquire(out var names))
  188. {
  189. foreach (var item in collection)
  190. {
  191. if (item == null)
  192. continue;
  193. var itemName = item.ToString();
  194. if (string.IsNullOrEmpty(itemName))
  195. continue;
  196. names.Add(itemName);
  197. }
  198. if (names.Count == 0)
  199. throw new ArgumentException($"The collection retrieved from {type.GetNameCS()}.{name} is empty");
  200. Names = names.ToArray();
  201. }
  202. #endif
  203. }
  204. /************************************************************************************************************************/
  205. #if UNITY_EDITOR
  206. /************************************************************************************************************************/
  207. /// <summary>Initializes the <see cref="Names"/> if they weren't already set in the constructor.</summary>
  208. public void Initialize(MemberInfo member)
  209. {
  210. if (HasNames)
  211. return;
  212. if (member == null)
  213. throw new ArgumentNullException(nameof(member));
  214. if (member is Type type)
  215. {
  216. Initialize(type);
  217. }
  218. else
  219. {
  220. Names = GatherNames(member.DeclaringType);
  221. }
  222. }
  223. /************************************************************************************************************************/
  224. /// <summary>Initializes the <see cref="Names"/> if they weren't already set in the constructor.</summary>
  225. public void Initialize(Type type)
  226. {
  227. if (HasNames)
  228. return;
  229. if (type == null)
  230. throw new ArgumentNullException(nameof(type));
  231. if (type.IsEnum)
  232. {
  233. Names = StringReference.Get(Enum.GetNames(type));
  234. }
  235. else
  236. {
  237. Names = GatherNames(type);
  238. }
  239. }
  240. /************************************************************************************************************************/
  241. private static object GetValue(Type type, string name)
  242. {
  243. if (type == null)
  244. throw new ArgumentNullException(nameof(type));
  245. if (name == null)
  246. throw new ArgumentNullException(nameof(name));
  247. var field = type.GetField(name, AnimancerReflection.StaticBindings);
  248. if (field != null)
  249. return field.GetValue(null);
  250. var property = type.GetProperty(name, AnimancerReflection.StaticBindings);
  251. if (property != null)
  252. return property.GetValue(null, null);
  253. var method = type.GetMethod(name, AnimancerReflection.StaticBindings, null, Type.EmptyTypes, null);
  254. if (method != null)
  255. return method.Invoke(null, null);
  256. throw new ArgumentException($"{type.GetNameCS()} does not contain a member named '{name}'");
  257. }
  258. /************************************************************************************************************************/
  259. private static StringReference[] GatherNames(Type type)
  260. {
  261. using (ListPool<StringReference>.Instance.Acquire(out var names))
  262. {
  263. while (type != null)
  264. {
  265. var fields = type.GetFields(AnimancerReflection.StaticBindings | BindingFlags.DeclaredOnly);
  266. for (int i = 0; i < fields.Length; i++)
  267. {
  268. var field = fields[i];
  269. if (field.DeclaringType.Assembly.FullName.StartsWith("Unity"))
  270. continue;
  271. StringReference name;
  272. if (field.FieldType == typeof(string))
  273. {
  274. name = (string)field.GetValue(null);
  275. }
  276. else if (field.FieldType == typeof(StringReference))
  277. {
  278. name = (StringReference)field.GetValue(null);
  279. }
  280. else continue;
  281. if (!name.IsNullOrEmpty() && !names.Contains(name))
  282. names.Add(name);
  283. }
  284. type = type.BaseType;
  285. }
  286. if (names.Count == 0)
  287. return null;
  288. names.Sort();
  289. return names.ToArray();
  290. }
  291. }
  292. /************************************************************************************************************************/
  293. private string _Prefix;
  294. private string _NamesToString;
  295. /// <summary>Returns a string containing all the <see cref="Names"/>.</summary>
  296. public string NamesToString(string prefix, string delimiter = "\n• ")
  297. {
  298. if (!HasNames)
  299. return prefix;
  300. if (_NamesToString != null && _Prefix == prefix)
  301. return _NamesToString;
  302. var text = StringBuilderPool.Instance.Acquire();
  303. _Prefix = prefix;
  304. text.Append(prefix);
  305. for (int i = 0; i < Names.Length; i++)
  306. text.Append(delimiter).Append(Names[i]);
  307. return _NamesToString = text.ReleaseToString();
  308. }
  309. /************************************************************************************************************************/
  310. #endif
  311. /************************************************************************************************************************/
  312. }
  313. }