// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Tags { using System; using System.Collections.Generic; using System.Reflection; using Core.Extension; using Core.Helper; using UnityEngine; using WallstopStudios.UnityHelpers.Utils; using Object = UnityEngine.Object; /// /// Provides utility methods and extension methods for working with the attribute/effect system. /// Includes methods for applying/removing effects, checking tags, and discovering attribute fields via reflection. /// /// /// /// Key features: /// - Extension methods for applying effects to any Unity Object /// - Tag checking utilities /// - Reflection-based attribute field discovery with caching /// - Integration with AttributeMetadataCache for performance /// /// /// Example usage: /// /// // Extension method usage /// GameObject player = ...; /// AttributeEffect speedBoost = ...; /// /// // Apply an effect /// EffectHandle? handle = player.ApplyEffect(speedBoost); /// /// // Check tags /// if (player.HasTag("Stunned")) /// { /// // Can't move /// } /// /// // Remove effect /// if (handle.HasValue) /// { /// player.RemoveEffect(handle.Value); /// } /// /// /// public static class AttributeUtilities { internal static string[] AllAttributeNames; internal static readonly Dictionary> AttributeFields = new(); private static readonly Dictionary< Type, Dictionary> > OptimizedAttributeFields = new(); /// /// Gets an array of all unique attribute field names across all AttributesComponent subclasses. /// Results are cached for performance. Uses AttributeMetadataCache if available, otherwise uses reflection. /// /// An array of all attribute names discovered in the project. public static string[] GetAllAttributeNames() { if (AllAttributeNames != null) { return AllAttributeNames; } // Try to load from cache first AttributeMetadataCache cache = AttributeMetadataCache.Instance; if (cache != null && cache.AllAttributeNames.Length > 0) { AllAttributeNames = cache.AllAttributeNames; return AllAttributeNames; } using PooledResource> uniqueNamesLease = Buffers.HashSet.Get( out HashSet uniqueNames ); uniqueNames.Clear(); IEnumerable loadedTypes = ReflectionHelpers.GetAllLoadedTypes(); foreach (Type type in loadedTypes) { if ( type == null || type.IsAbstract || !type.IsSubclassOf(typeof(AttributesComponent)) ) { continue; } FieldInfo[] fields = type.GetFields( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); foreach (FieldInfo fieldInfo in fields) { if (fieldInfo.FieldType == typeof(Attribute)) { uniqueNames.Add(fieldInfo.Name); } } } if (uniqueNames.Count == 0) { AllAttributeNames = Array.Empty(); return AllAttributeNames; } using PooledResource> orderedNamesLease = Buffers.GetList( uniqueNames.Count, out List orderedNames ); orderedNames.Clear(); orderedNames.AddRange(uniqueNames); orderedNames.Sort(StringComparer.Ordinal); AllAttributeNames = orderedNames.ToArray(); return AllAttributeNames; } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void ClearCache() { AllAttributeNames = null; AttributeFields.Clear(); OptimizedAttributeFields.Clear(); } /// /// Extension method to check if a Unity Object has a specific tag. /// /// The Unity Object (GameObject or Component) to check. /// The tag to check for. /// true if the target has a TagHandler with the specified tag; otherwise, false. /// /// /// if (player.HasTag("Stunned")) /// { /// DisableMovement(); /// } /// /// public static bool HasTag(this Object target, string effectTag) { if (target == null) { return false; } return target.TryGetComponent(out TagHandler tagHandler) && tagHandler.HasTag(effectTag); } /// /// Extension method to check if a Unity Object has any of the specified tags. /// /// The Unity Object (GameObject or Component) to check. /// The collection of tags to check for. /// true if the target has any of the specified tags; otherwise, false. /// /// /// string[] crowdControlTags = { "Stunned", "Frozen", "KnockedDown" }; /// if (player.HasAnyTag(crowdControlTags)) /// { /// ShowCrowdControlUI(); /// } /// /// public static bool HasAnyTag(this Object target, IEnumerable effectTags) { if (target == null) { return false; } return target.TryGetComponent(out TagHandler tagHandler) && tagHandler.HasAnyTag(effectTags); } /// /// Extension method to check if a Unity Object has any of the specified tags (IReadOnlyList overload for performance). /// /// The Unity Object (GameObject or Component) to check. /// The list of tags to check for. /// true if the target has any of the specified tags; otherwise, false. /// Equivalent to but optimized for indexable lists. public static bool HasAnyTag(this Object target, IReadOnlyList effectTags) { if (target == null) { return false; } return target.TryGetComponent(out TagHandler tagHandler) && tagHandler.HasAnyTag(effectTags); } /// /// Extension method to check if a Unity Object has all of the specified tags. /// /// The Unity Object (GameObject or Component) to check. /// The collection of tags that must all be active. /// true if all tags are active; otherwise, false. /// /// /// string[] stealthRequirements = { "Invisible", "Silenced" }; /// if (player.HasAllTags(stealthRequirements)) /// { /// EnableBackstabBonus(); /// } /// /// public static bool HasAllTags(this Object target, IEnumerable effectTags) { if (target == null) { return false; } return target.TryGetComponent(out TagHandler tagHandler) && tagHandler.HasAllTags(effectTags); } /// /// Extension method to check if a Unity Object has all of the specified tags. /// /// The Unity Object (GameObject or Component) to check. /// The list of tags that must all be active. /// true if all tags are active; otherwise, false. /// Equivalent to but optimized for indexable lists. public static bool HasAllTags(this Object target, IReadOnlyList effectTags) { if (target == null) { return false; } return target.TryGetComponent(out TagHandler tagHandler) && tagHandler.HasAllTags(effectTags); } /// /// Extension method to determine whether none of the specified tags are active on the target. /// /// The Unity Object (GameObject or Component) to check. /// The collection of tags to inspect. /// true if no tags in the collection are active; otherwise, false. public static bool HasNoneOfTags(this Object target, IEnumerable effectTags) { if (target == null) { return true; } return !target.TryGetComponent(out TagHandler tagHandler) || tagHandler.HasNoneOfTags(effectTags); } /// /// Extension method to determine whether none of the specified tags are active on the target. /// /// The Unity Object (GameObject or Component) to check. /// The list of tags to inspect. /// true if no tags in the collection are active; otherwise, false. public static bool HasNoneOfTags(this Object target, IReadOnlyList effectTags) { if (target == null) { return true; } return !target.TryGetComponent(out TagHandler tagHandler) || tagHandler.HasNoneOfTags(effectTags); } /// /// Attempts to retrieve the active count for a specific tag on the target. /// /// The Unity Object (GameObject or Component) to inspect. /// The tag whose count should be retrieved. /// /// When this method returns, contains the tag count (cast to ) if available; otherwise, zero. /// /// true if the target has a and the tag is tracked; otherwise, false. /// /// /// if (target.TryGetTagCount("Bleeding", out int stacks) && stacks >= 5) /// { /// ApplyMajorWoundPenalty(); /// } /// /// public static bool TryGetTagCount(this Object target, string effectTag, out int count) { count = 0; if (target == null) { return false; } return target.TryGetComponent(out TagHandler tagHandler) && tagHandler.TryGetTagCount(effectTag, out count); } /// /// Retrieves the active tags on the target into an optional buffer. /// /// The Unity Object (GameObject or Component) to inspect. /// /// Optional buffer to populate. When null, a new list is created. The buffer is cleared before population. /// /// The populated buffer of active tags. The buffer is empty when no tags are present or no handler exists. /// /// /// List<string> activeTags = target.GetActiveTags(_tagBuffer); /// if (activeTags.Contains("Invisible")) /// { /// EnableDetectionShader(); /// } /// /// public static List GetActiveTags(this Object target, List buffer = null) { if (target == null) { return buffer ?? new List(0); } if (!target.TryGetComponent(out TagHandler tagHandler)) { return buffer ?? new List(0); } List targetBuffer = buffer ?? new List(); targetBuffer.Clear(); return tagHandler.GetActiveTags(targetBuffer); } /// /// Returns an allocation-free enumerable view of the active tags on the target. /// /// The Unity Object (GameObject or Component) to inspect. /// A struct enumerable that yields each active tag exactly once. public static TagHandler.ActiveTagEnumerable EnumerateActiveTags(this Object target) { if (target == null) { return TagHandler.ActiveTagEnumerable.Empty; } return target.TryGetComponent(out TagHandler tagHandler) ? tagHandler.EnumerateActiveTags() : TagHandler.ActiveTagEnumerable.Empty; } /// /// Returns an allocation-free enumerable of handles that contribute the specified tag. /// public static TagHandler.HandleEnumerable EnumerateHandlesWithTag( this Object target, string effectTag ) { if (target == null || string.IsNullOrEmpty(effectTag)) { return TagHandler.HandleEnumerable.Empty; } return target.TryGetComponent(out TagHandler tagHandler) ? tagHandler.EnumerateHandlesWithTag(effectTag) : TagHandler.HandleEnumerable.Empty; } /// /// Retrieves all effect handles that contributed a specific tag on the target. /// /// The Unity Object (GameObject or Component) to inspect. /// The tag to query. /// /// Optional buffer to populate. When null, a new list is created. The buffer is cleared before population. /// /// The populated buffer of effect handles whose effects contain . /// /// /// List<EffectHandle> taggedHandles = target.GetHandlesWithTag("Burning", _handleBuffer); /// foreach (EffectHandle handle in taggedHandles) /// { /// target.RefreshEffect(handle); /// } /// /// public static List GetHandlesWithTag( this Object target, string effectTag, List buffer = null ) { if (string.IsNullOrEmpty(effectTag)) { return buffer ?? new List(0); } if (target == null) { return buffer ?? new List(0); } if (!target.TryGetComponent(out TagHandler tagHandler)) { return buffer ?? new List(0); } List targetBuffer = buffer ?? new List(); targetBuffer.Clear(); return tagHandler.GetHandlesWithTag(effectTag, targetBuffer); } /// /// Extension method to apply an effect to a Unity Object. /// Automatically adds an EffectHandler component if one doesn't exist. /// /// The Unity Object (GameObject or Component) to apply the effect to. /// The effect to apply. /// An EffectHandle for non-instant effects, or null for instant effects. /// /// /// EffectHandle? handle = enemy.ApplyEffect(poisonEffect); /// if (!handle.HasValue) /// { /// // Instant effects do not return a handle /// return; /// } /// activeHandles.Add(handle.Value); /// /// public static EffectHandle? ApplyEffect(this Object target, AttributeEffect attributeEffect) { if (target == null) { return null; } GameObject go = target.GetGameObject(); if (go == null) { return null; } EffectHandler effectHandler = go.GetOrAddComponent(); return effectHandler.ApplyEffect(attributeEffect); } /// /// Applies a list of effects to the target without allocating a result collection. /// /// The Unity Object (GameObject or Component) to modify. /// The list of effects to apply. /// Effects are applied sequentially; instant effects still return null handles internally. /// /// /// AttributeUtilities.ApplyEffectsNoAlloc(player, _precomputedEffects); /// /// public static void ApplyEffectsNoAlloc( this Object target, List attributeEffects ) { if (attributeEffects is not { Count: > 0 }) { return; } if (target == null) { return; } GameObject go = target.GetGameObject(); if (go == null) { return; } EffectHandler effectHandler = go.GetOrAddComponent(); foreach (AttributeEffect attributeEffect in attributeEffects) { _ = effectHandler.ApplyEffect(attributeEffect); } } /// /// Applies a sequence of effects to the target without allocations, iterating any enumerable. /// /// The Unity Object (GameObject or Component) to modify. /// The enumerable of effects to apply. /// Use when you are streaming effects from a generator or LINQ query. public static void ApplyEffectsNoAlloc( this Object target, IEnumerable attributeEffects ) { if (target == null) { return; } GameObject go = target.GetGameObject(); if (go == null) { return; } EffectHandler effectHandler = go.GetOrAddComponent(); foreach (AttributeEffect attributeEffect in attributeEffects) { _ = effectHandler.ApplyEffect(attributeEffect); } } /// /// Applies a list of effects to the target and collects any returned handles into the supplied buffer. /// /// The Unity Object (GameObject or Component) to modify. /// The list of effects to apply. /// Buffer that receives non-null handles. The buffer is not cleared automatically. /// /// /// _handles.Clear(); /// target.ApplyEffectsNoAlloc(burstEffects, _handles); /// // _handles now contains handles for duration and infinite effects. /// /// public static void ApplyEffectsNoAlloc( this Object target, List attributeEffects, List effectHandles ) { if (target == null) { return; } GameObject go = target.GetGameObject(); if (go == null) { return; } EffectHandler effectHandler = go.GetOrAddComponent(); foreach (AttributeEffect attributeEffect in attributeEffects) { EffectHandle? handle = effectHandler.ApplyEffect(attributeEffect); if (handle.HasValue) { effectHandles.Add(handle.Value); } } } /// /// Applies a list of effects to the target and returns the collected handles. /// /// The Unity Object (GameObject or Component) to modify. /// The list of effects to apply. /// A list containing handles for every duration or infinite effect that was applied. /// /// /// List<EffectHandle> handles = player.ApplyEffects(bossOpeners); /// _activeBossEffects.AddRange(handles); /// /// public static List ApplyEffects( this Object target, List attributeEffects ) { List handles = new(attributeEffects.Count); target.ApplyEffectsNoAlloc(attributeEffects, handles); return handles; } /// /// Removes a previously applied effect by handle. /// /// The Unity Object (GameObject or Component) to modify. /// The handle returned by . /// /// /// if (_slowHandle.HasValue) /// { /// enemy.RemoveEffect(_slowHandle.Value); /// _slowHandle = null; /// } /// /// public static void RemoveEffect(this Object target, EffectHandle effectHandle) { if (target == null) { return; } if (target.TryGetComponent(out EffectHandler effectHandler)) { effectHandler.RemoveEffect(effectHandle); } } /// /// Removes a collection of effect handles from the target. /// /// The Unity Object (GameObject or Component) to modify. /// Handles to remove. The list is iterated as-is. /// /// /// target.RemoveEffects(_queuedDispels); /// _queuedDispels.Clear(); /// /// public static void RemoveEffects(this Object target, List effectHandles) { if (target == null || effectHandles.Count <= 0) { return; } if (target.TryGetComponent(out EffectHandler effectHandler)) { foreach (EffectHandle effectHandle in effectHandles) { effectHandler.RemoveEffect(effectHandle); } } } /// /// Removes every active effect from the target. /// /// The Unity Object (GameObject or Component) to modify. /// /// /// // Cleanse all effects when respawning the character /// character.RemoveAllEffects(); /// /// public static void RemoveAllEffects(this Object target) { if (target == null) { return; } if (target.TryGetComponent(out EffectHandler effectHandler)) { effectHandler.RemoveAllEffects(); } } /// /// Determines whether the specified effect is currently active on the target. /// /// The Unity Object (GameObject or Component) to inspect. /// The effect to query. /// true if the effect is active; otherwise, false. /// /// /// if (!enemy.IsEffectActive(slowEffect)) /// { /// enemy.ApplyEffect(slowEffect); /// } /// /// public static bool IsEffectActive(this Object target, AttributeEffect attributeEffect) { if (target == null) { return false; } return target.TryGetComponent(out EffectHandler effectHandler) && effectHandler.IsEffectActive(attributeEffect); } /// /// Retrieves the number of active handles for the specified effect on the target. /// /// The Unity Object (GameObject or Component) to inspect. /// The effect to count. /// The number of active handles; zero when inactive. /// /// /// int stacks = target.GetEffectStackCount(bleedEffect); /// bleedStacksLabel.text = stacks.ToString(); /// /// public static int GetEffectStackCount(this Object target, AttributeEffect attributeEffect) { if (target == null) { return 0; } return target.TryGetComponent(out EffectHandler effectHandler) ? effectHandler.GetEffectStackCount(attributeEffect) : 0; } /// /// Copies all active effect handles on the target into the provided buffer. /// /// The Unity Object (GameObject or Component) to inspect. /// /// Optional buffer to populate. When null, a new list is created. The buffer is cleared before population. /// /// The populated buffer containing every active effect handle. /// /// /// List<EffectHandle> handles = target.GetActiveEffects(_handleBuffer); /// foreach (EffectHandle handle in handles) /// { /// Debug.Log(handle); /// } /// /// public static List GetActiveEffects( this Object target, List buffer = null ) { if (target == null) { return buffer ?? new List(0); } if (!target.TryGetComponent(out EffectHandler effectHandler)) { return buffer ?? new List(0); } List targetBuffer = buffer ?? new List(); targetBuffer.Clear(); return effectHandler.GetActiveEffects(targetBuffer); } /// /// Attempts to retrieve the remaining duration for the specified effect handle on the target. /// /// The Unity Object (GameObject or Component) to inspect. /// The handle to query. /// When this method returns, contains the remaining time if available; otherwise, zero. /// true if a duration was found; otherwise, false. public static bool TryGetRemainingDuration( this Object target, EffectHandle effectHandle, out float remainingDuration ) { remainingDuration = 0f; if (target == null) { return false; } return target.TryGetComponent(out EffectHandler effectHandler) && effectHandler.TryGetRemainingDuration(effectHandle, out remainingDuration); } /// /// Ensures an effect handle exists on the target, adding an EffectHandler when necessary. /// /// The Unity Object (GameObject or Component) to modify. /// The effect to apply or refresh. /// An active handle for the effect, or null for instant effects. public static EffectHandle? EnsureHandle( this Object target, AttributeEffect attributeEffect ) { return target.EnsureHandle(attributeEffect, refreshDuration: true); } /// /// Ensures an effect handle exists on the target, adding an EffectHandler when necessary. /// /// The Unity Object (GameObject or Component) to modify. /// The effect to apply or refresh. /// /// When true, attempts to refresh the duration of an existing handle if the effect supports it. /// /// An active handle for the effect, or null for instant effects. public static EffectHandle? EnsureHandle( this Object target, AttributeEffect attributeEffect, bool refreshDuration ) { if (target == null) { return null; } GameObject go = target.GetGameObject(); if (go == null) { return null; } EffectHandler effectHandler = go.GetOrAddComponent(); return effectHandler.EnsureHandle(attributeEffect, refreshDuration); } /// /// Attempts to refresh the duration of an effect handle on the target. /// /// The Unity Object (GameObject or Component) to inspect. /// The handle to refresh. /// /// When true, refreshes the duration even if the effect disallows reapplication resets. /// /// true if the duration was refreshed; otherwise, false. /// /// /// if (!target.RefreshEffect(handle)) /// { /// target.RefreshEffect(handle, ignoreReapplicationPolicy: true); /// } /// /// public static bool RefreshEffect( this Object target, EffectHandle effectHandle, bool ignoreReapplicationPolicy = false ) { if (target == null) { return false; } return target.TryGetComponent(out EffectHandler effectHandler) && effectHandler.RefreshEffect(effectHandle, ignoreReapplicationPolicy); } /// /// Retrieves a dictionary of attribute fields for the specified component type, keyed by field name. /// Uses cached metadata when available and falls back to reflection otherwise. /// /// Component type that declares fields. /// A dictionary mapping attribute field names to their . /// /// /// Dictionary<string, FieldInfo> fields = AttributeUtilities.GetAttributeFields(typeof(CharacterStats)); /// if (fields.TryGetValue("Health", out FieldInfo healthField)) /// { /// Debug.Log($"Health base value: {healthField.GetValue(stats)}"); /// } /// /// public static Dictionary GetAttributeFields(Type type) { return AttributeFields.GetOrAdd( type, inputType => { // Try to use cached field names first AttributeMetadataCache cache = AttributeMetadataCache.Instance; if (cache != null && cache.TryGetFieldNames(inputType, out string[] fieldNames)) { // Build dictionary from cached field names Dictionary result = new( fieldNames.Length, StringComparer.Ordinal ); foreach (string fieldName in fieldNames) { FieldInfo field = inputType.GetField( fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); if (field != null && field.FieldType == typeof(Attribute)) { result[fieldName] = field; } } return result; } else { // Fallback to runtime reflection FieldInfo[] fields = inputType.GetFields( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); Dictionary result = new( fields.Length, StringComparer.Ordinal ); for (int i = 0; i < fields.Length; i++) { FieldInfo field = fields[i]; if (field.FieldType == typeof(Attribute)) { result[field.Name] = field; } } return result; } } ); } /// /// Retrieves attribute fields for the specified component type with compiled getters for fast access. /// Prefers cached metadata and generates delegates on demand when the cache is unavailable. /// /// Component type that declares fields. /// A dictionary mapping attribute field names to compiled getter delegates. /// /// /// Dictionary<string, Func<object, Attribute>> getters = AttributeUtilities.GetOptimizedAttributeFields(typeof(CharacterStats)); /// Attribute health = getters["Health"](stats); /// Debug.Log($"Current health: {health.CurrentValue}"); /// /// public static Dictionary> GetOptimizedAttributeFields( Type type ) { return OptimizedAttributeFields.GetOrAdd( type, inputType => { // Try to use cached field names first AttributeMetadataCache cache = AttributeMetadataCache.Instance; if (cache != null && cache.TryGetFieldNames(inputType, out string[] fieldNames)) { // Build dictionary from cached field names Dictionary> result = new( fieldNames.Length, StringComparer.Ordinal ); foreach (string fieldName in fieldNames) { FieldInfo field = inputType.GetField( fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); if (field != null && field.FieldType == typeof(Attribute)) { result[fieldName] = ReflectionHelpers.GetFieldGetter< object, Attribute >(field); } } return result; } else { // Fallback to runtime reflection FieldInfo[] fields = inputType.GetFields( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); Dictionary> result = new( fields.Length, StringComparer.Ordinal ); for (int i = 0; i < fields.Length; i++) { FieldInfo field = fields[i]; if (field.FieldType == typeof(Attribute)) { result[field.Name] = ReflectionHelpers.GetFieldGetter< object, Attribute >(field); } } return result; } } ); } } }