// MIT License - Copyright (c) 2023 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Attributes { using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Runtime.CompilerServices; using Extension; using Helper; using UnityEngine; using WallstopStudios.UnityHelpers.Utils; using Object = UnityEngine.Object; /// /// Specifies the type of message displayed in the inspector when a field fails validation. /// public enum ValidateAssignmentMessageType { /// /// Displays as a warning (yellow) in the inspector. /// Warning = 0, /// /// Displays as an error (red) in the inspector. /// Error = 1, } /// /// Validates that a field is properly assigned at edit time. Displays a warning or error in the inspector /// when the field is null, empty (for strings), or has no elements (for collections). /// Use to log warnings for all invalid fields, /// or to check validity programmatically. /// /// /// Unlike , this attribute validates: /// /// Object references (null check) /// Strings (empty or whitespace check) /// Lists and Collections (empty check) /// Enumerables (empty check) /// /// /// /// /// public class EnemySpawner : MonoBehaviour /// { /// [ValidateAssignment] /// public GameObject enemyPrefab; /// /// [ValidateAssignment(ValidateAssignmentMessageType.Error, "Spawn points list cannot be empty")] /// public List<Transform> spawnPoints; /// /// private void Start() /// { /// this.ValidateAssignments(); /// } /// } /// /// [AttributeUsage(AttributeTargets.Field)] public sealed class ValidateAssignmentAttribute : PropertyAttribute { /// /// The type of message to display in the inspector when the field is invalid. /// public ValidateAssignmentMessageType MessageType { get; } /// /// An optional custom message to display in the inspector when the field is invalid. /// If null or empty, a default message will be generated based on the field name. /// public string CustomMessage { get; } /// /// Creates a new ValidateAssignment attribute with default settings (warning message type, auto-generated message). /// public ValidateAssignmentAttribute() : this(ValidateAssignmentMessageType.Warning, null) { } /// /// Creates a new ValidateAssignment attribute with the specified message type and auto-generated message. /// /// The type of message to display when invalid. public ValidateAssignmentAttribute(ValidateAssignmentMessageType messageType) : this(messageType, null) { } /// /// Creates a new ValidateAssignment attribute with default message type (warning) and a custom message. /// /// The custom message to display when invalid. public ValidateAssignmentAttribute(string customMessage) : this(ValidateAssignmentMessageType.Warning, customMessage) { } /// /// Creates a new ValidateAssignment attribute with the specified message type and custom message. /// /// The type of message to display when invalid. /// The custom message to display when invalid. public ValidateAssignmentAttribute( ValidateAssignmentMessageType messageType, string customMessage ) { MessageType = messageType; CustomMessage = customMessage; } } public static class ValidateAssignmentExtensions { private static readonly Dictionary FieldsByType = new(); private static FieldInfo[] GetOrAdd(Type objectType) { return FieldsByType.GetOrAdd( objectType, type => { FieldInfo[] allFields = type.GetFields( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); if (allFields.Length == 0) { return Array.Empty(); } using PooledResource> bufferResource = Buffers.List.Get(out List result); for (int i = 0; i < allFields.Length; i++) { FieldInfo field = allFields[i]; if ( field.IsAttributeDefined( out _, inherit: false ) ) { result.Add(field); } } if (result.Count == 0) { return Array.Empty(); } return result.ToArray(); } ); } public static void ValidateAssignments(this Object o) { #if UNITY_EDITOR if (o == null) { return; } Type objectType = o.GetType(); FieldInfo[] fields = GetOrAdd(objectType); foreach (FieldInfo field in fields) { bool logNotAssigned = IsFieldInvalid(field, o); if (logNotAssigned) { o.LogNotAssigned(field.Name); } } #endif } public static bool AreAnyAssignmentsInvalid(this Object o) { Type objectType = o.GetType(); FieldInfo[] fields = GetOrAdd(objectType); foreach (FieldInfo field in fields) { if (IsFieldInvalid(field, o)) { return true; } } return false; } private static bool IsInvalid(IEnumerable enumerable) { try { IEnumerator enumerator = enumerable.GetEnumerator(); try { return !enumerator.MoveNext(); } finally { if (enumerator is IDisposable disposable) { disposable.Dispose(); } } } catch { return true; } } private static bool IsFieldInvalid(FieldInfo field, Object o) { object fieldValue = field.GetValue(o); return IsValueInvalid(fieldValue); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsValueInvalid(object value) { return value switch { Object unityObject => unityObject == null, string stringValue => string.IsNullOrWhiteSpace(stringValue), IList list => list.Count <= 0, ICollection collection => collection.Count <= 0, IEnumerable enumerable => IsInvalid(enumerable), _ => value == null, }; } } }