working on persistence for spawned objects

dev
Anton Franzluebbers 2024-03-20 16:51:30 -04:00
parent a68d12aab5
commit ad778a616b
5 changed files with 477 additions and 110 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

@ -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<string> spawnedObjectList = JsonConvert.DeserializeObject<List<string>>(spawnedObjects);
List<NetworkObject> spawnedNetworkObjects = new List<NetworkObject>();
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<VELConnectManager.PersistObject> obj =
JsonConvert.DeserializeObject<VELConnectManager.RecordList<VELConnectManager.PersistObject>>(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<VelNetPersist>();
persist.persistId = persistObject.id;
persist.LoadData(persistObject);
}
}, s => { Debug.LogError("Failed to get persisted spawned objects", this); });
//
// string spawnedObjects = VELConnectManager.GetRoomData("spawned_objects", "[]");
// List<string> spawnedObjectList = JsonConvert.DeserializeObject<List<string>>(spawnedObjects);
// List<NetworkObject> spawnedNetworkObjects = new List<NetworkObject>();
// 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<VelNetPersist>();
// 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<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;
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<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] 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<ComponentState> componentData = JsonConvert.DeserializeObject<List<ComponentState>>(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<VELConnectManager.PersistObject>(s);
LoadData(obj);
},
s => { loading = false; });
}
}
public void Save(Action<VELConnectManager.State.DataBlock> 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<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);
//
// List<SyncState> syncStateComponents = networkObject.syncedComponents.OfType<SyncState>().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<VELConnectManager.PersistObject> successCallback = null)
{
if (debugLogs) Debug.Log($"[VelNetPersist] Saving {name}");
List<SyncState> syncStateComponents = networkObject.syncedComponents.OfType<SyncState>().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<VELConnectManager.ComponentState> componentData = new List<VELConnectManager.ComponentState>();
// 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<string, string>
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<VELConnectManager.PersistObject>(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<VELConnectManager.PersistObject>(s);
successCallback?.Invoke(resp);
}, method: "PATCH");
}
}
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 =>
{
Debug.Log(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

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