// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
#if VCONTAINER_PRESENT
namespace WallstopStudios.UnityHelpers.Integrations.VContainer
{
using System;
using System.Collections.Generic;
using global::VContainer.Unity;
using UnityEngine;
using UnityEngine.SceneManagement;
using WallstopStudios.UnityHelpers.Core.Attributes;
using WallstopStudios.UnityHelpers.Tags;
using WallstopStudios.UnityHelpers.Utils;
///
/// Listens for additive scene loads and assigns relational fields for components in the
/// newly loaded scene using the configured options.
///
public sealed class RelationalSceneLoadListener : IInitializable, IDisposable
{
private readonly IRelationalComponentAssigner _assigner;
private readonly AttributeMetadataCache _metadataCache;
private readonly RelationalSceneAssignmentOptions _options;
public RelationalSceneLoadListener(
IRelationalComponentAssigner assigner,
AttributeMetadataCache metadataCache,
RelationalSceneAssignmentOptions options
)
{
_assigner = assigner ?? throw new ArgumentNullException(nameof(assigner));
_metadataCache = metadataCache;
_options = options;
}
public void Initialize()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}
public void Dispose()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
internal void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
if (!scene.IsValid())
{
return;
}
AttributeMetadataCache cache = _metadataCache ?? AttributeMetadataCache.Instance;
if (cache == null)
{
return;
}
using PooledResource> pooledTypes = Buffers.List.Get(
out List relationalTypes
);
cache.CollectRelationalComponentTypes(relationalTypes);
bool includeInactive = _options.IncludeInactive;
if (relationalTypes.Count == 0)
{
// Fallback: scan all components once and assign when type has relational fields
Component[] all = includeInactive
? UnityEngine.Object.FindObjectsOfType(true)
: UnityEngine.Object.FindObjectsOfType(false);
for (int i = 0; i < all.Length; i++)
{
Component c = all[i];
if (c == null || c.gameObject.scene != scene)
{
continue;
}
if (_assigner.HasRelationalAssignments(c.GetType()))
{
_assigner.Assign(c);
}
}
return;
}
if (_options.UseSinglePassScan)
{
AssignBySinglePass(scene, relationalTypes, includeInactive);
}
else
{
AssignByTypePass(scene, relationalTypes, includeInactive);
}
// Safety net in Editor/tests: walk scene roots to catch any missed components
#if UNITY_EDITOR
if (!Application.isPlaying)
{
AssignBySceneRoots(scene, includeInactive);
// In EditMode, object registration can lag a frame after scene creation.
// Schedule a follow-up pass to catch late-registered components.
UnityEditor.EditorApplication.delayCall += () =>
{
if (scene.IsValid())
{
AssignBySceneRoots(scene, includeInactive);
}
};
}
#endif
}
private void AssignByTypePass(
Scene target,
List relationalTypes,
bool includeInactive
)
{
using PooledResource> rootGoBuffer = Buffers.List.Get(
out List roots
);
target.GetRootGameObjects(roots);
if (roots.Count == 0)
{
return;
}
using PooledResource> typeSetPool = Buffers.HashSet.Get(
out HashSet relationalSet
);
for (int i = 0; i < relationalTypes.Count; i++)
{
Type relationalType = relationalTypes[i];
if (relationalType != null)
{
relationalSet.Add(relationalType);
}
}
using PooledResource> componentBuffer = Buffers.List.Get(
out List components
);
for (int i = 0; i < roots.Count; i++)
{
GameObject root = roots[i];
if (root == null)
{
continue;
}
root.GetComponentsInChildren(includeInactive, components);
for (int j = 0; j < components.Count; j++)
{
Component component = components[j];
if (component == null || component.gameObject.scene != target)
{
continue;
}
Type current = component.GetType();
while (current != null && typeof(Component).IsAssignableFrom(current))
{
if (relationalSet.Contains(current))
{
_assigner.Assign(component);
break;
}
current = current.BaseType;
}
}
components.Clear();
}
}
private void AssignBySinglePass(
Scene target,
List relationalTypes,
bool includeInactive
)
{
using PooledResource> pooledSet = Buffers.HashSet.Get(
out HashSet relationalSet
);
foreach (Type t in relationalTypes)
{
if (t != null)
{
relationalSet.Add(t);
}
}
Component[] all = includeInactive
? UnityEngine.Object.FindObjectsOfType(true)
: UnityEngine.Object.FindObjectsOfType(false);
foreach (Component component in all)
{
if (component == null || component.gameObject.scene != target)
{
continue;
}
Type t = component.GetType();
while (t != null && typeof(Component).IsAssignableFrom(t))
{
if (relationalSet.Contains(t))
{
_assigner.Assign(component);
break;
}
t = t.BaseType;
}
}
}
private void AssignBySceneRoots(Scene target, bool includeInactive)
{
int rootCount = target.rootCount;
if (rootCount == 0)
{
return;
}
using PooledResource> rootGoBuffer = Buffers.List.Get(
out List roots
);
target.GetRootGameObjects(roots);
foreach (GameObject root in roots)
{
if (root == null)
{
continue;
}
_assigner.AssignHierarchy(root, includeInactive);
}
}
}
}
#endif