// 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;
}
}
);
}
}
}