using System.Collections.Generic;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
namespace jeanf.scenemanagement
{
///
/// Bridges SubScene-authored GameObject constructs () back
/// into the GameObject world. A baked buffer is a list
/// of spawn records on one root entity: while the root exists (its section is streamed in)
/// this bridge keeps an instance of each recorded prefab subtree alive at rootL2W × LocalFromRoot
/// in the main world; streaming out destroys them. Mirrors TooltipDataBridge's
/// reconcile: spawns never move and only exist while their SubScene is loaded, so a slow
/// re-scan is enough. Drop one on a persistent GameObject; it's a singleton.
///
public class HybridPrefabBridge : MonoBehaviour
{
public static HybridPrefabBridge Instance { get; private set; }
[SerializeField] private bool isDebug = false;
[Tooltip("Seconds between re-scans of baked spawn entities (handles SubScene streaming). Spawns never move, so this can be slow.")]
[SerializeField] private float refreshInterval = 0.25f;
private EntityManager _em;
private EntityQuery _query;
private bool _worldReady;
private float _timer;
private int _lastLoggedCount = -1;
private Transform _container;
private readonly Dictionary _instances = new Dictionary(32);
private readonly HashSet _seen = new HashSet();
private readonly HashSet _invalid = new HashSet();
private readonly List _toRemove = new List(8);
private void Awake()
{
if (Instance != null && Instance != this) { Destroy(this); return; }
Instance = this;
// A root container at identity scale, so an instance's resolved scale is applied verbatim.
_container = new GameObject("SubSceneHybridPrefabs").transform;
}
private void OnDestroy()
{
if (Instance == this) Instance = null;
if (_container != null) Destroy(_container.gameObject);
}
private void OnEnable()
{
_timer = float.MaxValue; // scan on the first Update
TryInitWorld();
}
private void TryInitWorld()
{
var world = World.DefaultGameObjectInjectionWorld;
if (world == null || !world.IsCreated) { _worldReady = false; return; }
_em = world.EntityManager;
// IncludeDisabledEntities: the authoring root may be deactivated in a variant; the
// spawn records must still be seen so behaviour matches the enabled case.
_query = _em.CreateEntityQuery(new EntityQueryDesc
{
All = new[] { ComponentType.ReadOnly() },
Options = EntityQueryOptions.IncludeDisabledEntities,
});
_worldReady = true;
}
private void Update()
{
if (!_worldReady)
{
TryInitWorld();
if (!_worldReady) return;
}
_timer += Time.deltaTime;
if (_timer < refreshInterval) return;
_timer = 0f;
Reconcile();
}
private void Reconcile()
{
_seen.Clear();
var entities = _query.ToEntityArray(Allocator.Temp);
for (var i = 0; i < entities.Length; i++)
{
var e = entities[i];
_seen.Add(e);
if (_invalid.Contains(e)) continue;
if (_instances.ContainsKey(e)) continue; // static: place once
if (!_em.HasComponent(e)) continue; // pose not resolved yet — retry next tick
var buffer = _em.GetBuffer(e);
if (buffer.Length == 0) { _invalid.Add(e); continue; } // sweep found nothing; already warned at bake
var rootL2W = _em.GetComponentData(e);
var spawned = new GameObject[buffer.Length];
var missing = false;
for (var j = 0; j < buffer.Length; j++)
{
var element = buffer[j];
if (element.Prefab.Value == null)
{
// The baker refuses unresolvable sources, so this means the asset went missing after baking.
missing = true;
Debug.LogWarning($"{HybridPrefabBake.LogPrefix} HybridPrefabBridge: baked spawn e{e.Index}[{j}] has no prefab — re-bake its SubScene.", this);
continue;
}
spawned[j] = Spawn(element, rootL2W);
}
_instances[e] = spawned;
if (missing) _invalid.Add(e); // keep what did spawn, but flag the record
}
entities.Dispose();
if (isDebug && _instances.Count != _lastLoggedCount)
{
_lastLoggedCount = _instances.Count;
Debug.Log($"{HybridPrefabBake.LogPrefix} HybridPrefabBridge: {_instances.Count} SubScene hybrid root(s) spawned.", this);
}
_toRemove.Clear();
foreach (var kv in _instances)
if (!_seen.Contains(kv.Key)) _toRemove.Add(kv.Key);
for (var i = 0; i < _toRemove.Count; i++)
{
if (_instances.TryGetValue(_toRemove[i], out var gos))
foreach (var go in gos)
if (go != null) Destroy(go);
_instances.Remove(_toRemove[i]);
}
_invalid.RemoveWhere(e => !_seen.Contains(e)); // re-warn if a fixed bake reloads
}
private GameObject Spawn(in HybridPrefabSpawnElement element, in LocalToWorld rootL2W)
{
var prefab = element.Prefab.Value;
var instance = Instantiate(prefab, _container);
var t = instance.transform;
var world = math.mul(rootL2W.Value, element.LocalFromRoot);
StaticColliderBake.DecomposeTrs(world, out var position, out var rotation, out var scale);
t.SetPositionAndRotation(position, rotation);
t.localScale = HybridPrefabBake.ResolveInstanceScale(
new Vector3(scale.x, scale.y, scale.z), t.localScale, element.ComposePrefabScale == 1);
// A subtree deactivated in its prefab still means "spawn me"; an instance that stays
// inactive forever would be pointless.
if (!instance.activeSelf) instance.SetActive(true);
if (isDebug) Debug.Log($"{HybridPrefabBake.LogPrefix} HybridPrefabBridge: spawned '{prefab.name}' at {(Vector3)position}.", this);
return instance;
}
}
}