Files
LateRecNetServices/Core.cs
2026-07-02 08:22:39 +00:00

787 lines
29 KiB
C#

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<T>;
namespace RecNet
{
public class Core
{
public delegate void ApiCallback(string error);
public delegate void ApiCallback<T>(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<string, object> dict)
{
ValidVersion = Util.GetKey<bool>("ValidVersion", dict);
}
}
private delegate HTTPRequest ConstructRequestCallback(Uri uri);
private delegate IEnumerator AsyncConstructRequestCallback(Uri uri, Action<HTTPRequest> callback);
private class QueuedApiCall
{
public Service Service;
public string Uri;
public ConstructRequestCallback ConstructRequest;
public AsyncConstructRequestCallback AsyncConstructRequest;
public Promise<HTTPResponse> Promise;
public QueuedApiCall(Service service, string uri, ConstructRequestCallback constructRequest)
{
Service = service;
Uri = uri;
ConstructRequest = constructRequest;
AsyncConstructRequest = null;
Promise = new Promise<HTTPResponse>();
}
public QueuedApiCall(Service service, string uri, AsyncConstructRequestCallback asyncConstructRequest)
{
Service = service;
Uri = uri;
ConstructRequest = null;
AsyncConstructRequest = asyncConstructRequest;
Promise = new Promise<HTTPResponse>();
}
}
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<QueuedApiCall> loginQueue;
private static Queue<QueuedApiCall> serialQueue;
private static Queue<QueuedApiCall> parallelQueue;
private static Queue<QueuedApiCall> imageQueue;
private static Queue<Action> eventQueue;
public static long LocalProfileId { get; private set; }
private static Dictionary<Service, Uri> ServiceUris;
private static HashSet<string> 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<Service, Uri>();
HostsRequiringRecNetAccessToken = new HashSet<string>();
loginQueue = new Queue<QueuedApiCall>();
serialQueue = new Queue<QueuedApiCall>();
parallelQueue = new Queue<QueuedApiCall>();
imageQueue = new Queue<QueuedApiCall>();
eventQueue = new Queue<Action>();
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<HTTPResponse> Get(Service service, string requestUri, bool executeSerially = true)
{
return CallApi(service, requestUri, (Uri uri) => new HTTPRequest(uri, HTTPMethods.Get), executeSerially);
}
public static IPromise<HTTPResponse> Get(string requestUri, bool executeSerially = true)
{
return Get(Service.API, requestUri, executeSerially);
}
public static IPromise<HTTPResponse> GetFromCDN(string requestUri)
{
return Get(Service.CDN, requestUri, false);
}
public static IPromise<HTTPResponse> GetFromImageCDN(string requestUri)
{
return CallApi(Service.Images, requestUri, (Uri uri) => new HTTPRequest(uri, HTTPMethods.Get), imageQueue);
}
public static IPromise<HTTPResponse> Post(Service service, string requestUri, Dictionary<string, string> form, bool executeSerially = true)
{
return CallApi(service, requestUri, (Uri uri) => ConstructSimpleFormPost(uri, form), executeSerially);
}
public static IPromise<HTTPResponse> Post(Service service, string requestUri, WWWForm form, bool executeSerially = true)
{
return CallApi(service, requestUri, (Uri uri) => ConstructUnityFormPost(uri, form), executeSerially);
}
public static IPromise<HTTPResponse> Post(Service service, string requestUri, HTTPFormBase form, bool executeSerially = true)
{
return CallApi(service, requestUri, (Uri uri) => ConstructUnityFormPost(uri, form), executeSerially);
}
public static IPromise<HTTPResponse> Post(Service service, string requestUri, HTTPMultiPartForm multiPartForm, bool executeSerially = true)
{
return Post(service, requestUri, (HTTPFormBase)multiPartForm, executeSerially);
}
public static IPromise<HTTPResponse> Post(Service service, string requestUri, string json, bool executeSerially = true)
{
return CallApi(service, requestUri, (Uri uri) => ConstructJsonFormPost(uri, json), executeSerially);
}
public static IPromise<HTTPResponse> Post<TRequest>(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<HTTPResponse> Post(string requestUri, Dictionary<string, string> form, bool executeSerially = true)
{
return Post(Service.API, requestUri, form, executeSerially);
}
public static IPromise<HTTPResponse> Post(string requestUri, WWWForm form, bool executeSerially = true)
{
return Post(Service.API, requestUri, form, executeSerially);
}
public static IPromise<HTTPResponse> Post(string requestUri, HTTPFormBase form, bool executeSerially = true)
{
return Post(Service.API, requestUri, form, executeSerially);
}
public static IPromise<HTTPResponse> Post(string requestUri, HTTPMultiPartForm multiPartForm, bool executeSerially = true)
{
return Post(requestUri, (HTTPFormBase)multiPartForm, executeSerially);
}
public static IPromise<HTTPResponse> Post(string requestUri, string json, bool executeSerially = true)
{
return Post(Service.API, requestUri, json, executeSerially);
}
public static IPromise<HTTPResponse> Post<TRequest>(string requestUri, TRequest request, bool executeSerially = true) where TRequest : IRecNetRequestObject
{
return Post(Service.API, requestUri, request, executeSerially);
}
public static IPromise<HTTPResponse> PostUsingLoginQueue(Service service, string requestUri, string json)
{
return CallApi(service, requestUri, (Uri uri) => ConstructJsonFormPost(uri, json), loginQueue);
}
public static IPromise<HTTPResponse> PostUsingBackgroundImageUploadQueue(Service service, string requestUri, HTTPFormBase form)
{
return CallApi(service, requestUri, (Uri uri) => ConstructUnityFormPost(uri, form), imageQueue);
}
public static IPromise<HTTPResponse> PostUsingLoginQueue(string requestUri, string json)
{
return PostUsingLoginQueue(Service.API, requestUri, json);
}
public static IPromise<HTTPResponse> 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<NameServerResponse>(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<string>();
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<VersionCheckResponse>().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>();
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<string, string> form)
{
var request = new HTTPRequest(uri, HTTPMethods.Post);
foreach (KeyValuePair<string, string> 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<string, object> 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<HTTPResponse> CallApi(Service service, string uri, ConstructRequestCallback constructRequest, bool executeSerially)
{
Queue<QueuedApiCall> queue = executeSerially ? serialQueue : parallelQueue;
return CallApi(service, uri, constructRequest, queue);
}
private static IPromise<HTTPResponse> CallApi(Service service, string uri, ConstructRequestCallback constructRequest, Queue<QueuedApiCall> queue)
{
LazyInit();
QueuedApiCall queuedApiCall = new QueuedApiCall(service, uri, constructRequest);
queue.Enqueue(queuedApiCall);
return queuedApiCall.Promise;
}
private static IPromise<HTTPResponse> CallApi(Service service, string uri, AsyncConstructRequestCallback asyncConstructRequest, Queue<QueuedApiCall> queue)
{
LazyInit();
QueuedApiCall queuedApiCall = new QueuedApiCall(service, uri, asyncConstructRequest);
queue.Enqueue(queuedApiCall);
return queuedApiCall.Promise;
}
private static IEnumerator ProcessCallApiQueue(Queue<QueuedApiCall> 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<T>(ApiCallback<T> 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);
}
}
}
}
}