diff --git a/Runtime/Components/TuningVariablesComponent.cs b/Runtime/Components/TuningVariablesComponent.cs new file mode 100644 index 00000000..6ac6dac7 --- /dev/null +++ b/Runtime/Components/TuningVariablesComponent.cs @@ -0,0 +1,274 @@ +using UnityEngine; +using System.Collections; +using System.Collections.Generic; + +/// +/// gets variables from the cognitive3d backend to configure your app +/// + +//fetches the variables immediately if the participantId is already set at session start +//if not, it will wait 5 seconds for a participantId to be set +//if the timer elapses, use the deviceId as the argument +//if fetchVariablesAutomatically is false, call Cognitive3D.TuningVariables.FetchVariables + +//CONSIDER custom editor to display tuning variables + +//set session properties DONE +//static generic funciton to get values DONE +//locally cache in network class DONE +//read from local cache if it fails DONE + +namespace Cognitive3D +{ + public static class TuningVariables + { + static Dictionary tuningVariables = new Dictionary(); + + public delegate void onTuningVariablesAvailable(); + /// + /// Called after tuning variables are available (also called after a delay if no response) + /// + public static event onTuningVariablesAvailable OnTuningVariablesAvailable; + internal static void InvokeOnTuningVariablesAvailable() { if (OnTuningVariablesAvailable != null) { OnTuningVariablesAvailable.Invoke(); } } + + internal static void Reset() + { + tuningVariables.Clear(); + } + internal static void SetVariable(TuningVariableItem entry) + { + tuningVariables.Add(entry.name, entry); + } + + /// + /// Returns a variable of a type using the variableName. Returns the default value if no variable is found + /// + /// The expected type of variable + /// The name of the variable to read + /// The value to return if no variable is found + /// The value of the tuning varible, or the defaultValue if not found + public static T GetValue(string variableName, T defaultValue) + { + Cognitive3D.TuningVariableItem returnItem; + if (tuningVariables.TryGetValue(variableName, out returnItem)) + { + if (typeof(T) == typeof(string)) + { + return (T)(object)returnItem.valueString; + } + if (typeof(T) == typeof(bool)) + { + return (T)(object)returnItem.valueBool; + } + if (typeof(T) == typeof(int) || typeof(T) == typeof(long) || typeof(T) == typeof(float) || typeof(T) == typeof(double)) + { + return (T)(object)returnItem.valueInt; + } + } + return defaultValue; + } + + public static Dictionary GetAllTuningVariables() + { + return tuningVariables; + } + } + + [System.Serializable] + public class TuningVariableItem + { + public string name; + public string description; + public string type; + public int valueInt; + public string valueString; + public bool valueBool; + + public override string ToString() + { + if (type == "string") + { + return string.Format("name:{0}, description:{1}, type:{2}, value:{3}", name, description, type, valueString); + } + if (type == "int") + { + return string.Format("name:{0}, description:{1}, type:{2}, value:{3}", name, description, type, valueInt); + } + if (type == "bool") + { + return string.Format("name:{0}, description:{1}, type:{2}, value:{3}", name, description, type, valueBool); + } + + return string.Format("name:{0}, description:{1}, type:{2}", name, description, type); + } + } +} + + +namespace Cognitive3D.Components +{ + #region Json + internal class TuningVariableCollection + { + public List tuningVariables = new List(); + } + #endregion + + [DisallowMultipleComponent] + [AddComponentMenu("Cognitive3D/Components/Tuning Variables Component")] + public class TuningVariablesComponent : AnalyticsComponentBase + { + static bool hasFetchedVariables; + /// + /// the delay to hear a response from our backend. If there is no response in this time, try to use a local cache of variables + /// + const float requestTuningVariablesTimeout = 3; + /// + /// the delay waiting for participant id to be set (if not already set at the start of the session) + /// + public float waitForParticipantIdTimeout = 5; + /// + /// if true, uses the participant id (possibly with a delay) to get tuning variables. Otherwise, use the device id + /// + public bool useParticipantId = true; + /// + /// if true, sends identifying data to retrieve variables as soon as possible + /// + public bool fetchVariablesAutomatically = true; + + protected override void OnSessionBegin() + { + base.OnSessionBegin(); + Cognitive3D_Manager.OnPostSessionEnd += Cognitive3D_Manager_OnPostSessionEnd; + + if (fetchVariablesAutomatically) + { + if (useParticipantId) + { + //get variables if participant id is already set + if (!string.IsNullOrEmpty(Cognitive3D_Manager.ParticipantId)) + { + FetchVariables(Cognitive3D_Manager.ParticipantId); + } + else + { + //listen for event + Cognitive3D_Manager.OnParticipantIdSet += Cognitive3D_Manager_OnParticipantIdSet; + //also start a timer + StartCoroutine(DelayFetch()); + } + } + else //just use the hardware id to identify the user + { + FetchVariables(Cognitive3D_Manager.DeviceId); + } + } + } + + IEnumerator DelayFetch() + { + yield return new WaitForSeconds(waitForParticipantIdTimeout); + FetchVariables(Cognitive3D_Manager.DeviceId); + } + + private void Cognitive3D_Manager_OnParticipantIdSet(string participantId) + { + FetchVariables(participantId); + } + + public static void FetchVariables() + { + if (!hasFetchedVariables) + { + NetworkManager.GetTuningVariables(Cognitive3D_Manager.DeviceId, TuningVariableResponse, requestTuningVariablesTimeout); + } + } + + public static void FetchVariables(string participantId) + { + if (!hasFetchedVariables) + { + NetworkManager.GetTuningVariables(participantId, TuningVariableResponse, requestTuningVariablesTimeout); + } + } + + static void TuningVariableResponse(int responsecode, string error, string text) + { + if (hasFetchedVariables) + { + Util.logDevelopment("TuningVariableResponse called multiple times!"); + return; + } + + if (responsecode != 200) + { + Util.logDevelopment("Tuning Variable reponse code " + responsecode + " " + error); + } + + try + { + var tvc = JsonUtility.FromJson(text); + if (tvc != null) + { + TuningVariables.Reset(); + foreach (var entry in tvc.tuningVariables) + { + TuningVariables.SetVariable(entry); + + if (entry.type == "string") + { + Cognitive3D_Manager.SetSessionProperty(entry.name, entry.valueString); + } + else if (entry.type == "int") + { + Cognitive3D_Manager.SetSessionProperty(entry.name, entry.valueInt); + } + else if (entry.type == "boolean") + { + Cognitive3D_Manager.SetSessionProperty(entry.name, entry.valueBool); + } + } + } + else + { + Util.logDevelopment("TuningVariableCollection is null"); + } + } + catch (System.Exception e) + { + Debug.LogException(e); + } + + hasFetchedVariables = true; + TuningVariables.InvokeOnTuningVariablesAvailable(); + } + + private void Cognitive3D_Manager_OnPostSessionEnd() + { + hasFetchedVariables = false; + Cognitive3D_Manager.OnParticipantIdSet -= Cognitive3D_Manager_OnParticipantIdSet; + Cognitive3D_Manager.OnPostSessionEnd -= Cognitive3D_Manager_OnPostSessionEnd; + } + + public override string GetDescription() + { + return "Retrieves variables from the Cognitive3D server to adjust the user's experience"; + } + + public override bool GetWarning() + { + return false; + } + + [ContextMenu("Test Fetch with DeviceId")] + void TestFetch() + { + FetchVariables(Cognitive3D_Manager.DeviceId); + } + [ContextMenu("Test Fetch with Random GUID")] + void TestFetchGUID() + { + FetchVariables(System.Guid.NewGuid().ToString()); + } + } +} diff --git a/Runtime/Components/TuningVariablesComponent.cs.meta b/Runtime/Components/TuningVariablesComponent.cs.meta new file mode 100644 index 00000000..977549f9 --- /dev/null +++ b/Runtime/Components/TuningVariablesComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c2813dd9237bf1c4ead0e6276a28ed1e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/CognitiveStatics.cs b/Runtime/Internal/CognitiveStatics.cs index d3e8a27e..220a68a5 100644 --- a/Runtime/Internal/CognitiveStatics.cs +++ b/Runtime/Internal/CognitiveStatics.cs @@ -143,6 +143,12 @@ internal static string PostExitpollResponses(string questionsetname, int questio return string.Concat(Cognitive3D_Preferences.Instance.Protocol, "://", Cognitive3D_Preferences.Instance.Gateway, "/v", version,"/questionSets/", questionsetname, "/",questionsetversion.ToString(), "/responses"); } + //GET request question set + internal static string GetTuningVariableURL(string userId) + { + return string.Concat(Cognitive3D_Preferences.Instance.Protocol, "://", Cognitive3D_Preferences.Instance.Gateway, "/v", version, "/tuningvariables?identifier=", userId); + } + /// /// Creates the GET request endpoint with access token and search parameters /// diff --git a/Runtime/Internal/ILocalTuningVariables.cs b/Runtime/Internal/ILocalTuningVariables.cs new file mode 100644 index 00000000..dcb3380d --- /dev/null +++ b/Runtime/Internal/ILocalTuningVariables.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using UnityEngine; + +namespace Cognitive3D +{ + internal interface ILocalTuningVariables + { + bool GetTuningVariables(out string text); + void WriteTuningVariables(string text); + } + + internal class TuningVariablesLocalDataHandler : ILocalTuningVariables + { + readonly string localTuningVariablePath; + + public TuningVariablesLocalDataHandler(string path) + { + localTuningVariablePath = path; + } + + public bool GetTuningVariables(out string text) + { + try + { + if (File.Exists(localTuningVariablePath + "tuningVariables")) + { + text = File.ReadAllText(localTuningVariablePath + "tuningVariables"); + return true; + } + } + catch (Exception e) + { + Debug.LogException(e); + } + text = ""; + return false; + } + + public void WriteTuningVariables(string text) + { + if (!Directory.Exists(localTuningVariablePath)) + Directory.CreateDirectory(localTuningVariablePath); + + try + { + File.WriteAllText(localTuningVariablePath + "tuningVariables", text); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + } +} diff --git a/Runtime/Internal/ILocalTuningVariables.cs.meta b/Runtime/Internal/ILocalTuningVariables.cs.meta new file mode 100644 index 00000000..3252f983 --- /dev/null +++ b/Runtime/Internal/ILocalTuningVariables.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 26f7878bbdb61b34d833431294758211 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/NetworkManager.cs b/Runtime/Internal/NetworkManager.cs index bd75ec6f..f05a99a4 100644 --- a/Runtime/Internal/NetworkManager.cs +++ b/Runtime/Internal/NetworkManager.cs @@ -29,19 +29,17 @@ internal class NetworkManager : MonoBehaviour static HashSet activeRequests = new HashSet(); internal ICache runtimeCache; - internal ILocalExitpoll exitpollCache; int lastDataResponse = 0; const float cacheUploadInterval = 2; const float minRetryDelay = 60; const float maxRetryDelay = 240; - internal void Initialize(ICache runtimeCache, ILocalExitpoll exitpollCache) + internal void Initialize(ICache runtimeCache) { DontDestroyOnLoad(gameObject); instance = this; this.runtimeCache = runtimeCache; - this.exitpollCache = exitpollCache; } System.Collections.IEnumerator WaitForExitpollResponse(UnityWebRequest www, string hookname, Response callback, float timeout) @@ -502,6 +500,95 @@ private float GetExponentialBackoff(float retryDelay) return minRetryDelay; } + public static void GetTuningVariables(string participantId, Response callback, float timeout) + { + string url = CognitiveStatics.GetTuningVariableURL(participantId); + var request = UnityWebRequest.Get(url); + request.SetRequestHeader("Content-Type", "application/json"); + request.SetRequestHeader("X-HTTP-Method-Override", "GET"); + request.SetRequestHeader("Authorization", CognitiveStatics.ApplicationKey); + request.SendWebRequest(); + + instance.StartCoroutine(instance.WaitForTuningVariableResponse(request, callback, timeout)); + } + + System.Collections.IEnumerator WaitForTuningVariableResponse(UnityWebRequest www, Response callback, float timeout) + { + float time = 0; + while (time < timeout) + { + yield return null; + if (www.isDone) break; + time += Time.deltaTime; + } + + var headers = www.GetResponseHeaders(); + int responsecode = (int)www.responseCode; + lastDataResponse = responsecode; + //check cvr header to make sure not blocked by capture portal + +#if UNITY_WEBGL + //webgl cors issue doesn't seem to accept this required header + if (!headers.ContainsKey("cvr-request-time")) + { + headers.Add("cvr-request-time", string.Empty); + } +#endif + + if (!www.isDone) + Util.logWarning("Network::WaitForTuningVariableResponse timeout"); + if (responsecode != 200) + Util.logWarning("Network::WaitForTuningVariableResponse responsecode is " + responsecode); + + if (headers != null) + { + if (!headers.ContainsKey("cvr-request-time")) + Util.logWarning("Network::WaitForTuningVariableResponse does not contain cvr-request-time header"); + } + + if (!www.isDone || responsecode != 200 || (headers != null && !headers.ContainsKey("cvr-request-time"))) + { + if (Cognitive3D_Preferences.Instance.LocalStorage) + { + string text; + if (Cognitive3D_Manager.TuningVariableHandler.GetTuningVariables(out text)) + { + if (callback != null) + { + callback.Invoke(responsecode, "", text); + } + } + else + { + if (callback != null) + { + callback.Invoke(responsecode, "", ""); + } + } + } + else + { + if (callback != null) + { + callback.Invoke(responsecode, "", ""); + } + } + } + else + { + if (callback != null) + { + callback.Invoke(responsecode, www.error, www.downloadHandler.text); + } + if (Cognitive3D_Preferences.Instance.LocalStorage) + { + Cognitive3D_Manager.TuningVariableHandler.WriteTuningVariables(www.downloadHandler.text); + } + } + www.Dispose(); + activeRequests.Remove(www); + } + //skip network cleanup if immediately/manually destroyed //gameobject is destroyed at end of frame //issue if ending session/destroy gameobject/new session all in one frame diff --git a/Runtime/Scripts/Cognitive3D_Manager.cs b/Runtime/Scripts/Cognitive3D_Manager.cs index a5156a81..1d12a10b 100644 --- a/Runtime/Scripts/Cognitive3D_Manager.cs +++ b/Runtime/Scripts/Cognitive3D_Manager.cs @@ -156,6 +156,7 @@ public void BeginSession() #endif ExitpollHandler = new ExitPollLocalDataHandler(Application.persistentDataPath + "/c3dlocal/exitpoll/"); + TuningVariableHandler = new TuningVariablesLocalDataHandler(Application.persistentDataPath + "/c3dlocal/tuningvariables/"); if (Cognitive3D_Preferences.Instance.LocalStorage) { @@ -172,7 +173,7 @@ public void BeginSession() GameObject networkGo = new GameObject("Cognitive Network"); networkGo.hideFlags = HideFlags.HideInInspector | HideFlags.HideInHierarchy; NetworkManager = networkGo.AddComponent(); - NetworkManager.Initialize(DataCache, ExitpollHandler); + NetworkManager.Initialize(DataCache); GameplayReferences.Initialize(); DynamicManager.Initialize(); @@ -826,7 +827,15 @@ public static void FlushData() public static event onLevelLoaded OnLevelLoaded; private static void InvokeLevelLoadedEvent(Scene scene, LoadSceneMode mode, bool newSceneId) { if (OnLevelLoaded != null) { OnLevelLoaded(scene, mode, newSceneId); } } + public delegate void onParticipantIdSet(string participantId); + /// + /// Called after a participant id is set. May be called multiple times + /// + public static event onParticipantIdSet OnParticipantIdSet; + private static void InvokeOnParticipantIdSet(string participantId) { if (OnParticipantIdSet != null) { OnParticipantIdSet.Invoke(participantId); } } + internal static ILocalExitpoll ExitpollHandler; + internal static ILocalTuningVariables TuningVariableHandler; internal static ICache DataCache; internal static NetworkManager NetworkManager; @@ -986,6 +995,7 @@ public static void SetParticipantId(string id) } ParticipantId = id; SetParticipantProperty("id", id); + InvokeOnParticipantIdSet(id); } ///