Add package sync workflow

This commit is contained in:
2026-04-10 22:06:39 +08:00
parent 47f9a8bafa
commit 48eeb79d7d
128 changed files with 4748 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public static class Brisk
{
private static BriskContext s_context;
static Brisk()
{
Auth = new BriskAuthModule();
Player = new BriskPlayerModule();
Config = new BriskConfigModule();
Announcements = new BriskAnnouncementsModule();
Leaderboard = new BriskLeaderboardModule();
Archive = new BriskArchiveModule();
Space = new BriskSpaceModule();
}
public static event Action OnInitialized;
public static event Action OnLoggedIn;
public static event Action OnLoggedOut;
public static event Action<BriskBlockingException> OnBlockingError;
public static event Action<BriskAuthExpiredException> OnAuthExpired;
public static BriskAuthModule Auth { get; }
public static BriskPlayerModule Player { get; }
public static BriskConfigModule Config { get; }
public static BriskAnnouncementsModule Announcements { get; }
public static BriskLeaderboardModule Leaderboard { get; }
public static BriskArchiveModule Archive { get; }
public static BriskSpaceModule Space { get; }
public static bool IsInitialized => s_context != null;
public static bool IsLoggedIn => s_context != null && s_context.Session.HasAccessToken;
public static string AccessToken => s_context != null ? s_context.Session.AccessToken : null;
public static string PlayerId => s_context != null ? s_context.Session.PlayerId : null;
public static BriskIdentity Identity => s_context != null ? s_context.Session.Identity : null;
public static BriskOptions Options => s_context != null ? s_context.Options : null;
public static BriskBootstrapResult Bootstrap => s_context != null ? s_context.Bootstrap : null;
public static async Task InitializeAsync(BriskOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
options.Validate();
var context = new BriskContext(options);
s_context = context;
try
{
await BootstrapAsync(context);
await RestoreSessionAsync(context);
OnInitialized?.Invoke();
}
catch
{
if (context.Bootstrap == null)
{
s_context = null;
}
throw;
}
}
public static void Shutdown()
{
s_context = null;
}
public static void SetErrorPresenter(IBriskErrorPresenter presenter)
{
GetRequiredContext().ErrorPresenter = presenter ?? BriskDefaultErrorPresenter.Instance;
}
public static void SetExitHandler(Action exitHandler)
{
GetRequiredContext().ExitHandler = exitHandler;
}
internal static BriskContext GetRequiredContext()
{
if (s_context == null)
{
throw new BriskNotInitializedException();
}
return s_context;
}
internal static void NotifyLoggedIn()
{
OnLoggedIn?.Invoke();
}
internal static void NotifyLoggedOut()
{
OnLoggedOut?.Invoke();
}
internal static void NotifyBlockingError(BriskBlockingException exception)
{
s_context?.ErrorPresenter?.ShowBlockingError(exception);
OnBlockingError?.Invoke(exception);
}
internal static void NotifyAuthExpired(BriskAuthExpiredException exception)
{
s_context?.ErrorPresenter?.ShowAuthExpired(exception);
OnAuthExpired?.Invoke(exception);
}
private static async Task BootstrapAsync(BriskContext context)
{
var query = new Dictionary<string, string>
{
{ "game_key", context.Options.GameKey },
{ "client_version", context.Options.ClientVersion },
{ "device_id", context.Options.DeviceId }
};
var bootstrapData = await context.HttpClient.GetDataAsync("/client/bootstrap", query, false);
var bootstrap = BriskModelMapper.ToBootstrapResult(bootstrapData);
context.Bootstrap = bootstrap;
if (bootstrap.MaintenanceMode)
{
var message = string.IsNullOrWhiteSpace(bootstrap.MaintenanceMessage)
? "Server is under maintenance."
: bootstrap.MaintenanceMessage;
var exception = new BriskMaintenanceException(message);
NotifyBlockingError(exception);
throw exception;
}
if (BriskVersionComparer.IsLessThan(context.Options.ClientVersion, bootstrap.MinClientVersion))
{
var exception = new BriskClientUpdateRequiredException("Client version is lower than the minimum supported version.");
NotifyBlockingError(exception);
throw exception;
}
}
private static async Task RestoreSessionAsync(BriskContext context)
{
var storedSession = await context.TokenStore.LoadAsync();
if (storedSession == null || string.IsNullOrWhiteSpace(storedSession.AccessToken))
{
return;
}
context.Session.Update(
storedSession.AccessToken,
storedSession.ExpiresAt,
storedSession.PlayerId,
storedSession.ProjectAccountId,
storedSession.LoginProvider,
storedSession.LoginUserId);
if (!context.Options.ValidateSessionOnInitialize)
{
return;
}
try
{
var meData = await context.HttpClient.GetDataAsync("/player/me", null, true);
var me = BriskModelMapper.ToPlayerMe(meData);
context.Session.Update(
storedSession.AccessToken,
storedSession.ExpiresAt,
me.PlayerId,
me.ProjectAccountId,
me.LoginProvider,
me.LoginUserId);
}
catch (BriskAuthExpiredException exception)
{
context.Session.Clear();
await context.TokenStore.ClearAsync();
NotifyAuthExpired(exception);
}
catch (BriskBlockingException exception)
{
NotifyBlockingError(exception);
throw;
}
}
}

View File

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

View File

@@ -0,0 +1,7 @@
using System.Collections.Generic;
public sealed class BriskBinaryResponse
{
public byte[] Bytes;
public Dictionary<string, string> Headers;
}

View File

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

View File

@@ -0,0 +1,28 @@
using System;
public sealed class BriskContext
{
public BriskContext(BriskOptions options)
{
Options = options ?? throw new ArgumentNullException(nameof(options));
Session = new BriskSession();
ErrorPresenter = options.ErrorPresenter ?? BriskDefaultErrorPresenter.Instance;
ExitHandler = options.ExitHandler;
TokenStore = options.TokenStore ?? new BriskPlayerPrefsTokenStore();
HttpClient = new BriskHttpClient(this);
}
public BriskOptions Options { get; }
public BriskSession Session { get; }
public BriskBootstrapResult Bootstrap { get; set; }
public IBriskTokenStore TokenStore { get; }
public BriskHttpClient HttpClient { get; }
public IBriskErrorPresenter ErrorPresenter { get; set; }
public Action ExitHandler { get; set; }
}

View File

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

View File

@@ -0,0 +1,88 @@
using System;
using UnityEngine;
internal sealed class BriskDefaultErrorDialog : MonoBehaviour
{
private const string HostName = "BriskDefaultErrorDialog";
private static BriskDefaultErrorDialog s_instance;
private string _title;
private string _message;
private string _confirmText;
private Action _onConfirm;
private bool _visible;
public static void Show(string title, string message, string confirmText, Action onConfirm = null)
{
var host = EnsureInstance();
host._title = string.IsNullOrWhiteSpace(title) ? "Brisk" : title;
host._message = string.IsNullOrWhiteSpace(message) ? "Unknown error." : message;
host._confirmText = string.IsNullOrWhiteSpace(confirmText) ? "OK" : confirmText;
host._onConfirm = onConfirm;
host._visible = true;
}
private static BriskDefaultErrorDialog EnsureInstance()
{
if (s_instance != null)
{
return s_instance;
}
var existing = GameObject.Find(HostName);
if (existing != null)
{
s_instance = existing.GetComponent<BriskDefaultErrorDialog>();
if (s_instance != null)
{
return s_instance;
}
}
var gameObject = new GameObject(HostName);
DontDestroyOnLoad(gameObject);
s_instance = gameObject.AddComponent<BriskDefaultErrorDialog>();
return s_instance;
}
private void OnGUI()
{
if (!_visible)
{
return;
}
var overlayRect = new Rect(0, 0, Screen.width, Screen.height);
var previousColor = GUI.color;
GUI.color = new Color(0f, 0f, 0f, 0.65f);
GUI.Box(overlayRect, GUIContent.none);
GUI.color = previousColor;
var width = Mathf.Min(460f, Screen.width - 40f);
var height = 220f;
var dialogRect = new Rect(
(Screen.width - width) * 0.5f,
(Screen.height - height) * 0.5f,
width,
height);
GUILayout.BeginArea(dialogRect, GUI.skin.window);
GUILayout.Space(8f);
GUILayout.Label(_title, GUI.skin.label);
GUILayout.Space(12f);
GUILayout.Label(_message, GUI.skin.label);
GUILayout.FlexibleSpace();
if (GUILayout.Button(_confirmText, GUILayout.Height(36f)))
{
var callback = _onConfirm;
_visible = false;
_onConfirm = null;
callback?.Invoke();
}
GUILayout.Space(8f);
GUILayout.EndArea();
}
}

View File

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

View File

@@ -0,0 +1,66 @@
using System;
using UnityEngine;
public sealed class BriskDefaultErrorPresenter : IBriskErrorPresenter
{
private static readonly BriskDefaultErrorPresenter InstanceValue = new BriskDefaultErrorPresenter();
private BriskDefaultErrorPresenter()
{
}
public static BriskDefaultErrorPresenter Instance => InstanceValue;
public void ShowBlockingError(BriskBlockingException exception)
{
var message = exception != null && !string.IsNullOrWhiteSpace(exception.Message)
? exception.Message
: "A blocking error occurred.";
BriskDefaultErrorDialog.Show(
title: GetBlockingTitle(exception),
message: message,
confirmText: "OK",
onConfirm: () =>
{
var exitHandler = Brisk.IsInitialized ? Brisk.Options?.ExitHandler : null;
exitHandler?.Invoke();
});
Debug.LogError("[Brisk] Blocking error: " + message);
}
public void ShowAuthExpired(BriskAuthExpiredException exception)
{
var message = exception != null && !string.IsNullOrWhiteSpace(exception.Message)
? exception.Message
: "Login expired. Please sign in again.";
BriskDefaultErrorDialog.Show(
title: "Login Expired",
message: message,
confirmText: "OK");
Debug.LogWarning("[Brisk] Auth expired: " + message);
}
private static string GetBlockingTitle(BriskBlockingException exception)
{
if (exception is BriskMaintenanceException)
{
return "Maintenance";
}
if (exception is BriskAccountBannedException)
{
return "Account Restricted";
}
if (exception is BriskClientUpdateRequiredException)
{
return "Update Required";
}
return "Service Unavailable";
}
}

View File

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

View File

@@ -0,0 +1,21 @@
public static class BriskErrorClassifier
{
public static BriskException Classify(int httpStatus, int code, string message)
{
switch (code)
{
case 10001:
case 10002:
case 10023:
return new BriskAuthExpiredException(message, code);
case 10003:
case 10025:
return new BriskMaintenanceException(message, code);
case 10004:
case 10026:
return new BriskAccountBannedException(message, code);
default:
return new BriskApiException(httpStatus, code, message);
}
}
}

View File

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

View File

@@ -0,0 +1,253 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
public sealed class BriskHttpClient
{
private readonly BriskContext _context;
public BriskHttpClient(BriskContext context)
{
_context = context;
}
public Task<Dictionary<string, object>> GetDataAsync(string path, Dictionary<string, string> query = null, bool auth = false)
{
return SendForDictionaryAsync(UnityWebRequest.kHttpVerbGET, path, query, null, auth);
}
public Task<object> GetRawDataAsync(string path, Dictionary<string, string> query = null, bool auth = false)
{
return SendRawJsonAsync(UnityWebRequest.kHttpVerbGET, path, query, null, auth);
}
public Task<Dictionary<string, object>> PostJsonAsync(string path, object body, bool auth = false)
{
return SendForDictionaryAsync(UnityWebRequest.kHttpVerbPOST, path, null, body, auth);
}
public Task<object> PostJsonRawAsync(string path, object body, bool auth = false)
{
return SendRawJsonAsync(UnityWebRequest.kHttpVerbPOST, path, null, body, auth);
}
public Task<object> PostJsonRawAsync(string path, object body, bool auth, Dictionary<string, string> query)
{
return SendRawJsonAsync(UnityWebRequest.kHttpVerbPOST, path, query, body, auth);
}
public Task<object> SendPutJsonRawAsync(string path, object body, bool auth = false)
{
return SendRawJsonAsync(UnityWebRequest.kHttpVerbPUT, path, null, body, auth);
}
public Task<object> SendDeleteJsonRawAsync(string path, Dictionary<string, string> query = null, bool auth = false)
{
return SendRawJsonAsync(UnityWebRequest.kHttpVerbDELETE, path, query, null, auth);
}
public async Task<Dictionary<string, object>> PostMultipartAsync(string path, List<IMultipartFormSection> formSections, bool auth = false)
{
using (var request = UnityWebRequest.Post(BuildUrl(path, null), formSections))
{
if (auth)
{
AddAuthorizationHeader(request);
}
request.SetRequestHeader("Accept", "application/json");
await SendRequestAsync(request);
return ParseEnvelope(request) as Dictionary<string, object> ?? new Dictionary<string, object>();
}
}
public async Task<BriskBinaryResponse> GetBytesAsync(string path, Dictionary<string, string> query = null, bool auth = false)
{
using (var request = BuildRequest(UnityWebRequest.kHttpVerbGET, path, query, null, auth))
{
await SendRequestAsync(request);
EnsureSuccessOrThrow(request);
return new BriskBinaryResponse
{
Bytes = request.downloadHandler != null ? request.downloadHandler.data : Array.Empty<byte>(),
Headers = request.GetResponseHeaders() ?? new Dictionary<string, string>()
};
}
}
private async Task<Dictionary<string, object>> SendForDictionaryAsync(string method, string path, Dictionary<string, string> query, object body, bool auth)
{
var data = await SendRawJsonAsync(method, path, query, body, auth);
return data as Dictionary<string, object> ?? new Dictionary<string, object>();
}
private async Task<object> SendRawJsonAsync(string method, string path, Dictionary<string, string> query, object body, bool auth)
{
using (var request = BuildRequest(method, path, query, body, auth))
{
await SendRequestAsync(request);
return ParseEnvelope(request);
}
}
private UnityWebRequest BuildRequest(string method, string path, Dictionary<string, string> query, object body, bool auth)
{
var url = BuildUrl(path, query);
var request = new UnityWebRequest(url, method)
{
downloadHandler = new DownloadHandlerBuffer()
};
request.SetRequestHeader("Accept", "application/json");
if (body != null)
{
var json = BriskJson.Serialize(body);
var bytes = Encoding.UTF8.GetBytes(json);
request.uploadHandler = new UploadHandlerRaw(bytes);
request.SetRequestHeader("Content-Type", "application/json");
}
if (auth)
{
AddAuthorizationHeader(request);
}
return request;
}
private async Task SendRequestAsync(UnityWebRequest request)
{
try
{
var operation = request.SendWebRequest();
var taskSource = new TaskCompletionSource<bool>();
operation.completed += _ => taskSource.TrySetResult(true);
await taskSource.Task;
}
catch (Exception exception)
{
throw new BriskNetworkException("Failed to send request.", exception);
}
if (request.result == UnityWebRequest.Result.ConnectionError || request.result == UnityWebRequest.Result.DataProcessingError)
{
throw new BriskNetworkException(request.error ?? "Network error.");
}
}
private object ParseEnvelope(UnityWebRequest request)
{
var httpStatus = (int)request.responseCode;
var responseText = request.downloadHandler != null ? request.downloadHandler.text : null;
if (string.IsNullOrWhiteSpace(responseText))
{
if (httpStatus >= 200 && httpStatus < 300)
{
return new Dictionary<string, object>();
}
throw new BriskHttpException(httpStatus, request.error ?? "HTTP error.");
}
var payload = BriskJson.Deserialize(responseText) as Dictionary<string, object>;
if (payload == null)
{
throw new BriskHttpException(httpStatus, "Invalid JSON response.");
}
var code = BriskValueReader.GetInt(payload, "code");
var message = BriskValueReader.GetString(payload, "message") ?? request.error ?? "Request failed.";
if (code != 0 || httpStatus < 200 || httpStatus >= 300)
{
throw BriskErrorClassifier.Classify(httpStatus, code, message);
}
if (BriskValueReader.TryGetValue(payload, "data", out var dataValue))
{
return dataValue;
}
return new Dictionary<string, object>();
}
private void EnsureSuccessOrThrow(UnityWebRequest request)
{
var httpStatus = (int)request.responseCode;
if (httpStatus >= 200 && httpStatus < 300)
{
return;
}
throw CreateExceptionFromResponse(request);
}
private BriskException CreateExceptionFromResponse(UnityWebRequest request)
{
var httpStatus = (int)request.responseCode;
var responseText = request.downloadHandler != null ? request.downloadHandler.text : null;
if (!string.IsNullOrWhiteSpace(responseText))
{
var payload = BriskJson.Deserialize(responseText) as Dictionary<string, object>;
if (payload != null)
{
var code = BriskValueReader.GetInt(payload, "code");
var message = BriskValueReader.GetString(payload, "message") ?? request.error ?? "Request failed.";
return BriskErrorClassifier.Classify(httpStatus, code, message);
}
}
return new BriskHttpException(httpStatus, request.error ?? "HTTP error.");
}
private void AddAuthorizationHeader(UnityWebRequest request)
{
var token = _context.Session.AccessToken;
if (string.IsNullOrWhiteSpace(token))
{
throw new BriskAuthExpiredException("Missing access token.");
}
request.SetRequestHeader("Authorization", "Bearer " + token);
}
private string BuildUrl(string path, Dictionary<string, string> query)
{
var trimmedPath = path.StartsWith("/") ? path : "/" + path;
var url = _context.Options.BaseUrl + trimmedPath;
if (query == null || query.Count == 0)
{
return url;
}
var builder = new StringBuilder(url);
var first = true;
foreach (var pair in query)
{
if (string.IsNullOrWhiteSpace(pair.Value))
{
continue;
}
if (first)
{
builder.Append('?');
}
else
{
builder.Append('&');
}
builder.Append(UnityWebRequest.EscapeURL(pair.Key));
builder.Append('=');
builder.Append(UnityWebRequest.EscapeURL(pair.Value));
first = false;
}
return builder.ToString();
}
}

View File

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

View File

@@ -0,0 +1,443 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
internal static class BriskJson
{
public static object Deserialize(string json)
{
if (string.IsNullOrWhiteSpace(json))
{
return null;
}
return Parser.Parse(json);
}
public static string Serialize(object value)
{
return Serializer.Serialize(value);
}
private sealed class Parser : IDisposable
{
private readonly StringReader _reader;
private Parser(string json)
{
_reader = new StringReader(json);
}
public static object Parse(string json)
{
using (var parser = new Parser(json))
{
return parser.ParseValue();
}
}
public void Dispose()
{
_reader.Dispose();
}
private object ParseValue()
{
EatWhitespace();
if (_reader.Peek() == -1)
{
return null;
}
switch (PeekChar)
{
case '{':
return ParseObject();
case '[':
return ParseArray();
case '"':
return ParseString();
case 't':
return ParseTrue();
case 'f':
return ParseFalse();
case 'n':
return ParseNull();
default:
return ParseNumber();
}
}
private Dictionary<string, object> ParseObject()
{
var table = new Dictionary<string, object>();
ReadChar();
while (true)
{
EatWhitespace();
if (PeekChar == '}')
{
ReadChar();
return table;
}
var key = ParseString();
EatWhitespace();
ReadChar();
var value = ParseValue();
table[key] = value;
EatWhitespace();
if (PeekChar == ',')
{
ReadChar();
continue;
}
if (PeekChar == '}')
{
ReadChar();
return table;
}
}
}
private List<object> ParseArray()
{
var array = new List<object>();
ReadChar();
while (true)
{
EatWhitespace();
if (PeekChar == ']')
{
ReadChar();
return array;
}
array.Add(ParseValue());
EatWhitespace();
if (PeekChar == ',')
{
ReadChar();
continue;
}
if (PeekChar == ']')
{
ReadChar();
return array;
}
}
}
private string ParseString()
{
var builder = new StringBuilder();
ReadChar();
while (true)
{
if (_reader.Peek() == -1)
{
break;
}
var ch = ReadChar();
if (ch == '"')
{
break;
}
if (ch == '\\')
{
if (_reader.Peek() == -1)
{
break;
}
ch = ReadChar();
switch (ch)
{
case '"':
case '\\':
case '/':
builder.Append(ch);
break;
case 'b':
builder.Append('\b');
break;
case 'f':
builder.Append('\f');
break;
case 'n':
builder.Append('\n');
break;
case 'r':
builder.Append('\r');
break;
case 't':
builder.Append('\t');
break;
case 'u':
builder.Append((char)Convert.ToInt32(ReadChars(4), 16));
break;
}
continue;
}
builder.Append(ch);
}
return builder.ToString();
}
private object ParseNumber()
{
var token = NextToken();
if (token.IndexOf('.') >= 0 || token.IndexOf('e') >= 0 || token.IndexOf('E') >= 0)
{
double.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue);
return doubleValue;
}
long.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue);
return longValue;
}
private bool ParseTrue()
{
ReadChars(4);
return true;
}
private bool ParseFalse()
{
ReadChars(5);
return false;
}
private object ParseNull()
{
ReadChars(4);
return null;
}
private string NextToken()
{
var builder = new StringBuilder();
while (_reader.Peek() != -1)
{
var ch = PeekChar;
if (char.IsWhiteSpace(ch) || ch == ',' || ch == ']' || ch == '}')
{
break;
}
builder.Append(ReadChar());
}
return builder.ToString();
}
private void EatWhitespace()
{
while (_reader.Peek() != -1 && char.IsWhiteSpace(PeekChar))
{
_reader.Read();
}
}
private string ReadChars(int count)
{
var buffer = new char[count];
_reader.Read(buffer, 0, count);
return new string(buffer);
}
private char PeekChar => Convert.ToChar(_reader.Peek());
private char ReadChar() => Convert.ToChar(_reader.Read());
}
private static class Serializer
{
public static string Serialize(object value)
{
var builder = new StringBuilder();
SerializeValue(value, builder);
return builder.ToString();
}
private static void SerializeValue(object value, StringBuilder builder)
{
if (value == null)
{
builder.Append("null");
return;
}
if (value is string str)
{
SerializeString(str, builder);
return;
}
if (value is bool boolean)
{
builder.Append(boolean ? "true" : "false");
return;
}
if (value is IDictionary dictionary)
{
SerializeObject(dictionary, builder);
return;
}
if (value is IEnumerable enumerable && !(value is string))
{
SerializeArray(enumerable, builder);
return;
}
if (IsNumeric(value))
{
builder.Append(Convert.ToString(value, CultureInfo.InvariantCulture));
return;
}
SerializeObject(ToReflectionDictionary(value), builder);
}
private static void SerializeObject(IDictionary dictionary, StringBuilder builder)
{
var first = true;
builder.Append('{');
foreach (DictionaryEntry entry in dictionary)
{
if (!first)
{
builder.Append(',');
}
SerializeString(Convert.ToString(entry.Key, CultureInfo.InvariantCulture), builder);
builder.Append(':');
SerializeValue(entry.Value, builder);
first = false;
}
builder.Append('}');
}
private static void SerializeArray(IEnumerable array, StringBuilder builder)
{
var first = true;
builder.Append('[');
foreach (var item in array)
{
if (!first)
{
builder.Append(',');
}
SerializeValue(item, builder);
first = false;
}
builder.Append(']');
}
private static void SerializeString(string value, StringBuilder builder)
{
builder.Append('"');
foreach (var ch in value)
{
switch (ch)
{
case '"':
builder.Append("\\\"");
break;
case '\\':
builder.Append("\\\\");
break;
case '\b':
builder.Append("\\b");
break;
case '\f':
builder.Append("\\f");
break;
case '\n':
builder.Append("\\n");
break;
case '\r':
builder.Append("\\r");
break;
case '\t':
builder.Append("\\t");
break;
default:
if (ch < 32)
{
builder.Append("\\u");
builder.Append(((int)ch).ToString("x4"));
}
else
{
builder.Append(ch);
}
break;
}
}
builder.Append('"');
}
private static bool IsNumeric(object value)
{
switch (Type.GetTypeCode(value.GetType()))
{
case TypeCode.Byte:
case TypeCode.SByte:
case TypeCode.UInt16:
case TypeCode.UInt32:
case TypeCode.UInt64:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.Decimal:
case TypeCode.Double:
case TypeCode.Single:
return true;
default:
return false;
}
}
private static IDictionary ToReflectionDictionary(object value)
{
var result = new Dictionary<string, object>();
var type = value.GetType();
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public))
{
result[field.Name] = field.GetValue(value);
}
foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (!property.CanRead || property.GetIndexParameters().Length > 0)
{
continue;
}
result[property.Name] = property.GetValue(value, null);
}
return result;
}
}
}

View File

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

View File

@@ -0,0 +1,309 @@
using System.Collections.Generic;
using System.Linq;
internal static class BriskModelMapper
{
public static BriskLoginResult ToLoginResult(Dictionary<string, object> data)
{
var profileData = BriskValueReader.GetDictionary(data, "profile");
return new BriskLoginResult
{
AccessToken = BriskValueReader.GetString(data, "access_token"),
ExpiresIn = BriskValueReader.GetInt(data, "expires_in"),
PlayerId = BriskValueReader.GetString(data, "player_id"),
ProjectAccountId = BriskValueReader.GetString(data, "project_account_id"),
LoginProvider = BriskValueReader.GetString(profileData, "login_provider"),
LoginUserId = BriskValueReader.GetString(profileData, "login_user_id"),
IsNewPlayer = BriskValueReader.GetBool(data, "is_new_player"),
Profile = ToProfile(profileData)
};
}
public static BriskBootstrapResult ToBootstrapResult(Dictionary<string, object> data)
{
return new BriskBootstrapResult
{
ProjectName = BriskValueReader.GetString(data, "project_name"),
ServerTime = BriskValueReader.GetString(data, "server_time"),
MaintenanceMode = BriskValueReader.GetBool(data, "maintenance_mode"),
MaintenanceMessage = BriskValueReader.GetString(data, "maintenance_message"),
MinClientVersion = BriskValueReader.GetString(data, "min_client_version"),
FeatureFlags = BriskValueReader.GetDictionary(data, "feature_flags") ?? new Dictionary<string, object>(),
DynamicConfig = BriskValueReader.GetDictionary(data, "dynamic_config") ?? new Dictionary<string, object>()
};
}
public static BriskConfigCurrent ToConfigCurrent(Dictionary<string, object> data)
{
return new BriskConfigCurrent
{
FeatureFlags = BriskValueReader.GetDictionary(data, "feature_flags") ?? new Dictionary<string, object>(),
DynamicConfig = BriskValueReader.GetDictionary(data, "dynamic_config") ?? new Dictionary<string, object>()
};
}
public static BriskPlayerMe ToPlayerMe(Dictionary<string, object> data)
{
return new BriskPlayerMe
{
PlayerId = BriskValueReader.GetString(data, "player_id"),
ProjectAccountId = BriskValueReader.GetString(data, "project_account_id"),
LoginProvider = BriskValueReader.GetString(data, "login_provider"),
LoginUserId = BriskValueReader.GetString(data, "login_user_id"),
Nickname = BriskValueReader.GetString(data, "nickname"),
AvatarUrl = BriskValueReader.GetString(data, "avatar_url"),
ProfileJson = BriskValueReader.GetDictionary(data, "profile_json")
};
}
public static BriskProfile ToProfile(Dictionary<string, object> data)
{
if (data == null)
{
return null;
}
return new BriskProfile
{
Nickname = BriskValueReader.GetString(data, "nickname"),
AvatarUrl = BriskValueReader.GetString(data, "avatar_url"),
ProfileJson = BriskValueReader.GetDictionary(data, "profile_json")
};
}
public static BriskLeaderboardEntry ToLeaderboardEntry(Dictionary<string, object> data)
{
if (data == null)
{
return null;
}
return new BriskLeaderboardEntry
{
Rank = BriskValueReader.GetInt(data, "rank"),
PlayerId = BriskValueReader.GetString(data, "player_id"),
ProjectAccountId = BriskValueReader.GetString(data, "project_account_id"),
LoginProvider = BriskValueReader.GetString(data, "login_provider"),
LoginUserId = BriskValueReader.GetString(data, "login_user_id"),
Score = BriskValueReader.GetLong(data, "score"),
Nickname = BriskValueReader.GetString(data, "nickname"),
AvatarUrl = BriskValueReader.GetString(data, "avatar_url")
};
}
public static BriskLeaderboardPlayerRank ToLeaderboardPlayerRank(Dictionary<string, object> data)
{
var entry = ToLeaderboardEntry(data);
if (entry == null)
{
return null;
}
return new BriskLeaderboardPlayerRank
{
Rank = entry.Rank,
Score = entry.Score,
PlayerId = entry.PlayerId,
ProjectAccountId = entry.ProjectAccountId,
LoginProvider = entry.LoginProvider,
LoginUserId = entry.LoginUserId,
Nickname = entry.Nickname,
AvatarUrl = entry.AvatarUrl
};
}
public static BriskRankSeasonInfo ToRankSeasonInfo(Dictionary<string, object> data)
{
if (data == null)
{
return null;
}
return new BriskRankSeasonInfo
{
SeasonId = BriskValueReader.GetString(data, "season_id") ?? BriskValueReader.GetString(data, "id"),
Name = BriskValueReader.GetString(data, "name"),
StartAt = BriskValueReader.GetString(data, "start_at"),
EndAt = BriskValueReader.GetString(data, "end_at")
};
}
public static List<BriskLeaderboardEntry> ToLeaderboardEntries(object data)
{
return ExtractList(data)
.Select(item => ToLeaderboardEntry(item as Dictionary<string, object>))
.Where(item => item != null)
.ToList();
}
public static List<BriskRankSeasonInfo> ToRankSeasonInfos(object data)
{
return ExtractList(data)
.Select(item => ToRankSeasonInfo(item as Dictionary<string, object>))
.Where(item => item != null)
.ToList();
}
public static BriskAnnouncementItem ToAnnouncementItem(Dictionary<string, object> data)
{
if (data == null)
{
return null;
}
return new BriskAnnouncementItem
{
Id = BriskValueReader.GetLong(data, "id"),
Title = BriskValueReader.GetString(data, "title"),
Content = BriskValueReader.GetString(data, "content"),
ContentType = BriskValueReader.GetString(data, "content_type"),
StartAt = BriskValueReader.GetString(data, "start_at"),
EndAt = BriskValueReader.GetString(data, "end_at"),
IsRead = BriskValueReader.GetBool(data, "is_read")
};
}
public static List<BriskAnnouncementItem> ToAnnouncementItems(object data)
{
return ExtractList(data)
.Select(item => ToAnnouncementItem(item as Dictionary<string, object>))
.Where(item => item != null)
.ToList();
}
public static BriskArchiveSlot ToArchiveSlot(Dictionary<string, object> data)
{
if (data == null)
{
return null;
}
return new BriskArchiveSlot
{
SlotNo = BriskValueReader.GetInt(data, "slot_no"),
Version = BriskValueReader.GetInt(data, "version"),
SizeBytes = BriskValueReader.GetLong(data, "size_bytes"),
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
};
}
public static BriskArchiveMeta ToArchiveMeta(Dictionary<string, object> data)
{
if (data == null)
{
return null;
}
return new BriskArchiveMeta
{
SlotNo = BriskValueReader.GetInt(data, "slot_no"),
Version = BriskValueReader.GetInt(data, "version"),
Checksum = BriskValueReader.GetString(data, "checksum"),
SizeBytes = BriskValueReader.GetLong(data, "size_bytes"),
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
};
}
public static BriskArchiveUploadResult ToArchiveUploadResult(Dictionary<string, object> data)
{
if (data == null)
{
return null;
}
return new BriskArchiveUploadResult
{
SlotNo = BriskValueReader.GetInt(data, "slot_no"),
Version = BriskValueReader.GetInt(data, "version"),
SizeBytes = BriskValueReader.GetLong(data, "size_bytes"),
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
};
}
public static List<BriskArchiveSlot> ToArchiveSlots(object data)
{
return ExtractList(data)
.Select(item => ToArchiveSlot(item as Dictionary<string, object>))
.Where(item => item != null)
.ToList();
}
public static BriskSpaceView ToSpaceView(Dictionary<string, object> data)
{
if (data == null)
{
return null;
}
return new BriskSpaceView
{
PlayerId = BriskValueReader.GetString(data, "player_id"),
LoginProvider = BriskValueReader.GetString(data, "login_provider"),
LoginUserId = BriskValueReader.GetString(data, "login_user_id"),
Payload = BriskValueReader.GetDictionary(data, "payload_json") ?? BriskValueReader.GetDictionary(data, "payload")
};
}
public static BriskSpaceStats ToSpaceStats(Dictionary<string, object> data)
{
if (data == null)
{
return null;
}
return new BriskSpaceStats
{
LikeCount = BriskValueReader.GetInt(data, "like_count"),
VisitCount = BriskValueReader.GetInt(data, "visit_count")
};
}
public static BriskSpaceVisit ToSpaceVisit(Dictionary<string, object> data)
{
if (data == null)
{
return null;
}
return new BriskSpaceVisit
{
VisitorPlayerId = BriskValueReader.GetString(data, "visitor_player_id") ?? BriskValueReader.GetString(data, "player_id"),
VisitorLoginProvider = BriskValueReader.GetString(data, "visitor_login_provider") ?? BriskValueReader.GetString(data, "login_provider"),
VisitorLoginUserId = BriskValueReader.GetString(data, "visitor_login_user_id") ?? BriskValueReader.GetString(data, "login_user_id"),
VisitorNickname = BriskValueReader.GetString(data, "visitor_nickname") ?? BriskValueReader.GetString(data, "nickname"),
VisitorAvatarUrl = BriskValueReader.GetString(data, "visitor_avatar_url") ?? BriskValueReader.GetString(data, "avatar_url"),
VisitedAt = BriskValueReader.GetString(data, "visited_at")
};
}
public static List<BriskSpaceVisit> ToSpaceVisits(object data)
{
return ExtractList(data)
.Select(item => ToSpaceVisit(item as Dictionary<string, object>))
.Where(item => item != null)
.ToList();
}
public static Dictionary<string, object> ExtractObject(object data)
{
return data as Dictionary<string, object>;
}
private static List<object> ExtractList(object data)
{
if (data is List<object> list)
{
return list;
}
if (data is Dictionary<string, object> dictionary)
{
return BriskValueReader.GetList(dictionary, "items")
?? BriskValueReader.GetList(dictionary, "list")
?? BriskValueReader.GetList(dictionary, "records")
?? new List<object>();
}
return new List<object>();
}
}

View File

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

View File

@@ -0,0 +1,74 @@
using System;
using System.Threading.Tasks;
public abstract class BriskModuleBase
{
protected static BriskContext GetContext()
{
return Brisk.GetRequiredContext();
}
protected static Task<T> ExecuteAsync<T>(Func<BriskContext, Task<T>> action)
{
return BriskModuleExecutor.ExecuteAsync(action);
}
protected static Task ExecuteAsync(Func<BriskContext, Task> action)
{
return BriskModuleExecutor.ExecuteAsync(action);
}
protected static Task<T> ExecutePublicAsync<T>(Func<BriskContext, Task<T>> action)
{
return BriskModuleExecutor.ExecutePublicAsync(action);
}
protected static Task ExecutePublicAsync(Func<BriskContext, Task> action)
{
return BriskModuleExecutor.ExecutePublicAsync(action);
}
protected static string RequireNotEmpty(string value, string paramName)
{
GetContext();
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException(paramName + " is required.", paramName);
}
return value;
}
protected static int RequirePositive(int value, string paramName, string message)
{
GetContext();
if (value <= 0)
{
throw new ArgumentOutOfRangeException(paramName, message);
}
return value;
}
protected static long RequirePositive(long value, string paramName, string message)
{
GetContext();
if (value <= 0)
{
throw new ArgumentOutOfRangeException(paramName, message);
}
return value;
}
protected static T RequireNotNull<T>(T value, string paramName) where T : class
{
GetContext();
if (value == null)
{
throw new ArgumentNullException(paramName);
}
return value;
}
}

View File

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

View File

@@ -0,0 +1,59 @@
using System;
using System.Threading.Tasks;
internal static class BriskModuleExecutor
{
public static Task<T> ExecuteAsync<T>(Func<BriskContext, Task<T>> action)
{
return ExecuteAsync(action, clearSessionOnAuthExpired: true);
}
public static Task<T> ExecutePublicAsync<T>(Func<BriskContext, Task<T>> action)
{
return ExecuteAsync(action, clearSessionOnAuthExpired: false);
}
public static async Task ExecuteAsync(Func<BriskContext, Task> action)
{
await ExecuteAsync(async context =>
{
await action(context);
return true;
}, clearSessionOnAuthExpired: true);
}
public static async Task ExecutePublicAsync(Func<BriskContext, Task> action)
{
await ExecuteAsync(async context =>
{
await action(context);
return true;
}, clearSessionOnAuthExpired: false);
}
private static async Task<T> ExecuteAsync<T>(Func<BriskContext, Task<T>> action, bool clearSessionOnAuthExpired)
{
var context = Brisk.GetRequiredContext();
try
{
return await action(context);
}
catch (BriskAuthExpiredException exception)
{
if (clearSessionOnAuthExpired)
{
context.Session.Clear();
await context.TokenStore.ClearAsync();
Brisk.NotifyAuthExpired(exception);
}
throw;
}
catch (BriskBlockingException exception)
{
Brisk.NotifyBlockingError(exception);
throw;
}
}
}

View File

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

View File

@@ -0,0 +1,40 @@
using System;
public sealed class BriskOptions
{
public string BaseUrl;
public string GameKey;
public string ClientVersion;
public string DeviceId;
public bool EnableLog;
public bool ValidateSessionOnInitialize = true;
public IBriskTokenStore TokenStore;
public IBriskErrorPresenter ErrorPresenter;
public Action ExitHandler;
public void Validate()
{
if (string.IsNullOrWhiteSpace(BaseUrl))
{
throw new ArgumentException("BriskOptions.BaseUrl is required.", nameof(BaseUrl));
}
if (string.IsNullOrWhiteSpace(GameKey))
{
throw new ArgumentException("BriskOptions.GameKey is required.", nameof(GameKey));
}
BaseUrl = NormalizeBaseUrl(BaseUrl);
}
private static string NormalizeBaseUrl(string baseUrl)
{
var normalized = baseUrl.Trim().TrimEnd('/');
if (normalized.EndsWith("/api", StringComparison.OrdinalIgnoreCase))
{
return normalized;
}
return normalized + "/api";
}
}

View File

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

View File

@@ -0,0 +1,40 @@
using System;
public sealed class BriskSession
{
public string AccessToken { get; private set; }
public DateTimeOffset? ExpiresAt { get; private set; }
public BriskIdentity Identity { get; private set; } = new BriskIdentity();
public string PlayerId => Identity.PlayerId;
public string ProjectAccountId => Identity.ProjectAccountId;
public string LoginProvider => Identity.LoginProvider;
public string LoginUserId => Identity.LoginUserId;
public bool HasAccessToken => !string.IsNullOrWhiteSpace(AccessToken);
public void Update(string accessToken, DateTimeOffset? expiresAt, string playerId, string projectAccountId, string loginProvider = null, string loginUserId = null)
{
AccessToken = accessToken;
ExpiresAt = expiresAt;
Identity = new BriskIdentity
{
PlayerId = playerId,
ProjectAccountId = projectAccountId,
LoginProvider = loginProvider,
LoginUserId = loginUserId
};
}
public void Clear()
{
AccessToken = null;
ExpiresAt = null;
Identity = new BriskIdentity();
}
}

View File

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

View File

@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
internal static class BriskValueReader
{
public static string GetString(Dictionary<string, object> data, string key)
{
if (!TryGetValue(data, key, out var value) || value == null)
{
return null;
}
return Convert.ToString(value);
}
public static bool GetBool(Dictionary<string, object> data, string key)
{
if (!TryGetValue(data, key, out var value) || value == null)
{
return false;
}
if (value is bool boolValue)
{
return boolValue;
}
if (bool.TryParse(Convert.ToString(value), out var parsed))
{
return parsed;
}
return false;
}
public static int GetInt(Dictionary<string, object> data, string key)
{
if (!TryGetValue(data, key, out var value) || value == null)
{
return 0;
}
try
{
return Convert.ToInt32(value);
}
catch
{
return 0;
}
}
public static long GetLong(Dictionary<string, object> data, string key)
{
if (!TryGetValue(data, key, out var value) || value == null)
{
return 0L;
}
try
{
return Convert.ToInt64(value);
}
catch
{
return 0L;
}
}
public static Dictionary<string, object> GetDictionary(Dictionary<string, object> data, string key)
{
if (!TryGetValue(data, key, out var value) || value == null)
{
return null;
}
return value as Dictionary<string, object>;
}
public static List<object> GetList(Dictionary<string, object> data, string key)
{
if (!TryGetValue(data, key, out var value) || value == null)
{
return null;
}
return value as List<object>;
}
public static bool TryGetValue(Dictionary<string, object> data, string key, out object value)
{
value = null;
return data != null && !string.IsNullOrWhiteSpace(key) && data.TryGetValue(key, out value);
}
}

View File

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

View File

@@ -0,0 +1,32 @@
using System;
internal static class BriskVersionComparer
{
public static bool IsLessThan(string currentVersion, string minVersion)
{
if (string.IsNullOrWhiteSpace(currentVersion) || string.IsNullOrWhiteSpace(minVersion))
{
return false;
}
if (!Version.TryParse(Normalize(currentVersion), out var current))
{
return false;
}
if (!Version.TryParse(Normalize(minVersion), out var minimum))
{
return false;
}
return current < minimum;
}
private static string Normalize(string version)
{
var normalized = version.Trim();
return normalized.StartsWith("v", StringComparison.OrdinalIgnoreCase)
? normalized.Substring(1)
: normalized;
}
}

View File

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

View File

@@ -0,0 +1,6 @@
public interface IBriskErrorPresenter
{
void ShowBlockingError(BriskBlockingException exception);
void ShowAuthExpired(BriskAuthExpiredException exception);
}

View File

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

View File

@@ -0,0 +1,10 @@
using System.Threading.Tasks;
public interface IBriskTokenStore
{
Task SaveAsync(BriskStoredSession session);
Task<BriskStoredSession> LoadAsync();
Task ClearAsync();
}

View File

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