using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Linq; using System; using UnityEngine.UI; using Postgrest; using Cysharp.Threading.Tasks; using UnityEngine.Rendering.Universal; using System.Globalization; using Oculus.Interaction; using System.Reactive; using Postgrest.Interfaces; using UnityEngine.InputSystem; // Dieses Script ist für die Wiedergabe des Replays mit den Daten aus der Datenbank zuständig. namespace GhostSystem { public class GhostReplayManager : MonoBehaviour { [Header("Replay Settings")] public string sceneId; public string UserFilter = null; // optional: filtere nach User-ID public bool Loop = true; public DateTime selectedStartTime; public DateSelector dateSelector; [Tooltip("Optional: Begrenze die Frame-Abfrage auf einen Zeitraum ab Startzeit.")] public bool LimitTimeRange = true; [Tooltip("Zeitraum in Minuten, der ab Startzeit ausgewertet wird.")] public int TimeRangeMinutes = 15; [Header("Target Objects")] public List TrackedObjects; [Header("Playback")] public float PlaybackSpeed = 1f; public bool AutoStart = true; public Slider playbackSlider; private bool isScrubbing = false; private bool isPlaying = false; private float playbackTime = 0f; private float totalDuration = 0f; private string firstTimestamp = ""; private string lastTimestamp = ""; private bool awaitingFrames; private Dictionary> objectTracks = new(); private bool supabaseReady = false; private bool sceneIdReady = false; private string loadedSceneId; private int doorCount; private List heatTargets; private DateTime latestLoadedTimestamp; public static event Action OnRestartLoop; public GameObject loadingScreen; string startTimeString; string endTimeString; [Serializable] public class TrackedObjectBinding { public string objectId; public GameObject targetObject; } private async void OnEnable() { SupabaseManager.OnSuperbaseReady += HandleSupabaseReady; RoomManager.OnSceneIdReady += HandleSceneIdReady; } private void OnDisable() { SupabaseManager.OnSuperbaseReady -= HandleSupabaseReady; RoomManager.OnSceneIdReady -= HandleSceneIdReady; } private void HandleSupabaseReady() // Wen die Verbindung zu Supabase aufgebaut ist, wird versucht, das Replay der Szene zu initialisieren. { supabaseReady = true; TryInitializeReplay(); } private void HandleSceneIdReady(string id, List doors, List targets) // Sobald die Szene geladen ist, wird ebenfalls versucht, das Replay zu { // initialisieren. loadingScreen.SetActive(true); // Während der erste Batch an Frames aus der Datenbank geladen wird, wird ein Ladekringel angezeigt. sceneIdReady = true; loadedSceneId = id; // die aktuelle SceneID wird gesetzt. foreach (GameObject door in doors) // Auch hier müssen die Türen zugeordnet werden. { TrackedObjectBinding trackedObjectBinding = new TrackedObjectBinding(); trackedObjectBinding.objectId = "Door"; trackedObjectBinding.targetObject = door.GetComponentInChildren().gameObject; TrackedObjects.Add(trackedObjectBinding); doorCount++; } heatTargets = targets; TryInitializeReplay(); } private async UniTaskVoid TryInitializeReplay() // Das Replay wird aber nur initialisiert, wenn Supabase und die Szene bereit sind. { if (!supabaseReady || !sceneIdReady) return; sceneId = loadedSceneId; awaitingFrames = true; await LoadAllFrames(); // Es wird auf die ersten 1000 Frames (siehe Batchsize) bzw. alle verfügbaren Frames (falls unter 1000) gewartet. loadingScreen.SetActive(false); // Sind die Frames geladen, wird der Ladescreen wieder deaktiviert StartReplay(); // und das Replay gestartet. _ = PeriodicUpdateFrames(); // Zusätzlich wird ein Unitask ausgelöst, der immer wieder Frames nachlädt, solange noch Frames in der Datenbank vorhanden sind, die } // den Filtern entsprechen und noch nicht geladen wurden. public async UniTask LoadAllFrames() { Debug.Log("SceneId used in query: " + sceneId); var startTime = dateSelector.selectedDateTime(); // Erstellt einen Zeitstring aus den im UI eingegebenen Session-Daten var endTime = dateSelector.EndTime(); // und einen zweiten für den Endpunkt der Trainingssession. if (dateSelector.startTimeInput.text != "") // Es wird dabei geprüft, ob ein fertiger Timestring in das vorgesehene Feld kopiert wurde. Dann werden die { // Dropdowns ignoriert. startTimeString = dateSelector.startTimeInput.text; } else startTimeString = startTime.ToString("o"); if (dateSelector.endTimeInput.text != "") { endTimeString = dateSelector.endTimeInput.text; } else endTimeString = endTime.ToString("o"); Debug.Log("StartTime filter: " + startTimeString); Debug.Log("EndTime filter: " + endTimeString); objectTracks.Clear(); int batchSize = 1000; int offset = 0; bool more = true; List allFrames = new(); while (more) // Solange Frames mit dem Filter (in chronologischer Reihenfolge) in der Datenbank vorhanden sind, wird immer ein neuer Batch von maximal 1000 Frames { // heruntergeladen. List timestampFilter = new List(); // Es wird eine Filterverkettung für Startzeit und Endzeit erstellt. timestampFilter.Add(new QueryFilter("timestamp", Postgrest.Constants.Operator.GreaterThanOrEqual, startTimeString)); timestampFilter.Add(new QueryFilter("timestamp", Postgrest.Constants.Operator.LessThan, endTimeString)); var query = SupabaseManager.instance.supabase .From() .Filter("scene_id", Postgrest.Constants.Operator.Equals, sceneId) // und neben der SceneID... .And(timestampFilter) // ...dem SQL-Command hinzugefügt. .Order("timestamp", Postgrest.Constants.Ordering.Ascending) .Limit(batchSize) // Limit, wie viele Frames in einem Batch geladen werden. (Standardmäßig 1000) .Offset(offset); // Um zu wissen, wo der nächste Batch anfängt if (!string.IsNullOrEmpty(UserFilter)) query = query.Filter("user_id", Postgrest.Constants.Operator.Equals, UserFilter); // Wenn die User ID angegebeen wurde, wird auch danach gefiltert. var result = await query.Get(); // Es werden die Frames aus der Datenbank geladen if (result.Models.Count == 0) break; if (result.Models[0].SceneId != sceneId) break; Debug.LogError("Model Count: " + result.Models.Count); allFrames.AddRange(result.Models); offset += batchSize; more = result.Models.Count == batchSize; // Und in einer lokalen Liste gespeichert. Ähnlich dem Buffer beim Ghost Recorder. } foreach (var frame in allFrames) // Die Frames werden den Tracked Objects zugeordnet. { if (!objectTracks.ContainsKey(frame.ObjectId)) objectTracks[frame.ObjectId] = new List(); objectTracks[frame.ObjectId].Add(frame); } if (allFrames.Count > 1) { latestLoadedTimestamp = allFrames.Last().TimestampParsed; totalDuration = (float)(latestLoadedTimestamp - allFrames.First().TimestampParsed).TotalSeconds; firstTimestamp = allFrames.First().TimestampParsed.ToLongTimeString(); lastTimestamp = latestLoadedTimestamp.ToLongTimeString(); } Debug.LogWarning($"Loaded {allFrames.Count} frames across {objectTracks.Count} objects."); } private async UniTaskVoid PeriodicUpdateFrames() // lädt alle 10 s neue Frames nach, falls verfügbar { while (awaitingFrames) { await UniTask.Delay(TimeSpan.FromSeconds(10)); await LoadNewFramesAfter(latestLoadedTimestamp); } } private async UniTask LoadNewFramesAfter(DateTime afterTime) // lädt einen neuen Batch nach. { //var endTime = dateSelector.EndTime(); Debug.Log("Load After EndTime filter: " + endTimeString); List timestampFilter = new List(); timestampFilter.Add(new QueryFilter("timestamp", Postgrest.Constants.Operator.GreaterThan, afterTime.ToString("o"))); timestampFilter.Add(new QueryFilter("timestamp", Postgrest.Constants.Operator.LessThan, endTimeString)); var query = SupabaseManager.instance.supabase .From() .Filter("scene_id", Postgrest.Constants.Operator.Equals, sceneId) .And(timestampFilter) //.Filter("timestamp", Postgrest.Constants.Operator.GreaterThan, afterTime.ToString("o")) //.Filter("timestamp", Postgrest.Constants.Operator.LessThanOrEqual, endTime.ToString("o")) .Order("timestamp", Postgrest.Constants.Ordering.Ascending); if (!string.IsNullOrEmpty(UserFilter)) query = query.Filter("user_id", Postgrest.Constants.Operator.Equals, UserFilter); var result = await query.Get(); var newFrames = result.Models; foreach (var frame in newFrames) { if (!objectTracks.ContainsKey(frame.ObjectId)) objectTracks[frame.ObjectId] = new List(); objectTracks[frame.ObjectId].Add(frame); } if (newFrames.Count > 0) { latestLoadedTimestamp = newFrames.Last().TimestampParsed; totalDuration = (float)(latestLoadedTimestamp - objectTracks.Values.SelectMany(f => f).First().TimestampParsed).TotalSeconds; } Debug.Log($"Appended {newFrames.Count} new frames."); } public void StartReplay() // Für das Play UI-Element, um das Replay zu starten { isPlaying = true; playbackTime = 0f; Debug.LogError("Replay started: total duration = " + totalDuration + " s, First: " + firstTimestamp + ", Last: " + lastTimestamp); } public void PauseReplay() => isPlaying = !isPlaying; public void StopReplay() // Für das Stop UI-Element, um das Replay zu beenden { isPlaying = false; playbackTime = 0f; } public void SetPlaybackTime(float time) // Für das Slider UI-Element zum Scrubben { playbackTime = Mathf.Clamp(time, 0f, totalDuration); } private void Update() // Solange das Replay auf "Play" steht, wird durch die Liste der Frames gegangen und die Positions- und Rotationsdaten aus selbigen angewendet, { // wenn die Differenz zwischen Startzeit und Timestamp des Frames der bisherigen Playback-Time entspricht. if (!isPlaying || objectTracks.Count == 0) return; playbackTime += Time.deltaTime * PlaybackSpeed; if (playbackTime > totalDuration) // Erreicht das Replay das Ende der Frame-Liste, wird es entwerder gestoppt { if (Loop) { playbackTime = 0f; OnRestartLoop?.Invoke(); // oder neu gestartet, wenn die Loop-Funktion aktiviert ist. } else isPlaying = false; } DateTime currentTimestamp = objectTracks.Values .SelectMany(list => list) .First().TimestampParsed + TimeSpan.FromSeconds(playbackTime); foreach (var binding in TrackedObjects) { if (!objectTracks.ContainsKey(binding.objectId)) continue; var frames = objectTracks[binding.objectId]; if (frames.Count < 2) continue; FrameModel a = null, b = null; for (int i = 0; i < frames.Count - 1; i++) { var tA = frames[i].TimestampParsed; var tB = frames[i + 1].TimestampParsed; if (tA <= currentTimestamp && tB >= currentTimestamp) { a = frames[i]; b = frames[i + 1]; break; } } if (a != null && b != null) { double tA = a.TimestampParsed.Subtract(DateTime.UnixEpoch).TotalSeconds; double tB = b.TimestampParsed.Subtract(DateTime.UnixEpoch).TotalSeconds; double tCurrent = currentTimestamp.Subtract(DateTime.UnixEpoch).TotalSeconds; float t = Mathf.Clamp01((float)((tCurrent - tA) / (tB - tA))); Vector3 pos = Vector3.Lerp(a.Position, b.Position, t); Quaternion rot = Quaternion.Slerp(a.Rotation, b.Rotation, t); binding.targetObject.transform.SetPositionAndRotation(pos, rot); } } if (!isScrubbing && playbackSlider != null && totalDuration > 0f) { playbackSlider.value = playbackTime / totalDuration; } } public void SetSliderScrubValue(float sliderValue) // der aktuelle Playback-Zeitpunkt wird aktualisiert solange gescrubbt wird. { if (isScrubbing) SetPlaybackTime(sliderValue * totalDuration); } public void OnSliderStartDrag() // Das Scrubbing wird gestartet, sobald der Slider angefasst wird. { isScrubbing = true; } public void OnSliderEndDrag() // Setzt den aktuellen Playback-Zeitpunkt auf den Timestamp, der dem übergebenen Slider-Wert entspricht. { isScrubbing = false; SetPlaybackTime(playbackSlider.value * totalDuration); } public void RemoveDoors() // Auch hier gibt es wieder eine Funktion, um vor dem Raumwechsel die Türen aus den Tracked Objects zu entfernen. { TrackedObjects.RemoveRange(TrackedObjects.Count - doorCount, doorCount); doorCount = 0; } public void StopAwaitingFrames() // Kann zum Szenenwechsel aufgerufen werden, um das aktuelle Replay und das Nachladen von Frames abzubrechen. { awaitingFrames = false; } } }