// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE #if ZENJECT_PRESENT namespace WallstopStudios.UnityHelpers.Integrations.Zenject { using System; using System.Collections.Generic; using global::Zenject; 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 within the newly loaded scene. /// 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); if (relationalTypes.Count == 0) { // Fallback: scan all components in new scene and assign when type has relational fields bool includeInactiveAll = _options.IncludeInactive; Component[] all = includeInactiveAll ? 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; } bool includeInactive = _options.IncludeInactive; if (_options.UseSinglePassScan) { AssignBySinglePass(scene, relationalTypes, includeInactive); } else { AssignByTypePass(scene, relationalTypes, includeInactive); } // Safety net in Editor/tests: also walk scene roots #if UNITY_EDITOR if (!Application.isPlaying) { AssignBySceneRoots(scene, includeInactive); // In EditMode, some components may be registered on the following editor tick. // Schedule a delayed pass to ensure complete hydration in tests/tools. 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 type = relationalTypes[i]; if (type != null) { relationalSet.Add(type); } } 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 ); for (int i = 0; i < relationalTypes.Count; i++) { if (relationalTypes[i] != null) { relationalSet.Add(relationalTypes[i]); } } Component[] all = includeInactive ? UnityEngine.Object.FindObjectsOfType(true) : UnityEngine.Object.FindObjectsOfType(false); for (int i = 0; i < all.Length; i++) { Component component = all[i]; 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