using System.Collections; using System.Collections.Generic; using UnityEngine; using Cysharp.Threading.Tasks; using Newtonsoft.Json; using System; using Oculus.Interaction; using System.IO; // Dieses Script ist für die Aufnahme des Replays während des Trainingsdurchlaufs zuständig. namespace GhostSystem { public class GhostRecorderBatch : MonoBehaviour { public string SceneId; // SceneID zum Übergeben des Raumnamens an die Datenbank. public float SampleRate = 0.05f; // Entspricht einer Framerate von 20 FPS. public int BatchSize = 10; // Anzahl der Frames nach denen ein neuer Batch beginnt. Wird nicht mehr genutzt, da manueller Flush. public float FlushInterval = 2f; // Länge des Intervalls bis zum nächsten Flush. Wird nicht mehr genutzt, da manueller Flush. public List TrackedObjects; private int doorCount; private float _sampleTimer; private float _flushTimer; private bool _isRecording; private bool supabaseReady = false; private bool sceneIdReady = false; private readonly List _frameBuffer = new(); private string _offlinePath; public static event Action OnRecord; public static event Action OnStopRecord; public static event Action OnUploadFinished; /*private void Awake() { DontDestroyOnLoad(this); }*/ private void OnEnable() { SupabaseManager.OnSuperbaseReady += HandleSupabaseReady; // Das Skript horcht nach den beiden Events, da eine Aufnahme in die Datenbank nur möglich sien soll, RoomManager.OnSceneIdReady += HandleSceneIdReady; // wenn die Szene komplett geladen und die Verbindung zu Supabase aufgebaut ist. } private void OnDisable() { SupabaseManager.OnSuperbaseReady -= HandleSupabaseReady; RoomManager.OnSceneIdReady -= HandleSceneIdReady; } private void Start() { string now = DateTime.Now.ToString("yyyyMMdd_HHmmss"); string dateiname = now + ".jsonl"; _offlinePath = Path.Combine(Application.persistentDataPath, dateiname); } private void HandleSupabaseReady() { supabaseReady = true; } private void HandleSceneIdReady(string id, List doors, List targets) // Die Szene muss komplett geladen sein, da mit dem Event die SceneID { // übergeben wird. SceneId = id; sceneIdReady = true; if (SceneId == "Base") { //_flushTimer = 0f; //_ = FlushBatchAsync(); // Fire and forget } else { foreach (GameObject door in doors) // neben den Tracked Objects, die in jeder Szene gleich sind (Player-Root, Kopf, Hände, Augen), müssen die Türen jedes Raums { // zu den Tracked Objects hinzugefügt werden. Nur so wird die Bewegung der Tür auch im Replay wiedergegeben. var rotator = door.GetComponentInChildren(); if (rotator != null) { TrackedObjects.Add(rotator.transform); doorCount++; } } } } private void Update() { if (!_isRecording) return; // Code im Update wird nur ausgeführt, während die Aufnahme läuft. _sampleTimer += Time.deltaTime; _flushTimer += Time.deltaTime; if (_sampleTimer >= SampleRate) // Es wird immer die Dauer bis zum nächsten Record-Frame runter gezählt und ein Sample aufgenommen. { _sampleTimer = 0f; RecordSample(); } // Würde genutzt werden, um live in die Datenbank hochzuladen. /*if (_frameBuffer.Count >= BatchSize || _flushTimer >= FlushInterval) { _flushTimer = 0f; _ = FlushBatchAsync(); // Fire and forget }*/ } private void RecordSample() { foreach (var obj in TrackedObjects) // Ein Sample besteht aus einer Liste von FrameModels, die für den Zustand jedes getrackten Objekts zum aktuellen Zeitpunkt { // erstellt werden. var frame = new GhostFrame(SceneId, obj.name, obj.position, obj.rotation); _frameBuffer.Add(FrameModel.FromGhostFrame(frame)); // Die Frames werden an den Buffer angehängt, um noch während der Aufnahme noch nciht so viele System- } // Ressourcen zu verbrauchen. } private async UniTaskVoid FlushBatchAsync() // Die eigentliche Methode zum Hochladen eines Batches/leeren des Buffers. { if (_frameBuffer.Count == 0) return; var batch = new List(_frameBuffer); _frameBuffer.Clear(); Debug.Log("Flushing Frame Buffer"); await FallbackToOfflineAsync(batch); OnUploadFinished?.Invoke(); // Die Action für das Ende des Ladescreens wird trotzdem ausgelöst. /*try { var response = await SupabaseManager.instance.supabase.From().Insert(batch); // Versucht, den Batch an Superbase hochzuladen if (!response.ResponseMessage.IsSuccessStatusCode) { Debug.LogWarning($"Batch insert failed: {response.ResponseMessage.StatusCode}, fallback to offline."); await FallbackToOfflineAsync(batch); OnUploadFinished?.Invoke(); // Die Action für das Ende des Ladescreens wird trotzdem ausgelöst. Debug.Log("Upload Finished"); } if (response.ResponseMessage.IsSuccessStatusCode && sceneIdReady) // Löst Action aus, wenn der Upload geklappt hat und die neue Szene geladen ist. { OnUploadFinished?.Invoke(); Debug.Log("Upload Finished"); } } catch (Exception ex) { Debug.LogWarning($"Insert failed, fallback to offline: {ex.Message}"); await FallbackToOfflineAsync(batch); // Bei nicht erfolgtem Upload wird der Batch lokal gescpeichert. OnUploadFinished?.Invoke(); // Die Action für das Ende des Ladescreens wird trotzdem ausgelöst. Debug.Log("Upload Finished"); }*/ } private async UniTask FallbackToOfflineAsync(List batch) // Schreibt den Buffer in eine Datei, falls Upload nicht möglich. Kann zu Rucklern führen. { try { using var writer = new StreamWriter(_offlinePath, append: true); foreach (var model in batch) { string jsonLine = JsonConvert.SerializeObject(model); await writer.WriteLineAsync(jsonLine); } } catch (Exception ex) { Debug.LogError($"Offline write failed: {ex.Message}"); } } public void StartRecording() // Wenn die Bedingungen erfüllt sind, startet diese Methode die Aufnahme. Sie kann von anderen Skripten aufgerufen werden. { if (!supabaseReady || !sceneIdReady) return; _isRecording = true; OnRecord?.Invoke(); Debug.Log($"Recording {SceneId}"); } public void StopRecording() // Diese Methode stoppt die Aufnahme, löst ein entsprechendes Event aus, das abonniert werden kann und entfernt die Türen des aktuellen { // Raums wieder aus der Liste der Tracked Objects. _isRecording = false; sceneIdReady = false; OnStopRecord?.Invoke(); Debug.Log($"Stop recording: {SceneId}"); if (doorCount > 0 && TrackedObjects.Count >= doorCount) { TrackedObjects.RemoveRange(TrackedObjects.Count - doorCount, doorCount); } doorCount = 0; } public void Flush() // Manueller Flush { _flushTimer = 0f; _ = FlushBatchAsync(); // Fire and forget } } }