using AmplitudeMiniJSON; using BestHTTP; using BestHTTP.Forms; using BestHTTP.Logger; using Discord; using ExitGames.Client.Photon.LoadBalancing; using RecRoom.Async; using System; using System.Collections; using System.Collections.Generic; using System.Text; using System.Threading; using UnityEditor.Build.Content; using UnityEditor.VersionControl; using UnityEngine; using UnityEngine.Diagnostics; using static UnityEngine.Networking.SyncList; namespace RecNet { public class Core { public delegate void ApiCallback(string error); public delegate void ApiCallback(string error, T result); [Serializable] private class NameServerResponse { // This too :P public string RecNetStatus; public string Auth; public string API; public string WWW; public string Notifications; public string Images; // new adds... public string CDN; public string Commerce; public string Matchmaking; public string Storage; public string Chat; public string Leaderboard; public string Accounts; public string Rooms; } private class VersionCheckResponse : IRecNetObject { public bool ValidVersion { get; private set; } public void Deserialize(Dictionary dict) { ValidVersion = Util.GetKey("ValidVersion", dict); } } private delegate HTTPRequest ConstructRequestCallback(Uri uri); private delegate IEnumerator AsyncConstructRequestCallback(Uri uri, Action callback); private class QueuedApiCall { public Service Service; public string Uri; public ConstructRequestCallback ConstructRequest; public AsyncConstructRequestCallback AsyncConstructRequest; public Promise Promise; public QueuedApiCall(Service service, string uri, ConstructRequestCallback constructRequest) { Service = service; Uri = uri; ConstructRequest = constructRequest; AsyncConstructRequest = null; Promise = new Promise(); } public QueuedApiCall(Service service, string uri, AsyncConstructRequestCallback asyncConstructRequest) { Service = service; Uri = uri; ConstructRequest = null; AsyncConstructRequest = asyncConstructRequest; Promise = new Promise(); } } private const int API_RETRIES = 3; private const int PARALLEL_WORKER_THREAD_COUNT = 4; private const int MAX_BACKOFF_EXPONENT = 6; private const float REFRESH_LOGIN_INTERVAL = 3600f; public static readonly Uri RecNetOfflineUri; public const bool REC_NET_OFFLINE_MODE = false; public static readonly string RecNetEnv; private static bool inScreenMode; private static bool initialized; private static Queue loginQueue; private static Queue serialQueue; private static Queue parallelQueue; private static Queue imageQueue; private static Queue eventQueue; public static long LocalProfileId { get; private set; } private static Dictionary ServiceUris; private static HashSet HostsRequiringRecNetAccessToken; public static bool IsProdOrStagingEnv => string.IsNullOrWhiteSpace(RecNetEnv) || RecNetEnv.Equals("Prod", StringComparison.OrdinalIgnoreCase) || RecNetEnv.Equals("Staging", StringComparison.OrdinalIgnoreCase); public static bool IsDevBuildOrDevEnv => !IsProdOrStagingEnv; public static bool InScreenMode { get { return inScreenMode; } set { if (inScreenMode != value) { inScreenMode = value; } } } static Core() { RecNetOfflineUri = new Uri("http://offline"); inScreenMode = true; initialized = false; ServiceUris = new Dictionary(); HostsRequiringRecNetAccessToken = new HashSet(); loginQueue = new Queue(); serialQueue = new Queue(); parallelQueue = new Queue(); imageQueue = new Queue(); eventQueue = new Queue(); HTTPManager.UseAlternateSSLDefaultValue = true; #if !UNITY_EDITOR HTTPManager.Logger.Level = Loglevels.All; #endif string[] commandLineArgs = Environment.GetCommandLineArgs(); if (commandLineArgs == null) { return; } string[] array = commandLineArgs; foreach (string text in array) { if (text != null && text.StartsWith("+RecNet:", StringComparison.InvariantCultureIgnoreCase)) { RecNetEnv = text.Substring("+RecNet:".Length); } } } public static IPromise Get(Service service, string requestUri, bool executeSerially = true) { return CallApi(service, requestUri, (Uri uri) => new HTTPRequest(uri, HTTPMethods.Get), executeSerially); } public static IPromise Get(string requestUri, bool executeSerially = true) { return Get(Service.API, requestUri, executeSerially); } public static IPromise GetFromCDN(string requestUri) { return Get(Service.CDN, requestUri, false); } public static IPromise GetFromImageCDN(string requestUri) { return CallApi(Service.Images, requestUri, (Uri uri) => new HTTPRequest(uri, HTTPMethods.Get), imageQueue); } public static IPromise Post(Service service, string requestUri, Dictionary form, bool executeSerially = true) { return CallApi(service, requestUri, (Uri uri) => ConstructSimpleFormPost(uri, form), executeSerially); } public static IPromise Post(Service service, string requestUri, WWWForm form, bool executeSerially = true) { return CallApi(service, requestUri, (Uri uri) => ConstructUnityFormPost(uri, form), executeSerially); } public static IPromise Post(Service service, string requestUri, HTTPFormBase form, bool executeSerially = true) { return CallApi(service, requestUri, (Uri uri) => ConstructUnityFormPost(uri, form), executeSerially); } public static IPromise Post(Service service, string requestUri, HTTPMultiPartForm multiPartForm, bool executeSerially = true) { return Post(service, requestUri, (HTTPFormBase)multiPartForm, executeSerially); } public static IPromise Post(Service service, string requestUri, string json, bool executeSerially = true) { return CallApi(service, requestUri, (Uri uri) => ConstructJsonFormPost(uri, json), executeSerially); } public static IPromise Post(Service service, string requestUri, TRequest request, bool executeSerially = true) where TRequest : IRecNetRequestObject { return CallApi(service, requestUri, (Uri uri) => ConstructRecNetObjectPost(uri, request), executeSerially); } public static IPromise Post(string requestUri, Dictionary form, bool executeSerially = true) { return Post(Service.API, requestUri, form, executeSerially); } public static IPromise Post(string requestUri, WWWForm form, bool executeSerially = true) { return Post(Service.API, requestUri, form, executeSerially); } public static IPromise Post(string requestUri, HTTPFormBase form, bool executeSerially = true) { return Post(Service.API, requestUri, form, executeSerially); } public static IPromise Post(string requestUri, HTTPMultiPartForm multiPartForm, bool executeSerially = true) { return Post(requestUri, (HTTPFormBase)multiPartForm, executeSerially); } public static IPromise Post(string requestUri, string json, bool executeSerially = true) { return Post(Service.API, requestUri, json, executeSerially); } public static IPromise Post(string requestUri, TRequest request, bool executeSerially = true) where TRequest : IRecNetRequestObject { return Post(Service.API, requestUri, request, executeSerially); } public static IPromise PostUsingLoginQueue(Service service, string requestUri, string json) { return CallApi(service, requestUri, (Uri uri) => ConstructJsonFormPost(uri, json), loginQueue); } public static IPromise PostUsingBackgroundImageUploadQueue(Service service, string requestUri, HTTPFormBase form) { return CallApi(service, requestUri, (Uri uri) => ConstructUnityFormPost(uri, form), imageQueue); } public static IPromise PostUsingLoginQueue(string requestUri, string json) { return PostUsingLoginQueue(Service.API, requestUri, json); } public static IPromise PostUsingBackgroundImageUploadQueue(string requestUri, HTTPFormBase form) { return PostUsingBackgroundImageUploadQueue(Service.API, requestUri, form); } public static IEnumerator ConnectToRecNet(ApiCallback callback) { string url = "https://ns.rec.net/?v=2"; string log = "Connecting to RecNet"; if (!string.IsNullOrEmpty(RecNetEnv)) { url = $"{url}&e={Uri.EscapeDataString(RecNetEnv)}"; log = $"{log} {RecNetEnv}"; } Debug.Log(log); HTTPRequest request = new HTTPRequest(new Uri(url), HTTPMethods.Get); yield return request.Send(); string error = GetError(request, true); if (!string.IsNullOrEmpty(error)) { request = new HTTPRequest(new Uri("http://www.google.com/generate_204"), HTTPMethods.Get); yield return request.Send(); if (request.Response != null && request.Response.StatusCode == 204) { Debug.LogError($"RecNet name server query failed (error code: summer): {error}"); SafeInvoke(callback, "Failed to connect to RecNet (error code: summer)"); } else { Debug.LogError($"Network connectivity test failed: {GetError(request, true)}"); SafeInvoke(callback, "No internet connection"); } yield break; } try { NameServerResponse nameServerResponse = JsonUtility.FromJson(request.Response.DataAsText); if (nameServerResponse.RecNetStatus != null) { Debug.LogError(nameServerResponse.RecNetStatus); SafeInvoke(callback, nameServerResponse.RecNetStatus); yield break; } TrySetServiceUri(Service.Auth, nameServerResponse.Auth); TrySetServiceUri(Service.API, nameServerResponse.API); TrySetServiceUri(Service.WWW, nameServerResponse.WWW); TrySetServiceUri(Service.Commerce, nameServerResponse.Commerce); TrySetServiceUri(Service.Matchmaking, nameServerResponse.API); TrySetServiceUri(Service.Notifications, nameServerResponse.Notifications); TrySetServiceUri(Service.Images, nameServerResponse.Images); TrySetServiceUri(Service.CDN, nameServerResponse.CDN); TrySetServiceUri(Service.Storage, nameServerResponse.API); TrySetServiceUri(Service.Chat, nameServerResponse.API); TrySetServiceUri(Service.Leaderboard, nameServerResponse.API); TrySetServiceUri(Service.Accounts, nameServerResponse.API); TrySetServiceUri(Service.Rooms, nameServerResponse.API); Service[] servicesRequiringAuth = new[] { Service.Auth, Service.API, Service.Commerce, Service.Matchmaking, Service.Notifications, //Service.Images, //Service.CDN, Service.Storage, Service.Chat, Service.Leaderboard, Service.Accounts, Service.Rooms }; HostsRequiringRecNetAccessToken = new HashSet(); foreach (Service service in servicesRequiringAuth) if (ServiceUris.ContainsKey(service) && ServiceUris[service] != null) HostsRequiringRecNetAccessToken.Add(ServiceUris[service].Host); } catch (Exception exception) { Debug.LogException(exception); SafeInvoke(callback, "Failed to connect to RecNet (error code: locker)"); yield break; } var requestUri = $"api/versioncheck/v3?v={Uri.EscapeDataString(BuildSettings.Version)}&p={Convert.ToInt32(PlatformManager.Instance.CurrentPlatform)}"; yield return Get(requestUri).ExpectResponse().Error(delegate (string err) { Debug.LogError($"RecNet version check failed: {err}"); SafeInvoke(callback, "Failed to connect to RecNet (error code: student)"); }).Then(delegate (VersionCheckResponse response) { if (!response.ValidVersion) SafeInvoke(callback, "Rec Room update required"); else SafeInvoke(callback, null); }); } private static void LazyInit() { if (!initialized) { HTTPUpdateDelegator.OnBeforeApplicationQuit = delegate { Notifications.Disconnect(); Login.Logout(); Thread.Sleep(1000); return true; }; Notifications.Initialize(); GameSessions.Initialize(); Images.Initialize(); Messages.Initialize(); Moderation.Initialize(); Presence.Initialize(); Profiles.Initialize(); Relationships.Initialize(); PlayerSubscriptions.Initialize(); PlayerReporting.Initialize(); Avatars.Initialize(); Rooms.Initialize(); Storefronts.Initialize(); Consumables.Initialize(); PlayerEvents.Initialize(); CommunityBoard.Initialize(); Chats.Initialize(); Login.Initialize(); GameObject gameObject = new GameObject("RecNetCoreCoroutineObject"); UnityEngine.Object.DontDestroyOnLoad(gameObject); gameObject.hideFlags = HideFlags.HideInHierarchy; RecNetCore recNetCore = gameObject.AddComponent(); recNetCore.StartCoroutine(ProcessCallApiQueue(loginQueue)); recNetCore.StartCoroutine(ProcessCallApiQueue(serialQueue)); recNetCore.StartCoroutine(PeriodicRefreshLogin()); for (int num = 0; num < PARALLEL_WORKER_THREAD_COUNT; num++) recNetCore.StartCoroutine(ProcessCallApiQueue(parallelQueue)); for (int num2 = 0; num2 < PARALLEL_WORKER_THREAD_COUNT; num2++) recNetCore.StartCoroutine(ProcessCallApiQueue(imageQueue)); recNetCore.StartCoroutine(ProcessEventQueue()); initialized = true; } } private static void TrySetServiceUri(Service service, string uri) { if (!string.IsNullOrEmpty(uri)) ServiceUris[service] = new Uri(uri); } private static float GetExponentialBackoffTime(int retryCount) => (retryCount <= 0) ? 0f : ((float)(1 << Mathf.Min(retryCount - 1, MAX_BACKOFF_EXPONENT))); private static HTTPRequest ConstructSimpleFormPost(Uri uri, Dictionary form) { var request = new HTTPRequest(uri, HTTPMethods.Post); foreach (KeyValuePair item in form) request.AddField(item.Key, item.Value); return request; } private static HTTPRequest ConstructUnityFormPost(Uri uri, WWWForm form) { HTTPRequest hTTPRequest = new HTTPRequest(uri, HTTPMethods.Post); hTTPRequest.SetFields(form); return hTTPRequest; } private static HTTPRequest ConstructUnityFormPost(Uri uri, HTTPFormBase form) { HTTPRequest hTTPRequest = new HTTPRequest(uri, HTTPMethods.Post); hTTPRequest.SetForm(form); return hTTPRequest; } private static HTTPRequest ConstructJsonFormPost(Uri uri, string json) { HTTPRequest hTTPRequest = new HTTPRequest(uri, HTTPMethods.Post); hTTPRequest.SetHeader("Content-Type", "application/json"); hTTPRequest.RawData = Encoding.UTF8.GetBytes(json); return hTTPRequest; } private static HTTPRequest ConstructRecNetObjectPost(Uri uri, IRecNetRequestObject request) { Dictionary obj = request.Serialize(); string json = Json.Serialize(obj); return ConstructJsonFormPost(uri, json); } private static void AddRecNetAuthToken(HTTPRequest request) { if (RequiresRecNetAccessToken(request.CurrentUri)) { if (Login.AccessToken != null) request.SetHeader("Authorization", $"Bearer {Login.AccessToken}"); request.OnBeforeRedirection += ClearRecNetAuthToken; } } private static bool ClearRecNetAuthToken(HTTPRequest request, HTTPResponse response, Uri redirectUri) { if (!RequiresRecNetAccessToken(redirectUri)) { request.RemoveHeader("Authorization"); } return true; } public static Uri GetServiceUri(Service service) { ServiceUris.TryGetValue(service, out var uri); return uri; } private static bool RequiresRecNetAccessToken(Uri uri) => HostsRequiringRecNetAccessToken.Contains(uri.Host); private static IEnumerator PeriodicRefreshLogin() { WaitForSeconds wait = new WaitForSeconds(REFRESH_LOGIN_INTERVAL); while (true) { yield return wait; if (Login.AccessToken != null) { yield return Login.RefreshLogin(); } } } public static void SetLocalProfileId(long profileId) { LocalProfileId = profileId; } private static IPromise CallApi(Service service, string uri, ConstructRequestCallback constructRequest, bool executeSerially) { Queue queue = executeSerially ? serialQueue : parallelQueue; return CallApi(service, uri, constructRequest, queue); } private static IPromise CallApi(Service service, string uri, ConstructRequestCallback constructRequest, Queue queue) { LazyInit(); QueuedApiCall queuedApiCall = new QueuedApiCall(service, uri, constructRequest); queue.Enqueue(queuedApiCall); return queuedApiCall.Promise; } private static IPromise CallApi(Service service, string uri, AsyncConstructRequestCallback asyncConstructRequest, Queue queue) { LazyInit(); QueuedApiCall queuedApiCall = new QueuedApiCall(service, uri, asyncConstructRequest); queue.Enqueue(queuedApiCall); return queuedApiCall.Promise; } private static IEnumerator ProcessCallApiQueue(Queue queue) { int consecutiveRetries = 0; float previousErrorTime = 0f; while (true) { yield return new WaitUntil(() => queue.Count > 0); if (queue.Count == 0) { continue; } QueuedApiCall apiCall = queue.Dequeue(); Uri baseUri = GetServiceUri(apiCall.Service); if (baseUri == null) { continue; } Uri uri = new Uri(baseUri, apiCall.Uri); HTTPRequest request = null; for (int retry = 0; retry < API_RETRIES; retry++) { if (request != null) { request.Dispose(); request = null; } if (consecutiveRetries > 0) { float exponentialBackoff = GetExponentialBackoffTime(consecutiveRetries); float timeToWait = exponentialBackoff - (Time.realtimeSinceStartup - previousErrorTime); if (timeToWait > 0f) { yield return new WaitForSecondsRealtime(timeToWait); } } if (apiCall.ConstructRequest != null) { try { request = apiCall.ConstructRequest(uri); if (queue == imageQueue) { request.Priority = -1; } } catch (Exception exception) { Debug.LogException(exception); } } else { yield return apiCall.AsyncConstructRequest(uri, delegate (HTTPRequest result) { request = result; }); } if (request == null) { break; } AddRecNetAuthToken(request); yield return request.Send(); if (request.Response == null || request.Response.StatusCode == 429 || (request.Response.StatusCode >= 500 && request.Response.StatusCode < 600)) { consecutiveRetries++; previousErrorTime = Time.realtimeSinceStartup; continue; } if (request.Response != null && request.Response.StatusCode == 401) { consecutiveRetries++; previousErrorTime = Time.realtimeSinceStartup; } else { consecutiveRetries = 0; } break; } try { string error = GetError(request, false); if (error == null) { apiCall.Promise.Complete(request.Response); } else { apiCall.Promise.Error(error); } } catch (Exception exception2) { Debug.LogException(exception2); } request?.Dispose(); request = null; } } public static string GetError(HTTPRequest request, bool handleHttpStatusCodes) { if (request == null) { return "Failed to construct Web request"; } switch (request.State) { case HTTPRequestStates.Aborted: return "Aborted"; case HTTPRequestStates.ConnectionTimedOut: return "Connection timed out"; case HTTPRequestStates.TimedOut: return "Response timed out"; case HTTPRequestStates.Error: Debug.LogException(request.Exception); return (request.Exception == null) ? "Unspecified error" : request.Exception.Message; case HTTPRequestStates.Finished: return (!handleHttpStatusCodes || request.Response.StatusCode < 400) ? null : ("HTTP Error " + request.Response.StatusCode); default: Debug.LogError("HTTPRequest is in an invalid state: " + request.State); return "Unexpected state"; } } public static void SafeInvoke(ApiCallback callback, string error) { try { if (callback != null) { callback(error); } } catch (Exception exception) { Debug.LogException(exception); } } public static void SafeInvoke(ApiCallback callback, string error, T response) { try { if (callback != null) { callback(error, response); } } catch (Exception exception) { Debug.LogException(exception); } } public static void DispatchOnUnityThread(Action callback) { lock (eventQueue) { eventQueue.Enqueue(callback); } } public static IEnumerator RunOnBackgroundThread(Action callback) { bool complete = false; Thread thread = new Thread((ThreadStart)delegate { try { callback(); } catch (Exception ex) { Exception ex2 = ex; Exception ex3 = ex2; DispatchOnUnityThread(delegate { Debug.LogException(ex3); }); } finally { complete = true; } }); thread.Start(); return new WaitUntil(() => complete); } private static IEnumerator ProcessEventQueue() { while (true) { yield return new WaitUntil(() => eventQueue.Count > 0); Action callback; lock (eventQueue) { callback = eventQueue.Dequeue(); } try { callback(); } catch (Exception exception) { Debug.LogException(exception); } } } } }