using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Reflection; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using UnityEngine; using UnityEngine.Networking; using VelNet; namespace VELConnect { public class VELConnectManager : MonoBehaviour { public string velConnectUrl = "http://localhost"; public static string VelConnectUrl => _instance.velConnectUrl; private static VELConnectManager _instance; public class State { public class User { public string id; public string email; public string username; public string date_created; public string last_modified; public Dictionary data; } public class Device { public string hw_id; public string os_info; public string friendly_name; public string modified_by; public string current_app; public string current_room; public int pairing_code; public string date_created; public string last_modified; public Dictionary data; /// /// Returns the value if it exists, otherwise null /// public string TryGetData(string key) { string val = null; return data?.TryGetValue(key, out val) == true ? val : null; } } public class RoomState { public string error; public string id; public string category; public string date_created; public string modified_by; public string last_modified; public string last_accessed; public Dictionary data; /// /// Returns the value if it exists, otherwise null /// public string TryGetData(string key) { string val = null; return data?.TryGetValue(key, out val) == true ? val : null; } } public User user; public Device device; public RoomState room; } public State lastState; public static Action OnInitialState; public static Action OnDeviceFieldChanged; public static Action OnDeviceDataChanged; public static Action OnRoomDataChanged; private static readonly Dictionary> deviceFieldCallbacks = new Dictionary>(); private static readonly Dictionary> deviceDataCallbacks = new Dictionary>(); private static readonly Dictionary> roomDataCallbacks = new Dictionary>(); private struct CallbackListener { /// /// Used so that other objects don't have to remove listeners themselves /// public MonoBehaviour keepAliveObject; public Action callback; /// /// Sends the first state received from the network or the state at binding time /// public bool sendInitialState; } public static int PairingCode { get { Hash128 hash = new Hash128(); hash.Append(DeviceId); // change once a day hash.Append(DateTime.UtcNow.DayOfYear); // between 1000 and 9999 inclusive (any 4 digit number) return Math.Abs(hash.GetHashCode()) % 9000 + 1000; } } private static string DeviceId { get { #if UNITY_EDITOR // allows running multiple builds on the same computer // return SystemInfo.deviceUniqueIdentifier + Hash128.Compute(Application.dataPath); return SystemInfo.deviceUniqueIdentifier + "_EDITOR"; #else return SystemInfo.deviceUniqueIdentifier; #endif } } private void Awake() { if (_instance != null) Debug.LogError("VELConnectManager instance already exists", this); _instance = this; } // Start is called before the first frame update private void Start() { SetDeviceField(new Dictionary { { "current_app", Application.productName }, { "pairing_code", PairingCode } }); UpdateUserCount(); StartCoroutine(SlowLoop()); VelNetManager.OnJoinedRoom += room => { SetDeviceField(new Dictionary { { "current_app", Application.productName }, { "current_room", room }, }); }; } private void UpdateUserCount(bool leaving = false) { if (!VelNetManager.InRoom) return; VelNetManager.GetRooms(rooms => { Dictionary postData = new Dictionary { { "hw_id", DeviceId }, { "app_id", Application.productName }, { "room_id", VelNetManager.Room ?? "" }, { "total_users", rooms.rooms.Sum(r => r.numUsers) - (leaving ? 1 : 0) }, { "room_users", VelNetManager.PlayerCount - (leaving ? 1 : 0) }, { "version", Application.version }, { "platform", SystemInfo.operatingSystem }, }; PostRequestCallback(velConnectUrl + "/api/update_user_count", JsonConvert.SerializeObject(postData)); }); } private IEnumerator SlowLoop() { while (true) { try { GetRequestCallback(velConnectUrl + "/api/device/get_data/" + DeviceId, json => { State state = JsonConvert.DeserializeObject(json); if (state == null) return; bool isInitialState = false; // 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) { FieldInfo[] fields = state.device.GetType().GetFields(); foreach (FieldInfo fieldInfo in fields) { string newValue = fieldInfo.GetValue(state.device) as string; string oldValue = lastState != null ? fieldInfo.GetValue(lastState.device) as string : null; if (newValue != oldValue) { try { if (!isInitialState) OnDeviceFieldChanged?.Invoke(fieldInfo.Name, newValue); } catch (Exception e) { Debug.LogError(e); } // send specific listeners data if (deviceFieldCallbacks.ContainsKey(fieldInfo.Name)) { // clear the list of old listeners deviceFieldCallbacks[fieldInfo.Name].RemoveAll(e => e.keepAliveObject == null); // send the callbacks deviceFieldCallbacks[fieldInfo.Name].ForEach(e => { if (!isInitialState || e.sendInitialState) { try { e.callback(newValue); } catch (Exception ex) { Debug.LogError(ex); } } }); } } } if (state.device.data != null) { foreach (KeyValuePair elem in state.device.data) { string oldValue = null; lastState?.device.data.TryGetValue(elem.Key, out oldValue); if (elem.Value != oldValue) { try { if (!isInitialState) OnDeviceDataChanged?.Invoke(elem.Key, elem.Value); } catch (Exception ex) { Debug.LogError(ex); } // send specific listeners data if (deviceDataCallbacks.ContainsKey(elem.Key)) { // clear the list of old listeners deviceDataCallbacks[elem.Key].RemoveAll(e => e.keepAliveObject == null); // send the callbacks deviceDataCallbacks[elem.Key].ForEach(e => { if (!isInitialState || e.sendInitialState) { try { e.callback(elem.Value); } catch (Exception ex) { Debug.LogError(ex); } } }); } } } } } if (state.room.modified_by != DeviceId && state.room.data != null) { foreach (KeyValuePair elem in state.room.data) { string oldValue = null; lastState?.room.data.TryGetValue(elem.Key, out oldValue); if (elem.Value != oldValue) { try { if (!isInitialState) OnRoomDataChanged?.Invoke(elem.Key, elem.Value); } catch (Exception e) { Debug.LogError(e); } // send specific listeners data if (roomDataCallbacks.ContainsKey(elem.Key)) { // clear the list of old listeners roomDataCallbacks[elem.Key].RemoveAll(e => e.keepAliveObject == null); // send the callbacks roomDataCallbacks[elem.Key].ForEach(e => { if (!isInitialState || e.sendInitialState) { try { e.callback(elem.Value); } catch (Exception ex) { Debug.LogError(ex); } } }); } } } } lastState = state; }); } catch (Exception e) { Debug.LogError(e); // this make sure the coroutine never quits } yield return new WaitForSeconds(1); } } /// /// Adds a change listener callback to a particular field name within the Device main fields. /// public static void AddDeviceFieldListener(string key, MonoBehaviour keepAliveObject, Action callback, bool sendInitialState = false) { if (!deviceFieldCallbacks.ContainsKey(key)) { deviceFieldCallbacks[key] = new List(); } deviceFieldCallbacks[key].Add(new CallbackListener() { keepAliveObject = keepAliveObject, callback = callback, sendInitialState = sendInitialState }); if (sendInitialState) { if (_instance != null && _instance.lastState?.device != null) { if (_instance.lastState.device.GetType().GetField(key) ?.GetValue(_instance.lastState.device) is string val) { try { callback(val); } catch (Exception e) { Debug.LogError(e); } } } } } /// /// Adds a change listener callback to a particular field name within the Device data JSON. /// public static void AddDeviceDataListener(string key, MonoBehaviour keepAliveObject, Action callback, bool sendInitialState = false) { if (!deviceDataCallbacks.ContainsKey(key)) { deviceDataCallbacks[key] = new List(); } deviceDataCallbacks[key].Add(new CallbackListener() { keepAliveObject = keepAliveObject, callback = callback, sendInitialState = sendInitialState }); if (sendInitialState) { string val = GetDeviceData(key); if (val != null) { try { callback(val); } catch (Exception e) { Debug.LogError(e); } } } } /// /// Adds a change listener callback to a particular field name within the Room data JSON. /// public static void AddRoomDataListener(string key, MonoBehaviour keepAliveObject, Action callback, bool sendInitialState = false) { if (!roomDataCallbacks.ContainsKey(key)) { roomDataCallbacks[key] = new List(); } roomDataCallbacks[key].Add(new CallbackListener() { keepAliveObject = keepAliveObject, callback = callback, sendInitialState = sendInitialState }); if (sendInitialState) { string val = GetRoomData(key); if (val != null) { try { callback(val); } catch (Exception e) { Debug.LogError(e); } } } } public static string GetDeviceData(string key) { return _instance != null ? _instance.lastState?.device?.TryGetData(key) : null; } public static string GetRoomData(string key) { return _instance != null ? _instance.lastState?.room?.TryGetData(key) : null; } /// /// Sets data on the device keys themselves /// public static void SetDeviceField(Dictionary device) { PostRequestCallback( _instance.velConnectUrl + "/api/device/set_data/" + DeviceId, JsonConvert.SerializeObject(device), new Dictionary { { "modified_by", DeviceId } } ); } /// /// Sets the 'data' object of the Device table /// public static void SetDeviceData(Dictionary data) { PostRequestCallback( _instance.velConnectUrl + "/api/device/set_data/" + DeviceId, JsonConvert.SerializeObject(new Dictionary { { "data", data } }), new Dictionary { { "modified_by", DeviceId } } ); } public static void SetRoomData(Dictionary data) { if (!VelNetManager.InRoom) { Debug.LogError("Can't set data for a room if you're not in a room."); return; } PostRequestCallback( _instance.velConnectUrl + "/api/set_data/" + Application.productName + "_" + VelNetManager.Room, JsonConvert.SerializeObject(data), new Dictionary { { "modified_by", DeviceId } } ); } public static void UploadFile(string fileName, byte[] fileData, Action successCallback = null) { MultipartFormDataContent requestContent = new MultipartFormDataContent(); ByteArrayContent fileContent = new ByteArrayContent(fileData); requestContent.Add(fileContent, "file", fileName); Task.Run(async () => { HttpResponseMessage r = await new HttpClient().PostAsync(_instance.velConnectUrl + "/api/upload_file", requestContent); string resp = await r.Content.ReadAsStringAsync(); Dictionary dict = JsonConvert.DeserializeObject>(resp); successCallback?.Invoke(dict["key"]); }); } public static void GetRequestCallback(string url, Action successCallback = null, Action failureCallback = null) { _instance.StartCoroutine(_instance.GetRequestCallbackCo(url, successCallback, failureCallback)); } private IEnumerator GetRequestCallbackCo(string url, Action successCallback = null, Action failureCallback = null) { using UnityWebRequest webRequest = UnityWebRequest.Get(url); // Request and wait for the desired page. yield return webRequest.SendWebRequest(); switch (webRequest.result) { case UnityWebRequest.Result.ConnectionError: case UnityWebRequest.Result.DataProcessingError: case UnityWebRequest.Result.ProtocolError: Debug.LogError(url + ": Error: " + webRequest.error + "\n" + Environment.StackTrace); failureCallback?.Invoke(webRequest.error); break; case UnityWebRequest.Result.Success: successCallback?.Invoke(webRequest.downloadHandler.text); break; } } public static void PostRequestCallback(string url, string postData, Dictionary headers = null, Action successCallback = null, Action failureCallback = null) { _instance.StartCoroutine(PostRequestCallbackCo(url, postData, headers, successCallback, failureCallback)); } private static IEnumerator PostRequestCallbackCo(string url, string postData, Dictionary headers = null, Action successCallback = null, Action failureCallback = null) { UnityWebRequest webRequest = new UnityWebRequest(url, "POST"); byte[] bodyRaw = Encoding.UTF8.GetBytes(postData); UploadHandlerRaw uploadHandler = new UploadHandlerRaw(bodyRaw); webRequest.uploadHandler = uploadHandler; webRequest.SetRequestHeader("Content-Type", "application/json"); if (headers != null) { foreach (KeyValuePair keyValuePair in headers) { webRequest.SetRequestHeader(keyValuePair.Key, keyValuePair.Value); } } yield return webRequest.SendWebRequest(); switch (webRequest.result) { case UnityWebRequest.Result.ConnectionError: case UnityWebRequest.Result.DataProcessingError: case UnityWebRequest.Result.ProtocolError: Debug.LogError(url + ": Error: " + webRequest.error + "\n" + Environment.StackTrace); failureCallback?.Invoke(webRequest.error); break; case UnityWebRequest.Result.Success: successCallback?.Invoke(webRequest.downloadHandler.text); break; } uploadHandler.Dispose(); webRequest.Dispose(); } private void OnApplicationFocus(bool focus) { UpdateUserCount(!focus); } } }