Compare commits

..

15 Commits

Author SHA1 Message Date
Anton Franzluebbers a68d12aab5 reworking persistence to be per-object, working on system to persist spawned objects 2024-03-08 16:15:17 -05:00
Anton Franzluebbers b16de5434d
Merge pull request #1 from velaboratory/main
Update dev with main
2024-03-08 16:13:52 -05:00
kjjohnsen df5946e0a6
Update quick-start.md
Use getUserData instead of state directly, now that it works as of 4.0.7 in OnInitialState
2024-03-07 14:57:27 -05:00
kjjohnsen d3c5a582ad
Update quick-start.md
Adjusted JSON.net example a bit to make it consistent with other example.
2024-03-07 14:36:59 -05:00
kjjohnsen 99c604424f
Simplify JSON.net persist example 2024-03-07 14:13:15 -05:00
kjjohnsen 5ad3ef7eb6
Update quick-start.md
added a JSON.net example
2024-03-07 13:57:19 -05:00
Anton Franzluebbers 4ef4a96f74 bump version 4.0.7 2024-03-06 20:12:55 -05:00
Anton Franzluebbers db4b0017f2 call oninitialstate after state has been added to local buffer, add examples to quick-start docs 2024-03-06 20:12:34 -05:00
Anton Franzluebbers 4c359345be fix velnet dependency version, don't require room data 2024-03-06 19:11:59 -05:00
Anton Franzluebbers 5d2e379205 initial velconnect docs site 2024-03-06 17:02:20 -05:00
Anton Franzluebbers d3f466d3c4 make more things null instead 2024-02-22 16:48:31 -05:00
Anton Franzluebbers 703c29f2e9 prevent null rooms a little bit 2024-02-22 16:01:08 -05:00
Anton Franzluebbers 89da691111 null check for room data 2024-02-22 13:46:47 -05:00
Anton Franzluebbers 4b2eb1b258 increment velconnect-npm 2024-02-21 14:54:23 -05:00
Anton Franzluebbers 1f9bb4f97f added generic spawned object loader for images 2024-02-16 16:36:38 -05:00
32 changed files with 992 additions and 3255 deletions

View File

@ -0,0 +1,47 @@
env:
SUBFOLDER: docs_website
name: Publish Docs to docs.velconnect.ugavel.com (Cloudflare Pages)
on:
push:
paths:
- docs_website/**
jobs:
publish-docs:
defaults:
run:
working-directory: ${{env.SUBFOLDER}}
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
name: Publish to Cloudflare Pages
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.x
- run: echo "CACHE_ID=$(date --utc '+%V')" >> $GITHUB_ENV
- uses: actions/cache@v3
with:
key: mkdocs-material-${{ env.CACHE_ID }}
path: .cache
restore-keys: |
mkdocs-material-
- run: pip install -r requirements.txt
- name: Build
run: mkdocs build --site-dir public
- name: Upload
env:
PROJECT_NAME: velconnect-docs
CLOUDFLARE_ACCOUNT_ID: 8077b5b1f8e2ade41874cbaa3f883069
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: npx wrangler@3.1.1 pages deploy public --project-name="${{env.PROJECT_NAME}}" --branch="${{env.GITHUB_REF_NAME}}"

View File

@ -20,6 +20,6 @@ jobs:
registry-url: "https://registry.npmjs.org" registry-url: "https://registry.npmjs.org"
- run: npm install - run: npm install
- run: npm ci - run: npm ci
- run: npm publish - run: npm publish --access public
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -20,6 +20,6 @@ jobs:
registry-url: "https://registry.npmjs.org" registry-url: "https://registry.npmjs.org"
- run: npm install - run: npm install
- run: npm ci - run: npm ci
- run: npm publish - run: npm publish --access public
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

4
docs_website/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
env/
public/
site/
.venv/

10
docs_website/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"configurations": [
{
"name": "Serve Docs",
"type": "node-terminal",
"request": "launch",
"command": "mkdocs serve"
}
]
}

3
docs_website/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"python.terminal.activateEnvironment": true,
}

17
docs_website/.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Autogenerate docs from code",
"detail": "Runs generate_docs.py",
"command": "python generate_docs.py",
"type": "shell",
"args": [],
"problemMatcher": [],
"presentation": {
"reveal": "always"
},
"group": "build"
}
]
}

19
docs_website/README.md Normal file
View File

@ -0,0 +1,19 @@
# VEL-Connect Docs
## Setup
1. Create or activate a pip environment
- Create:
- `python -m venv env`
- Activate:
- PowerShell: `.\env\Scripts\Activate.ps1`
- CMD: `.\env\Scripts\Activate.bat`
2. Install requirements:
- `pip install -r requirements.txt`
3. Run:
- `mkdocs serve`
- or use `F5` in VSCode
4. Build and Deploy
- Building and deploying happens automatically using a GitHub Action on push. If you want to build manually, use this command:
- `mkdocs build`
- For more information, visit these docs pages: https://squidfunk.github.io/mkdocs-material/getting-started/

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -0,0 +1,8 @@
# VEL-Connect
VEL-Connect is a persistent shared storage mechanism for Unity projects. It can be used as a key-value store as a networked replacement for PlayerPrefs, to share a profile of user data across multiple devices, or to easily switch between different user profiles on a single device.
Important pages:
- [Installation](/installation)
- [Quick Start](/quick-start)

View File

@ -0,0 +1,40 @@
Install the UPM package in Unity:
=== "**Option 1:** Add the VEL package registry"
![Scoped registry example](assets/screenshots/scoped_registry.png){ align=right }
Using the scoped registry allows you to easily install a specific version of the package by using the Version History tab.
- In Unity, go to `Edit->Project Settings...->Package Manager`
- Under "Scoped Registries" click the + icon
- Add the following details, then click Apply
- Name: `VEL` (or anything you want)
- URL: `https://npm.ugavel.com`
- Scope(s): `edu.uga.engr.vel`
- Install the package:
- In the package manager, select `My Registries` from the dropdown
- Install the `VEL-Connect` package.
=== "**Option 2:** Add the package by git url"
1. Open the Package Manager in Unity with `Window->Package Manager`
- Add the local package:
- `+`->`Add package from git URL...`
- Set the path to `https://github.com/velaboratory/VEL-Connect`
To update the package, click the `Update` button in the Package Manager, or delete the `packages-lock.json` file.
=== "**Option 3:** Add the package locally"
1. Clone the repository on your computer:
`git clone git@github.com:velaboratory/VEL-Connect.git`
- Open the Package Manager in Unity with `Window->Package Manager`
- Add the local package:
- `+`->`Add package from disk...`
- Set the path to `VEL-Connect/package.json` on your hard drive.
To update the package, use `git pull` in the VEL-Connect folder.
Then check out the [quick start guide](quick-start.md).

View File

@ -0,0 +1,180 @@
---
title: Quick Start
---
## Setup
1. [Install the package](/)
2. Add the VelConnectManager script to an object in your scene. If you transition between scenes in your application, mark the object as `DontDestroyOnLoad`
3. Set the `Vel Connect Url` field on the component to a valid velconnect server. `https://velconnect-v4.ugavel.com` is useful for VEL projects.
## Usage
### Setting data
To set user data in VEL-Connect use the static function `SetUserData`.
You can add a single key and value:
```cs
VELConnectManager.SetUserData("key1", "val1");
```
Or set multiple keys with the dictionary syntax:
```cs
VELConnectManager.SetUserData(new Dictionary<string, string>
{
{ "key2", "val2" },
{ "key3", "val3" }
});
```
Data will be set instantly locally, then pushed to the server. You don't have to wait for VEL-Connect to initialize at the beginning of your game to set data.
### Getting data
Fetching data from a remote server can be more tricky because it won't be available immediately when the game starts. Data can also be set from other applications (such as a dashboard or other users in the case of room data), so change listeners are useful.
To fetch a single value from a key:
```cs
string value1 = VELConnectManager.GetUserData("key1");
```
The latest local value will be returned. This will always return null in `Start()` because no data has been fetched yet, so you could wrap this call in the `OnInitialState` callback:
```cs
VELConnectManager.OnInitialState += state =>
{
VELConnectManager.GetUserData("key1");
};
```
If the data was already on the server before the start of your application, the correct value will be returned.
#### Change listeners
If you want to subscribe to changes in a key you can set up change listeners:
```cs
VELConnectManager.AddUserDataListener("key1", this, value =>
{
Debug.Log($"key1: {value}");
}, true);
```
Passing in `this` binds the lifetime of the listener to the lifetime of the current script. It is often tedious to make sure to unsubscribe to all of your listeners OnDisable or OnDestroy to prevent the event emitter from sending events to objects that no longer exist, but VEL-Connect will remove listeners when their `keepAliveObject` parameter becomes null. The last parameter in this function (`true` in the example) tells VEL-Connect to activate the callback immediately or when the first value is received. You can add the listener on `Start()` and the first invokation of the callback will have the previous value of the server.
---
Full example:
```cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using VELConnect;
public class VELConnectTesting : MonoBehaviour
{
private IEnumerator Start()
{
VELConnectManager.OnInitialState += state =>
{
Debug.Log($"[OnInitialState] key1: {VELConnectManager.GetUserData("key1")}");
};
VELConnectManager.AddUserDataListener("key1", this, value =>
{
Debug.Log($"[Listener] key1: {value}");
}, true);
VELConnectManager.AddUserDataListener("key2", this, value =>
{
Debug.Log($"[Listener] key2: {value}");
}, false);
yield return new WaitForSeconds(1f);
VELConnectManager.SetUserData("key1", "val1");
VELConnectManager.SetUserData(new Dictionary<string, string>
{
{ "key1", "val1" },
{ "key2", "val2" },
});
yield return new WaitForSeconds(1f);
VELConnectManager.SetUserData("key1", "val1_later");
}
}
```
---
JSON.Net Example that illustrates how to persist a complex object of data using VEL-Connect, initializing at start, and saving on application quit
```cs
using Newtonsoft.Json;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using VELConnect;
public class VelConnectDemo1 : MonoBehaviour
{
class ExampleJSON
{
public string a_string="a"; //you can use initializers
public int a_int=0;
public List<ExampleChildJSON> a_list = new List<ExampleChildJSON>(); // you can use lists of objects
}
class ExampleChildJSON
{
public string a_string; //if you don't, that's fine too, but you probably want a constructor then
public int a_int;
public ExampleChildJSON() { } //you need to make sure you have a blank constructor for deserialization
public ExampleChildJSON(string a_string, int a_int)
{
this.a_string = a_string;
this.a_int = a_int;
}
}
ExampleJSON dataToPersist = null;
IEnumerator Start()
{
VELConnectManager.OnInitialState += (state) =>
{
var s = VELConnectManager.GetUserData("mydata");
Debug.Log("Retrieved: " + s);
try
{
dataToPersist = JsonConvert.DeserializeObject<ExampleJSON>(s);
}
catch (Exception e)
{
Debug.Log("Error serializing state: " + e.Message);
}
if(dataToPersist == null)
{
Debug.Log("Null state, initializing");
dataToPersist =new ExampleJSON();
}
};
while (dataToPersist == null) yield return null;
dataToPersist.a_list.Add(
new ExampleChildJSON("" + UnityEngine.Random.Range(0, 10),
UnityEngine.Random.Range(0, 10))
);
Debug.Log(JsonConvert.SerializeObject(dataToPersist));
}
private void OnApplicationQuit()
{
VELConnectManager.SetUserData("mydata", JsonConvert.SerializeObject(dataToPersist));
}
}
```

View File

@ -0,0 +1,17 @@
:root {
--md-primary-fg-color: #7a2020;
--md-primary-fg-color--light: #ffffff;
--md-primary-fg-color--dark: #e4002b;
--md-primary-bg-color: hsla(0, 0%, 100%, 1);
--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7);
/* --md-accent-fg-color: #ffffff;
--md-accent-fg-color--transparent: #ffffff11;
--md-accent-bg-color: hsla(0, 0%, 100%, 1);
--md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7); */
}
[data-md-color-scheme="slate"] {
--md-hue: 34;
--md-default-bg-color: #191818;
--md-code-bg-color: #252525;
}

59
docs_website/mkdocs.yml Normal file
View File

@ -0,0 +1,59 @@
site_name: VEL-Connect Docs
site_url: https://docs.velconnect.ugavel.com
repo_url: https://github.com/velaboratory/VEL-Connect
repo_name: velaboratory/VEL-Connect
edit_uri: edit/main/docs_website/docs
theme:
name: material
features:
- content.action.edit
- navigation.instant
# - navigation.sections
- navigation.expand
- navigation.path
- navigation.indexes
- toc.follow
- toc.integrate
- content.code.copy
palette:
scheme: slate
primary: custom
accent: red
# background: custom
font: false
# text: Oswald
# text: Merriweather
# text: Merriweather Sans
logo: assets/vel_logo_3d.png
favicon: assets/vel_logo_3d_square.png
plugins:
- search:
# - social:
# cards_layout_options:
# font_family: Oswald
- git-revision-date-localized:
enable_creation_date: true
markdown_extensions:
- attr_list
- md_in_html
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- admonition
- pymdownx.details
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.inlinehilite
- pymdownx.snippets
extra_css:
- stylesheets/extra.css

View File

@ -0,0 +1,2 @@
mkdocs-material
mkdocs-git-revision-date-localized-plugin

View File

@ -0,0 +1,70 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using VelNet;
namespace VELConnect
{
public class GenericSpawnedObject : SyncState
{
private string url;
private string Url
{
get => url;
set
{
if (url == value) return;
url = value;
if (url.EndsWith(".png") || url.EndsWith(".jpg"))
{
StartCoroutine(DownloadImage(url));
}
else
{
Debug.LogError("Invalid image url: " + url);
}
}
}
public RawImage rawImage;
public void Init(string dataUrl)
{
Url = dataUrl;
}
protected override void SendState(BinaryWriter binaryWriter)
{
binaryWriter.Write(Url);
}
protected override void ReceiveState(BinaryReader binaryReader)
{
Url = binaryReader.ReadString();
}
private IEnumerator DownloadImage(string downloadUrl)
{
UnityWebRequest request = UnityWebRequestTexture.GetTexture(downloadUrl);
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.Log(request.error);
yield break;
}
rawImage.texture = ((DownloadHandlerTexture)request.downloadHandler).texture;
float aspect = (float)rawImage.texture.width / rawImage.texture.height;
Transform t = transform;
Vector3 s = t.localScale;
s = new Vector3(aspect * s.y, s.y, s.z);
t.localScale = s;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 16f283d9b4aeffc429940a70600d2e5e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -71,7 +71,7 @@ namespace VELConnect
public class DataBlock public class DataBlock
{ {
public readonly string id; public string id;
public readonly DateTime created; public readonly DateTime created;
public readonly DateTime updated; public readonly DateTime updated;
public string block_id; public string block_id;
@ -171,21 +171,18 @@ namespace VELConnect
private void Awake() private void Awake()
{ {
velConnectUrl = velConnectUrl.TrimEnd('/');
if (instance != null) Debug.LogError("VELConnectManager instance already exists", this); if (instance != null) Debug.LogError("VELConnectManager instance already exists", this);
instance = this; instance = this;
// Compute device id
MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
StringBuilder sb = new StringBuilder(SystemInfo.deviceUniqueIdentifier);
sb.Append(Application.productName);
#if UNITY_EDITOR
// allows running multiple builds on the same computer
// return SystemInfo.deviceUniqueIdentifier + Hash128.Compute(Application.dataPath);
sb.Append(Application.dataPath);
sb.Append("EDITOR");
#endif
string id = Convert.ToBase64String(md5.ComputeHash(Encoding.UTF8.GetBytes(sb.ToString())));
deviceId = CreateDeviceId(); deviceId = CreateDeviceId();
VelNetManager.OnLocalNetworkObjectSpawned += networkObject =>
{
if (!networkObject.ownershipLocked)
{
// TODO
// SetRoomData("spawned_" + networkObject.networkId, networkObject.prefabName);
}
};
} }
// Computes 15-char device id compatibly with pocketbase // Computes 15-char device id compatibly with pocketbase
@ -264,25 +261,7 @@ namespace VELConnect
state = JsonConvert.DeserializeObject<State>(json); state = JsonConvert.DeserializeObject<State>(json);
if (state == null) return; if (state == null) return;
bool isInitialState = false; bool isInitialState = lastState == null;
// first load stuff
if (lastState == null)
{
try
{
OnInitialState?.Invoke(state);
}
catch (Exception e)
{
Debug.LogError(e);
}
isInitialState = true;
// lastState = state;
// return;
}
// if (state.device.modified_by != DeviceId) // if (state.device.modified_by != DeviceId)
{ {
@ -413,7 +392,7 @@ namespace VELConnect
foreach (KeyValuePair<string, string> elem in state.room.data) foreach (KeyValuePair<string, string> elem in state.room.data)
{ {
string oldValue = null; string oldValue = null;
lastState?.room.data.TryGetValue(elem.Key, out oldValue); lastState?.room?.data.TryGetValue(elem.Key, out oldValue);
if (elem.Value != oldValue) if (elem.Value != oldValue)
{ {
try try
@ -482,6 +461,18 @@ namespace VELConnect
{ {
Debug.LogError("Pairing code nulllll"); Debug.LogError("Pairing code nulllll");
} }
if (isInitialState)
{
try
{
OnInitialState?.Invoke(state);
}
catch (Exception e)
{
Debug.LogError(e);
}
}
}); });
} }
catch (Exception e) catch (Exception e)
@ -689,6 +680,11 @@ namespace VELConnect
); );
} }
public static void SetUserData(string key, string value)
{
SetUserData(new Dictionary<string, string> { { key, value } });
}
/// <summary> /// <summary>
/// Sets the 'data' object of the Device table /// Sets the 'data' object of the Device table
/// </summary> /// </summary>
@ -755,6 +751,7 @@ namespace VELConnect
State.DataBlock room = new State.DataBlock State.DataBlock room = new State.DataBlock
{ {
category = "room", category = "room",
modified_by = "Unity",
data = data data = data
}; };
@ -795,6 +792,9 @@ namespace VELConnect
); );
} }
/// <summary>
/// Unpairs this device from the current user.
/// </summary>
public static void Unpair() public static void Unpair()
{ {
if (instance.state?.device != null) if (instance.state?.device != null)
@ -917,7 +917,7 @@ namespace VELConnect
case UnityWebRequest.Result.ConnectionError: case UnityWebRequest.Result.ConnectionError:
case UnityWebRequest.Result.DataProcessingError: case UnityWebRequest.Result.DataProcessingError:
case UnityWebRequest.Result.ProtocolError: case UnityWebRequest.Result.ProtocolError:
Debug.LogError(url + ": Error: " + webRequest.error + "\n" + webRequest.downloadHandler.text + "\n" + Environment.StackTrace); Debug.LogWarning(url + ": Error: " + webRequest.error + "\n" + webRequest.downloadHandler.text + "\n" + Environment.StackTrace);
failureCallback?.Invoke(webRequest.error); failureCallback?.Invoke(webRequest.error);
break; break;
case UnityWebRequest.Result.Success: case UnityWebRequest.Result.Success:
@ -929,12 +929,41 @@ namespace VELConnect
webRequest.Dispose(); webRequest.Dispose();
} }
public static void SetDataBlock(string blockId, State.DataBlock dataBlock) public static void SetDataBlock(State.DataBlock dataBlock, Action<State.DataBlock> successCallback = null)
{
PostRequestCallback(instance.velConnectUrl + "/api/collections/DataBlock/records", JsonConvert.SerializeObject(dataBlock, Formatting.None,
new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
}), null, s =>
{
if (successCallback != null)
{
State.DataBlock resp = JsonConvert.DeserializeObject<State.DataBlock>(s);
successCallback?.Invoke(resp);
}
});
}
/// <summary>
/// Setting with a block ID will update the existing block, otherwise it will create a new one
/// </summary>
/// <param name="blockId"></param>
/// <param name="dataBlock"></param>
/// <param name="successCallback"></param>
public static void SetDataBlock([CanBeNull] string blockId, State.DataBlock dataBlock, Action<State.DataBlock> successCallback = null)
{ {
PostRequestCallback(instance.velConnectUrl + "/data_block/" + blockId, JsonConvert.SerializeObject(dataBlock, Formatting.None, new JsonSerializerSettings PostRequestCallback(instance.velConnectUrl + "/data_block/" + blockId, JsonConvert.SerializeObject(dataBlock, Formatting.None, new JsonSerializerSettings
{ {
NullValueHandling = NullValueHandling.Ignore NullValueHandling = NullValueHandling.Ignore
})); }), null, s =>
{
if (successCallback != null)
{
State.DataBlock resp = JsonConvert.DeserializeObject<State.DataBlock>(s);
successCallback?.Invoke(resp);
}
});
} }
public static void GetDataBlock(string blockId, Action<State.DataBlock> successCallback = null, Action<string> failureCallback = null) public static void GetDataBlock(string blockId, Action<State.DataBlock> successCallback = null, Action<string> failureCallback = null)

View File

@ -0,0 +1,108 @@
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
{
public class VelConnectPersistenceManager : MonoBehaviour
{
public static VelConnectPersistenceManager instance;
public class SpawnedObjectData
{
public string prefabName;
public string base64ObjectData;
public string networkId;
public int componentIdx;
}
private void Awake()
{
instance = this;
}
private void OnEnable()
{
VelNetManager.OnJoinedRoom += OnJoinedRoom;
}
private void OnDisable()
{
VelNetManager.OnJoinedRoom -= OnJoinedRoom;
}
private void OnJoinedRoom(string roomName)
{
if (VelNetManager.Players.Count == 0)
{
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));
}
});
}
}
private class DataBlocksResponse
{
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;
foreach (VelNetPersist velNetPersist in persistedComponents)
{
velNetPersist.Save(s => { responses.Add(s); });
}
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

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 85006f287450ecc4caedd7925e1198e4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using UnityEngine; using UnityEngine;
using VelNet; using VelNet;
@ -7,8 +9,16 @@ namespace VELConnect
{ {
public class VelNetPersist : MonoBehaviour public class VelNetPersist : MonoBehaviour
{ {
public SyncState syncState; private class ComponentState
private string Id => $"{Application.productName}_{VelNetManager.Room}_{syncState.networkObject.sceneNetworkId}_{syncState.networkObject.syncedComponents.IndexOf(syncState)}"; {
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 const float interval = 5f;
private double nextUpdate; private double nextUpdate;
private bool loading; private bool loading;
@ -19,7 +29,7 @@ namespace VELConnect
if (Time.timeAsDouble > nextUpdate && VelNetManager.InRoom && !loading) if (Time.timeAsDouble > nextUpdate && VelNetManager.InRoom && !loading)
{ {
nextUpdate = Time.timeAsDouble + interval + UnityEngine.Random.Range(0, interval); nextUpdate = Time.timeAsDouble + interval + UnityEngine.Random.Range(0, interval);
if (syncState.networkObject.IsMine) if (syncStateComponents.FirstOrDefault()?.networkObject.IsMine == true)
{ {
Save(); Save();
} }
@ -47,39 +57,75 @@ namespace VELConnect
if (debugLogs) Debug.Log($"[VelNetPersist] Loading {Id}"); if (debugLogs) Debug.Log($"[VelNetPersist] Loading {Id}");
VELConnectManager.GetDataBlock(Id, data => VELConnectManager.GetDataBlock(Id, data =>
{ {
if (!data.data.TryGetValue("state", out string d)) if (!data.data.TryGetValue("components", out string d))
{ {
Debug.LogError($"[VelNetPersist] Failed to parse {Id}"); Debug.LogError($"[VelNetPersist] Failed to parse {Id}");
return; return;
} }
if (syncState == null)
List<ComponentState> componentData = JsonConvert.DeserializeObject<List<ComponentState>>(d);
if (componentData.Count != syncStateComponents.Length)
{ {
Debug.LogError("[VelNetPersist] Object doesn't exist anymore"); Debug.LogError($"[VelNetPersist] Different number of components");
return;
}
for (int i = 0; i < syncStateComponents.Length; i++)
{
syncStateComponents[i].UnpackState(Convert.FromBase64String(componentData[i].state));
} }
syncState.UnpackState(Convert.FromBase64String(d));
if (debugLogs) Debug.Log($"[VelNetPersist] Loaded {Id}"); if (debugLogs) Debug.Log($"[VelNetPersist] Loaded {Id}");
loading = false; loading = false;
}, s => }, s => { loading = false; });
{
Debug.LogError(s);
loading = false;
});
} }
private void Save()
public void Save(Action<VELConnectManager.State.DataBlock> successCallback = null)
{ {
if (debugLogs) Debug.Log($"[VelNetPersist] Saving {Id}"); if (debugLogs) Debug.Log($"[VelNetPersist] Saving {Id}");
if (syncStateComponents.FirstOrDefault()?.networkObject == null)
{
Debug.LogError("First SyncState doesn't have a NetworkObject", this);
return;
}
List<ComponentState> componentData = new List<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 ComponentState()
{
componentIdx = syncState.networkObject.syncedComponents.IndexOf(syncState),
state = Convert.ToBase64String(syncState.PackState())
});
}
VELConnectManager.SetDataBlock(Id, new VELConnectManager.State.DataBlock() VELConnectManager.SetDataBlock(Id, new VELConnectManager.State.DataBlock()
{ {
id = Id,
block_id = Id,
category = "object_persist", category = "object_persist",
data = new Dictionary<string, string> data = new Dictionary<string, string>
{ {
{ "name", syncState.networkObject.name }, { "name", syncStateComponents.FirstOrDefault()?.networkObject.name },
{ "state", Convert.ToBase64String(syncState.PackState()) } { "components", JsonConvert.SerializeObject(componentData) }
} }
}); }, s => { successCallback?.Invoke(s); });
} }
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "edu.uga.engr.vel.vel-connect", "name": "edu.uga.engr.vel.vel-connect",
"displayName": "VEL-Connect", "displayName": "VEL-Connect",
"version": "4.0.1", "version": "4.0.8",
"unity": "2019.1", "unity": "2019.1",
"description": "Web-based configuration for VR applications", "description": "Web-based configuration for VR applications",
"keywords": [], "keywords": [],
@ -13,6 +13,6 @@
"samples": [], "samples": [],
"dependencies": { "dependencies": {
"com.unity.nuget.newtonsoft-json": "3.0.0", "com.unity.nuget.newtonsoft-json": "3.0.0",
"edu.uga.engr.vel.velnet": "1.1.8" "edu.uga.engr.vel.velnet": "1.3.8"
} }
} }

View File

@ -1,12 +1,12 @@
{ {
"name": "@velaboratory/velconnect", "name": "@velaboratory/velconnect",
"version": "1.0.0", "version": "1.0.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@velaboratory/velconnect", "name": "@velaboratory/velconnect",
"version": "1.0.0", "version": "1.0.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"pocketbase": "^0.20.3" "pocketbase": "^0.20.3"

View File

@ -1,6 +1,6 @@
{ {
"name": "@velaboratory/velconnect", "name": "@velaboratory/velconnect",
"version": "1.0.0", "version": "1.0.1",
"description": "Use VEL-Connect with a dashboard", "description": "Use VEL-Connect with a dashboard",
"main": "src/index.js", "main": "src/index.js",
"files": [ "files": [

View File

@ -36,7 +36,7 @@ export type AuthSystemFields<T = never> = {
// Record types for each collection // Record types for each collection
export type DataBlockRecord<Tdata = unknown> = { export type DataBlockRecord<Tdata = { [key: string]: any }> = {
block_id?: string block_id?: string
category?: string category?: string
data?: null | Tdata data?: null | Tdata
@ -74,10 +74,10 @@ export type UsersRecord = {
} }
// Response types include system fields and match responses from the PocketBase API // Response types include system fields and match responses from the PocketBase API
export type DataBlockResponse<Tdata = unknown, Texpand = unknown> = Required<DataBlockRecord<Tdata>> & BaseSystemFields<Texpand> export type DataBlockResponse<Tdata = { [key: string]: any }, Texpand = unknown> = Required<DataBlockRecord<Tdata>> & BaseSystemFields<Texpand>
export type DeviceResponse<Texpand = unknown> = Required<DeviceRecord> & BaseSystemFields<Texpand> export type DeviceResponse<Texpand = unknown> = Required<DeviceRecord> & BaseSystemFields<Texpand>
export type UserCountResponse<Texpand = unknown> = Required<UserCountRecord> & BaseSystemFields<Texpand> export type UserCountResponse<Texpand = unknown> = Required<UserCountRecord> & BaseSystemFields<Texpand>
export type UsersResponse<Texpand = unknown> = Required<UsersRecord> & AuthSystemFields<Texpand> export type UsersResponse<Texpand = { devices: DeviceResponse[]; profiles: DataBlockResponse[] }> = Required<UsersRecord> & AuthSystemFields<Texpand>
// Types containing all Records and Responses, useful for creating typing helper functions // Types containing all Records and Responses, useful for creating typing helper functions

View File

@ -1,88 +1,87 @@
module velaboratory/velconnect module velaboratory/velconnect
go 1.18 go 1.22.1
require ( require (
github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198 github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
github.com/pocketbase/dbx v1.10.0 github.com/pocketbase/dbx v1.10.1
github.com/pocketbase/pocketbase v0.16.7 github.com/pocketbase/pocketbase v0.22.3
) )
require ( require (
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go v1.44.289 // indirect github.com/aws/aws-sdk-go v1.50.32 // indirect
github.com/aws/aws-sdk-go-v2 v1.18.1 // indirect github.com/aws/aws-sdk-go-v2 v1.25.2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.27 // indirect github.com/aws/aws-sdk-go-v2/config v1.27.6 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.26 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.70 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.8 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.2 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.35.0 // indirect github.com/aws/aws-sdk-go-v2/service/s3 v1.51.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.28.3 // indirect
github.com/aws/smithy-go v1.13.5 // indirect github.com/aws/smithy-go v1.20.1 // indirect
github.com/disintegration/imaging v1.6.2 // indirect github.com/disintegration/imaging v1.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.6.0 // indirect github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.15.0 // indirect github.com/fatih/color v1.16.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/ganigeorgiev/fexpr v0.3.0 // indirect github.com/ganigeorgiev/fexpr v0.4.0 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/google/wire v0.5.0 // indirect github.com/google/wire v0.6.0 // indirect
github.com/googleapis/gax-go/v2 v2.11.0 // indirect github.com/googleapis/gax-go/v2 v2.12.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spf13/cast v1.5.1 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
gocloud.dev v0.30.0 // indirect gocloud.dev v0.36.0 // indirect
golang.org/x/crypto v0.10.0 // indirect golang.org/x/crypto v0.21.0 // indirect
golang.org/x/image v0.8.0 // indirect golang.org/x/image v0.15.0 // indirect
golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.22.0 // indirect
golang.org/x/net v0.11.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/oauth2 v0.9.0 // indirect golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.9.0 // indirect golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.9.0 // indirect golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.10.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.10.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.168.0 // indirect
google.golang.org/api v0.128.0 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect google.golang.org/grpc v1.62.1 // indirect
google.golang.org/grpc v1.56.1 // indirect google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect
lukechampine.com/uint128 v1.3.0 // indirect modernc.org/libc v1.41.0 // indirect
modernc.org/cc/v3 v3.41.0 // indirect modernc.org/mathutil v1.6.0 // indirect
modernc.org/ccgo/v3 v3.16.14 // indirect modernc.org/memory v1.7.2 // indirect
modernc.org/libc v1.24.1 // indirect modernc.org/sqlite v1.29.2 // indirect
modernc.org/mathutil v1.5.0 // indirect modernc.org/strutil v1.2.0 // indirect
modernc.org/memory v1.6.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.23.1 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.1.0 // indirect modernc.org/token v1.1.0 // indirect
) )

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,8 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
"os"
"strings"
_ "velaboratory/velconnect/pb_migrations" _ "velaboratory/velconnect/pb_migrations"
@ -19,16 +21,18 @@ func main() {
app := pocketbase.New() app := pocketbase.New()
// loosely check if it was executed using "go run" // loosely check if it was executed using "go run"
// isGoRun := strings.HasPrefix(os.Args[0], os.TempDir()) isGoRun := strings.HasPrefix(os.Args[0], os.TempDir())
migratecmd.MustRegister(app, app.RootCmd, &migratecmd.Options{ migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{
// enable auto creation of migration files when making collection changes // enable auto creation of migration files when making collection changes in the Admin UI
// (the isGoRun check is to enable it only during development) // (the isGoRun check is to enable it only during development)
Automigrate: true, Automigrate: isGoRun,
}) })
app.OnBeforeServe().Add(func(e *core.ServeEvent) error { app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// or you can also use the shorter e.Router.GET("/articles/:slug", handler, middlewares...) // or you can also use the shorter e.Router.GET("/articles/:slug", handler, middlewares...)
e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS("./pb_public"), false))
e.Router.POST("/data_block/:block_id", func(c echo.Context) error { e.Router.POST("/data_block/:block_id", func(c echo.Context) error {
dao := app.Dao() dao := app.Dao()
@ -260,7 +264,7 @@ func main() {
} }
} }
func mergeDataBlock(requestData *models.RequestData, record *models.Record) { func mergeDataBlock(requestData *models.RequestInfo, record *models.Record) {
// get the new data // get the new data
newData, hasNewData := requestData.Data["data"] newData, hasNewData := requestData.Data["data"]