Release v0.2.0 archive and space API update

This commit is contained in:
2026-04-11 01:56:47 +08:00
parent e07a9086b1
commit 07fc690e67
42 changed files with 3391 additions and 1316 deletions

View File

@@ -1,5 +1,13 @@
# Changelog
## 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

View File

@@ -27,6 +27,52 @@ 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:

View File

@@ -24,6 +24,32 @@ Sync package content from the Unity source project with:
- Player space
- Default blocking error UI
## Archive checksum
Archive upload checksum is handled by the SDK by default.
- For quick use, the archive module now provides:
- `UploadAsync(slotNo, bytes)` for binary
- `UploadTextAsync(slotNo, text)` for UTF-8 text
- `UploadJsonAsync(slotNo, payload)` for JSON objects
- `DownloadAsync(slotNo)` for raw bytes + metadata
- `DownloadTextAsync(slotNo)` for UTF-8 text
- `DownloadJsonAsync(slotNo)` for JSON payloads
- The SDK computes SHA256 automatically when uploading archive bytes
- The current Brisk archive API expects a plain lowercase SHA256 hex string
- Do not send values with a `sha256:` prefix
- If a manual checksum includes that prefix, the SDK will normalize it before sending
## Space content
Player space now follows a metadata + binary content model.
- `GetByPlayerIdAsync(...)` and `GetByLoginIdentityAsync(...)` return metadata only
- `DownloadContentByPlayerIdAsync(...)` and `DownloadContentByLoginIdentityAsync(...)` return raw bytes
- `UpdateMyAsync(string)` uploads text content directly
- `UpdateMyAsync(byte[])` uploads binary content directly
- `UpdateMyAsync(object)` serializes the object as JSON automatically
## Package layout
- `Runtime`

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,21 @@ 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"),
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
LikedByMe = BriskValueReader.GetBool(data, "liked_by_me"),
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
};
}
@@ -253,8 +264,52 @@ 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"),
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")
};
}
@@ -284,6 +339,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,5 @@
public sealed class BriskSpaceLikeResult
{
public bool Liked;
public long LikeCount;
}

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,17 @@
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 VisitCount;
public string UpdatedAt;
}

View File

@@ -1,7 +1,18 @@
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 VisitCount;
public bool LikedByMe;
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,179 @@ 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)
{
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", CreateLimitQuery(limit), true);
return (IReadOnlyList<BriskSpaceLikeItem>)BriskModelMapper.ToSpaceLikeItems(data);
});
}
/// <summary>
/// 按登录身份获取最近点赞列表。
/// </summary>
public async Task<IReadOnlyList<BriskSpaceLikeItem>> GetLikesByLoginIdentityAsync(string loginProvider, string loginUserId, int limit = 20)
{
ValidateLoginIdentity(loginProvider, loginUserId);
return await ExecuteAsync(async context =>
{
var query = CreateLoginIdentityQuery(loginProvider, loginUserId);
query["limit"] = NormalizeLimit(limit);
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 = 20)
{
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 +254,14 @@ public sealed class BriskSpaceModule
};
}
private static Dictionary<string, string> CreateLimitQuery(int limit)
{
return new Dictionary<string, string>
{
{ "limit", NormalizeLimit(limit) }
};
}
private static void ValidatePlayerId(string playerId)
{
RequireNotEmpty(playerId, nameof(playerId));
@@ -159,4 +272,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;
}
}

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: briskh5verify
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: h5-verify-rank-20260410034312-5cdd
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,7 +1,7 @@
{
"name": "com.foldcc.cc-framework.BriskGameServer",
"displayName": "FoldCC Brisk Game Server SDK",
"version": "0.1.0",
"version": "0.2.0",
"unity": "2021.3",
"description": "A lightweight Unity SDK for Brisk game services, including bootstrap, auth, announcements, leaderboard, archive, and player space modules.",
"keywords": [