using System; using System.Collections.Generic; using jeanf.EventSystem; using jeanf.universalplayer; using UnityEngine; namespace jeanf.scenemanagement { /// /// Notes where objects stood before a scenario moved them, so the unload can put them back. /// /// Feeds on , which fires before the object /// is moved - the pose captured is therefore the one the object had when the scenario started /// playing with it. Only the FIRST move of a given object is kept: a prop teleported three /// times goes back to where it was before the first teleport, not before the last. /// /// /// Player teleports are never journaled (the player is handled by the evacuation step), and /// neither are teleports carrying a filter listed in /// . /// /// public sealed class ScenarioTeleportJournal { public readonly struct Entry { public readonly Transform Target; public readonly Transform Parent; public readonly Vector3 Position; public readonly Quaternion Rotation; public readonly string ScenarioId; public readonly string SourceName; public Entry(Transform target, string scenarioId, string sourceName) { Target = target; Parent = target.parent; Position = target.position; Rotation = target.rotation; ScenarioId = scenarioId; SourceName = sourceName; } } private readonly Func _currentScenarioId; private readonly Func _options; private readonly List _entries = new List(32); // Keyed on the transform itself rather than an instance id: Unity objects hash by // reference, and the id accessors are deprecated in this Unity version. private readonly HashSet _known = new HashSet(); private bool _isSubscribed; public IReadOnlyList Entries => _entries; public ScenarioTeleportJournal(Func currentScenarioId, Func options) { _currentScenarioId = currentScenarioId; _options = options; } public void Enable() { if (_isSubscribed) return; // Both hooks, on purpose: SendTeleportTarget always fires its own, while the hub event // only carries teleports that go through the PlayerEventBridge. Recording the same // object twice is a no-op (the first capture wins), so covering both is free. SendTeleportTarget.TeleportRequested += OnTeleportRequested; PlayerEvents.TeleportRequested += OnTeleportRequested; _isSubscribed = true; } public void Disable() { if (!_isSubscribed) return; SendTeleportTarget.TeleportRequested -= OnTeleportRequested; PlayerEvents.TeleportRequested -= OnTeleportRequested; _isSubscribed = false; } private void OnTeleportRequested(TeleportInformation information) { if (information == null || information.objectIsPlayer) return; var options = _options?.Invoke(); if (options == null || !options.restoreTeleportedObjects) return; var subject = information.objectToTeleport; if (subject == null) return; // No scenario running: nothing would ever restore this entry. var scenarioId = _currentScenarioId?.Invoke(); if (string.IsNullOrEmpty(scenarioId)) return; if (information.isUsingFilter && information.filter != null && options.ignoredTeleportFilters != null && options.ignoredTeleportFilters.Contains(information.filter)) return; if (!_known.Add(subject)) return; // already journaled: keep the ORIGINAL pose _entries.Add(new Entry(subject, scenarioId, information.targetDestination != null ? information.targetDestination.name : "")); } /// /// Puts back every object journaled for one of , most recent /// first, and drops those entries. Objects destroyed since (they lived in a scene already /// unloaded) are skipped silently. /// public int Restore(ICollection scenarioIds, bool resetVelocity, bool isDebug) { if (_entries.Count == 0) return 0; var restored = 0; for (var i = _entries.Count - 1; i >= 0; i--) { var entry = _entries[i]; if (scenarioIds != null && scenarioIds.Count > 0 && !scenarioIds.Contains(entry.ScenarioId)) continue; _entries.RemoveAt(i); _known.Remove(entry.Target); if (entry.Target == null) continue; // destroyed with its scene if (entry.Parent != null && entry.Target.parent != entry.Parent) entry.Target.SetParent(entry.Parent, false); TeleportUtility.PlaceAt(entry.Target.gameObject, entry.Position, entry.Rotation, resetVelocity); ObjectZoneTrackingBridge.MarkDirty(entry.Target); restored++; if (isDebug) Debug.Log($"[ScenarioManager] restored '{entry.Target.name}' to {entry.Position} " + $"(was teleported to '{entry.SourceName}').", entry.Target); } return restored; } /// Drops everything without moving anything (scene reset, world re-init). public void Clear() { _entries.Clear(); _known.Clear(); } } }