Release v0.3.0 lowercase UPM package name

This commit is contained in:
2026-04-11 14:44:13 +08:00
parent 07fc690e67
commit 7a8dd4cea0
142 changed files with 149 additions and 78 deletions

View File

@@ -0,0 +1,22 @@
# Changelog
## 0.3.0
- Renamed the UPM package identifier to `com.foldcc.cc-framework.brisk-game-server`
- Renamed the publish directory to the all-lowercase package path
- Updated sync tooling and integration documentation for Git-based package import
## 0.2.0
- Refined archive APIs with direct text and JSON upload/download helpers
- Refactored player space to the latest metadata plus binary content architecture
- Added space content download/update result models and like list models
- Updated quick start sample to use the latest archive and space flows
- Updated package and integration docs to match the current API surface
## 0.1.0
- Initial embedded package structure
- Added Brisk runtime facade and service modules
- Added default error presenter
- Added quick start sample script and sample scene

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cc4dc90e0c0542adbd88e58e9daf4336
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,89 @@
# Quick Start
## Initialize
```csharp
await Brisk.InitializeAsync(new BriskOptions
{
BaseUrl = "https://brisk.lightyears.ltd",
GameKey = "demo-game",
ClientVersion = Application.version,
DeviceId = SystemInfo.deviceUniqueIdentifier
});
```
## Login
```csharp
await Brisk.Auth.LoginWithUserIdAsync("tap", "tap_user_10001");
```
## Common calls
```csharp
var me = await Brisk.Player.GetMeAsync();
var config = await Brisk.Config.GetCurrentAsync();
var top = await Brisk.Leaderboard.GetTopAsync("season-score", 20);
await Brisk.Leaderboard.SubmitScoreAsync("season-score", 128);
```
## Archive upload
```csharp
await Brisk.Archive.UploadTextAsync(1, "{\"save\":1}");
await Brisk.Archive.UploadJsonAsync(2, new
{
save = 1,
coins = 128
});
var text = await Brisk.Archive.DownloadTextAsync(1);
var json = await Brisk.Archive.DownloadJsonAsync(2);
```
Notes:
- if you already have binary data, keep using `Brisk.Archive.UploadAsync(slotNo, bytes)`
- if you need version and checksum, keep using `Brisk.Archive.DownloadAsync(slotNo)`
- `checksum` is optional in normal use
- the SDK computes SHA256 for you automatically
- if you pass a manual checksum, use plain SHA256 hex
- values like `sha256:abcd...` will be normalized by the SDK before upload
## Space content
```csharp
await Brisk.Space.UpdateMyAsync("Hello Brisk Space");
await Brisk.Space.UpdateMyAsync(new
{
mood = "ready",
title = "hello"
});
var mySpace = await Brisk.Space.GetByPlayerIdAsync(Brisk.PlayerId);
var myContent = await Brisk.Space.DownloadContentByPlayerIdAsync(Brisk.PlayerId);
var text = Encoding.UTF8.GetString(myContent.Bytes);
```
Notes:
- space metadata and space content are now separated
- `GetByPlayerIdAsync(...)` returns metadata only
- use `DownloadContentByPlayerIdAsync(...)` to read the actual content bytes
- `UpdateMyAsync(...)` automatically picks text / binary / json behavior from the payload type
## Sample
For the current source project, open directly:
- `Assets/BriskSdk/Samples/QuickStart/BriskQuickStartSample.cs`
- `Assets/Scenes/BriskQuickStartScene.unity`
The sample scene uses an IMGUI debug panel and can directly test:
- initialize and restore
- login by `login_user_id`
- login by `code`
- player, config, announcement, leaderboard, archive, and space flows
- global event logs and request results

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9972933694db41dfb51eaf3332ef8374
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,102 @@
# FoldCC Brisk Game Server SDK
Brisk Unity SDK 的 UPM 发布目录。
当前仓库地址:
- `http://private.lightyears.ltd:18650/foldcc/CC-Framework.BriskGameServer`
当前包名:
- `com.foldcc.cc-framework.brisk-game-server`
## 如何引入到项目
推荐通过 Unity Package Manager 的 Git URL 方式引入。
### 方式一Package Manager
1. 打开 `Window > Package Manager`
2. 点击左上角 `+`
3. 选择 `Add package from git URL...`
4. 输入:
```text
http://private.lightyears.ltd:18650/foldcc/CC-Framework.BriskGameServer.git?path=/PackageSource/com.foldcc.cc-framework.brisk-game-server#v0.3.0
```
### 方式二:修改 `Packages/manifest.json`
```json
{
"dependencies": {
"com.foldcc.cc-framework.brisk-game-server": "http://private.lightyears.ltd:18650/foldcc/CC-Framework.BriskGameServer.git?path=/PackageSource/com.foldcc.cc-framework.brisk-game-server#v0.3.0"
}
}
```
如果需要跟随主分支最新代码,可将末尾的 `#v0.3.0` 改成 `#main`;正式环境建议固定到发布 tag。
## 开发态源码位置
开发工程中的活代码位于:
- `Assets/BriskSdk/Runtime`
- `Assets/BriskSdk/Samples/QuickStart`
- `Assets/Scenes/BriskQuickStartScene.unity`
同步 package 内容时执行:
```powershell
./Tools/Sync-BriskPackage.ps1
```
## 已包含模块
- Bootstrap 与初始化
- Auth 与会话恢复
- 玩家信息
- 动态配置
- 公告
- 排行榜
- 云存档上传下载
- 玩家空间
- 默认阻断式错误 UI
## Archive checksum
云存档上传时SDK 默认自动处理 checksum。
- `UploadAsync(slotNo, bytes)` 用于二进制
- `UploadTextAsync(slotNo, text)` 用于 UTF-8 文本
- `UploadJsonAsync(slotNo, payload)` 用于 JSON 对象
- `DownloadAsync(slotNo)` 返回原始 bytes 和元信息
- `DownloadTextAsync(slotNo)` 返回 UTF-8 文本
- `DownloadJsonAsync(slotNo)` 返回 JSON 结果
- SDK 会自动计算 SHA256
- 当前 Brisk archive API 要求纯小写 SHA256 十六进制字符串
- 不要传 `sha256:` 前缀
- 若手动传入带前缀的 checksumSDK 会自动归一化
## Space content
玩家空间当前采用 metadata + binary content 模型。
- `GetByPlayerIdAsync(...)``GetByLoginIdentityAsync(...)` 返回元数据
- `DownloadContentByPlayerIdAsync(...)``DownloadContentByLoginIdentityAsync(...)` 返回原始 bytes
- `UpdateMyAsync(string)` 直接上传文本
- `UpdateMyAsync(byte[])` 直接上传二进制
- `UpdateMyAsync(object)` 自动序列化为 JSON
## 目录结构
- `Runtime`
- `Samples~`
- `Documentation~`
## 快速开始
查看:
- `Documentation~/QuickStart.md`
- `Samples~/QuickStart`

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e05fdcaece3a1344ca3e98889cba8fc7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
/// <summary>
/// 公告模块。
/// </summary>
public sealed class BriskAnnouncementsModule
: BriskModuleBase
{
/// <summary>
/// 获取公告列表。
/// </summary>
public async Task<IReadOnlyList<BriskAnnouncementItem>> GetListAsync()
{
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.GetRawDataAsync("/announcements", null, true);
return (IReadOnlyList<BriskAnnouncementItem>)BriskModelMapper.ToAnnouncementItems(data);
});
}
/// <summary>
/// 标记指定公告为已读。
/// </summary>
public async Task MarkReadAsync(long id)
{
RequirePositive(id, nameof(id), "Announcement id must be greater than 0.");
await ExecuteAsync(async context =>
{
await context.HttpClient.PostJsonRawAsync($"/announcements/{id}/read", new Dictionary<string, object>(), true);
});
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d88c956c19b23c8499e142fff707b76c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using UnityEngine.Networking;
/// <summary>
/// 云存档模块。
/// </summary>
public sealed class BriskArchiveModule
: BriskModuleBase
{
/// <summary>
/// 获取当前账号的存档槽位列表。
/// </summary>
public async Task<IReadOnlyList<BriskArchiveSlot>> GetSlotsAsync()
{
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.GetRawDataAsync("/archives/slots", null, true);
return (IReadOnlyList<BriskArchiveSlot>)BriskModelMapper.ToArchiveSlots(data);
});
}
/// <summary>
/// 获取指定槽位的元信息。
/// </summary>
public async Task<BriskArchiveMeta> GetMetaAsync(int slotNo)
{
ValidateSlotNo(slotNo);
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.GetDataAsync($"/archives/slot/{slotNo}/meta", null, true);
return BriskModelMapper.ToArchiveMeta(data);
});
}
/// <summary>
/// 上传指定槽位的二进制存档。
/// </summary>
public async Task<BriskArchiveUploadResult> UploadAsync(int slotNo, byte[] bytes, int? baseVersion = null, string checksum = null)
{
ValidateSlotNo(slotNo);
if (bytes == null || bytes.Length == 0)
{
throw new ArgumentException("bytes is required.", nameof(bytes));
}
return await ExecuteAsync(async context =>
{
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : NormalizeChecksum(checksum);
var sections = new List<IMultipartFormSection>
{
new MultipartFormDataSection("base_version", (baseVersion ?? 0).ToString()),
new MultipartFormDataSection("checksum", finalChecksum),
new MultipartFormFileSection("file", bytes, "archive.bin", "application/octet-stream")
};
var data = await context.HttpClient.PostMultipartAsync($"/archives/slot/{slotNo}/upload", sections, true);
return BriskModelMapper.ToArchiveUploadResult(data);
});
}
/// <summary>
/// 以 UTF-8 文本形式上传指定槽位的存档。
/// </summary>
public Task<BriskArchiveUploadResult> UploadTextAsync(int slotNo, string text, int? baseVersion = null, string checksum = null)
{
RequireNotNull(text, nameof(text));
return UploadAsync(slotNo, Encoding.UTF8.GetBytes(text), baseVersion, checksum);
}
/// <summary>
/// 以 JSON 文本形式上传指定槽位的存档。
/// </summary>
public Task<BriskArchiveUploadResult> UploadJsonAsync(int slotNo, object payload, int? baseVersion = null, string checksum = null)
{
RequireNotNull(payload, nameof(payload));
return UploadAsync(slotNo, Encoding.UTF8.GetBytes(BriskJson.Serialize(payload)), baseVersion, checksum);
}
/// <summary>
/// 下载指定槽位的二进制存档。
/// </summary>
public async Task<BriskArchiveDownloadResult> DownloadAsync(int slotNo)
{
ValidateSlotNo(slotNo);
return await ExecuteAsync(async context =>
{
var response = await context.HttpClient.GetBytesAsync($"/archives/slot/{slotNo}/download", null, true);
return new BriskArchiveDownloadResult
{
Bytes = response.Bytes,
Version = ReadHeaderInt(response.Headers, "X-Archive-Version"),
Checksum = ReadHeader(response.Headers, "X-Archive-Checksum")
};
});
}
/// <summary>
/// 以 UTF-8 文本形式下载指定槽位的存档。
/// </summary>
public async Task<string> DownloadTextAsync(int slotNo)
{
var result = await DownloadAsync(slotNo);
return result == null || result.Bytes == null ? string.Empty : Encoding.UTF8.GetString(result.Bytes);
}
/// <summary>
/// 以 JSON 对象形式下载指定槽位的存档。
/// </summary>
public async Task<object> DownloadJsonAsync(int slotNo)
{
var text = await DownloadTextAsync(slotNo);
return string.IsNullOrWhiteSpace(text) ? null : BriskJson.Deserialize(text);
}
private static void ValidateSlotNo(int slotNo)
{
RequirePositive(slotNo, nameof(slotNo), "slotNo must be greater than 0.");
}
private static string ComputeSha256(byte[] bytes)
{
using (var sha = SHA256.Create())
{
var hash = sha.ComputeHash(bytes);
var builder = new StringBuilder(hash.Length * 2);
for (var i = 0; i < hash.Length; i++)
{
builder.Append(hash[i].ToString("x2"));
}
return builder.ToString();
}
}
private static string NormalizeChecksum(string checksum)
{
if (string.IsNullOrWhiteSpace(checksum))
{
return checksum;
}
var value = checksum.Trim();
return value.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)
? value.Substring("sha256:".Length)
: value;
}
private static int ReadHeaderInt(Dictionary<string, string> headers, string key)
{
var value = ReadHeader(headers, key);
return int.TryParse(value, out var result) ? result : 0;
}
private static string ReadHeader(Dictionary<string, string> headers, string key)
{
if (headers == null || string.IsNullOrWhiteSpace(key))
{
return null;
}
foreach (var pair in headers)
{
if (string.Equals(pair.Key, key, StringComparison.OrdinalIgnoreCase))
{
return pair.Value;
}
}
return null;
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3a26d0e58a94c264198010eae586c7b3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
/// <summary>
/// Brisk 认证模块。
/// </summary>
public sealed class BriskAuthModule
: BriskModuleBase
{
/// <summary>
/// 通过稳定的第三方用户 ID 换取 Brisk 登录态。
/// </summary>
public async Task<BriskLoginResult> LoginWithUserIdAsync(string loginProvider, string loginUserId, BriskProfile profile = null)
{
RequireNotEmpty(loginProvider, nameof(loginProvider));
RequireNotEmpty(loginUserId, nameof(loginUserId));
return await LoginInternalAsync(CreateLoginBody(loginProvider, profile, loginUserId, null), loginProvider, loginUserId);
}
/// <summary>
/// 通过第三方返回的 code 换取 Brisk 登录态。
/// </summary>
public async Task<BriskLoginResult> LoginWithCodeAsync(string loginProvider, string code, BriskProfile profile = null)
{
RequireNotEmpty(loginProvider, nameof(loginProvider));
RequireNotEmpty(code, nameof(code));
return await LoginInternalAsync(CreateLoginBody(loginProvider, profile, null, code), loginProvider, null);
}
/// <summary>
/// 登出当前账号并清理本地会话。
/// </summary>
public async Task LogoutAsync()
{
var context = GetContext();
try
{
if (context.Session.HasAccessToken)
{
await context.HttpClient.PostJsonAsync("/auth/logout", new Dictionary<string, object>(), true);
}
}
catch (BriskAuthExpiredException)
{
// Local logout should still succeed even if the remote token has already expired.
}
catch (BriskBlockingException exception)
{
Brisk.NotifyBlockingError(exception);
throw;
}
finally
{
context.Session.Clear();
await context.TokenStore.ClearAsync();
Brisk.NotifyLoggedOut();
}
}
private async Task<BriskLoginResult> LoginInternalAsync(Dictionary<string, object> body, string requestedLoginProvider, string requestedLoginUserId)
{
return await ExecutePublicAsync(async context =>
{
var data = await context.HttpClient.PostJsonAsync("/auth/login/exchange", body, false);
var result = BriskModelMapper.ToLoginResult(data);
if (string.IsNullOrWhiteSpace(result.LoginProvider))
{
result.LoginProvider = requestedLoginProvider;
}
if (string.IsNullOrWhiteSpace(result.LoginUserId))
{
result.LoginUserId = requestedLoginUserId;
}
await UpdateSessionAsync(context, result);
Brisk.NotifyLoggedIn();
return result;
});
}
private Dictionary<string, object> CreateLoginBody(string loginProvider, BriskProfile profile, string loginUserId, string code)
{
var context = GetContext();
var body = new Dictionary<string, object>
{
{ "game_key", context.Options.GameKey },
{ "login_provider", loginProvider }
};
AddIfNotEmpty(body, "login_user_id", loginUserId);
AddIfNotEmpty(body, "code", code);
AddIfNotEmpty(body, "device_id", context.Options.DeviceId);
AddIfNotEmpty(body, "client_version", context.Options.ClientVersion);
if (profile != null)
{
AddIfNotEmpty(body, "nickname", profile.Nickname);
AddIfNotEmpty(body, "avatar_url", profile.AvatarUrl);
if (profile.ProfileJson != null)
{
body["profile_json"] = profile.ProfileJson;
}
}
return body;
}
private static void AddIfNotEmpty(Dictionary<string, object> body, string key, string value)
{
if (!string.IsNullOrWhiteSpace(value))
{
body[key] = value;
}
}
private async Task UpdateSessionAsync(BriskContext context, BriskLoginResult result)
{
var expiresAt = result.ExpiresIn > 0
? DateTimeOffset.UtcNow.AddSeconds(result.ExpiresIn)
: (DateTimeOffset?)null;
context.Session.Update(
result.AccessToken,
expiresAt,
result.PlayerId,
result.ProjectAccountId,
result.LoginProvider,
result.LoginUserId);
var storedSession = new BriskStoredSession
{
AccessToken = result.AccessToken,
ExpiresAt = expiresAt,
PlayerId = result.PlayerId,
ProjectAccountId = result.ProjectAccountId,
LoginProvider = result.LoginProvider,
LoginUserId = result.LoginUserId
};
await context.TokenStore.SaveAsync(storedSession);
}
}

View File

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

View File

@@ -0,0 +1,14 @@
{
"name": "FoldCC.CCFramework.BriskGameServer.Runtime",
"rootNamespace": "FoldCC.CCFramework.BriskGameServer",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f8ab9462792dad445acc3b318dfa2372
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8b88bc9f438e4d64089acbb0ac667c03
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
/// <summary>
/// 动态配置模块。
/// </summary>
public sealed class BriskConfigModule
: BriskModuleBase
{
/// <summary>
/// 获取当前客户端命中的动态配置。
/// </summary>
public async Task<BriskConfigCurrent> GetCurrentAsync()
{
return await ExecutePublicAsync(async context =>
{
var data = await context.HttpClient.GetDataAsync("/config/current", CreateQuery(context), false);
return BriskModelMapper.ToConfigCurrent(data);
});
}
/// <summary>
/// 刷新并重新获取动态配置。
/// </summary>
public Task<BriskConfigCurrent> RefreshAsync()
{
return GetCurrentAsync();
}
private static Dictionary<string, string> CreateQuery(BriskContext context)
{
return new Dictionary<string, string>
{
{ "game_key", context.Options.GameKey },
{ "client_version", context.Options.ClientVersion }
};
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a5b8196730f24c0438900010f497476f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,281 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
/// <summary>
/// Brisk SDK 的静态总入口。
/// 初始化完成后,开发者可以通过 <c>Brisk.Auth</c>、<c>Brisk.Leaderboard</c> 等模块直接访问能力。
/// </summary>
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();
}
/// <summary>
/// SDK 初始化完成后触发。
/// </summary>
public static event Action OnInitialized;
/// <summary>
/// 登录成功并保存会话后触发。
/// </summary>
public static event Action OnLoggedIn;
/// <summary>
/// 主动登出并清理本地会话后触发。
/// </summary>
public static event Action OnLoggedOut;
/// <summary>
/// 发生维护、封禁、强更等严重阻断错误时触发。
/// </summary>
public static event Action<BriskBlockingException> OnBlockingError;
/// <summary>
/// 登录态失效并清空本地会话时触发。
/// </summary>
public static event Action<BriskAuthExpiredException> OnAuthExpired;
/// <summary>
/// 认证模块入口。
/// </summary>
public static BriskAuthModule Auth { get; }
/// <summary>
/// 玩家模块入口。
/// </summary>
public static BriskPlayerModule Player { get; }
/// <summary>
/// 动态配置模块入口。
/// </summary>
public static BriskConfigModule Config { get; }
/// <summary>
/// 公告模块入口。
/// </summary>
public static BriskAnnouncementsModule Announcements { get; }
/// <summary>
/// 排行榜模块入口。
/// </summary>
public static BriskLeaderboardModule Leaderboard { get; }
/// <summary>
/// 云存档模块入口。
/// </summary>
public static BriskArchiveModule Archive { get; }
/// <summary>
/// 玩家空间模块入口。
/// </summary>
public static BriskSpaceModule Space { get; }
/// <summary>
/// 当前 SDK 是否已完成初始化。
/// </summary>
public static bool IsInitialized => s_context != null;
/// <summary>
/// 当前是否存在可用登录态。
/// </summary>
public static bool IsLoggedIn => s_context != null && s_context.Session.HasAccessToken;
/// <summary>
/// 当前访问令牌。
/// </summary>
public static string AccessToken => s_context != null ? s_context.Session.AccessToken : null;
/// <summary>
/// 当前玩家 ID。
/// </summary>
public static string PlayerId => s_context != null ? s_context.Session.PlayerId : null;
/// <summary>
/// 当前登录身份摘要。
/// </summary>
public static BriskIdentity Identity => s_context != null ? s_context.Session.Identity : null;
/// <summary>
/// 当前初始化选项。
/// </summary>
public static BriskOptions Options => s_context != null ? s_context.Options : null;
/// <summary>
/// 初始化阶段获取到的 bootstrap 结果。
/// </summary>
public static BriskBootstrapResult Bootstrap => s_context != null ? s_context.Bootstrap : null;
/// <summary>
/// 初始化 SDK并执行 bootstrap 与本地会话恢复。
/// </summary>
/// <param name="options">初始化选项。</param>
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;
}
}
/// <summary>
/// 关闭 SDK 并清空当前上下文。
/// </summary>
public static void Shutdown()
{
s_context = null;
}
/// <summary>
/// 设置严重错误的展示器。
/// </summary>
/// <param name="presenter">自定义错误展示器;传入 null 时恢复默认展示器。</param>
public static void SetErrorPresenter(IBriskErrorPresenter presenter)
{
GetRequiredContext().ErrorPresenter = presenter ?? BriskDefaultErrorPresenter.Instance;
}
/// <summary>
/// 设置阻断错误确认后的退出回调。
/// </summary>
/// <param name="exitHandler">项目方自定义退出逻辑。</param>
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,273 @@
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)
{
return await SendMultipartAsync(UnityWebRequest.kHttpVerbPOST, path, formSections, auth);
}
public async Task<Dictionary<string, object>> PutMultipartAsync(string path, List<IMultipartFormSection> formSections, bool auth = false)
{
return await SendMultipartAsync(UnityWebRequest.kHttpVerbPUT, path, formSections, auth);
}
public async Task<BriskBinaryResponse> GetBytesAsync(string path, Dictionary<string, string> query = null, bool auth = false)
{
using (var request = new UnityWebRequest(BuildUrl(path, query), UnityWebRequest.kHttpVerbGET))
{
request.downloadHandler = new DownloadHandlerBuffer();
if (auth)
{
AddAuthorizationHeader(request);
}
request.SetRequestHeader("Accept", "*/*");
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 async Task<Dictionary<string, object>> SendMultipartAsync(string method, string path, List<IMultipartFormSection> formSections, bool auth)
{
using (var request = UnityWebRequest.Post(BuildUrl(path, null), formSections))
{
request.method = method;
if (auth)
{
AddAuthorizationHeader(request);
}
request.SetRequestHeader("Accept", "application/json");
await SendRequestAsync(request);
return ParseEnvelope(request) as Dictionary<string, object> ?? new Dictionary<string, object>();
}
}
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,388 @@
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
{
ProjectAccountId = BriskValueReader.GetString(data, "project_account_id"),
PlayerId = BriskValueReader.GetString(data, "player_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"),
ContentExists = BriskValueReader.GetBool(data, "content_exists"),
ContentVersion = BriskValueReader.GetLong(data, "content_version"),
ContentType = BriskValueReader.GetString(data, "content_type"),
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
LikeCount = BriskValueReader.GetLong(data, "like_count"),
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
LikedByMe = BriskValueReader.GetBool(data, "liked_by_me"),
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
};
}
public static BriskSpaceStats ToSpaceStats(Dictionary<string, object> data)
{
if (data == null)
{
return null;
}
return new BriskSpaceStats
{
ProjectAccountId = BriskValueReader.GetString(data, "project_account_id"),
PlayerId = BriskValueReader.GetString(data, "player_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"),
ContentExists = BriskValueReader.GetBool(data, "content_exists"),
ContentVersion = BriskValueReader.GetLong(data, "content_version"),
ContentType = BriskValueReader.GetString(data, "content_type"),
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
LikeCount = BriskValueReader.GetLong(data, "like_count"),
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
};
}
public static BriskSpaceContentUpdateResult ToSpaceContentUpdateResult(Dictionary<string, object> data)
{
if (data == null)
{
return null;
}
return new BriskSpaceContentUpdateResult
{
PlayerId = BriskValueReader.GetString(data, "player_id"),
ContentVersion = BriskValueReader.GetLong(data, "content_version"),
ContentType = BriskValueReader.GetString(data, "content_type"),
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
};
}
public static BriskSpaceLikeResult ToSpaceLikeResult(Dictionary<string, object> data)
{
if (data == null)
{
return null;
}
return new BriskSpaceLikeResult
{
Liked = BriskValueReader.GetBool(data, "liked"),
LikeCount = BriskValueReader.GetLong(data, "like_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 BriskSpaceLikeItem ToSpaceLikeItem(Dictionary<string, object> data)
{
if (data == null)
{
return null;
}
return new BriskSpaceLikeItem
{
PlayerId = BriskValueReader.GetString(data, "player_id"),
Nickname = BriskValueReader.GetString(data, "nickname"),
AvatarUrl = BriskValueReader.GetString(data, "avatar_url"),
CreatedAt = BriskValueReader.GetString(data, "created_at")
};
}
public static List<BriskSpaceLikeItem> ToSpaceLikeItems(object data)
{
return ExtractList(data)
.Select(item => ToSpaceLikeItem(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,73 @@
using System;
/// <summary>
/// Brisk SDK 初始化参数。
/// </summary>
public sealed class BriskOptions
{
/// <summary>
/// 服务端基础地址。可以传主机地址SDK 会自动补上 <c>/api</c>。
/// </summary>
public string BaseUrl;
/// <summary>
/// 项目标识。
/// </summary>
public string GameKey;
/// <summary>
/// 客户端版本号。
/// </summary>
public string ClientVersion;
/// <summary>
/// 设备标识。
/// </summary>
public string DeviceId;
/// <summary>
/// 是否启用调试日志。
/// </summary>
public bool EnableLog;
/// <summary>
/// 初始化时是否主动校验本地会话有效性。
/// </summary>
public bool ValidateSessionOnInitialize = true;
/// <summary>
/// 自定义登录态持久化实现。
/// </summary>
public IBriskTokenStore TokenStore;
/// <summary>
/// 自定义严重错误展示器。
/// </summary>
public IBriskErrorPresenter ErrorPresenter;
/// <summary>
/// 阻断错误确认后的退出处理逻辑。
/// </summary>
public Action ExitHandler;
/// <summary>
/// 校验并规范化初始化参数。
/// </summary>
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,15 @@
/// <summary>
/// 自定义严重错误展示接口。
/// </summary>
public interface IBriskErrorPresenter
{
/// <summary>
/// 展示维护、封禁、强更等阻断错误。
/// </summary>
void ShowBlockingError(BriskBlockingException exception);
/// <summary>
/// 展示登录态失效提示。
/// </summary>
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,22 @@
using System.Threading.Tasks;
/// <summary>
/// 自定义登录态持久化接口。
/// </summary>
public interface IBriskTokenStore
{
/// <summary>
/// 保存当前登录会话。
/// </summary>
Task SaveAsync(BriskStoredSession session);
/// <summary>
/// 读取本地保存的登录会话。
/// </summary>
Task<BriskStoredSession> LoadAsync();
/// <summary>
/// 清空本地保存的登录会话。
/// </summary>
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:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 49fea6e9e08845941b4ed3bb8e2beb68
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,6 @@
public sealed class BriskAccountBannedException : BriskBlockingException
{
public BriskAccountBannedException(string message, int code = 0) : base(message, code)
{
}
}

View File

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

View File

@@ -0,0 +1,15 @@
public class BriskApiException : BriskException
{
public BriskApiException(int httpStatus, int code, string serverMessage) : base(serverMessage)
{
HttpStatus = httpStatus;
Code = code;
ServerMessage = serverMessage;
}
public int HttpStatus { get; }
public int Code { get; }
public string ServerMessage { get; }
}

View File

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

View File

@@ -0,0 +1,9 @@
public sealed class BriskAuthExpiredException : BriskException
{
public BriskAuthExpiredException(string message, int code = 0) : base(message)
{
Code = code;
}
public int Code { get; }
}

View File

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

View File

@@ -0,0 +1,9 @@
public class BriskBlockingException : BriskException
{
public BriskBlockingException(string message, int code = 0) : base(message)
{
Code = code;
}
public int Code { get; }
}

View File

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

View File

@@ -0,0 +1,6 @@
public sealed class BriskClientUpdateRequiredException : BriskBlockingException
{
public BriskClientUpdateRequiredException(string message, int code = 0) : base(message, code)
{
}
}

View File

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

View File

@@ -0,0 +1,16 @@
using System;
public class BriskException : Exception
{
public BriskException()
{
}
public BriskException(string message) : base(message)
{
}
public BriskException(string message, Exception innerException) : base(message, innerException)
{
}
}

View File

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

View File

@@ -0,0 +1,9 @@
public sealed class BriskHttpException : BriskException
{
public BriskHttpException(int httpStatus, string message) : base(message)
{
HttpStatus = httpStatus;
}
public int HttpStatus { get; }
}

View File

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

View File

@@ -0,0 +1,6 @@
public sealed class BriskMaintenanceException : BriskBlockingException
{
public BriskMaintenanceException(string message, int code = 0) : base(message, code)
{
}
}

View File

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

View File

@@ -0,0 +1,8 @@
using System;
public sealed class BriskNetworkException : BriskException
{
public BriskNetworkException(string message, Exception innerException = null) : base(message, innerException)
{
}
}

View File

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

View File

@@ -0,0 +1,6 @@
public sealed class BriskNotInitializedException : BriskException
{
public BriskNotInitializedException() : base("Brisk has not been initialized. Call Brisk.InitializeAsync first.")
{
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4d16fffbab52af64eb1906f343d5382d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
/// <summary>
/// 排行榜模块。
/// </summary>
public sealed class BriskLeaderboardModule
: BriskModuleBase
{
/// <summary>
/// 获取排行榜 Top 列表。
/// </summary>
public async Task<IReadOnlyList<BriskLeaderboardEntry>> GetTopAsync(string rankKey, int limit = 20)
{
ValidateRankKey(rankKey);
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.GetRawDataAsync(
$"/ranks/{rankKey}/top",
CreateLimitQuery(limit),
true);
return (IReadOnlyList<BriskLeaderboardEntry>)BriskModelMapper.ToLeaderboardEntries(data);
});
}
/// <summary>
/// 获取当前玩家在排行榜中的信息。
/// </summary>
public async Task<BriskLeaderboardPlayerRank> GetMeAsync(string rankKey)
{
ValidateRankKey(rankKey);
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.GetRawDataAsync($"/ranks/{rankKey}/me", null, true);
return BriskModelMapper.ToLeaderboardPlayerRank(BriskModelMapper.ExtractObject(data));
});
}
/// <summary>
/// 获取当前玩家附近的排名区间。
/// </summary>
public async Task<IReadOnlyList<BriskLeaderboardEntry>> GetAroundMeAsync(string rankKey, int range = 10)
{
ValidateRankKey(rankKey);
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.GetRawDataAsync(
$"/ranks/{rankKey}/around-me",
CreateRangeQuery(range),
true);
return (IReadOnlyList<BriskLeaderboardEntry>)BriskModelMapper.ToLeaderboardEntries(data);
});
}
/// <summary>
/// 提交当前玩家分数。
/// </summary>
public async Task SubmitScoreAsync(string rankKey, long score)
{
ValidateRankKey(rankKey);
await ExecuteAsync(async context =>
{
await context.HttpClient.PostJsonRawAsync(
$"/ranks/{rankKey}/score",
new Dictionary<string, object> { { "score", score } },
true);
});
}
/// <summary>
/// 获取当前赛季信息。
/// </summary>
public async Task<BriskRankSeasonInfo> GetCurrentSeasonAsync(string rankKey)
{
ValidateRankKey(rankKey);
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.GetRawDataAsync($"/ranks/{rankKey}/season/current", null, true);
return BriskModelMapper.ToRankSeasonInfo(BriskModelMapper.ExtractObject(data));
});
}
/// <summary>
/// 获取赛季历史列表。
/// </summary>
public async Task<IReadOnlyList<BriskRankSeasonInfo>> GetSeasonHistoryAsync(string rankKey, int limit = 20)
{
ValidateRankKey(rankKey);
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.GetRawDataAsync(
$"/ranks/{rankKey}/history",
CreateLimitQuery(limit),
true);
return (IReadOnlyList<BriskRankSeasonInfo>)BriskModelMapper.ToRankSeasonInfos(data);
});
}
/// <summary>
/// 获取指定历史赛季的排行榜详情。
/// </summary>
public async Task<IReadOnlyList<BriskLeaderboardEntry>> GetSeasonHistoryDetailAsync(string rankKey, string seasonId, int limit = 20)
{
ValidateRankKey(rankKey);
RequireNotEmpty(seasonId, nameof(seasonId));
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.GetRawDataAsync(
$"/ranks/{rankKey}/history/{seasonId}",
CreateLimitQuery(limit),
true);
return (IReadOnlyList<BriskLeaderboardEntry>)BriskModelMapper.ToLeaderboardEntries(data);
});
}
private static Dictionary<string, string> CreateLimitQuery(int limit)
{
return limit > 0
? new Dictionary<string, string> { { "limit", limit.ToString() } }
: null;
}
private static Dictionary<string, string> CreateRangeQuery(int range)
{
return range > 0
? new Dictionary<string, string> { { "range", range.ToString() } }
: null;
}
private static void ValidateRankKey(string rankKey)
{
RequireNotEmpty(rankKey, nameof(rankKey));
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2dec6cd98bed43a4b9c2bbddd56d9db8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@
public sealed class BriskAnnouncementItem
{
public long Id;
public string Title;
public string Content;
public string ContentType;
public string StartAt;
public string EndAt;
public bool IsRead;
}

View File

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

View File

@@ -0,0 +1,6 @@
public sealed class BriskArchiveDownloadResult
{
public byte[] Bytes;
public int Version;
public string Checksum;
}

View File

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

View File

@@ -0,0 +1,8 @@
public sealed class BriskArchiveMeta
{
public int SlotNo;
public int Version;
public string Checksum;
public long SizeBytes;
public string UpdatedAt;
}

View File

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

View File

@@ -0,0 +1,7 @@
public sealed class BriskArchiveSlot
{
public int SlotNo;
public int Version;
public long SizeBytes;
public string UpdatedAt;
}

View File

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

View File

@@ -0,0 +1,7 @@
public sealed class BriskArchiveUploadResult
{
public int SlotNo;
public int Version;
public long SizeBytes;
public string UpdatedAt;
}

View File

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

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
public sealed class BriskBootstrapResult
{
public string ProjectName;
public string ServerTime;
public bool MaintenanceMode;
public string MaintenanceMessage;
public string MinClientVersion;
public Dictionary<string, object> FeatureFlags;
public Dictionary<string, object> DynamicConfig;
}

View File

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

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
/// <summary>
/// 当前客户端命中的动态配置结果。
/// </summary>
public sealed class BriskConfigCurrent
{
/// <summary>
/// 功能开关集合。
/// </summary>
public Dictionary<string, object> FeatureFlags;
/// <summary>
/// 动态配置集合。
/// </summary>
public Dictionary<string, object> DynamicConfig;
}

View File

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

View File

@@ -0,0 +1,22 @@
/// <summary>
/// 当前登录身份摘要。
/// </summary>
public sealed class BriskIdentity
{
/// <summary>
/// 登录提供方。
/// </summary>
public string LoginProvider;
/// <summary>
/// 登录用户 ID。
/// </summary>
public string LoginUserId;
/// <summary>
/// 玩家 ID。
/// </summary>
public string PlayerId;
/// <summary>
/// 项目账号 ID。
/// </summary>
public string ProjectAccountId;
}

View File

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

View File

@@ -0,0 +1,11 @@
public sealed class BriskLeaderboardEntry
{
public int Rank;
public string PlayerId;
public string ProjectAccountId;
public string LoginProvider;
public string LoginUserId;
public long Score;
public string Nickname;
public string AvatarUrl;
}

View File

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

View File

@@ -0,0 +1,11 @@
public sealed class BriskLeaderboardPlayerRank
{
public int Rank;
public long Score;
public string PlayerId;
public string ProjectAccountId;
public string LoginProvider;
public string LoginUserId;
public string Nickname;
public string AvatarUrl;
}

View File

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

View File

@@ -0,0 +1,38 @@
/// <summary>
/// 登录成功后的返回结果。
/// </summary>
public sealed class BriskLoginResult
{
/// <summary>
/// Brisk 访问令牌。
/// </summary>
public string AccessToken;
/// <summary>
/// 令牌有效时长,单位秒。
/// </summary>
public int ExpiresIn;
/// <summary>
/// 玩家 ID。
/// </summary>
public string PlayerId;
/// <summary>
/// 项目账号 ID。
/// </summary>
public string ProjectAccountId;
/// <summary>
/// 登录提供方。
/// </summary>
public string LoginProvider;
/// <summary>
/// 登录用户 ID。
/// </summary>
public string LoginUserId;
/// <summary>
/// 是否为新玩家。
/// </summary>
public bool IsNewPlayer;
/// <summary>
/// 玩家资料。
/// </summary>
public BriskProfile Profile;
}

Some files were not shown because too many files have changed in this diff Show More