// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Core.Attributes
{
using System;
using System.Collections.Generic;
using Helper;
using Tags;
using UnityEngine;
using WallstopStudios.UnityHelpers.Utils;
///
/// Default implementation of that delegates to the
/// existing relational component extensions.
///
///
/// Thread-safety note: The _metadataCache reference is assigned once during construction and never changed.
/// The instance itself is thread-safe for concurrent reads, as its internal
/// dictionaries are only populated during static initialization before any instance is exposed.
/// The _hasAssignmentsCache dictionary is protected by _cacheLock for concurrent access.
///
public sealed class RelationalComponentAssigner : IRelationalComponentAssigner
{
// Immutable after construction - assigned in constructor and never modified.
// The AttributeMetadataCache instance is thread-safe for reads after initialization.
private readonly AttributeMetadataCache _metadataCache;
// Guarded by _cacheLock for all access.
private readonly Dictionary _hasAssignmentsCache;
private readonly object _cacheLock = new();
///
/// Creates a new assigner using the active .
///
public RelationalComponentAssigner()
: this(AttributeMetadataCache.Instance) { }
///
/// Creates a new assigner using the supplied metadata cache.
///
public RelationalComponentAssigner(AttributeMetadataCache metadataCache)
{
_metadataCache = metadataCache;
_hasAssignmentsCache = new Dictionary();
}
///
public bool HasRelationalAssignments(Type componentType)
{
if (componentType == null)
{
return false;
}
lock (_cacheLock)
{
if (_hasAssignmentsCache.TryGetValue(componentType, out bool cachedResult))
{
return cachedResult;
}
}
AttributeMetadataCache cache = _metadataCache ?? AttributeMetadataCache.Instance;
if (cache == null)
{
bool reflectionResult = HasRelationalAttributesViaReflection(componentType);
StoreCacheResult(componentType, reflectionResult);
return reflectionResult;
}
Type current = componentType;
while (current != null && typeof(Component).IsAssignableFrom(current))
{
if (
cache.TryGetRelationalFields(
current,
out AttributeMetadataCache.RelationalFieldMetadata[] fields
)
&& fields.Length > 0
)
{
StoreCacheResult(componentType, true);
return true;
}
current = current.BaseType;
}
// Fallback: inspect fields via reflection to detect relational attributes
bool result = HasRelationalAttributesViaReflection(componentType);
StoreCacheResult(componentType, result);
return result;
}
private static readonly Type[] RelationalAttributeTypes =
{
typeof(ParentComponentAttribute),
typeof(ChildComponentAttribute),
typeof(SiblingComponentAttribute),
};
private void StoreCacheResult(Type componentType, bool result)
{
lock (_cacheLock)
{
_hasAssignmentsCache[componentType] = result;
}
}
private static bool HasRelationalAttributesViaReflection(Type componentType)
{
Type current = componentType;
while (current != null && typeof(Component).IsAssignableFrom(current))
{
// IsDefined checks for exact attribute types, not derived types.
// Must check each concrete relational attribute type separately.
if (current.HasAnyFieldWithAttributes(RelationalAttributeTypes))
{
return true;
}
current = current.BaseType;
}
return false;
}
///
public void Assign(Component component)
{
if (component == null)
{
return;
}
if (!HasRelationalAssignments(component.GetType()))
{
return;
}
component.AssignRelationalComponents();
}
///
public void Assign(IEnumerable components)
{
if (components == null)
{
return;
}
if (components is IReadOnlyList readonlyList)
{
for (int i = 0; i < readonlyList.Count; i++)
{
Assign(readonlyList[i]);
}
return;
}
foreach (Component component in components)
{
Assign(component);
}
}
///
public void AssignHierarchy(GameObject root, bool includeInactiveChildren = true)
{
if (root == null)
{
return;
}
using PooledResource> componentBuffer = Buffers.List.Get(
out List components
);
root.GetComponentsInChildren(includeInactiveChildren, components);
Assign(components);
}
}
}