From d02eddfe79f2f9686b8b5bd075b8467297577f19 Mon Sep 17 00:00:00 2001 From: Anton Franzluebbers Date: Wed, 3 Jul 2024 23:23:01 -0400 Subject: [PATCH] optimizations and code simplification for loading persistent object data, fixes for deleting persistent objects --- unity_package/Runtime/VELConnectManager.cs | 23 ++-- .../Runtime/VELConnectPersistenceManager.cs | 126 ++++++++++++++---- unity_package/Runtime/VelNetPersist.cs | 69 +++++----- unity_package/package.json | 2 +- velconnect-npm/src/types/pocketbase-types.ts | 1 + 5 files changed, 152 insertions(+), 69 deletions(-) diff --git a/unity_package/Runtime/VELConnectManager.cs b/unity_package/Runtime/VELConnectManager.cs index 988100c..ebea06e 100644 --- a/unity_package/Runtime/VELConnectManager.cs +++ b/unity_package/Runtime/VELConnectManager.cs @@ -19,10 +19,12 @@ namespace VELConnect public class VELConnectManager : MonoBehaviour { public string velConnectUrl = "http://localhost"; + public static string VelConnectUrl { get => instance.velConnectUrl; - set { + set + { instance.velConnectUrl = value; SetDeviceField(new Dictionary { @@ -33,6 +35,7 @@ namespace VELConnect }); } } + private static VELConnectManager instance; public class State @@ -116,7 +119,7 @@ namespace VELConnect public string name; public string data; } - + public class RecordList { public int page; @@ -897,7 +900,6 @@ namespace VELConnect } } - public static void GetRequestCallback(string url, Action successCallback = null, Action failureCallback = null) { @@ -940,12 +942,17 @@ namespace VELConnect private static IEnumerator PostRequestCallbackCo(string url, string postData, Dictionary headers = null, Action successCallback = null, - Action failureCallback = null, string method="POST") + Action failureCallback = null, string method = "POST") { UnityWebRequest webRequest = new UnityWebRequest(url, method); - byte[] bodyRaw = Encoding.UTF8.GetBytes(postData); - UploadHandlerRaw uploadHandler = new UploadHandlerRaw(bodyRaw); - webRequest.uploadHandler = uploadHandler; + UploadHandlerRaw uploadHandler = null; + if (!string.IsNullOrEmpty(postData)) + { + byte[] bodyRaw = Encoding.UTF8.GetBytes(postData); + uploadHandler = new UploadHandlerRaw(bodyRaw); + webRequest.uploadHandler = uploadHandler; + } + webRequest.downloadHandler = new DownloadHandlerBuffer(); webRequest.SetRequestHeader("Content-Type", "application/json"); if (headers != null) @@ -971,7 +978,7 @@ namespace VELConnect break; } - uploadHandler.Dispose(); + uploadHandler?.Dispose(); webRequest.Dispose(); } diff --git a/unity_package/Runtime/VELConnectPersistenceManager.cs b/unity_package/Runtime/VELConnectPersistenceManager.cs index 5d038f3..4fb84e1 100644 --- a/unity_package/Runtime/VELConnectPersistenceManager.cs +++ b/unity_package/Runtime/VELConnectPersistenceManager.cs @@ -1,7 +1,10 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using UnityEngine; +using UnityEngine.Networking; using VelNet; namespace VELConnect @@ -10,6 +13,9 @@ namespace VELConnect { public static VelConnectPersistenceManager instance; + // The items in this list are used when loading existing data from the server on room join + private List sceneObjects = new List(); + private void Awake() { instance = this; @@ -27,39 +33,113 @@ namespace VELConnect private void OnJoinedRoom(string roomName) { - // if we're the first to join this room + StartCoroutine(OnJoinedRoomCo(roomName)); + } + + private IEnumerator OnJoinedRoomCo(string roomName) + { + foreach (VelNetPersist velNetPersist in sceneObjects) + { + velNetPersist.loading = true; + } + + int pagesLeft = 1; + int totalCounter = 200; + int page = 1; + List allResults = new List(); + while (pagesLeft > 0) + { + if (totalCounter < 0) + { + Debug.LogError("Too many pages of persisted objects. This must be a bug."); + break; + } + + using UnityWebRequest webRequest = + UnityWebRequest.Get(VELConnectManager.VelConnectUrl + $"/api/collections/PersistObject/records?filter=(app='{Application.productName}')&page={page++}"); + yield return webRequest.SendWebRequest(); + + switch (webRequest.result) + { + case UnityWebRequest.Result.ConnectionError: + case UnityWebRequest.Result.DataProcessingError: + case UnityWebRequest.Result.ProtocolError: + Debug.LogError("Error: " + webRequest.error + "\n" + Environment.StackTrace); + Debug.LogError("Failed to get persisted spawned objects"); + yield break; + break; + case UnityWebRequest.Result.Success: + string text = webRequest.downloadHandler.text; + VELConnectManager.RecordList obj = + JsonConvert.DeserializeObject>(text); + allResults.AddRange(obj.items); + pagesLeft = obj.totalPages - obj.page; + totalCounter--; + break; + } + } + + // Spawn items if we're the first to join this room if (VelNetManager.Players.Count == 1) { - VELConnectManager.GetRequestCallback( - VELConnectManager.VelConnectUrl + - $"/api/collections/PersistObject/records?filter=(app='{Application.productName}')", - s => + List spawnedItems = allResults.Where(i => i.spawned && i.room == VelNetManager.Room).ToList(); + foreach (VELConnectManager.PersistObject persistObject in spawnedItems) + { + if (string.IsNullOrEmpty(persistObject.data)) { - VELConnectManager.RecordList obj = - JsonConvert.DeserializeObject>(s); - obj.items = obj.items.Where(i => i.spawned && i.room == VelNetManager.Room).ToList(); + Debug.LogError("Persisted object has no data"); + continue; + } - foreach (VELConnectManager.PersistObject persistObject in obj.items) + NetworkObject spawnedObj = VelNetManager.NetworkInstantiate(persistObject.name, Convert.FromBase64String(persistObject.data)); + VelNetPersist persist = spawnedObj.GetComponent(); + + persist.persistId = persistObject.id; + persist.LoadData(persistObject); + } + } + + // load data for scene objects + List sceneObjectData = allResults.Where(i => i.room == VelNetManager.Room).ToList(); + foreach (VelNetPersist velNetPersist in sceneObjects) + { + List thisObjectData = sceneObjectData.Where(i => i.network_id == velNetPersist.networkObject.sceneNetworkId.ToString()).ToList(); + switch (thisObjectData.Count) + { + case < 1: + Debug.LogError($"[VelNetPersist] No data found for {velNetPersist.name} (network_id='{velNetPersist.networkObject.sceneNetworkId}')"); + velNetPersist.loading = false; + continue; + case > 1: + Debug.LogError( + $"[VelNetPersist] Multiple records found for app='{Application.productName}' && room='{VelNetManager.Room}' && network_id='{velNetPersist.networkObject.sceneNetworkId}'. Deleting all but the first one."); + IEnumerable toDelete = thisObjectData.Skip(1); + foreach (VELConnectManager.PersistObject persistObject in toDelete) { - if (string.IsNullOrEmpty(persistObject.data)) - { - Debug.LogError("Persisted object has no data"); - continue; - } - - NetworkObject spawnedObj = VelNetManager.NetworkInstantiate(persistObject.name, Convert.FromBase64String(persistObject.data)); - VelNetPersist persist = spawnedObj.GetComponent(); - - persist.persistId = persistObject.id; - persist.LoadData(persistObject); + VELConnectManager.PostRequestCallback(VELConnectManager.VelConnectUrl + "/api/collections/PersistObject/records/" + persistObject.id, null, null, null, + Debug.LogError, method: "DELETE"); } - }, s => { Debug.LogError("Failed to get persisted spawned objects", this); }); + + break; + } + + velNetPersist.LoadData(thisObjectData.FirstOrDefault()); } } + public static void RegisterSceneObject(VelNetPersist obj) + { + instance.sceneObjects.Add(obj); + } + + public static void UnregisterSceneObject(VelNetPersist obj) + { + instance.sceneObjects.Remove(obj); + } + // We don't need to register objects, because they will do that automatically when they spawn if they have the VelNetPersist component - // We need to unregister objects when they are destroyed because destroying could happen because we left the scene - public static void UnregisterObject(NetworkObject obj) + // We need to unregister objects when they are destroyed because destroying could happen because we left the scene (which shouldn't delete it from the server) + public static void DestroySpawnedObject(NetworkObject obj) { VelNetPersist[] persistedComponents = obj.GetComponents(); if (persistedComponents.Length > 1) diff --git a/unity_package/Runtime/VelNetPersist.cs b/unity_package/Runtime/VelNetPersist.cs index 2c56304..87afea9 100644 --- a/unity_package/Runtime/VelNetPersist.cs +++ b/unity_package/Runtime/VelNetPersist.cs @@ -12,7 +12,7 @@ namespace VELConnect { private const float interval = 5f; private double nextUpdate; - private bool loading; + public bool loading; private const bool debugLogs = false; public string persistId; @@ -30,17 +30,15 @@ namespace VELConnect private void OnEnable() { - VelNetManager.OnJoinedRoom += OnJoinedRoom; + if (networkObject.isSceneObject) + { + VelConnectPersistenceManager.RegisterSceneObject(this); + } } private void OnDisable() { - VelNetManager.OnJoinedRoom -= OnJoinedRoom; - } - - private void OnJoinedRoom(string roomName) - { - Load(); + VelConnectPersistenceManager.UnregisterSceneObject(this); } private void Load() @@ -52,31 +50,32 @@ namespace VELConnect { // It looks like a PocketBase bug is preventing full filtering from happening: // $"/api/collections/PersistObject/records?filter=(app='{Application.productName}' && room='{VelNetManager.Room}' && network_id='{networkObject.sceneNetworkId}')", - VELConnectManager.GetRequestCallback( - VELConnectManager.VelConnectUrl + - $"/api/collections/PersistObject/records?filter=(app='{Application.productName}')", - s => - { - VELConnectManager.RecordList obj = - JsonConvert.DeserializeObject>(s); - obj.items = obj.items.Where(i => i.network_id == networkObject.sceneNetworkId.ToString() && i.room == VelNetManager.Room).ToList(); - if (obj.items.Count < 1) - { - Debug.LogError("[VelNetPersist] No data found for " + name); - loading = false; - return; - } - else if (obj.items.Count > 1) - { - Debug.LogError( - $"[VelNetPersist] Multiple records found for app='{Application.productName}' && room='{VelNetManager.Room}' && network_id='{networkObject.sceneNetworkId}'. Using the first one."); - } - - LoadData(obj.items.FirstOrDefault()); - }, s => { loading = false; }); + // VELConnectManager.GetRequestCallback( + // VELConnectManager.VelConnectUrl + + // $"/api/collections/PersistObject/records?filter=(app='{Application.productName}')", + // s => + // { + // VELConnectManager.RecordList obj = + // JsonConvert.DeserializeObject>(s); + // obj.items = obj.items.Where(i => i.network_id == networkObject.sceneNetworkId.ToString() && i.room == VelNetManager.Room).ToList(); + // if (obj.items.Count < 1) + // { + // Debug.LogError("[VelNetPersist] No data found for " + name); + // loading = false; + // return; + // } + // else if (obj.items.Count > 1) + // { + // Debug.LogError( + // $"[VelNetPersist] Multiple records found for app='{Application.productName}' && room='{VelNetManager.Room}' && network_id='{networkObject.sceneNetworkId}'. Using the first one."); + // } + // + // LoadData(obj.items.FirstOrDefault()); + // }, s => { loading = false; }); } else { + Debug.LogError("TODO idk when this would happen"); VELConnectManager.GetRequestCallback(VELConnectManager.VelConnectUrl + "/api/collections/PersistObject/records/" + persistId, s => { VELConnectManager.PersistObject obj = JsonConvert.DeserializeObject(s); @@ -160,7 +159,7 @@ namespace VELConnect } } - public void Delete(Action successCallback = null) + public void Delete() { if (string.IsNullOrEmpty(persistId)) { @@ -168,12 +167,8 @@ namespace VELConnect return; } - VELConnectManager.PostRequestCallback(VELConnectManager.VelConnectUrl + "/api/collections/PersistObject/records/" + persistId, null, null, - s => - { - VELConnectManager.PersistObject resp = JsonConvert.DeserializeObject(s); - successCallback?.Invoke(resp); - }, Debug.LogError, + VELConnectManager.PostRequestCallback(VELConnectManager.VelConnectUrl + "/api/collections/PersistObject/records/" + persistId, + null, null, null, Debug.LogError, method: "DELETE"); } diff --git a/unity_package/package.json b/unity_package/package.json index c3769a7..08831f1 100644 --- a/unity_package/package.json +++ b/unity_package/package.json @@ -1,7 +1,7 @@ { "name": "edu.uga.engr.vel.vel-connect", "displayName": "VEL-Connect", - "version": "5.0.2", + "version": "5.0.3", "unity": "2019.1", "description": "Web-based configuration for VR applications", "keywords": [], diff --git a/velconnect-npm/src/types/pocketbase-types.ts b/velconnect-npm/src/types/pocketbase-types.ts index a176947..65b5377 100644 --- a/velconnect-npm/src/types/pocketbase-types.ts +++ b/velconnect-npm/src/types/pocketbase-types.ts @@ -49,6 +49,7 @@ export type DeviceRecord = { current_room?: string data: RecordIdString friendly_name?: string + friendlier_name?: string last_online?: IsoDateString modified_by?: string os_info?: string