diff --git a/unity_package/Runtime/VELConnectManager.cs b/unity_package/Runtime/VELConnectManager.cs index e993d6c..8872f6c 100644 --- a/unity_package/Runtime/VELConnectManager.cs +++ b/unity_package/Runtime/VELConnectManager.cs @@ -91,6 +91,34 @@ namespace VELConnect } } + public class PersistObject + { + public string id; + public readonly DateTime created; + public readonly DateTime updated; + public string app; + public string room; + public string network_id; + public bool spawned; + public string name; + public string data; + } + + public class RecordList + { + public int page; + public int perPage; + public int totalPages; + public int totalItems; + public List items; + } + + public class ComponentState + { + public int componentIdx; + public string state; + } + public class UserCount { [CanBeNull] public readonly string id; @@ -884,19 +912,24 @@ namespace VELConnect } } - public static void PostRequestCallback(string url, string postData, Dictionary headers = null, + public static void PostRequestCallback( + string url, + string postData, + Dictionary headers = null, Action successCallback = null, - Action failureCallback = null) + Action failureCallback = null, + string method = "POST" + ) { - instance.StartCoroutine(PostRequestCallbackCo(url, postData, headers, successCallback, failureCallback)); + instance.StartCoroutine(PostRequestCallbackCo(url, postData, headers, successCallback, failureCallback, method)); } private static IEnumerator PostRequestCallbackCo(string url, string postData, Dictionary headers = null, Action successCallback = null, - Action failureCallback = null) + Action failureCallback = null, string method="POST") { - UnityWebRequest webRequest = new UnityWebRequest(url, "POST"); + UnityWebRequest webRequest = new UnityWebRequest(url, method); byte[] bodyRaw = Encoding.UTF8.GetBytes(postData); UploadHandlerRaw uploadHandler = new UploadHandlerRaw(bodyRaw); webRequest.uploadHandler = uploadHandler; diff --git a/unity_package/Runtime/VELConnectPersistenceManager.cs b/unity_package/Runtime/VELConnectPersistenceManager.cs index 7ad0358..e539a0f 100644 --- a/unity_package/Runtime/VELConnectPersistenceManager.cs +++ b/unity_package/Runtime/VELConnectPersistenceManager.cs @@ -2,10 +2,8 @@ using System; using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text; using Newtonsoft.Json; using UnityEngine; -using VELConnect; using VelNet; namespace VELConnect @@ -39,25 +37,51 @@ namespace VELConnect private void OnJoinedRoom(string roomName) { - if (VelNetManager.Players.Count == 0) + // if we're the first to join this room + if (VelNetManager.Players.Count == 1) { - string spawnedObjects = VELConnectManager.GetRoomData("spawned_objects", "[]"); - List spawnedObjectList = JsonConvert.DeserializeObject>(spawnedObjects); - List spawnedNetworkObjects = new List(); - GetSpawnedObjectData(spawnedObjectList, (list) => - { - foreach (SpawnedObjectData obj in list) + VELConnectManager.GetRequestCallback( + VELConnectManager.VelConnectUrl + + $"/api/collections/PersistObject/records?filter=(app='{Application.productName}')", + s => { - NetworkObject spawnedObj = spawnedNetworkObjects.Find(i => i.networkId == obj.networkId); - if (spawnedObj == null) - { - spawnedObj = VelNetManager.NetworkInstantiate(obj.prefabName); - spawnedNetworkObjects.Add(spawnedObj); - } + VELConnectManager.RecordList obj = + JsonConvert.DeserializeObject>(s); + obj.items = obj.items.Where(i => i.spawned && i.room == VelNetManager.Room).ToList(); - spawnedObj.syncedComponents[obj.componentIdx].ReceiveBytes(Convert.FromBase64String(obj.base64ObjectData)); - } - }); + foreach (VELConnectManager.PersistObject persistObject in obj.items) + { + 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); + } + }, s => { Debug.LogError("Failed to get persisted spawned objects", this); }); + + // + // string spawnedObjects = VELConnectManager.GetRoomData("spawned_objects", "[]"); + // List spawnedObjectList = JsonConvert.DeserializeObject>(spawnedObjects); + // List spawnedNetworkObjects = new List(); + // GetSpawnedObjectData(spawnedObjectList, (list) => + // { + // foreach (SpawnedObjectData obj in list) + // { + // NetworkObject spawnedObj = spawnedNetworkObjects.Find(i => i.networkId == obj.networkId); + // if (spawnedObj == null) + // { + // spawnedObj = VelNetManager.NetworkInstantiate(obj.prefabName); + // spawnedNetworkObjects.Add(spawnedObj); + // } + // + // spawnedObj.syncedComponents[obj.componentIdx].ReceiveBytes(Convert.FromBase64String(obj.base64ObjectData)); + // } + // }); } } @@ -81,28 +105,40 @@ namespace VELConnect }); } - public static void RegisterObject(NetworkObject obj) - { - instance.StartCoroutine(instance.RegisterObjectCo(obj)); - } + // We don't need to register objects, because they will do that automatically when they spawn if they have the VelNetPersist component + // public static void RegisterObject(NetworkObject obj) + // { + // if (instance == null) + // { + // Debug.LogError("VelConnectPersistenceManager not found in scene"); + // return; + // } + // + // VelNetPersist[] persistedComponents = obj.GetComponents(); + // if (persistedComponents.Length > 1) + // { + // Debug.LogError("NetworkObject has more than one VelNetPersist component"); + // } + // + // foreach (VelNetPersist velNetPersist in persistedComponents) + // { + // velNetPersist.Save(); + // } + // } - private IEnumerator RegisterObjectCo(NetworkObject obj) + // We need to unregister objects when they are destroyed because destroying could happen because we left the scene + public static void UnregisterObject(NetworkObject obj) { - // upload all the persisted components, then add those components to the room data VelNetPersist[] persistedComponents = obj.GetComponents(); - List responses = new List(); - double startTime = Time.timeAsDouble; + if (persistedComponents.Length > 1) + { + Debug.LogError("NetworkObject has more than one VelNetPersist component"); + } + foreach (VelNetPersist velNetPersist in persistedComponents) { - velNetPersist.Save(s => { responses.Add(s); }); + velNetPersist.Delete(); } - - while (responses.Count < persistedComponents.Length && Time.timeAsDouble - startTime < 5) - { - yield return null; - } - - VELConnectManager.SetRoomData("spawned_objects", JsonConvert.SerializeObject(responses.Select(i => i.block_id).ToList())); } } } \ No newline at end of file diff --git a/unity_package/Runtime/VelNetPersist.cs b/unity_package/Runtime/VelNetPersist.cs index ab34d77..5d2b752 100644 --- a/unity_package/Runtime/VelNetPersist.cs +++ b/unity_package/Runtime/VelNetPersist.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Newtonsoft.Json; using UnityEngine; @@ -7,29 +8,20 @@ using VelNet; namespace VELConnect { - public class VelNetPersist : MonoBehaviour + public class VelNetPersist : NetworkComponent { - private class ComponentState - { - public int componentIdx; - public string state; - } - - public SyncState[] syncStateComponents; - - private string Id => $"{Application.productName}_{VelNetManager.Room}_{syncStateComponents.FirstOrDefault()?.networkObject.sceneNetworkId}"; - private const float interval = 5f; private double nextUpdate; private bool loading; - private const bool debugLogs = false; + private const bool debugLogs = true; + public string persistId; private void Update() { if (Time.timeAsDouble > nextUpdate && VelNetManager.InRoom && !loading) { nextUpdate = Time.timeAsDouble + interval + UnityEngine.Random.Range(0, interval); - if (syncStateComponents.FirstOrDefault()?.networkObject.IsMine == true) + if (networkObject.IsMine) { Save(); } @@ -54,78 +46,182 @@ namespace VELConnect private void Load() { loading = true; - if (debugLogs) Debug.Log($"[VelNetPersist] Loading {Id}"); - VELConnectManager.GetDataBlock(Id, data => + if (debugLogs) Debug.Log($"[VelNetPersist] Loading {name}"); + + if (networkObject.isSceneObject) { - if (!data.data.TryGetValue("components", out string d)) - { - Debug.LogError($"[VelNetPersist] Failed to parse {Id}"); - return; - } + // 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."); + } - - List componentData = JsonConvert.DeserializeObject>(d); - - if (componentData.Count != syncStateComponents.Length) - { - Debug.LogError($"[VelNetPersist] Different number of components"); - return; - } - - for (int i = 0; i < syncStateComponents.Length; i++) - { - syncStateComponents[i].UnpackState(Convert.FromBase64String(componentData[i].state)); - } - - if (debugLogs) Debug.Log($"[VelNetPersist] Loaded {Id}"); - loading = false; - }, s => { loading = false; }); + LoadData(obj.items.FirstOrDefault()); + }, s => { loading = false; }); + } + else + { + VELConnectManager.GetRequestCallback(VELConnectManager.VelConnectUrl + "/api/collections/PersistObject/records/" + persistId, s => + { + VELConnectManager.PersistObject obj = JsonConvert.DeserializeObject(s); + LoadData(obj); + }, + s => { loading = false; }); + } } - - public void Save(Action successCallback = null) + public void LoadData(VELConnectManager.PersistObject obj) { - if (debugLogs) Debug.Log($"[VelNetPersist] Saving {Id}"); - - if (syncStateComponents.FirstOrDefault()?.networkObject == null) + if (string.IsNullOrEmpty(obj.data)) { - Debug.LogError("First SyncState doesn't have a NetworkObject", this); + Debug.LogError($"[VelNetPersist] No data found for {name}"); + loading = false; return; } - List componentData = new List(); - foreach (SyncState syncState in syncStateComponents) + persistId = obj.id; + + using BinaryReader reader = new BinaryReader(new MemoryStream(Convert.FromBase64String(obj.data))); + networkObject.UnpackState(reader); + // + // List syncStateComponents = networkObject.syncedComponents.OfType().ToList(); + // if (obj.data.Count != syncStateComponents.Count) + // { + // Debug.LogError($"[VelNetPersist] Different number of components"); + // loading = false; + // return; + // } + // + // for (int i = 0; i < syncStateComponents.Count; i++) + // { + // Debug.Log($"[VelNetPersist] Unpacking {obj.name} {syncStateComponents[i].GetType().Name}"); + // syncStateComponents[i].UnpackState(Convert.FromBase64String(obj.data[i].state)); + // } + + if (debugLogs) Debug.Log($"[VelNetPersist] Loaded {name}"); + loading = false; + } + + + public void Save(Action successCallback = null) + { + if (debugLogs) Debug.Log($"[VelNetPersist] Saving {name}"); + + List syncStateComponents = networkObject.syncedComponents.OfType().ToList(); + + if (networkObject == null) { - if (syncState == null) - { - Debug.LogError("SyncState is null for Persist", this); - return; - } - - if (syncState.networkObject == null) - { - Debug.LogError("Network Object is null for SyncState", syncState); - return; - } - - componentData.Add(new ComponentState() - { - componentIdx = syncState.networkObject.syncedComponents.IndexOf(syncState), - state = Convert.ToBase64String(syncState.PackState()) - }); + Debug.LogError("NetworkObject is null on SyncState", this); + return; } - VELConnectManager.SetDataBlock(Id, new VELConnectManager.State.DataBlock() + // List componentData = new List(); + // foreach (SyncState syncState in syncStateComponents) + // { + // if (syncState == null) + // { + // Debug.LogError("SyncState is null for Persist", this); + // return; + // } + // + // if (syncState.networkObject == null) + // { + // Debug.LogError("Network Object is null for SyncState", syncState); + // return; + // } + // + // componentData.Add(new VELConnectManager.ComponentState() + // { + // componentIdx = networkObject.syncedComponents.IndexOf(syncState), + // state = Convert.ToBase64String(syncState.PackState()) + // }); + // } + + using BinaryWriter writer = new BinaryWriter(new MemoryStream()); + networkObject.PackState(writer); + string data = Convert.ToBase64String(((MemoryStream)writer.BaseStream).ToArray()); + + // if we have a persistId, update the record, otherwise create a new one + if (string.IsNullOrEmpty(persistId)) { - id = Id, - block_id = Id, - category = "object_persist", - data = new Dictionary + Debug.LogWarning($"We don't have an existing persistId, so we are creating a new record for {networkObject.prefabName}"); + VELConnectManager.PostRequestCallback(VELConnectManager.VelConnectUrl + "/api/collections/PersistObject/records", JsonConvert.SerializeObject( + new VELConnectManager.PersistObject() + { + app = Application.productName, + room = VelNetManager.Room, + network_id = networkObject.sceneNetworkId.ToString(), + spawned = !networkObject.isSceneObject, + name = networkObject.isSceneObject ? networkObject.name : networkObject.prefabName, + data = data, + }), null, s => { - { "name", syncStateComponents.FirstOrDefault()?.networkObject.name }, - { "components", JsonConvert.SerializeObject(componentData) } - } - }, s => { successCallback?.Invoke(s); }); + Debug.Log(s); + + VELConnectManager.PersistObject resp = JsonConvert.DeserializeObject(s); + persistId = resp.id; + successCallback?.Invoke(resp); + }); + } + else + { + VELConnectManager.PostRequestCallback(VELConnectManager.VelConnectUrl + "/api/collections/PersistObject/records/" + persistId, JsonConvert.SerializeObject( + new VELConnectManager.PersistObject() + { + app = Application.productName, + room = VelNetManager.Room, + network_id = networkObject.sceneNetworkId.ToString(), + spawned = !networkObject.isSceneObject, + name = networkObject.prefabName, + data = data, + }), null, s => + { + Debug.Log(s); + + VELConnectManager.PersistObject resp = JsonConvert.DeserializeObject(s); + successCallback?.Invoke(resp); + }, method: "PATCH"); + } + } + + public void Delete(Action successCallback = null) + { + if (string.IsNullOrEmpty(persistId)) + { + Debug.LogError("We can't delete an object that doesn't have a persistId"); + return; + } + + VELConnectManager.PostRequestCallback(VELConnectManager.VelConnectUrl + "/api/collections/PersistObject/records/" + persistId, null, null, + s => + { + Debug.Log(s); + + VELConnectManager.PersistObject resp = JsonConvert.DeserializeObject(s); + successCallback?.Invoke(resp); + }, Debug.LogError, + method: "DELETE"); + } + + public override void ReceiveBytes(byte[] message) + { + throw new NotImplementedException(); } } } \ No newline at end of file diff --git a/velconnect/migrations/1710447578_created_PersistObject.go b/velconnect/migrations/1710447578_created_PersistObject.go new file mode 100644 index 0000000..87e796a --- /dev/null +++ b/velconnect/migrations/1710447578_created_PersistObject.go @@ -0,0 +1,126 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/models" +) + +func init() { + m.Register(func(db dbx.Builder) error { + jsonData := `{ + "id": "zo5oymw0d6evw80", + "created": "2024-03-14 20:19:38.622Z", + "updated": "2024-03-14 20:19:38.622Z", + "name": "PersistObject", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "tvwuy2xt", + "name": "app", + "type": "text", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "mj06ihfs", + "name": "room", + "type": "text", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "mqw640xp", + "name": "network_id", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "f0wynbda", + "name": "data", + "type": "json", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSize": 2000000 + } + }, + { + "system": false, + "id": "tqei9ccu", + "name": "spawned", + "type": "bool", + "required": false, + "presentable": false, + "unique": false, + "options": {} + }, + { + "system": false, + "id": "sgkbflei", + "name": "name", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + } + ], + "indexes": [], + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": null, + "options": {} + }` + + collection := &models.Collection{} + if err := json.Unmarshal([]byte(jsonData), &collection); err != nil { + return err + } + + return daos.New(db).SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("zo5oymw0d6evw80") + if err != nil { + return err + } + + return dao.DeleteCollection(collection) + }) +} diff --git a/velconnect/migrations/1710966457_updated_PersistObject.go b/velconnect/migrations/1710966457_updated_PersistObject.go new file mode 100644 index 0000000..5c7881a --- /dev/null +++ b/velconnect/migrations/1710966457_updated_PersistObject.go @@ -0,0 +1,76 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/models/schema" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("zo5oymw0d6evw80") + if err != nil { + return err + } + + // remove + collection.Schema.RemoveField("f0wynbda") + + // add + new_data := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "9bremliu", + "name": "data", + "type": "text", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }`), new_data); err != nil { + return err + } + collection.Schema.AddField(new_data) + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("zo5oymw0d6evw80") + if err != nil { + return err + } + + // add + del_data := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "f0wynbda", + "name": "data", + "type": "json", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSize": 2000000 + } + }`), del_data); err != nil { + return err + } + collection.Schema.AddField(del_data) + + // remove + collection.Schema.RemoveField("9bremliu") + + return dao.SaveCollection(collection) + }) +}