6 Commits

169 changed files with 4257 additions and 1914 deletions

View File

@@ -50,7 +50,7 @@ public sealed class BriskArchiveModule
return await ExecuteAsync(async context =>
{
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : checksum;
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : NormalizeChecksum(checksum);
var sections = new List<IMultipartFormSection>
{
new MultipartFormDataSection("base_version", (baseVersion ?? 0).ToString()),
@@ -63,6 +63,24 @@ public sealed class BriskArchiveModule
});
}
/// <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>
@@ -82,6 +100,24 @@ public sealed class BriskArchiveModule
});
}
/// <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.");
@@ -92,8 +128,7 @@ public sealed class BriskArchiveModule
using (var sha = SHA256.Create())
{
var hash = sha.ComputeHash(bytes);
var builder = new StringBuilder(hash.Length * 2 + 7);
builder.Append("sha256:");
var builder = new StringBuilder(hash.Length * 2);
for (var i = 0; i < hash.Length; i++)
{
builder.Append(hash[i].ToString("x2"));
@@ -103,6 +138,19 @@ public sealed class BriskArchiveModule
}
}
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);

View File

@@ -51,23 +51,26 @@ public sealed class BriskHttpClient
public async Task<Dictionary<string, object>> PostMultipartAsync(string path, List<IMultipartFormSection> formSections, bool auth = false)
{
using (var request = UnityWebRequest.Post(BuildUrl(path, null), formSections))
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", "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))
{
request.SetRequestHeader("Accept", "*/*");
await SendRequestAsync(request);
EnsureSuccessOrThrow(request);
return new BriskBinaryResponse
@@ -93,6 +96,23 @@ public sealed class BriskHttpClient
}
}
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);

View File

@@ -237,10 +237,23 @@ internal static class BriskModelMapper
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"),
Payload = BriskValueReader.GetDictionary(data, "payload_json") ?? BriskValueReader.GetDictionary(data, "payload")
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"),
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
LikedByMe = BriskValueReader.GetBool(data, "liked_by_me"),
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at"),
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
};
}
@@ -253,8 +266,58 @@ internal static class BriskModelMapper
return new BriskSpaceStats
{
LikeCount = BriskValueReader.GetInt(data, "like_count"),
VisitCount = BriskValueReader.GetInt(data, "visit_count")
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"),
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at"),
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"),
Created = BriskValueReader.GetBool(data, "created"),
LikedByMe = BriskValueReader.GetBool(data, "liked_by_me"),
LikeCount = BriskValueReader.GetLong(data, "like_count"),
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at")
};
}
@@ -284,6 +347,30 @@ internal static class BriskModelMapper
.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>;

View File

@@ -0,0 +1,7 @@
public sealed class BriskSpaceContentDownloadResult
{
public byte[] Bytes;
public long Version;
public string Checksum;
public string ContentType;
}

View File

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

View File

@@ -0,0 +1,9 @@
public sealed class BriskSpaceContentUpdateResult
{
public string PlayerId;
public long ContentVersion;
public string ContentType;
public long ContentSizeBytes;
public string ContentChecksum;
public string UpdatedAt;
}

View File

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

View File

@@ -0,0 +1,7 @@
public sealed class BriskSpaceLikeItem
{
public string PlayerId;
public string Nickname;
public string AvatarUrl;
public string CreatedAt;
}

View File

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

View File

@@ -0,0 +1,9 @@
public sealed class BriskSpaceLikeResult
{
public bool Liked;
public bool Created;
public bool LikedByMe;
public long LikeCount;
public long TodayLikeCount;
public string LikeResetAt;
}

View File

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

View File

@@ -1,5 +1,19 @@
public sealed class BriskSpaceStats
{
public int LikeCount;
public int VisitCount;
public string ProjectAccountId;
public string PlayerId;
public string LoginProvider;
public string LoginUserId;
public string Nickname;
public string AvatarUrl;
public bool ContentExists;
public long ContentVersion;
public string ContentType;
public long ContentSizeBytes;
public string ContentChecksum;
public long LikeCount;
public long TodayLikeCount;
public long VisitCount;
public string LikeResetAt;
public string UpdatedAt;
}

View File

@@ -1,7 +1,20 @@
public sealed class BriskSpaceView
{
public string ProjectAccountId;
public string PlayerId;
public string LoginProvider;
public string LoginUserId;
public object Payload;
public string Nickname;
public string AvatarUrl;
public bool ContentExists;
public long ContentVersion;
public string ContentType;
public long ContentSizeBytes;
public string ContentChecksum;
public long LikeCount;
public long TodayLikeCount;
public long VisitCount;
public bool LikedByMe;
public string LikeResetAt;
public string UpdatedAt;
}

View File

@@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using UnityEngine.Networking;
/// <summary>
/// 玩家空间模块。
@@ -65,77 +68,183 @@ public sealed class BriskSpaceModule
}
/// <summary>
/// 按玩家 ID 点赞空间
/// 按玩家 ID 获取点赞列表
/// </summary>
public async Task LikeByPlayerIdAsync(string playerId)
public async Task<IReadOnlyList<BriskSpaceLikeItem>> GetLikesByPlayerIdAsync(string playerId, int limit = 20, bool currentCycleOnly = false)
{
ValidatePlayerId(playerId);
await ExecuteAsync(async context =>
return await ExecuteAsync(async context =>
{
await context.HttpClient.PostJsonRawAsync($"/spaces/{playerId}/like", new Dictionary<string, object>(), true);
var data = await context.HttpClient.GetRawDataAsync($"/spaces/{playerId}/likes", CreateLikesQuery(limit, currentCycleOnly), true);
return (IReadOnlyList<BriskSpaceLikeItem>)BriskModelMapper.ToSpaceLikeItems(data);
});
}
/// <summary>
/// 按登录身份获取点赞列表。
/// </summary>
public async Task<IReadOnlyList<BriskSpaceLikeItem>> GetLikesByLoginIdentityAsync(string loginProvider, string loginUserId, int limit = 20, bool currentCycleOnly = false)
{
ValidateLoginIdentity(loginProvider, loginUserId);
return await ExecuteAsync(async context =>
{
var query = CreateLoginIdentityQuery(loginProvider, loginUserId);
query["limit"] = NormalizeLimit(limit);
if (currentCycleOnly)
{
query["scope"] = "cycle";
}
var data = await context.HttpClient.GetRawDataAsync("/spaces/by-login/likes", query, true);
return (IReadOnlyList<BriskSpaceLikeItem>)BriskModelMapper.ToSpaceLikeItems(data);
});
}
/// <summary>
/// 按玩家 ID 点赞空间。
/// </summary>
public async Task<BriskSpaceLikeResult> LikeByPlayerIdAsync(string playerId)
{
ValidatePlayerId(playerId);
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.PostJsonRawAsync($"/spaces/{playerId}/like", new Dictionary<string, object>(), true);
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
});
}
/// <summary>
/// 按玩家 ID 取消点赞空间。
/// </summary>
public async Task UnlikeByPlayerIdAsync(string playerId)
public async Task<BriskSpaceLikeResult> UnlikeByPlayerIdAsync(string playerId)
{
ValidatePlayerId(playerId);
await ExecuteAsync(async context =>
return await ExecuteAsync(async context =>
{
await context.HttpClient.SendDeleteJsonRawAsync($"/spaces/{playerId}/like", null, true);
var data = await context.HttpClient.SendDeleteJsonRawAsync($"/spaces/{playerId}/like", null, true);
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
});
}
/// <summary>
/// 按登录身份点赞空间。
/// </summary>
public async Task LikeByLoginIdentityAsync(string loginProvider, string loginUserId)
public async Task<BriskSpaceLikeResult> LikeByLoginIdentityAsync(string loginProvider, string loginUserId)
{
ValidateLoginIdentity(loginProvider, loginUserId);
await ExecuteAsync(async context =>
return await ExecuteAsync(async context =>
{
await context.HttpClient.PostJsonRawAsync("/spaces/by-login/like", new Dictionary<string, object>(), true, CreateLoginIdentityQuery(loginProvider, loginUserId));
var data = await context.HttpClient.PostJsonRawAsync("/spaces/by-login/like", new Dictionary<string, object>(), true, CreateLoginIdentityQuery(loginProvider, loginUserId));
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
});
}
/// <summary>
/// 按登录身份取消点赞空间。
/// </summary>
public async Task UnlikeByLoginIdentityAsync(string loginProvider, string loginUserId)
public async Task<BriskSpaceLikeResult> UnlikeByLoginIdentityAsync(string loginProvider, string loginUserId)
{
ValidateLoginIdentity(loginProvider, loginUserId);
await ExecuteAsync(async context =>
return await ExecuteAsync(async context =>
{
await context.HttpClient.SendDeleteJsonRawAsync("/spaces/by-login/like", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
var data = await context.HttpClient.SendDeleteJsonRawAsync("/spaces/by-login/like", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
});
}
/// <summary>
/// 上传当前玩家自己的空间内容。
/// </summary>
public async Task<BriskSpaceContentUpdateResult> UploadMyContentAsync(long? baseVersion, string contentType, string checksum, byte[] bytes)
{
RequireNotNull(bytes, 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 ?? 0L).ToString()),
new MultipartFormFileSection("file", bytes, "space-content.bin", string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType)
};
if (!string.IsNullOrWhiteSpace(contentType))
{
sections.Insert(1, new MultipartFormDataSection("content_type", contentType));
}
if (!string.IsNullOrWhiteSpace(finalChecksum))
{
sections.Insert(string.IsNullOrWhiteSpace(contentType) ? 1 : 2, new MultipartFormDataSection("checksum", finalChecksum));
}
var data = await context.HttpClient.PutMultipartAsync("/spaces/me/content", sections, true);
return BriskModelMapper.ToSpaceContentUpdateResult(data);
});
}
/// <summary>
/// 更新当前玩家自己的空间内容。
/// </summary>
public async Task UpdateMyAsync(object payload)
public Task<BriskSpaceContentUpdateResult> UpdateMyAsync(object payload, long? baseVersion = null, string contentType = null, string checksum = null)
{
RequireNotNull(payload, nameof(payload));
await ExecuteAsync(async context =>
if (payload is byte[] bytes)
{
await context.HttpClient.SendPutJsonRawAsync(
"/spaces/me",
new Dictionary<string, object> { { "payload_json", payload } },
true);
return UploadMyContentAsync(baseVersion, contentType ?? "application/octet-stream", checksum, bytes);
}
if (payload is string text)
{
return UploadMyContentAsync(baseVersion, contentType ?? "text/plain", checksum, Encoding.UTF8.GetBytes(text));
}
var json = BriskJson.Serialize(payload);
return UploadMyContentAsync(baseVersion, contentType ?? "application/json", checksum, Encoding.UTF8.GetBytes(json));
}
/// <summary>
/// 按玩家 ID 下载空间内容。
/// </summary>
public async Task<BriskSpaceContentDownloadResult> DownloadContentByPlayerIdAsync(string playerId)
{
ValidatePlayerId(playerId);
return await ExecuteAsync(async context =>
{
var response = await context.HttpClient.GetBytesAsync($"/spaces/{playerId}/content", null, true);
return CreateDownloadResult(response);
});
}
/// <summary>
/// 按登录身份下载空间内容。
/// </summary>
public async Task<BriskSpaceContentDownloadResult> DownloadContentByLoginIdentityAsync(string loginProvider, string loginUserId)
{
ValidateLoginIdentity(loginProvider, loginUserId);
return await ExecuteAsync(async context =>
{
var response = await context.HttpClient.GetBytesAsync("/spaces/by-login/content", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
return CreateDownloadResult(response);
});
}
/// <summary>
/// 获取我的访客列表。
/// </summary>
public async Task<IReadOnlyList<BriskSpaceVisit>> GetMyVisitsAsync()
public async Task<IReadOnlyList<BriskSpaceVisit>> GetMyVisitsAsync(int limit = 50)
{
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.GetRawDataAsync("/spaces/me/visits", null, true);
var data = await context.HttpClient.GetRawDataAsync("/spaces/me/visits", CreateLimitQuery(limit), true);
return (IReadOnlyList<BriskSpaceVisit>)BriskModelMapper.ToSpaceVisits(data);
});
}
@@ -149,6 +258,25 @@ public sealed class BriskSpaceModule
};
}
private static Dictionary<string, string> CreateLimitQuery(int limit)
{
return new Dictionary<string, string>
{
{ "limit", NormalizeLimit(limit) }
};
}
private static Dictionary<string, string> CreateLikesQuery(int limit, bool currentCycleOnly)
{
var query = CreateLimitQuery(limit);
if (currentCycleOnly)
{
query["scope"] = "cycle";
}
return query;
}
private static void ValidatePlayerId(string playerId)
{
RequireNotEmpty(playerId, nameof(playerId));
@@ -159,4 +287,72 @@ public sealed class BriskSpaceModule
RequireNotEmpty(loginProvider, nameof(loginProvider));
RequireNotEmpty(loginUserId, nameof(loginUserId));
}
private static string NormalizeLimit(int limit)
{
return (limit > 0 ? limit : 20).ToString();
}
private static BriskSpaceContentDownloadResult CreateDownloadResult(BriskBinaryResponse response)
{
return new BriskSpaceContentDownloadResult
{
Bytes = response.Bytes,
Version = ReadHeaderLong(response.Headers, "X-Space-Version"),
Checksum = ReadHeader(response.Headers, "X-Space-Checksum"),
ContentType = ReadHeader(response.Headers, "Content-Type")
};
}
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 long ReadHeaderLong(Dictionary<string, string> headers, string key)
{
var value = ReadHeader(headers, key);
return long.TryParse(value, out var result) ? result : 0L;
}
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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -38,7 +38,6 @@ RenderSettings:
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
@@ -104,7 +103,7 @@ NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 2
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
@@ -117,7 +116,7 @@ NavMeshSettings:
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
accuratePlacement: 0
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
@@ -163,9 +162,17 @@ Camera:
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_FocalLength: 50
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
@@ -199,12 +206,13 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 519420028}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1600000000
GameObject:
@@ -230,12 +238,13 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1600000000}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1600000002
MonoBehaviour:
@@ -250,16 +259,16 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
BaseUrl: https://brisk.lightyears.ltd
GameKey: demo-game
GameKey: game_a1faf5ee93d0
ClientVersion: 1.0.0
DeviceId: editor-device
ValidateSessionOnInitialize: 1
LoginProvider: tap
LoginUserId: tap_user_10001
LoginCode:
Nickname: Unity示例玩家
Nickname: "Unity\u793A\u4F8B\u73A9\u5BB6"
AvatarUrl:
RankKey: season-score
RankKey: rank-20260411062004-2bce
SubmitScoreValue: 128
LeaderboardLimit: 10
AroundMeRange: 5
@@ -267,10 +276,18 @@ MonoBehaviour:
SeasonHistoryLimit: 20
ArchiveSlotNo: 1
ArchiveBaseVersion:
ArchiveContent: '{"save":1,"coins":128,"hero":"mage","title":"中文测试存档"}'
ArchiveContent: "{\n \"save\": 1,\n \"coins\": 128,\n \"hero\": \"mage\",\n
\"title\": \"\u4E2D\u6587\u6D4B\u8BD5\u5B58\u6863\"\n}"
AnnouncementId:
SpacePlayerId:
SpaceLoginProvider: tap
SpaceLoginUserId: tap_user_10001
SpacePayloadText: '{"mood":"ready","title":"你好 Brisk","desc":"这是中文测试空间数据"}'
SpacePayloadText: "{\n \"mood\": \"ready\",\n \"title\": \"\u4F60\u597D Brisk\",\n
\"desc\": \"\u8FD9\u662F\u4E2D\u6587\u6D4B\u8BD5\u7A7A\u95F4\u6570\u636E\"\n}"
AutoRunOnStart: 0
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 519420032}
- {fileID: 1600000001}

View File

@@ -1,8 +0,0 @@
# Changelog
## 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

@@ -1,43 +0,0 @@
# 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);
```
## 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

@@ -1,42 +0,0 @@
# FoldCC Brisk Game Server SDK
This directory is the package publishing skeleton for Brisk.
The active Unity project source currently lives under:
- `Assets/BriskSdk/Runtime`
- `Assets/BriskSdk/Samples/QuickStart`
- `Assets/Scenes/BriskQuickStartScene.unity`
Sync package content from the Unity source project with:
- `Tools/Sync-BriskPackage.ps1`
## Included runtime modules
- Bootstrap and initialization
- Auth and session restore
- Player profile
- Dynamic config
- Announcements
- Leaderboard
- Archive upload and download
- Player space
- Default blocking error UI
## Package layout
- `Runtime`
- `Samples~`
- `Documentation~`
## Quick start
See:
- `Documentation~/QuickStart.md`
- `Samples~/QuickStart`
This sample uses an IMGUI test panel for end-to-end SDK flow verification.
When preparing a publish branch, place the package-ready runtime, samples, and docs into this package directory and tag that branch for external consumption.

View File

@@ -1,5 +0,0 @@
public sealed class BriskSpaceStats
{
public int LikeCount;
public int VisitCount;
}

View File

@@ -1,7 +0,0 @@
public sealed class BriskSpaceView
{
public string PlayerId;
public string LoginProvider;
public string LoginUserId;
public object Payload;
}

View File

@@ -1,162 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
/// <summary>
/// 玩家空间模块。
/// </summary>
public sealed class BriskSpaceModule
: BriskModuleBase
{
/// <summary>
/// 按玩家 ID 获取空间数据。
/// </summary>
public async Task<BriskSpaceView> GetByPlayerIdAsync(string playerId)
{
ValidatePlayerId(playerId);
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.GetDataAsync($"/spaces/{playerId}", null, true);
return BriskModelMapper.ToSpaceView(data);
});
}
/// <summary>
/// 按登录身份获取空间数据。
/// </summary>
public async Task<BriskSpaceView> GetByLoginIdentityAsync(string loginProvider, string loginUserId)
{
ValidateLoginIdentity(loginProvider, loginUserId);
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.GetDataAsync("/spaces/by-login", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
return BriskModelMapper.ToSpaceView(data);
});
}
/// <summary>
/// 按玩家 ID 获取空间统计。
/// </summary>
public async Task<BriskSpaceStats> GetStatsByPlayerIdAsync(string playerId)
{
ValidatePlayerId(playerId);
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.GetDataAsync($"/spaces/{playerId}/stats", null, true);
return BriskModelMapper.ToSpaceStats(data);
});
}
/// <summary>
/// 按登录身份获取空间统计。
/// </summary>
public async Task<BriskSpaceStats> GetStatsByLoginIdentityAsync(string loginProvider, string loginUserId)
{
ValidateLoginIdentity(loginProvider, loginUserId);
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.GetDataAsync("/spaces/by-login/stats", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
return BriskModelMapper.ToSpaceStats(data);
});
}
/// <summary>
/// 按玩家 ID 点赞空间。
/// </summary>
public async Task LikeByPlayerIdAsync(string playerId)
{
ValidatePlayerId(playerId);
await ExecuteAsync(async context =>
{
await context.HttpClient.PostJsonRawAsync($"/spaces/{playerId}/like", new Dictionary<string, object>(), true);
});
}
/// <summary>
/// 按玩家 ID 取消点赞空间。
/// </summary>
public async Task UnlikeByPlayerIdAsync(string playerId)
{
ValidatePlayerId(playerId);
await ExecuteAsync(async context =>
{
await context.HttpClient.SendDeleteJsonRawAsync($"/spaces/{playerId}/like", null, true);
});
}
/// <summary>
/// 按登录身份点赞空间。
/// </summary>
public async Task LikeByLoginIdentityAsync(string loginProvider, string loginUserId)
{
ValidateLoginIdentity(loginProvider, loginUserId);
await ExecuteAsync(async context =>
{
await context.HttpClient.PostJsonRawAsync("/spaces/by-login/like", new Dictionary<string, object>(), true, CreateLoginIdentityQuery(loginProvider, loginUserId));
});
}
/// <summary>
/// 按登录身份取消点赞空间。
/// </summary>
public async Task UnlikeByLoginIdentityAsync(string loginProvider, string loginUserId)
{
ValidateLoginIdentity(loginProvider, loginUserId);
await ExecuteAsync(async context =>
{
await context.HttpClient.SendDeleteJsonRawAsync("/spaces/by-login/like", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
});
}
/// <summary>
/// 更新当前玩家自己的空间内容。
/// </summary>
public async Task UpdateMyAsync(object payload)
{
RequireNotNull(payload, nameof(payload));
await ExecuteAsync(async context =>
{
await context.HttpClient.SendPutJsonRawAsync(
"/spaces/me",
new Dictionary<string, object> { { "payload_json", payload } },
true);
});
}
/// <summary>
/// 获取我的访客列表。
/// </summary>
public async Task<IReadOnlyList<BriskSpaceVisit>> GetMyVisitsAsync()
{
return await ExecuteAsync(async context =>
{
var data = await context.HttpClient.GetRawDataAsync("/spaces/me/visits", null, true);
return (IReadOnlyList<BriskSpaceVisit>)BriskModelMapper.ToSpaceVisits(data);
});
}
private static Dictionary<string, string> CreateLoginIdentityQuery(string loginProvider, string loginUserId)
{
return new Dictionary<string, string>
{
{ "login_provider", loginProvider },
{ "login_user_id", loginUserId }
};
}
private static void ValidatePlayerId(string playerId)
{
RequireNotEmpty(playerId, nameof(playerId));
}
private static void ValidateLoginIdentity(string loginProvider, string loginUserId)
{
RequireNotEmpty(loginProvider, nameof(loginProvider));
RequireNotEmpty(loginUserId, nameof(loginUserId));
}
}

View File

@@ -1,922 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
public sealed class BriskQuickStartSample : MonoBehaviour
{
[Header("初始化")]
public string BaseUrl = "https://brisk.lightyears.ltd";
public string GameKey = "demo-game";
public string ClientVersion = "1.0.0";
public string DeviceId = "editor-device";
public bool ValidateSessionOnInitialize = true;
[Header("登录")]
public string LoginProvider = "tap";
public string LoginUserId = "tap_user_10001";
public string LoginCode = string.Empty;
public string Nickname = "Unity示例玩家";
public string AvatarUrl = string.Empty;
[Header("排行榜")]
public string RankKey = "season-score";
public string SubmitScoreValue = "128";
public string LeaderboardLimit = "10";
public string AroundMeRange = "5";
public string SeasonId = string.Empty;
public string SeasonHistoryLimit = "20";
[Header("云存档")]
public string ArchiveSlotNo = "1";
public string ArchiveBaseVersion = string.Empty;
[TextArea(3, 6)]
public string ArchiveContent = "{\n \"save\": 1,\n \"coins\": 128,\n \"hero\": \"mage\",\n \"title\": \"中文测试存档\"\n}";
[Header("公告")]
public string AnnouncementId = string.Empty;
[Header("玩家空间")]
public string SpacePlayerId = string.Empty;
public string SpaceLoginProvider = "tap";
public string SpaceLoginUserId = "tap_user_10001";
[TextArea(3, 6)]
public string SpacePayloadText = "{\n \"mood\": \"ready\",\n \"title\": \"你好 Brisk\",\n \"desc\": \"这是中文测试空间数据\"\n}";
[Header("演示")]
public bool AutoRunOnStart;
private readonly List<string> _eventLogs = new List<string>();
private Vector2 _pageScroll;
private Vector2 _resultScroll;
private Vector2 _logScroll;
private bool _isBusy;
private string _busyAction = string.Empty;
private string _statusText = "就绪";
private string _resultText = "尚未执行请求。";
private string _lastErrorText = string.Empty;
private IReadOnlyList<BriskAnnouncementItem> _announcementCache = Array.Empty<BriskAnnouncementItem>();
private IReadOnlyList<BriskRankSeasonInfo> _seasonHistoryCache = Array.Empty<BriskRankSeasonInfo>();
private void OnEnable()
{
Brisk.OnInitialized += HandleInitialized;
Brisk.OnLoggedIn += HandleLoggedIn;
Brisk.OnLoggedOut += HandleLoggedOut;
Brisk.OnAuthExpired += HandleAuthExpired;
Brisk.OnBlockingError += HandleBlockingError;
}
private void OnDisable()
{
Brisk.OnInitialized -= HandleInitialized;
Brisk.OnLoggedIn -= HandleLoggedIn;
Brisk.OnLoggedOut -= HandleLoggedOut;
Brisk.OnAuthExpired -= HandleAuthExpired;
Brisk.OnBlockingError -= HandleBlockingError;
}
private void Start()
{
if (AutoRunOnStart)
{
RunAction("自动冒烟流程", RunSmokeFlowAsync);
}
}
[ContextMenu("运行 Brisk 示例")]
public void RunFromContextMenu()
{
RunAction("右键菜单冒烟流程", RunSmokeFlowAsync);
}
private void OnGUI()
{
var area = new Rect(12f, 12f, Screen.width - 24f, Screen.height - 24f);
GUILayout.BeginArea(area, GUI.skin.box);
_pageScroll = GUILayout.BeginScrollView(_pageScroll);
DrawHeader();
DrawStatusPanel();
DrawInitSection();
DrawLoginSection();
DrawPlayerAndConfigSection();
DrawAnnouncementsSection();
DrawLeaderboardSection();
DrawArchiveSection();
DrawSpaceSection();
DrawOutputSection();
GUILayout.EndScrollView();
GUILayout.EndArea();
}
private void DrawHeader()
{
GUILayout.Label("Brisk IMGUI 测试面板", GUI.skin.box);
GUILayout.Label("这个场景用于在一个页面内测试 SDK 的完整流程。", GUI.skin.label);
}
private void DrawStatusPanel()
{
BeginSection("运行状态");
DrawReadOnlyRow("已初始化", Brisk.IsInitialized ? "是" : "否");
DrawReadOnlyRow("已登录", Brisk.IsLoggedIn ? "是" : "否");
DrawReadOnlyRow("PlayerId", Brisk.PlayerId);
DrawReadOnlyRow("当前身份", Brisk.Identity == null ? string.Empty : Brisk.Identity.LoginProvider + " / " + Brisk.Identity.LoginUserId);
DrawReadOnlyRow("当前动作", _isBusy ? _busyAction : "空闲");
DrawReadOnlyRow("状态", _statusText);
GUILayout.BeginHorizontal();
DrawButton("执行冒烟流程", RunSmokeFlowAsync);
DrawButton("查看 Bootstrap 缓存", () =>
{
SetResult("Bootstrap 缓存", Brisk.Bootstrap);
return Task.CompletedTask;
}, Brisk.IsInitialized);
DrawButton("关闭 SDK", () =>
{
Brisk.Shutdown();
Log("SDK 已关闭。");
_statusText = "SDK 已关闭";
return Task.CompletedTask;
});
DrawButton("清空输出", () =>
{
_resultText = "输出已清空。";
_lastErrorText = string.Empty;
_eventLogs.Clear();
_statusText = "输出已清空";
return Task.CompletedTask;
});
GUILayout.EndHorizontal();
EndSection();
}
private void DrawInitSection()
{
BeginSection("初始化");
BaseUrl = DrawEditableRow("服务地址", BaseUrl);
GameKey = DrawEditableRow("游戏 Key", GameKey);
ClientVersion = DrawEditableRow("客户端版本", ClientVersion);
DeviceId = DrawEditableRow("设备标识", DeviceId);
ValidateSessionOnInitialize = DrawToggleRow("初始化时校验旧会话", ValidateSessionOnInitialize);
GUILayout.BeginHorizontal();
DrawButton("初始化", InitializeAsync);
DrawButton("重新初始化", ReinitializeAsync);
GUILayout.EndHorizontal();
EndSection();
}
private void DrawLoginSection()
{
BeginSection("登录");
LoginProvider = DrawEditableRow("登录提供方", LoginProvider);
LoginUserId = DrawEditableRow("登录用户 ID", LoginUserId);
LoginCode = DrawEditableRow("登录 Code", LoginCode);
Nickname = DrawEditableRow("昵称", Nickname);
AvatarUrl = DrawEditableRow("头像地址", AvatarUrl);
GUILayout.BeginHorizontal();
DrawButton("按用户 ID 登录", LoginWithUserIdAsync, Brisk.IsInitialized);
DrawButton("按 Code 登录", LoginWithCodeAsync, Brisk.IsInitialized);
DrawButton("登出", LogoutAsync, Brisk.IsInitialized);
GUILayout.EndHorizontal();
EndSection();
}
private void DrawPlayerAndConfigSection()
{
BeginSection("玩家与配置");
GUILayout.BeginHorizontal();
DrawButton("获取当前玩家", GetMeAsync, Brisk.IsLoggedIn);
DrawButton("获取当前配置", GetConfigAsync, Brisk.IsInitialized);
DrawButton("同步当前身份到空间查询", () =>
{
ApplyCurrentIdentityToSpace();
SetResult("空间查询身份", new Dictionary<string, object>
{
{ "space_player_id", SpacePlayerId },
{ "space_login_provider", SpaceLoginProvider },
{ "space_login_user_id", SpaceLoginUserId }
});
return Task.CompletedTask;
}, Brisk.IsInitialized);
GUILayout.EndHorizontal();
EndSection();
}
private void DrawAnnouncementsSection()
{
BeginSection("公告");
AnnouncementId = DrawEditableRow("公告 ID", AnnouncementId);
GUILayout.BeginHorizontal();
DrawButton("获取公告列表", GetAnnouncementsAsync, Brisk.IsLoggedIn);
DrawButton("标记已读", MarkAnnouncementAsync, Brisk.IsLoggedIn);
DrawButton("标记首条缓存公告已读", MarkFirstCachedAnnouncementAsync, Brisk.IsLoggedIn && _announcementCache.Count > 0);
GUILayout.EndHorizontal();
EndSection();
}
private void DrawLeaderboardSection()
{
BeginSection("排行榜");
RankKey = DrawEditableRow("排行榜 Key", RankKey);
SubmitScoreValue = DrawEditableRow("提交分数", SubmitScoreValue);
LeaderboardLimit = DrawEditableRow("Top 数量", LeaderboardLimit);
AroundMeRange = DrawEditableRow("附近范围", AroundMeRange);
SeasonId = DrawEditableRow("赛季 ID", SeasonId);
SeasonHistoryLimit = DrawEditableRow("历史条数", SeasonHistoryLimit);
GUILayout.BeginHorizontal();
DrawButton("获取 Top", GetTopAsync, Brisk.IsLoggedIn);
DrawButton("获取我的排名", GetMyRankAsync, Brisk.IsLoggedIn);
DrawButton("获取我附近的排名", GetAroundMeAsync, Brisk.IsLoggedIn);
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
DrawButton("提交分数", SubmitScoreAsync, Brisk.IsLoggedIn);
DrawButton("获取当前赛季", GetCurrentSeasonAsync, Brisk.IsLoggedIn);
DrawButton("获取赛季历史", GetSeasonHistoryAsync, Brisk.IsLoggedIn);
DrawButton("获取赛季详情", GetSeasonDetailAsync, Brisk.IsLoggedIn);
GUILayout.EndHorizontal();
EndSection();
}
private void DrawArchiveSection()
{
BeginSection("云存档");
ArchiveSlotNo = DrawEditableRow("槽位号", ArchiveSlotNo);
ArchiveBaseVersion = DrawEditableRow("基准版本", ArchiveBaseVersion);
ArchiveContent = DrawTextAreaRow("存档内容", ArchiveContent, 90f);
GUILayout.BeginHorizontal();
DrawButton("获取槽位列表", GetArchiveSlotsAsync, Brisk.IsLoggedIn);
DrawButton("获取存档元信息", GetArchiveMetaAsync, Brisk.IsLoggedIn);
DrawButton("上传文本存档", UploadArchiveAsync, Brisk.IsLoggedIn);
DrawButton("下载存档", DownloadArchiveAsync, Brisk.IsLoggedIn);
GUILayout.EndHorizontal();
EndSection();
}
private void DrawSpaceSection()
{
BeginSection("玩家空间");
SpacePlayerId = DrawEditableRow("空间 PlayerId", SpacePlayerId);
SpaceLoginProvider = DrawEditableRow("空间登录提供方", SpaceLoginProvider);
SpaceLoginUserId = DrawEditableRow("空间登录用户 ID", SpaceLoginUserId);
SpacePayloadText = DrawTextAreaRow("空间 Payload 文本", SpacePayloadText, 90f);
GUILayout.BeginHorizontal();
DrawButton("按 PlayerId 获取空间", GetSpaceByPlayerIdAsync, Brisk.IsLoggedIn);
DrawButton("按登录身份获取空间", GetSpaceByLoginAsync, Brisk.IsLoggedIn);
DrawButton("按 PlayerId 获取统计", GetSpaceStatsByPlayerIdAsync, Brisk.IsLoggedIn);
DrawButton("按登录身份获取统计", GetSpaceStatsByLoginAsync, Brisk.IsLoggedIn);
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
DrawButton("按 PlayerId 点赞", LikeByPlayerIdAsync, Brisk.IsLoggedIn);
DrawButton("按 PlayerId 取消点赞", UnlikeByPlayerIdAsync, Brisk.IsLoggedIn);
DrawButton("按登录身份点赞", LikeByLoginAsync, Brisk.IsLoggedIn);
DrawButton("按登录身份取消点赞", UnlikeByLoginAsync, Brisk.IsLoggedIn);
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
DrawButton("更新我的空间", UpdateMySpaceAsync, Brisk.IsLoggedIn);
DrawButton("获取我的访客", GetMyVisitsAsync, Brisk.IsLoggedIn);
GUILayout.EndHorizontal();
EndSection();
}
private void DrawOutputSection()
{
BeginSection("输出");
GUILayout.Label("最近一次结果", GUI.skin.label);
_resultScroll = GUILayout.BeginScrollView(_resultScroll, GUILayout.Height(240f));
GUILayout.TextArea(_resultText, GUILayout.ExpandHeight(true));
GUILayout.EndScrollView();
GUILayout.Space(8f);
GUILayout.Label("最近一次错误", GUI.skin.label);
GUILayout.TextArea(string.IsNullOrWhiteSpace(_lastErrorText) ? "暂无错误。" : _lastErrorText, GUILayout.Height(90f));
GUILayout.Space(8f);
GUILayout.Label("事件日志", GUI.skin.label);
_logScroll = GUILayout.BeginScrollView(_logScroll, GUILayout.Height(220f));
GUILayout.TextArea(_eventLogs.Count == 0 ? "暂无事件。" : string.Join("\n", _eventLogs.ToArray()), GUILayout.ExpandHeight(true));
GUILayout.EndScrollView();
EndSection();
}
private async Task InitializeAsync()
{
await Brisk.InitializeAsync(new BriskOptions
{
BaseUrl = BaseUrl,
GameKey = GameKey,
ClientVersion = ClientVersion,
DeviceId = DeviceId,
ValidateSessionOnInitialize = ValidateSessionOnInitialize,
ExitHandler = HandleExitRequested
});
SetResult("初始化结果", Brisk.Bootstrap);
}
private async Task ReinitializeAsync()
{
if (Brisk.IsInitialized)
{
Brisk.Shutdown();
Log("重新初始化前已先关闭 SDK。");
}
await InitializeAsync();
}
private async Task LoginWithUserIdAsync()
{
var result = await Brisk.Auth.LoginWithUserIdAsync(LoginProvider, LoginUserId, CreateProfile());
ApplyIdentity(result.PlayerId, result.LoginProvider, result.LoginUserId);
SetResult("按用户 ID 登录结果", result);
}
private async Task LoginWithCodeAsync()
{
var result = await Brisk.Auth.LoginWithCodeAsync(LoginProvider, LoginCode, CreateProfile());
ApplyIdentity(result.PlayerId, result.LoginProvider, result.LoginUserId);
SetResult("按 Code 登录结果", result);
}
private async Task LogoutAsync()
{
await Brisk.Auth.LogoutAsync();
SetResult("登出结果", new Dictionary<string, object>
{
{ "logged_in", Brisk.IsLoggedIn },
{ "player_id", Brisk.PlayerId }
});
}
private async Task GetMeAsync()
{
var me = await Brisk.Player.GetMeAsync();
ApplyIdentity(me.PlayerId, me.LoginProvider, me.LoginUserId);
SetResult("当前玩家信息", me);
}
private async Task GetConfigAsync()
{
var config = await Brisk.Config.GetCurrentAsync();
SetResult("当前配置", config);
}
private async Task GetAnnouncementsAsync()
{
_announcementCache = await Brisk.Announcements.GetListAsync();
if (_announcementCache.Count > 0 && string.IsNullOrWhiteSpace(AnnouncementId))
{
AnnouncementId = _announcementCache[0].Id.ToString();
}
SetResult("公告列表", _announcementCache);
}
private async Task MarkAnnouncementAsync()
{
var id = ParseRequiredLong(AnnouncementId, nameof(AnnouncementId));
await Brisk.Announcements.MarkReadAsync(id);
SetResult("公告已标记已读", new Dictionary<string, object> { { "announcement_id", id } });
}
private async Task MarkFirstCachedAnnouncementAsync()
{
if (_announcementCache.Count == 0)
{
throw new InvalidOperationException("公告缓存为空,请先执行“获取公告列表”。");
}
var id = _announcementCache[0].Id;
AnnouncementId = id.ToString();
await Brisk.Announcements.MarkReadAsync(id);
SetResult("首条缓存公告已标记已读", _announcementCache[0]);
}
private async Task GetTopAsync()
{
var result = await Brisk.Leaderboard.GetTopAsync(RankKey, ParseOptionalInt(LeaderboardLimit, 10));
SetResult("排行榜 Top", result);
}
private async Task GetMyRankAsync()
{
var result = await Brisk.Leaderboard.GetMeAsync(RankKey);
SetResult("我的排行榜信息", result);
}
private async Task GetAroundMeAsync()
{
var result = await Brisk.Leaderboard.GetAroundMeAsync(RankKey, ParseOptionalInt(AroundMeRange, 5));
SetResult("我附近的排行榜", result);
}
private async Task SubmitScoreAsync()
{
var score = ParseRequiredLong(SubmitScoreValue, nameof(SubmitScoreValue));
await Brisk.Leaderboard.SubmitScoreAsync(RankKey, score);
SetResult("分数提交结果", new Dictionary<string, object>
{
{ "rank_key", RankKey },
{ "score", score }
});
}
private async Task GetCurrentSeasonAsync()
{
var result = await Brisk.Leaderboard.GetCurrentSeasonAsync(RankKey);
SeasonId = result == null ? SeasonId : result.SeasonId;
SetResult("当前赛季", result);
}
private async Task GetSeasonHistoryAsync()
{
_seasonHistoryCache = await Brisk.Leaderboard.GetSeasonHistoryAsync(RankKey, ParseOptionalInt(SeasonHistoryLimit, 20));
if (_seasonHistoryCache.Count > 0 && string.IsNullOrWhiteSpace(SeasonId))
{
SeasonId = _seasonHistoryCache[0].SeasonId;
}
SetResult("赛季历史", _seasonHistoryCache);
}
private async Task GetSeasonDetailAsync()
{
var result = await Brisk.Leaderboard.GetSeasonHistoryDetailAsync(RankKey, SeasonId, ParseOptionalInt(SeasonHistoryLimit, 20));
SetResult("赛季详情", result);
}
private async Task GetArchiveSlotsAsync()
{
var result = await Brisk.Archive.GetSlotsAsync();
SetResult("存档槽位列表", result);
}
private async Task GetArchiveMetaAsync()
{
var result = await Brisk.Archive.GetMetaAsync(ParseRequiredInt(ArchiveSlotNo, nameof(ArchiveSlotNo)));
ArchiveBaseVersion = result == null ? ArchiveBaseVersion : result.Version.ToString();
SetResult("存档元信息", result);
}
private async Task UploadArchiveAsync()
{
var slotNo = ParseRequiredInt(ArchiveSlotNo, nameof(ArchiveSlotNo));
var baseVersion = ParseNullableInt(ArchiveBaseVersion);
var bytes = Encoding.UTF8.GetBytes(ArchiveContent ?? string.Empty);
var result = await Brisk.Archive.UploadAsync(slotNo, bytes, baseVersion);
ArchiveBaseVersion = result == null ? ArchiveBaseVersion : result.Version.ToString();
SetResult("存档上传结果", result);
}
private async Task DownloadArchiveAsync()
{
var result = await Brisk.Archive.DownloadAsync(ParseRequiredInt(ArchiveSlotNo, nameof(ArchiveSlotNo)));
ArchiveBaseVersion = result == null ? ArchiveBaseVersion : result.Version.ToString();
var output = new Dictionary<string, object>
{
{ "version", result.Version },
{ "checksum", result.Checksum },
{ "byte_length", result.Bytes == null ? 0 : result.Bytes.Length },
{ "text_preview", result.Bytes == null ? string.Empty : Encoding.UTF8.GetString(result.Bytes) }
};
SetResult("存档下载结果", output);
}
private async Task GetSpaceByPlayerIdAsync()
{
var result = await Brisk.Space.GetByPlayerIdAsync(SpacePlayerId);
SetResult("按 PlayerId 获取空间", result);
}
private async Task GetSpaceByLoginAsync()
{
var result = await Brisk.Space.GetByLoginIdentityAsync(SpaceLoginProvider, SpaceLoginUserId);
SetResult("按登录身份获取空间", result);
}
private async Task GetSpaceStatsByPlayerIdAsync()
{
var result = await Brisk.Space.GetStatsByPlayerIdAsync(SpacePlayerId);
SetResult("按 PlayerId 获取空间统计", result);
}
private async Task GetSpaceStatsByLoginAsync()
{
var result = await Brisk.Space.GetStatsByLoginIdentityAsync(SpaceLoginProvider, SpaceLoginUserId);
SetResult("按登录身份获取空间统计", result);
}
private async Task LikeByPlayerIdAsync()
{
await Brisk.Space.LikeByPlayerIdAsync(SpacePlayerId);
SetResult("按 PlayerId 点赞结果", new Dictionary<string, object> { { "player_id", SpacePlayerId } });
}
private async Task UnlikeByPlayerIdAsync()
{
await Brisk.Space.UnlikeByPlayerIdAsync(SpacePlayerId);
SetResult("按 PlayerId 取消点赞结果", new Dictionary<string, object> { { "player_id", SpacePlayerId } });
}
private async Task LikeByLoginAsync()
{
await Brisk.Space.LikeByLoginIdentityAsync(SpaceLoginProvider, SpaceLoginUserId);
SetResult("按登录身份点赞结果", new Dictionary<string, object>
{
{ "login_provider", SpaceLoginProvider },
{ "login_user_id", SpaceLoginUserId }
});
}
private async Task UnlikeByLoginAsync()
{
await Brisk.Space.UnlikeByLoginIdentityAsync(SpaceLoginProvider, SpaceLoginUserId);
SetResult("按登录身份取消点赞结果", new Dictionary<string, object>
{
{ "login_provider", SpaceLoginProvider },
{ "login_user_id", SpaceLoginUserId }
});
}
private async Task UpdateMySpaceAsync()
{
var payload = new Dictionary<string, object>
{
{ "sample_text", SpacePayloadText },
{ "updated_at", DateTimeOffset.UtcNow.ToString("O") },
{ "player_id", Brisk.PlayerId },
{ "rank_key", RankKey }
};
await Brisk.Space.UpdateMyAsync(payload);
SetResult("更新我的空间结果", payload);
}
private async Task GetMyVisitsAsync()
{
var result = await Brisk.Space.GetMyVisitsAsync();
SetResult("我的访客列表", result);
}
private async Task RunSmokeFlowAsync()
{
await InitializeAsync();
if (!Brisk.IsLoggedIn)
{
await LoginWithUserIdAsync();
}
await GetMeAsync();
await GetConfigAsync();
await GetTopAsync();
await GetAnnouncementsAsync();
await GetArchiveSlotsAsync();
await GetMyVisitsAsync();
}
private void DrawButton(string label, Func<Task> action, bool enabled = true)
{
var previous = GUI.enabled;
GUI.enabled = previous && !_isBusy && enabled;
if (GUILayout.Button(label, GUILayout.Height(30f)))
{
RunAction(label, action);
}
GUI.enabled = previous;
}
private void RunAction(string actionName, Func<Task> action)
{
if (_isBusy)
{
return;
}
RunActionAsync(actionName, action);
}
private async void RunActionAsync(string actionName, Func<Task> action)
{
_isBusy = true;
_busyAction = actionName;
_statusText = "执行中: " + actionName;
_lastErrorText = string.Empty;
Log("开始执行: " + actionName);
try
{
await action();
_statusText = "执行成功: " + actionName;
Log("执行成功: " + actionName);
}
catch (Exception exception)
{
_statusText = "执行失败: " + actionName;
_lastErrorText = FormatException(exception);
Log("执行失败: " + actionName + " | " + exception.GetType().Name + " | " + exception.Message);
Debug.LogException(exception, this);
}
finally
{
_busyAction = string.Empty;
_isBusy = false;
}
}
private void SetResult(string title, object value)
{
_resultText = title + "\n" + new string('=', title.Length) + "\n" + FormatValue(value);
}
private void ApplyIdentity(string playerId, string loginProvider, string loginUserId)
{
if (!string.IsNullOrWhiteSpace(playerId))
{
SpacePlayerId = playerId;
}
if (!string.IsNullOrWhiteSpace(loginProvider))
{
SpaceLoginProvider = loginProvider;
LoginProvider = loginProvider;
}
if (!string.IsNullOrWhiteSpace(loginUserId))
{
SpaceLoginUserId = loginUserId;
LoginUserId = loginUserId;
}
}
private void ApplyCurrentIdentityToSpace()
{
if (Brisk.Identity != null)
{
ApplyIdentity(Brisk.Identity.PlayerId, Brisk.Identity.LoginProvider, Brisk.Identity.LoginUserId);
return;
}
if (Brisk.IsInitialized)
{
ApplyIdentity(Brisk.PlayerId, LoginProvider, LoginUserId);
}
}
private BriskProfile CreateProfile()
{
return new BriskProfile
{
Nickname = Nickname,
AvatarUrl = AvatarUrl
};
}
private void HandleInitialized()
{
Log("事件: 初始化完成");
}
private void HandleLoggedIn()
{
Log("事件: 登录完成");
ApplyCurrentIdentityToSpace();
}
private void HandleLoggedOut()
{
Log("事件: 登出完成");
}
private void HandleAuthExpired(BriskAuthExpiredException exception)
{
Log("事件: 登录态失效 | " + exception.Message);
}
private void HandleBlockingError(BriskBlockingException exception)
{
Log("事件: 严重阻断错误 | " + exception.Message);
}
private void HandleExitRequested()
{
Log("Brisk 阻断流程请求退出。");
}
private void Log(string message)
{
var line = "[" + DateTime.Now.ToString("HH:mm:ss") + "] " + message;
_eventLogs.Add(line);
if (_eventLogs.Count > 120)
{
_eventLogs.RemoveAt(0);
}
_logScroll.y = float.MaxValue;
}
private static int ParseRequiredInt(string value, string fieldName)
{
if (!int.TryParse(value, out var result))
{
throw new InvalidOperationException(fieldName + " 必须是合法整数。");
}
return result;
}
private static long ParseRequiredLong(string value, string fieldName)
{
if (!long.TryParse(value, out var result))
{
throw new InvalidOperationException(fieldName + " 必须是合法整数。");
}
return result;
}
private static int ParseOptionalInt(string value, int fallback)
{
return int.TryParse(value, out var result) ? result : fallback;
}
private static int? ParseNullableInt(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return int.TryParse(value, out var result) ? result : (int?)null;
}
private static string FormatException(Exception exception)
{
if (exception == null)
{
return "未知错误。";
}
var builder = new StringBuilder();
var current = exception;
while (current != null)
{
builder.AppendLine(current.GetType().Name + ": " + current.Message);
current = current.InnerException;
}
return builder.ToString().TrimEnd();
}
private static string FormatValue(object value)
{
var builder = new StringBuilder();
AppendValue(builder, value, 0, 0);
return builder.ToString().TrimEnd();
}
private static void AppendValue(StringBuilder builder, object value, int indent, int depth)
{
if (depth > 5)
{
builder.AppendLine(Indent(indent) + "...");
return;
}
if (value == null)
{
builder.AppendLine(Indent(indent) + "null");
return;
}
if (value is string stringValue)
{
builder.AppendLine(Indent(indent) + stringValue);
return;
}
if (value is byte[] bytes)
{
builder.AppendLine(Indent(indent) + "byte[" + bytes.Length + "]");
return;
}
var type = value.GetType();
if (type.IsPrimitive || value is decimal)
{
builder.AppendLine(Indent(indent) + value);
return;
}
if (value is IDictionary dictionary)
{
foreach (DictionaryEntry entry in dictionary)
{
builder.AppendLine(Indent(indent) + entry.Key + ":");
AppendValue(builder, entry.Value, indent + 2, depth + 1);
}
return;
}
if (value is IEnumerable enumerable)
{
var index = 0;
foreach (var item in enumerable)
{
builder.AppendLine(Indent(indent) + "[" + index + "]");
AppendValue(builder, item, indent + 2, depth + 1);
index++;
}
if (index == 0)
{
builder.AppendLine(Indent(indent) + "(empty)");
}
return;
}
var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public);
if (fields.Length == 0)
{
builder.AppendLine(Indent(indent) + value);
return;
}
foreach (var field in fields)
{
builder.AppendLine(Indent(indent) + field.Name + ":");
AppendValue(builder, field.GetValue(value), indent + 2, depth + 1);
}
}
private static string Indent(int indent)
{
return new string(' ', indent);
}
private static void BeginSection(string title)
{
GUILayout.Space(10f);
GUILayout.BeginVertical(GUI.skin.box);
GUILayout.Label(title, GUI.skin.label);
GUILayout.Space(4f);
}
private static void EndSection()
{
GUILayout.EndVertical();
}
private static string DrawEditableRow(string label, string value)
{
GUILayout.BeginHorizontal();
GUILayout.Label(label, GUILayout.Width(180f));
var next = GUILayout.TextField(value ?? string.Empty);
GUILayout.EndHorizontal();
return next;
}
private static string DrawTextAreaRow(string label, string value, float height)
{
GUILayout.Label(label, GUILayout.Width(180f));
return GUILayout.TextArea(value ?? string.Empty, GUILayout.Height(height));
}
private static bool DrawToggleRow(string label, bool value)
{
GUILayout.BeginHorizontal();
GUILayout.Label(label, GUILayout.Width(180f));
var next = GUILayout.Toggle(value, value ? "Enabled" : "Disabled");
GUILayout.EndHorizontal();
return next;
}
private static void DrawReadOnlyRow(string label, string value)
{
GUILayout.BeginHorizontal();
GUILayout.Label(label, GUILayout.Width(180f));
GUILayout.Label(string.IsNullOrWhiteSpace(value) ? "-" : value, GUI.skin.box);
GUILayout.EndHorizontal();
}
}

View File

@@ -0,0 +1,30 @@
# Changelog
## 0.4.0
- Added daily-cycle space like fields and result semantics to the Unity SDK models
- Added current-cycle filtering support for space like list APIs
- Updated the quick start sample to display total likes, today likes, like reset time, and current-cycle like actions
- Changed `GetMyVisitsAsync()` default page size to `50` while preserving the optional `limit` parameter
- Refreshed package and integration documentation to match the latest production API behavior
## 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,100 @@
# 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);
var likeResult = await Brisk.Space.LikeByPlayerIdAsync("target-player-id");
Debug.Log($"total={likeResult.LikeCount}, today={likeResult.TodayLikeCount}, created={likeResult.Created}");
var todayLikes = await Brisk.Space.GetLikesByPlayerIdAsync(Brisk.PlayerId, 20, true);
var visits = await Brisk.Space.GetMyVisitsAsync();
```
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
- space likes are now tracked by natural day in `Asia/Shanghai`
- `BriskSpaceView` / `BriskSpaceStats` include both `LikeCount` and `TodayLikeCount`
- `BriskSpaceView` also includes `LikedByMe` and `LikeResetAt`
- `GetLikesByPlayerIdAsync(..., currentCycleOnly: true)` returns current-cycle likes only
- `GetMyVisitsAsync()` now defaults to the latest `50` visits, with `100` as the server-side max
## 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,105 @@
# 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.4.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.4.0"
}
}
```
如果需要跟随主分支最新代码,可将末尾的 `#v0.4.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
- `LikeByPlayerIdAsync(...)` / `UnlikeByPlayerIdAsync(...)` 返回累计点赞数、今日点赞数、当前周期是否创建新点赞、重置时间
- `GetLikesByPlayerIdAsync(playerId, limit, true)``GetLikesByLoginIdentityAsync(..., true)` 可只读取当前周期点赞记录
- `GetMyVisitsAsync()` 默认读取最近 `50` 条访问记录,也可手动传入 `limit`
## 目录结构
- `Runtime`
- `Samples~`
- `Documentation~`
## 快速开始
查看:
- `Documentation~/QuickStart.md`
- `Samples~/QuickStart`

View File

@@ -50,7 +50,7 @@ public sealed class BriskArchiveModule
return await ExecuteAsync(async context =>
{
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : checksum;
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : NormalizeChecksum(checksum);
var sections = new List<IMultipartFormSection>
{
new MultipartFormDataSection("base_version", (baseVersion ?? 0).ToString()),
@@ -63,6 +63,24 @@ public sealed class BriskArchiveModule
});
}
/// <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>
@@ -82,6 +100,24 @@ public sealed class BriskArchiveModule
});
}
/// <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.");
@@ -92,8 +128,7 @@ public sealed class BriskArchiveModule
using (var sha = SHA256.Create())
{
var hash = sha.ComputeHash(bytes);
var builder = new StringBuilder(hash.Length * 2 + 7);
builder.Append("sha256:");
var builder = new StringBuilder(hash.Length * 2);
for (var i = 0; i < hash.Length; i++)
{
builder.Append(hash[i].ToString("x2"));
@@ -103,6 +138,19 @@ public sealed class BriskArchiveModule
}
}
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);

View File

@@ -51,23 +51,26 @@ public sealed class BriskHttpClient
public async Task<Dictionary<string, object>> PostMultipartAsync(string path, List<IMultipartFormSection> formSections, bool auth = false)
{
using (var request = UnityWebRequest.Post(BuildUrl(path, null), formSections))
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", "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))
{
request.SetRequestHeader("Accept", "*/*");
await SendRequestAsync(request);
EnsureSuccessOrThrow(request);
return new BriskBinaryResponse
@@ -93,6 +96,23 @@ public sealed class BriskHttpClient
}
}
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);

View File

@@ -237,10 +237,23 @@ internal static class BriskModelMapper
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"),
Payload = BriskValueReader.GetDictionary(data, "payload_json") ?? BriskValueReader.GetDictionary(data, "payload")
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"),
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
LikedByMe = BriskValueReader.GetBool(data, "liked_by_me"),
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at"),
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
};
}
@@ -253,8 +266,58 @@ internal static class BriskModelMapper
return new BriskSpaceStats
{
LikeCount = BriskValueReader.GetInt(data, "like_count"),
VisitCount = BriskValueReader.GetInt(data, "visit_count")
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"),
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at"),
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"),
Created = BriskValueReader.GetBool(data, "created"),
LikedByMe = BriskValueReader.GetBool(data, "liked_by_me"),
LikeCount = BriskValueReader.GetLong(data, "like_count"),
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at")
};
}
@@ -284,6 +347,30 @@ internal static class BriskModelMapper
.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>;

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