Compare commits

...

2 Commits

Author SHA1 Message Date
Anton Franzluebbers 48e55550c3 clean up old code, bump version 2024-03-21 14:48:04 -04:00
Anton Franzluebbers ad778a616b working on persistence for spawned objects 2024-03-20 16:51:30 -04:00
6 changed files with 398 additions and 141 deletions

View File

@ -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<T>
{
public int page;
public int perPage;
public int totalPages;
public int totalItems;
public List<T> 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<string, string> headers = null,
public static void PostRequestCallback(
string url,
string postData,
Dictionary<string, string> headers = null,
Action<string> successCallback = null,
Action<string> failureCallback = null)
Action<string> 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<string, string> headers = null, Action<string> successCallback = null,
Action<string> failureCallback = null)
Action<string> 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;

View File

@ -1,11 +1,7 @@
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
@ -14,14 +10,6 @@ namespace VELConnect
{
public static VelConnectPersistenceManager instance;
public class SpawnedObjectData
{
public string prefabName;
public string base64ObjectData;
public string networkId;
public int componentIdx;
}
private void Awake()
{
instance = this;
@ -39,70 +27,50 @@ 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<string> spawnedObjectList = JsonConvert.DeserializeObject<List<string>>(spawnedObjects);
List<NetworkObject> spawnedNetworkObjects = new List<NetworkObject>();
GetSpawnedObjectData(spawnedObjectList, (list) =>
VELConnectManager.GetRequestCallback(
VELConnectManager.VelConnectUrl +
$"/api/collections/PersistObject/records?filter=(app='{Application.productName}')",
s =>
{
foreach (SpawnedObjectData obj in list)
VELConnectManager.RecordList<VELConnectManager.PersistObject> obj =
JsonConvert.DeserializeObject<VELConnectManager.RecordList<VELConnectManager.PersistObject>>(s);
obj.items = obj.items.Where(i => i.spawned && i.room == VelNetManager.Room).ToList();
foreach (VELConnectManager.PersistObject persistObject in obj.items)
{
NetworkObject spawnedObj = spawnedNetworkObjects.Find(i => i.networkId == obj.networkId);
if (spawnedObj == null)
if (string.IsNullOrEmpty(persistObject.data))
{
spawnedObj = VelNetManager.NetworkInstantiate(obj.prefabName);
spawnedNetworkObjects.Add(spawnedObj);
Debug.LogError("Persisted object has no data");
continue;
}
spawnedObj.syncedComponents[obj.componentIdx].ReceiveBytes(Convert.FromBase64String(obj.base64ObjectData));
NetworkObject spawnedObj = VelNetManager.NetworkInstantiate(persistObject.name, Convert.FromBase64String(persistObject.data));
VelNetPersist persist = spawnedObj.GetComponent<VelNetPersist>();
persist.persistId = persistObject.id;
persist.LoadData(persistObject);
}
});
}, s => { Debug.LogError("Failed to get persisted spawned objects", this); });
}
}
private class DataBlocksResponse
// 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)
{
public List<VELConnectManager.State.DataBlock> items;
}
private static void GetSpawnedObjectData(List<string> spawnedObjectList, Action<List<SpawnedObjectData>> callback)
{
VELConnectManager.GetRequestCallback($"/api/collections/DataBlock/records?filter=({string.Join(" || ", "id=\"" + spawnedObjectList + "\"")})", (response) =>
{
DataBlocksResponse parsedResponse = JsonConvert.DeserializeObject<DataBlocksResponse>(response);
callback(parsedResponse.items.Select(i => new SpawnedObjectData()
{
networkId = i.block_id.Split("_")[-1],
componentIdx = int.Parse(i.block_id.Split("_").Last()),
prefabName = i.TryGetData("name"),
base64ObjectData = i.TryGetData("state")
}).ToList());
});
}
public static void RegisterObject(NetworkObject obj)
{
instance.StartCoroutine(instance.RegisterObjectCo(obj));
}
private IEnumerator RegisterObjectCo(NetworkObject obj)
{
// upload all the persisted components, then add those components to the room data
VelNetPersist[] persistedComponents = obj.GetComponents<VelNetPersist>();
List<VELConnectManager.State.DataBlock> responses = new List<VELConnectManager.State.DataBlock>();
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()));
}
}
}

View File

@ -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;
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,140 @@ 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))
// 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 =>
{
Debug.LogError($"[VelNetPersist] Failed to parse {Id}");
return;
}
List<ComponentState> componentData = JsonConvert.DeserializeObject<List<ComponentState>>(d);
if (componentData.Count != syncStateComponents.Length)
VELConnectManager.RecordList<VELConnectManager.PersistObject> obj =
JsonConvert.DeserializeObject<VELConnectManager.RecordList<VELConnectManager.PersistObject>>(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] 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}");
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; });
}
public void Save(Action<VELConnectManager.State.DataBlock> successCallback = null)
else
{
if (debugLogs) Debug.Log($"[VelNetPersist] Saving {Id}");
if (syncStateComponents.FirstOrDefault()?.networkObject == null)
VELConnectManager.GetRequestCallback(VELConnectManager.VelConnectUrl + "/api/collections/PersistObject/records/" + persistId, s =>
{
Debug.LogError("First SyncState doesn't have a NetworkObject", this);
VELConnectManager.PersistObject obj = JsonConvert.DeserializeObject<VELConnectManager.PersistObject>(s);
LoadData(obj);
},
s => { loading = false; });
}
}
public void LoadData(VELConnectManager.PersistObject obj)
{
if (string.IsNullOrEmpty(obj.data))
{
Debug.LogError($"[VelNetPersist] No data found for {name}");
loading = false;
return;
}
List<ComponentState> componentData = new List<ComponentState>();
foreach (SyncState syncState in syncStateComponents)
persistId = obj.id;
using BinaryReader reader = new BinaryReader(new MemoryStream(Convert.FromBase64String(obj.data)));
networkObject.UnpackState(reader);
if (debugLogs) Debug.Log($"[VelNetPersist] Loaded {name}");
loading = false;
}
public void Save(Action<VELConnectManager.PersistObject> successCallback = null)
{
if (syncState == null)
if (debugLogs) Debug.Log($"[VelNetPersist] Saving {name}");
List<SyncState> syncStateComponents = networkObject.syncedComponents.OfType<SyncState>().ToList();
if (networkObject == null)
{
Debug.LogError("SyncState is null for Persist", this);
Debug.LogError("NetworkObject is null on SyncState", this);
return;
}
if (syncState.networkObject == null)
{
Debug.LogError("Network Object is null for SyncState", syncState);
return;
}
using BinaryWriter writer = new BinaryWriter(new MemoryStream());
networkObject.PackState(writer);
string data = Convert.ToBase64String(((MemoryStream)writer.BaseStream).ToArray());
componentData.Add(new ComponentState()
// if we have a persistId, update the record, otherwise create a new one
if (string.IsNullOrEmpty(persistId))
{
componentIdx = syncState.networkObject.syncedComponents.IndexOf(syncState),
state = Convert.ToBase64String(syncState.PackState())
Debug.LogWarning($"We don't have an existing persistId, so we are creating a new record for {networkObject.name}");
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 =>
{
VELConnectManager.PersistObject resp = JsonConvert.DeserializeObject<VELConnectManager.PersistObject>(s);
persistId = resp.id;
successCallback?.Invoke(resp);
});
}
VELConnectManager.SetDataBlock(Id, new VELConnectManager.State.DataBlock()
else
{
id = Id,
block_id = Id,
category = "object_persist",
data = new Dictionary<string, string>
VELConnectManager.PostRequestCallback(VELConnectManager.VelConnectUrl + "/api/collections/PersistObject/records/" + persistId, JsonConvert.SerializeObject(
new VELConnectManager.PersistObject()
{
{ "name", syncStateComponents.FirstOrDefault()?.networkObject.name },
{ "components", JsonConvert.SerializeObject(componentData) }
app = Application.productName,
room = VelNetManager.Room,
network_id = networkObject.sceneNetworkId.ToString(),
spawned = !networkObject.isSceneObject,
name = networkObject.prefabName,
data = data,
}), null, s =>
{
VELConnectManager.PersistObject resp = JsonConvert.DeserializeObject<VELConnectManager.PersistObject>(s);
successCallback?.Invoke(resp);
}, method: "PATCH");
}
}, s => { successCallback?.Invoke(s); });
}
public void Delete(Action<VELConnectManager.PersistObject> 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 =>
{
VELConnectManager.PersistObject resp = JsonConvert.DeserializeObject<VELConnectManager.PersistObject>(s);
successCallback?.Invoke(resp);
}, Debug.LogError,
method: "DELETE");
}
public override void ReceiveBytes(byte[] message)
{
throw new NotImplementedException();
}
}
}

View File

@ -1,7 +1,7 @@
{
"name": "edu.uga.engr.vel.vel-connect",
"displayName": "VEL-Connect",
"version": "4.0.8",
"version": "4.0.9",
"unity": "2019.1",
"description": "Web-based configuration for VR applications",
"keywords": [],

View File

@ -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)
})
}

View File

@ -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)
})
}