diff --git a/Assets/BriskSdk/Runtime/Archive/BriskArchiveModule.cs b/Assets/BriskSdk/Runtime/Archive/BriskArchiveModule.cs index de9ed05..2d1bdff 100644 --- a/Assets/BriskSdk/Runtime/Archive/BriskArchiveModule.cs +++ b/Assets/BriskSdk/Runtime/Archive/BriskArchiveModule.cs @@ -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 { new MultipartFormDataSection("base_version", (baseVersion ?? 0).ToString()), @@ -63,6 +63,24 @@ public sealed class BriskArchiveModule }); } + /// + /// 以 UTF-8 文本形式上传指定槽位的存档。 + /// + public Task UploadTextAsync(int slotNo, string text, int? baseVersion = null, string checksum = null) + { + RequireNotNull(text, nameof(text)); + return UploadAsync(slotNo, Encoding.UTF8.GetBytes(text), baseVersion, checksum); + } + + /// + /// 以 JSON 文本形式上传指定槽位的存档。 + /// + public Task 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); + } + /// /// 下载指定槽位的二进制存档。 /// @@ -82,6 +100,24 @@ public sealed class BriskArchiveModule }); } + /// + /// 以 UTF-8 文本形式下载指定槽位的存档。 + /// + public async Task DownloadTextAsync(int slotNo) + { + var result = await DownloadAsync(slotNo); + return result == null || result.Bytes == null ? string.Empty : Encoding.UTF8.GetString(result.Bytes); + } + + /// + /// 以 JSON 对象形式下载指定槽位的存档。 + /// + public async Task 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 headers, string key) { var value = ReadHeader(headers, key); diff --git a/Assets/BriskSdk/Runtime/Core/BriskHttpClient.cs b/Assets/BriskSdk/Runtime/Core/BriskHttpClient.cs index 8c87207..3baf9f2 100644 --- a/Assets/BriskSdk/Runtime/Core/BriskHttpClient.cs +++ b/Assets/BriskSdk/Runtime/Core/BriskHttpClient.cs @@ -51,23 +51,26 @@ public sealed class BriskHttpClient public async Task> PostMultipartAsync(string path, List formSections, bool auth = false) { - using (var request = UnityWebRequest.Post(BuildUrl(path, null), formSections)) + return await SendMultipartAsync(UnityWebRequest.kHttpVerbPOST, path, formSections, auth); + } + + public async Task> PutMultipartAsync(string path, List formSections, bool auth = false) + { + return await SendMultipartAsync(UnityWebRequest.kHttpVerbPUT, path, formSections, auth); + } + + public async Task GetBytesAsync(string path, Dictionary 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 ?? new Dictionary(); - } - } - - public async Task GetBytesAsync(string path, Dictionary 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> SendMultipartAsync(string method, string path, List 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 ?? new Dictionary(); + } + } + private UnityWebRequest BuildRequest(string method, string path, Dictionary query, object body, bool auth) { var url = BuildUrl(path, query); diff --git a/Assets/BriskSdk/Runtime/Core/BriskModelMapper.cs b/Assets/BriskSdk/Runtime/Core/BriskModelMapper.cs index 6fd9cd3..20917da 100644 --- a/Assets/BriskSdk/Runtime/Core/BriskModelMapper.cs +++ b/Assets/BriskSdk/Runtime/Core/BriskModelMapper.cs @@ -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 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 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 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 ToSpaceLikeItems(object data) + { + return ExtractList(data) + .Select(item => ToSpaceLikeItem(item as Dictionary)) + .Where(item => item != null) + .ToList(); + } + public static Dictionary ExtractObject(object data) { return data as Dictionary; diff --git a/Assets/BriskSdk/Runtime/Models/BriskSpaceContentDownloadResult.cs b/Assets/BriskSdk/Runtime/Models/BriskSpaceContentDownloadResult.cs new file mode 100644 index 0000000..a8a2a87 --- /dev/null +++ b/Assets/BriskSdk/Runtime/Models/BriskSpaceContentDownloadResult.cs @@ -0,0 +1,7 @@ +public sealed class BriskSpaceContentDownloadResult +{ + public byte[] Bytes; + public long Version; + public string Checksum; + public string ContentType; +} diff --git a/Assets/BriskSdk/Runtime/Models/BriskSpaceContentDownloadResult.cs.meta b/Assets/BriskSdk/Runtime/Models/BriskSpaceContentDownloadResult.cs.meta new file mode 100644 index 0000000..5b029c6 --- /dev/null +++ b/Assets/BriskSdk/Runtime/Models/BriskSpaceContentDownloadResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e6f27d1ba8b24f5e85362b9c6c9f4c01 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/BriskSdk/Runtime/Models/BriskSpaceContentUpdateResult.cs b/Assets/BriskSdk/Runtime/Models/BriskSpaceContentUpdateResult.cs new file mode 100644 index 0000000..42fb550 --- /dev/null +++ b/Assets/BriskSdk/Runtime/Models/BriskSpaceContentUpdateResult.cs @@ -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; +} diff --git a/Assets/BriskSdk/Runtime/Models/BriskSpaceContentUpdateResult.cs.meta b/Assets/BriskSdk/Runtime/Models/BriskSpaceContentUpdateResult.cs.meta new file mode 100644 index 0000000..7332b34 --- /dev/null +++ b/Assets/BriskSdk/Runtime/Models/BriskSpaceContentUpdateResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4bd67f378d264cc69c71f1d03d5b564b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/BriskSdk/Runtime/Models/BriskSpaceLikeItem.cs b/Assets/BriskSdk/Runtime/Models/BriskSpaceLikeItem.cs new file mode 100644 index 0000000..7ab87d1 --- /dev/null +++ b/Assets/BriskSdk/Runtime/Models/BriskSpaceLikeItem.cs @@ -0,0 +1,7 @@ +public sealed class BriskSpaceLikeItem +{ + public string PlayerId; + public string Nickname; + public string AvatarUrl; + public string CreatedAt; +} diff --git a/Assets/BriskSdk/Runtime/Models/BriskSpaceLikeItem.cs.meta b/Assets/BriskSdk/Runtime/Models/BriskSpaceLikeItem.cs.meta new file mode 100644 index 0000000..08c5e05 --- /dev/null +++ b/Assets/BriskSdk/Runtime/Models/BriskSpaceLikeItem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ef7d53b3f0ab43e7b74a4c23a76f84f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/BriskSdk/Runtime/Models/BriskSpaceLikeResult.cs b/Assets/BriskSdk/Runtime/Models/BriskSpaceLikeResult.cs new file mode 100644 index 0000000..3637490 --- /dev/null +++ b/Assets/BriskSdk/Runtime/Models/BriskSpaceLikeResult.cs @@ -0,0 +1,5 @@ +public sealed class BriskSpaceLikeResult +{ + public bool Liked; + public long LikeCount; +} diff --git a/Assets/BriskSdk/Runtime/Models/BriskSpaceLikeResult.cs.meta b/Assets/BriskSdk/Runtime/Models/BriskSpaceLikeResult.cs.meta new file mode 100644 index 0000000..fa60d9c --- /dev/null +++ b/Assets/BriskSdk/Runtime/Models/BriskSpaceLikeResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 966f47c348db46dfb60120dba20f1ffb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/BriskSdk/Runtime/Models/BriskSpaceStats.cs b/Assets/BriskSdk/Runtime/Models/BriskSpaceStats.cs index d59e7e1..48fb95b 100644 --- a/Assets/BriskSdk/Runtime/Models/BriskSpaceStats.cs +++ b/Assets/BriskSdk/Runtime/Models/BriskSpaceStats.cs @@ -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; } diff --git a/Assets/BriskSdk/Runtime/Models/BriskSpaceView.cs b/Assets/BriskSdk/Runtime/Models/BriskSpaceView.cs index 9118e09..d6338e8 100644 --- a/Assets/BriskSdk/Runtime/Models/BriskSpaceView.cs +++ b/Assets/BriskSdk/Runtime/Models/BriskSpaceView.cs @@ -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; } diff --git a/Assets/BriskSdk/Runtime/Space/BriskSpaceModule.cs b/Assets/BriskSdk/Runtime/Space/BriskSpaceModule.cs index ebd61a8..8ef72e4 100644 --- a/Assets/BriskSdk/Runtime/Space/BriskSpaceModule.cs +++ b/Assets/BriskSdk/Runtime/Space/BriskSpaceModule.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; using System.Threading.Tasks; +using UnityEngine.Networking; /// /// 玩家空间模块。 @@ -65,77 +68,179 @@ public sealed class BriskSpaceModule } /// - /// 按玩家 ID 点赞空间。 + /// 按玩家 ID 获取最近点赞列表。 /// - public async Task LikeByPlayerIdAsync(string playerId) + public async Task> 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(), true); + var data = await context.HttpClient.GetRawDataAsync($"/spaces/{playerId}/likes", CreateLimitQuery(limit), true); + return (IReadOnlyList)BriskModelMapper.ToSpaceLikeItems(data); + }); + } + + /// + /// 按登录身份获取最近点赞列表。 + /// + public async Task> 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)BriskModelMapper.ToSpaceLikeItems(data); + }); + } + + /// + /// 按玩家 ID 点赞空间。 + /// + public async Task LikeByPlayerIdAsync(string playerId) + { + ValidatePlayerId(playerId); + + return await ExecuteAsync(async context => + { + var data = await context.HttpClient.PostJsonRawAsync($"/spaces/{playerId}/like", new Dictionary(), true); + return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data)); }); } /// /// 按玩家 ID 取消点赞空间。 /// - public async Task UnlikeByPlayerIdAsync(string playerId) + public async Task 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)); }); } /// /// 按登录身份点赞空间。 /// - public async Task LikeByLoginIdentityAsync(string loginProvider, string loginUserId) + public async Task 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(), true, CreateLoginIdentityQuery(loginProvider, loginUserId)); + var data = await context.HttpClient.PostJsonRawAsync("/spaces/by-login/like", new Dictionary(), true, CreateLoginIdentityQuery(loginProvider, loginUserId)); + return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data)); }); } /// /// 按登录身份取消点赞空间。 /// - public async Task UnlikeByLoginIdentityAsync(string loginProvider, string loginUserId) + public async Task 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)); + }); + } + + /// + /// 上传当前玩家自己的空间内容。 + /// + public async Task 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 + { + 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); }); } /// /// 更新当前玩家自己的空间内容。 /// - public async Task UpdateMyAsync(object payload) + public Task 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 { { "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)); + } + + /// + /// 按玩家 ID 下载空间内容。 + /// + public async Task DownloadContentByPlayerIdAsync(string playerId) + { + ValidatePlayerId(playerId); + + return await ExecuteAsync(async context => + { + var response = await context.HttpClient.GetBytesAsync($"/spaces/{playerId}/content", null, true); + return CreateDownloadResult(response); + }); + } + + /// + /// 按登录身份下载空间内容。 + /// + public async Task 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); }); } /// /// 获取我的访客列表。 /// - public async Task> GetMyVisitsAsync() + public async Task> 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)BriskModelMapper.ToSpaceVisits(data); }); } @@ -149,6 +254,14 @@ public sealed class BriskSpaceModule }; } + private static Dictionary CreateLimitQuery(int limit) + { + return new Dictionary + { + { "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 headers, string key) + { + var value = ReadHeader(headers, key); + return long.TryParse(value, out var result) ? result : 0L; + } + + private static string ReadHeader(Dictionary 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; + } } diff --git a/Assets/BriskSdk/Samples/QuickStart/BriskQuickStartSample.cs b/Assets/BriskSdk/Samples/QuickStart/BriskQuickStartSample.cs index b67477c..efc9d75 100644 --- a/Assets/BriskSdk/Samples/QuickStart/BriskQuickStartSample.cs +++ b/Assets/BriskSdk/Samples/QuickStart/BriskQuickStartSample.cs @@ -8,6 +8,20 @@ using UnityEngine; public sealed class BriskQuickStartSample : MonoBehaviour { + private const float DesignWidth = 720f; + private const float DesignHeight = 1280f; + + private enum SamplePage + { + Initialize, + Login, + Home, + Announcements, + Leaderboard, + Archive, + Space + } + [Header("初始化")] public string BaseUrl = "https://brisk.lightyears.ltd"; public string GameKey = "demo-game"; @@ -24,7 +38,7 @@ public sealed class BriskQuickStartSample : MonoBehaviour [Header("排行榜")] public string RankKey = "season-score"; - public string SubmitScoreValue = "128"; + public string SubmitScoreValue = "0"; public string LeaderboardLimit = "10"; public string AroundMeRange = "5"; public string SeasonId = string.Empty; @@ -44,23 +58,60 @@ public sealed class BriskQuickStartSample : MonoBehaviour 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}"; + public string SpacePayloadText = "这里是 unity 测试账户的空间内容。\n\n0.o"; [Header("演示")] public bool AutoRunOnStart; - private readonly List _eventLogs = new List(); + private readonly List _logs = new List(); - private Vector2 _pageScroll; - private Vector2 _resultScroll; - private Vector2 _logScroll; + private Vector2 _initScroll; + private Vector2 _loginScroll; + private Vector2 _homeScroll; + private Vector2 _announcementScroll; + private Vector2 _leaderboardScroll; + private Vector2 _archiveScroll; + private Vector2 _spaceScroll; + private Vector2 _modalScroll; + + private SamplePage _page = SamplePage.Initialize; private bool _isBusy; private string _busyAction = string.Empty; - private string _statusText = "就绪"; - private string _resultText = "尚未执行请求。"; - private string _lastErrorText = string.Empty; - private IReadOnlyList _announcementCache = Array.Empty(); - private IReadOnlyList _seasonHistoryCache = Array.Empty(); + private string _statusText = "等待开始"; + private string _errorText = string.Empty; + + private BriskPlayerMe _me; + private BriskConfigCurrent _config; + private IReadOnlyList _announcements = Array.Empty(); + private BriskAnnouncementItem _selectedAnnouncement; + private IReadOnlyList _leaderboardEntries = Array.Empty(); + private BriskLeaderboardPlayerRank _leaderboardMe; + private IReadOnlyList _archiveSlots = Array.Empty(); + private BriskArchiveMeta _archiveMeta; + private BriskSpaceView _mySpaceView; + private BriskSpaceStats _mySpaceStats; + private IReadOnlyList _mySpaceLikes = Array.Empty(); + private BriskSpaceView _targetSpaceView; + private BriskSpaceStats _targetSpaceStats; + private string _targetSpaceContentText = string.Empty; + private bool _viewAroundMe; + private bool _showAnnouncementModal; + private bool _viewingTargetSpace; + + private GUIStyle _pageStyle; + private GUIStyle _cardStyle; + private GUIStyle _titleStyle; + private GUIStyle _sectionTitleStyle; + private GUIStyle _bodyStyle; + private GUIStyle _hintStyle; + private GUIStyle _buttonStyle; + private GUIStyle _primaryButtonStyle; + private GUIStyle _dangerButtonStyle; + private GUIStyle _inputStyle; + private GUIStyle _textAreaStyle; + private GUIStyle _statusStyle; + private GUIStyle _listButtonStyle; + private GUIStyle _modalStyle; private void OnEnable() { @@ -82,239 +133,399 @@ public sealed class BriskQuickStartSample : MonoBehaviour private void Start() { + SyncPageBySdkState(); if (AutoRunOnStart) { - RunAction("自动冒烟流程", RunSmokeFlowAsync); + 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); + EnsureStyles(); - DrawHeader(); - DrawStatusPanel(); - DrawInitSection(); - DrawLoginSection(); - DrawPlayerAndConfigSection(); - DrawAnnouncementsSection(); - DrawLeaderboardSection(); - DrawArchiveSection(); - DrawSpaceSection(); - DrawOutputSection(); + var scale = Mathf.Min(Screen.width / DesignWidth, Screen.height / DesignHeight); + var offsetX = (Screen.width - (DesignWidth * scale)) * 0.5f; + var offsetY = (Screen.height - (DesignHeight * scale)) * 0.5f; + + var previousMatrix = GUI.matrix; + GUI.matrix = Matrix4x4.TRS(new Vector3(offsetX, offsetY, 0f), Quaternion.identity, new Vector3(scale, scale, 1f)); + + GUI.Box(new Rect(0f, 0f, DesignWidth, DesignHeight), GUIContent.none); + GUILayout.BeginArea(new Rect(0f, 0f, DesignWidth, DesignHeight)); + DrawCurrentPage(); + GUILayout.EndArea(); + + if (_showAnnouncementModal && _selectedAnnouncement != null) + { + DrawAnnouncementModal(); + } + + if (_isBusy) + { + DrawBusyOverlay(); + } + + GUI.matrix = previousMatrix; + } + + private void DrawCurrentPage() + { + switch (_page) + { + case SamplePage.Initialize: + DrawInitializePage(); + break; + case SamplePage.Login: + DrawLoginPage(); + break; + case SamplePage.Home: + DrawHomePage(); + break; + case SamplePage.Announcements: + DrawAnnouncementsPage(); + break; + case SamplePage.Leaderboard: + DrawLeaderboardPage(); + break; + case SamplePage.Archive: + DrawArchivePage(); + break; + default: + DrawSpacePage(); + break; + } + } + + private void DrawInitializePage() + { + var contentRect = DrawPageFrame("初始化", null, null); + GUILayout.BeginArea(contentRect); + _initScroll = GUILayout.BeginScrollView(_initScroll); + + DrawCard("当前配置", () => + { + BaseUrl = DrawInputRow("服务地址", BaseUrl); + GameKey = DrawInputRow("游戏 Key", GameKey); + ClientVersion = DrawInputRow("客户端版本", ClientVersion); + DeviceId = DrawInputRow("设备标识", DeviceId); + ValidateSessionOnInitialize = DrawToggleRow("初始化时校验旧会话", ValidateSessionOnInitialize); + }); + + DrawCard("说明", () => + { + GUILayout.Label("点击“初始化”后会直接调用 Brisk.InitializeAsync。", _bodyStyle); + GUILayout.Label("成功时自动进入登录页;如果已恢复登录态,则直接进入主页。", _bodyStyle); + GUILayout.Label("失败会停留在当前页,并把错误展示在底部状态栏。", _bodyStyle); + }); + + GUILayout.FlexibleSpace(); + GUILayout.EndScrollView(); + GUILayout.EndArea(); + + DrawBottomButtons( + new ButtonSpec("初始化", InitializeFlowAsync, true, true), + new ButtonSpec("重新填写", ResetTransientStatusAsync, false, true)); + } + + private void DrawLoginPage() + { + var contentRect = DrawPageFrame("登录", "返回初始化", () => _page = SamplePage.Initialize); + GUILayout.BeginArea(contentRect); + _loginScroll = GUILayout.BeginScrollView(_loginScroll); + + DrawCard("登录信息", () => + { + LoginProvider = DrawInputRow("登录提供方", LoginProvider); + LoginUserId = DrawInputRow("用户 ID", LoginUserId); + LoginCode = DrawInputRow("登录 Code(可选)", LoginCode); + Nickname = DrawInputRow("昵称", Nickname); + AvatarUrl = DrawInputRow("头像地址", AvatarUrl); + }); + + DrawCard("说明", () => + { + GUILayout.Label("点击“登录”时:如果填写了 LoginCode,则优先走 code 登录;否则走用户 ID 登录。", _bodyStyle); + GUILayout.Label("登录成功后自动拉取主页数据,并进入主页。", _bodyStyle); + }); + + GUILayout.EndScrollView(); + GUILayout.EndArea(); + + DrawBottomButtons(new ButtonSpec("登录", LoginFlowAsync, true, true)); + } + + private void DrawHomePage() + { + var contentRect = DrawPageFrame("主页", "退出登录", () => RunAction("退出登录", LogoutFlowAsync)); + GUILayout.BeginArea(contentRect); + _homeScroll = GUILayout.BeginScrollView(_homeScroll); + + DrawCard("当前用户状态", () => + { + DrawValueRow("已初始化", Brisk.IsInitialized ? "是" : "否"); + DrawValueRow("已登录", Brisk.IsLoggedIn ? "是" : "否"); + DrawValueRow("PlayerId", NullToDash(Brisk.PlayerId)); + DrawValueRow("登录身份", Brisk.Identity == null ? "-" : Brisk.Identity.LoginProvider + " / " + Brisk.Identity.LoginUserId); + DrawValueRow("昵称", _me == null ? "-" : NullToDash(_me.Nickname)); + DrawValueRow("项目账号", _me == null ? "-" : NullToDash(_me.ProjectAccountId)); + }); + + DrawCard("动态配置", () => + { + GUILayout.Label("FeatureFlags", _sectionTitleStyle); + GUILayout.TextArea(FormatValue(_config == null ? null : _config.FeatureFlags), _textAreaStyle, GUILayout.Height(120f)); + GUILayout.Space(8f); + GUILayout.Label("DynamicConfig", _sectionTitleStyle); + GUILayout.TextArea(FormatValue(_config == null ? null : _config.DynamicConfig), _textAreaStyle, GUILayout.Height(160f)); + }); + + DrawCard("快捷操作", () => + { + DrawInlineButtons( + new ButtonSpec("刷新主页数据", RefreshHomeAsync, false, true), + new ButtonSpec("查看日志", ShowHomeLogsAsync, false, true)); + }); + + GUILayout.EndScrollView(); + GUILayout.EndArea(); + + DrawBottomNav( + new NavSpec("公告", SamplePage.Announcements), + new NavSpec("排行榜", SamplePage.Leaderboard), + new NavSpec("云存档", SamplePage.Archive), + new NavSpec("空间", SamplePage.Space)); + } + + private void DrawAnnouncementsPage() + { + var contentRect = DrawPageFrame("公告", "返回主页", () => _page = SamplePage.Home, "刷新", LoadAnnouncementsAsync); + GUILayout.BeginArea(contentRect); + _announcementScroll = GUILayout.BeginScrollView(_announcementScroll); + + if (_announcements.Count == 0) + { + DrawCard("公告列表", () => GUILayout.Label("暂无公告,点击右上角“刷新”获取。", _bodyStyle)); + } + else + { + DrawCard("公告列表", () => + { + for (var i = 0; i < _announcements.Count; i++) + { + var item = _announcements[i]; + var prefix = item.IsRead ? "[已读] " : "[未读] "; + if (GUILayout.Button(prefix + item.Title, _listButtonStyle, GUILayout.Height(58f))) + { + _selectedAnnouncement = item; + _showAnnouncementModal = true; + } + } + }); + } GUILayout.EndScrollView(); GUILayout.EndArea(); } - private void DrawHeader() + private void DrawLeaderboardPage() { - GUILayout.Label("Brisk IMGUI 测试面板", GUI.skin.box); - GUILayout.Label("这个场景用于在一个页面内测试 SDK 的完整流程。", GUI.skin.label); - } + var contentRect = DrawPageFrame("排行榜", "返回主页", () => _page = SamplePage.Home); + GUILayout.BeginArea(contentRect); + _leaderboardScroll = GUILayout.BeginScrollView(_leaderboardScroll); - 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 缓存", () => + DrawCard("排行榜配置", () => { - SetResult("Bootstrap 缓存", Brisk.Bootstrap); - return Task.CompletedTask; - }, Brisk.IsInitialized); - DrawButton("关闭 SDK", () => - { - Brisk.Shutdown(); - Log("SDK 已关闭。"); - _statusText = "SDK 已关闭"; - return Task.CompletedTask; + RankKey = DrawInputRow("排行榜 Key", RankKey); + LeaderboardLimit = DrawInputRow("Top 数量", LeaderboardLimit); + AroundMeRange = DrawInputRow("附近范围", AroundMeRange); + DrawInlineButtons( + new ButtonSpec("更新列表", RefreshLeaderboardAsync, false, true), + new ButtonSpec(_viewAroundMe ? "切换到 Top" : "切换到我附近", ToggleLeaderboardModeAsync, false, true)); }); - DrawButton("清空输出", () => + + DrawCard("我的排名", () => { - _resultText = "输出已清空。"; - _lastErrorText = string.Empty; - _eventLogs.Clear(); - _statusText = "输出已清空"; - return Task.CompletedTask; + DrawValueRow("当前模式", _viewAroundMe ? "我附近的排名" : "Top 排行榜"); + DrawValueRow("我的名次", _leaderboardMe == null ? "-" : _leaderboardMe.Rank.ToString()); + DrawValueRow("我的分数", _leaderboardMe == null ? "-" : _leaderboardMe.Score.ToString()); + DrawValueRow("当前待提交分数", SubmitScoreValue); + DrawInlineButtons(new ButtonSpec("分数 +1 并提交", IncreaseAndSubmitScoreAsync, true, true)); }); - 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("同步当前身份到空间查询", () => + DrawCard("排行榜列表", () => { - ApplyCurrentIdentityToSpace(); - SetResult("空间查询身份", new Dictionary + if (_leaderboardEntries.Count == 0) { - { "space_player_id", SpacePlayerId }, - { "space_login_provider", SpaceLoginProvider }, - { "space_login_user_id", SpaceLoginUserId } - }); - return Task.CompletedTask; - }, Brisk.IsInitialized); - GUILayout.EndHorizontal(); - EndSection(); - } + GUILayout.Label("暂无数据,请先点击“更新列表”。", _bodyStyle); + return; + } - private void DrawAnnouncementsSection() - { - BeginSection("公告"); - AnnouncementId = DrawEditableRow("公告 ID", AnnouncementId); + for (var i = 0; i < _leaderboardEntries.Count; i++) + { + var entry = _leaderboardEntries[i]; + GUILayout.BeginVertical(_cardStyle); + GUILayout.Label("#" + entry.Rank + " " + NullToDash(entry.Nickname), _sectionTitleStyle); + GUILayout.Label("PlayerId: " + NullToDash(entry.PlayerId), _bodyStyle); + GUILayout.Label("分数: " + entry.Score, _bodyStyle); + GUILayout.EndVertical(); + } + }); - 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(); + GUILayout.EndArea(); } - private async Task InitializeAsync() + private void DrawArchivePage() + { + var contentRect = DrawPageFrame("云存档", "返回主页", () => _page = SamplePage.Home); + GUILayout.BeginArea(contentRect); + _archiveScroll = GUILayout.BeginScrollView(_archiveScroll); + + DrawCard("槽位与操作", () => + { + ArchiveSlotNo = DrawInputRow("槽位号", ArchiveSlotNo); + ArchiveBaseVersion = DrawInputRow("基准版本", ArchiveBaseVersion); + DrawInlineButtons( + new ButtonSpec("获取槽位列表", GetArchiveSlotsAsync, false, true), + new ButtonSpec("获取元信息", GetArchiveMetaAsync, false, true), + new ButtonSpec("下载存档", DownloadArchiveAsync, false, true), + new ButtonSpec("上传存档", UploadArchiveAsync, true, true)); + }); + + DrawCard("文本存档内容", () => + { + ArchiveContent = GUILayout.TextArea(ArchiveContent ?? string.Empty, _textAreaStyle, GUILayout.Height(260f)); + }); + + DrawCard("槽位列表", () => + { + if (_archiveSlots.Count == 0) + { + GUILayout.Label("暂无槽位数据。", _bodyStyle); + } + else + { + for (var i = 0; i < _archiveSlots.Count; i++) + { + var slot = _archiveSlots[i]; + GUILayout.Label("槽位 " + slot.SlotNo + " | version " + slot.Version + " | " + slot.SizeBytes + " bytes", _bodyStyle); + } + } + + GUILayout.Space(8f); + GUILayout.Label("当前元信息", _sectionTitleStyle); + GUILayout.TextArea(FormatValue(_archiveMeta), _textAreaStyle, GUILayout.Height(120f)); + }); + + GUILayout.EndScrollView(); + GUILayout.EndArea(); + } + + private void DrawSpacePage() + { + var title = _viewingTargetSpace ? "TA 的空间" : "我的空间"; + var backLabel = _viewingTargetSpace ? "返回我的空间" : "返回主页"; + Action backAction = () => + { + if (_viewingTargetSpace) + { + _viewingTargetSpace = false; + _targetSpaceView = null; + _targetSpaceStats = null; + _targetSpaceContentText = string.Empty; + return; + } + + _page = SamplePage.Home; + }; + + var contentRect = DrawPageFrame(title, backLabel, backAction, "刷新", RefreshSpacePageAsync); + GUILayout.BeginArea(contentRect); + _spaceScroll = GUILayout.BeginScrollView(_spaceScroll); + + if (_viewingTargetSpace) + { + DrawTargetSpaceContent(); + } + else + { + DrawMySpaceContent(); + } + + GUILayout.EndScrollView(); + GUILayout.EndArea(); + } + + private void DrawMySpaceContent() + { + DrawCard("我的空间内容", () => + { + GUILayout.Label("当前空间文本", _sectionTitleStyle); + SpacePayloadText = GUILayout.TextArea(SpacePayloadText ?? string.Empty, _textAreaStyle, GUILayout.Height(240f)); + DrawInlineButtons( + new ButtonSpec("更新我的空间", UpdateMySpaceAsync, true, true), + new ButtonSpec("重新拉取我的空间", LoadMySpaceAsync, false, true)); + }); + + DrawCard("我的空间状态", () => + { + DrawValueRow("我的 PlayerId", NullToDash(Brisk.PlayerId)); + DrawValueRow("昵称", _mySpaceView == null ? "-" : NullToDash(_mySpaceView.Nickname)); + DrawValueRow("内容版本", _mySpaceView == null ? "-" : _mySpaceView.ContentVersion.ToString()); + DrawValueRow("内容类型", _mySpaceView == null ? "-" : NullToDash(_mySpaceView.ContentType)); + DrawValueRow("内容大小", _mySpaceView == null ? "-" : _mySpaceView.ContentSizeBytes + " bytes"); + DrawValueRow("点赞数", _mySpaceStats == null ? "-" : _mySpaceStats.LikeCount.ToString()); + DrawValueRow("访问数", _mySpaceStats == null ? "-" : _mySpaceStats.VisitCount.ToString()); + DrawValueRow("更新时间", _mySpaceView == null ? "-" : NullToDash(_mySpaceView.UpdatedAt)); + GUILayout.Space(8f); + GUILayout.Label("最近给我点赞的用户", _sectionTitleStyle); + if (_mySpaceLikes.Count == 0) + { + GUILayout.Label("暂无点赞记录。", _bodyStyle); + } + else + { + for (var i = 0; i < _mySpaceLikes.Count; i++) + { + var item = _mySpaceLikes[i]; + GUILayout.Label(NullToDash(item.Nickname) + " | " + NullToDash(item.PlayerId), _bodyStyle); + } + } + }); + + DrawCard("访问其他用户空间", () => + { + SpacePlayerId = DrawInputRow("目标用户 PlayerId", SpacePlayerId); + DrawInlineButtons(new ButtonSpec("前往该用户空间", VisitTargetSpaceAsync, true, true)); + }); + } + + private void DrawTargetSpaceContent() + { + DrawCard("TA 的空间内容", () => + { + DrawValueRow("昵称", _targetSpaceView == null ? "-" : NullToDash(_targetSpaceView.Nickname)); + DrawValueRow("PlayerId", _targetSpaceView == null ? "-" : NullToDash(_targetSpaceView.PlayerId)); + DrawValueRow("内容类型", _targetSpaceView == null ? "-" : NullToDash(_targetSpaceView.ContentType)); + DrawValueRow("内容大小", _targetSpaceView == null ? "-" : _targetSpaceView.ContentSizeBytes + " bytes"); + DrawValueRow("点赞数", _targetSpaceStats == null ? "-" : _targetSpaceStats.LikeCount.ToString()); + DrawValueRow("访问数", _targetSpaceStats == null ? "-" : _targetSpaceStats.VisitCount.ToString()); + DrawValueRow("我的点赞状态", _targetSpaceView != null && _targetSpaceView.LikedByMe ? "已点赞" : "未点赞"); + GUILayout.Space(8f); + GUILayout.Label("空间内容", _sectionTitleStyle); + GUILayout.TextArea(_targetSpaceContentText ?? string.Empty, _textAreaStyle, GUILayout.Height(240f)); + }); + + DrawCard("操作", () => + { + DrawInlineButtons( + new ButtonSpec("点赞", LikeTargetSpaceAsync, false, true), + new ButtonSpec("取消点赞", UnlikeTargetSpaceAsync, false, true)); + }); + } + + private async Task InitializeFlowAsync() { await Brisk.InitializeAsync(new BriskOptions { @@ -326,283 +537,371 @@ public sealed class BriskQuickStartSample : MonoBehaviour ExitHandler = HandleExitRequested }); - SetResult("初始化结果", Brisk.Bootstrap); + _page = Brisk.IsLoggedIn ? SamplePage.Home : SamplePage.Login; + if (Brisk.IsLoggedIn) + { + await RefreshHomeAsync(); + } } - private async Task ReinitializeAsync() + private async Task LoginFlowAsync() { - if (Brisk.IsInitialized) + if (string.IsNullOrWhiteSpace(LoginCode)) { - Brisk.Shutdown(); - Log("重新初始化前已先关闭 SDK。"); + await Brisk.Auth.LoginWithUserIdAsync(LoginProvider, LoginUserId, CreateProfile()); + } + else + { + await Brisk.Auth.LoginWithCodeAsync(LoginProvider, LoginCode, CreateProfile()); } - await InitializeAsync(); + await RefreshHomeAsync(); + _page = SamplePage.Home; } - private async Task LoginWithUserIdAsync() + private async Task RefreshHomeAsync() { - var result = await Brisk.Auth.LoginWithUserIdAsync(LoginProvider, LoginUserId, CreateProfile()); - ApplyIdentity(result.PlayerId, result.LoginProvider, result.LoginUserId); - SetResult("按用户 ID 登录结果", result); + _me = await Brisk.Player.GetMeAsync(); + _config = await Brisk.Config.GetCurrentAsync(); + ApplyIdentity(_me.PlayerId, _me.LoginProvider, _me.LoginUserId); + await LoadMySpaceAsync(); } - private async Task LoginWithCodeAsync() + private async Task LoadAnnouncementsAsync() { - 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 + _announcements = await Brisk.Announcements.GetListAsync(); + if (_announcements.Count > 0) { - { "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(); + AnnouncementId = _announcements[0].Id.ToString(); } - - SetResult("公告列表", _announcementCache); } - private async Task MarkAnnouncementAsync() + private async Task RefreshLeaderboardAsync() { - var id = ParseRequiredLong(AnnouncementId, nameof(AnnouncementId)); - await Brisk.Announcements.MarkReadAsync(id); - SetResult("公告已标记已读", new Dictionary { { "announcement_id", id } }); + _leaderboardMe = await Brisk.Leaderboard.GetMeAsync(RankKey); + SubmitScoreValue = (_leaderboardMe == null ? 0L : _leaderboardMe.Score).ToString(); + _leaderboardEntries = _viewAroundMe + ? await Brisk.Leaderboard.GetAroundMeAsync(RankKey, ParseOrDefault(AroundMeRange, 5)) + : await Brisk.Leaderboard.GetTopAsync(RankKey, ParseOrDefault(LeaderboardLimit, 10)); } - private async Task MarkFirstCachedAnnouncementAsync() + private async Task ToggleLeaderboardModeAsync() { - if (_announcementCache.Count == 0) - { - throw new InvalidOperationException("公告缓存为空,请先执行“获取公告列表”。"); - } - - var id = _announcementCache[0].Id; - AnnouncementId = id.ToString(); - await Brisk.Announcements.MarkReadAsync(id); - SetResult("首条缓存公告已标记已读", _announcementCache[0]); + _viewAroundMe = !_viewAroundMe; + await RefreshLeaderboardAsync(); } - private async Task GetTopAsync() + private async Task IncreaseAndSubmitScoreAsync() { - 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)); + var score = ParseOrDefault(SubmitScoreValue, 0) + 1; + SubmitScoreValue = score.ToString(); await Brisk.Leaderboard.SubmitScoreAsync(RankKey, score); - SetResult("分数提交结果", new Dictionary - { - { "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); + await RefreshLeaderboardAsync(); } private async Task GetArchiveSlotsAsync() { - var result = await Brisk.Archive.GetSlotsAsync(); - SetResult("存档槽位列表", result); + _archiveSlots = await Brisk.Archive.GetSlotsAsync(); } 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); + _archiveMeta = await Brisk.Archive.GetMetaAsync(ParseRequiredInt(ArchiveSlotNo, nameof(ArchiveSlotNo))); + ArchiveBaseVersion = _archiveMeta == null ? ArchiveBaseVersion : _archiveMeta.Version.ToString(); } private async Task DownloadArchiveAsync() { var result = await Brisk.Archive.DownloadAsync(ParseRequiredInt(ArchiveSlotNo, nameof(ArchiveSlotNo))); ArchiveBaseVersion = result == null ? ArchiveBaseVersion : result.Version.ToString(); + ArchiveContent = await Brisk.Archive.DownloadTextAsync(ParseRequiredInt(ArchiveSlotNo, nameof(ArchiveSlotNo))); + await GetArchiveMetaAsync(); + } - var output = new Dictionary + private async Task UploadArchiveAsync() + { + var baseVersion = ParseNullableInt(ArchiveBaseVersion); + var result = await Brisk.Archive.UploadTextAsync(ParseRequiredInt(ArchiveSlotNo, nameof(ArchiveSlotNo)), ArchiveContent ?? string.Empty, baseVersion); + ArchiveBaseVersion = result == null ? ArchiveBaseVersion : result.Version.ToString(); + await GetArchiveSlotsAsync(); + await GetArchiveMetaAsync(); + } + + private async Task LoadMySpaceAsync() + { + if (string.IsNullOrWhiteSpace(Brisk.PlayerId)) { - { "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) } - }; + return; + } - SetResult("存档下载结果", output); + _mySpaceView = await Brisk.Space.GetByPlayerIdAsync(Brisk.PlayerId); + _mySpaceStats = await Brisk.Space.GetStatsByPlayerIdAsync(Brisk.PlayerId); + _mySpaceLikes = await Brisk.Space.GetLikesByPlayerIdAsync(Brisk.PlayerId, 10); + SpacePayloadText = await DownloadSpaceTextAsync(_mySpaceView, () => Brisk.Space.DownloadContentByPlayerIdAsync(Brisk.PlayerId)); } - private async Task GetSpaceByPlayerIdAsync() + private async Task RefreshSpacePageAsync() { - 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 { { "player_id", SpacePlayerId } }); - } - - private async Task UnlikeByPlayerIdAsync() - { - await Brisk.Space.UnlikeByPlayerIdAsync(SpacePlayerId); - SetResult("按 PlayerId 取消点赞结果", new Dictionary { { "player_id", SpacePlayerId } }); - } - - private async Task LikeByLoginAsync() - { - await Brisk.Space.LikeByLoginIdentityAsync(SpaceLoginProvider, SpaceLoginUserId); - SetResult("按登录身份点赞结果", new Dictionary + if (_viewingTargetSpace) { - { "login_provider", SpaceLoginProvider }, - { "login_user_id", SpaceLoginUserId } - }); - } + await LoadTargetSpaceAsync(); + return; + } - private async Task UnlikeByLoginAsync() - { - await Brisk.Space.UnlikeByLoginIdentityAsync(SpaceLoginProvider, SpaceLoginUserId); - SetResult("按登录身份取消点赞结果", new Dictionary - { - { "login_provider", SpaceLoginProvider }, - { "login_user_id", SpaceLoginUserId } - }); + await LoadMySpaceAsync(); } private async Task UpdateMySpaceAsync() { - var payload = new Dictionary - { - { "sample_text", SpacePayloadText }, - { "updated_at", DateTimeOffset.UtcNow.ToString("O") }, - { "player_id", Brisk.PlayerId }, - { "rank_key", RankKey } - }; - - await Brisk.Space.UpdateMyAsync(payload); - SetResult("更新我的空间结果", payload); + var baseVersion = _mySpaceView != null && _mySpaceView.ContentExists ? _mySpaceView.ContentVersion : 0L; + await Brisk.Space.UpdateMyAsync(SpacePayloadText ?? string.Empty, baseVersion, "text/plain"); + await LoadMySpaceAsync(); } - private async Task GetMyVisitsAsync() + private async Task VisitTargetSpaceAsync() { - var result = await Brisk.Space.GetMyVisitsAsync(); - SetResult("我的访客列表", result); + await LoadTargetSpaceAsync(); + _viewingTargetSpace = true; + } + + private async Task LoadTargetSpaceAsync() + { + _targetSpaceView = await Brisk.Space.GetByPlayerIdAsync(SpacePlayerId); + _targetSpaceStats = await Brisk.Space.GetStatsByPlayerIdAsync(SpacePlayerId); + _targetSpaceContentText = await DownloadSpaceTextAsync(_targetSpaceView, () => Brisk.Space.DownloadContentByPlayerIdAsync(SpacePlayerId)); + } + + private async Task LikeTargetSpaceAsync() + { + await Brisk.Space.LikeByPlayerIdAsync(SpacePlayerId); + await LoadTargetSpaceAsync(); + } + + private async Task UnlikeTargetSpaceAsync() + { + await Brisk.Space.UnlikeByPlayerIdAsync(SpacePlayerId); + await LoadTargetSpaceAsync(); + } + + private async Task LogoutFlowAsync() + { + await Brisk.Auth.LogoutAsync(); + _page = SamplePage.Login; + } + + private Task ResetTransientStatusAsync() + { + _statusText = "等待开始"; + _errorText = string.Empty; + return Task.CompletedTask; + } + + private Task ShowHomeLogsAsync() + { + _errorText = string.Join("\n", _logs.ToArray()); + return Task.CompletedTask; } private async Task RunSmokeFlowAsync() { - await InitializeAsync(); - + await InitializeFlowAsync(); if (!Brisk.IsLoggedIn) { - await LoginWithUserIdAsync(); + await LoginFlowAsync(); } - - await GetMeAsync(); - await GetConfigAsync(); - await GetTopAsync(); - await GetAnnouncementsAsync(); - await GetArchiveSlotsAsync(); - await GetMyVisitsAsync(); } - private void DrawButton(string label, Func action, bool enabled = true) + private void DrawAnnouncementModal() { - var previous = GUI.enabled; - GUI.enabled = previous && !_isBusy && enabled; - if (GUILayout.Button(label, GUILayout.Height(30f))) + GUI.color = new Color(0f, 0f, 0f, 0.55f); + GUI.Box(new Rect(0f, 0f, DesignWidth, DesignHeight), GUIContent.none); + GUI.color = Color.white; + + var modalRect = new Rect(48f, 180f, DesignWidth - 96f, DesignHeight - 360f); + GUILayout.BeginArea(modalRect, GUI.skin.box); + GUILayout.Label(_selectedAnnouncement.Title, _titleStyle); + GUILayout.Label("开始: " + NullToDash(_selectedAnnouncement.StartAt), _bodyStyle); + GUILayout.Label("结束: " + NullToDash(_selectedAnnouncement.EndAt), _bodyStyle); + GUILayout.Label("状态: " + (_selectedAnnouncement.IsRead ? "已读" : "未读"), _bodyStyle); + GUILayout.Space(8f); + _modalScroll = GUILayout.BeginScrollView(_modalScroll, GUILayout.Height(520f)); + GUILayout.TextArea(NullToDash(_selectedAnnouncement.Content), _textAreaStyle, GUILayout.ExpandHeight(true)); + GUILayout.EndScrollView(); + GUILayout.Space(10f); + DrawInlineButtons( + new ButtonSpec("标记已读", MarkSelectedAnnouncementAsync, false, true), + new ButtonSpec("关闭", CloseAnnouncementModalAsync, false, true)); + GUILayout.EndArea(); + } + + private Task CloseAnnouncementModalAsync() + { + _showAnnouncementModal = false; + return Task.CompletedTask; + } + + private async Task MarkSelectedAnnouncementAsync() + { + if (_selectedAnnouncement == null) { - RunAction(label, action); + return; } - GUI.enabled = previous; + await Brisk.Announcements.MarkReadAsync(_selectedAnnouncement.Id); + _selectedAnnouncement.IsRead = true; + await LoadAnnouncementsAsync(); + _showAnnouncementModal = false; + } + + private void DrawBusyOverlay() + { + GUI.color = new Color(0f, 0f, 0f, 0.35f); + GUI.Box(new Rect(0f, 0f, DesignWidth, DesignHeight), GUIContent.none); + GUI.color = Color.white; + GUI.Box(new Rect(180f, 580f, 360f, 120f), "正在处理: " + _busyAction); + } + + private Rect DrawPageFrame(string title, string backLabel, Action backAction, string rightLabel = null, Func rightAction = null) + { + var outerRect = new Rect(20f, 20f, DesignWidth - 40f, DesignHeight - 40f); + GUI.Box(outerRect, GUIContent.none, _pageStyle); + + var headerRect = new Rect(outerRect.x + 20f, outerRect.y + 16f, outerRect.width - 40f, 70f); + GUILayout.BeginArea(headerRect); + GUILayout.BeginHorizontal(); + if (!string.IsNullOrWhiteSpace(backLabel)) + { + DrawHeaderButton(backLabel, backAction); + } + else + { + GUILayout.Space(150f); + } + + GUILayout.FlexibleSpace(); + GUILayout.Label(title, _titleStyle, GUILayout.Height(48f)); + GUILayout.FlexibleSpace(); + if (!string.IsNullOrWhiteSpace(rightLabel) && rightAction != null) + { + DrawHeaderButton(rightLabel, () => RunAction(rightLabel, rightAction)); + } + else + { + GUILayout.Space(150f); + } + + GUILayout.EndHorizontal(); + GUILayout.EndArea(); + + var statusRect = new Rect(outerRect.x + 20f, headerRect.yMax + 6f, outerRect.width - 40f, 74f); + GUI.Box(statusRect, GUIContent.none, _cardStyle); + GUILayout.BeginArea(new Rect(statusRect.x + 12f, statusRect.y + 10f, statusRect.width - 24f, statusRect.height - 20f)); + GUILayout.Label("状态: " + _statusText, _statusStyle); + GUILayout.Label("错误: " + (string.IsNullOrWhiteSpace(_errorText) ? "无" : _errorText), _hintStyle); + GUILayout.EndArea(); + + return new Rect(outerRect.x + 20f, statusRect.yMax + 12f, outerRect.width - 40f, outerRect.height - 250f); + } + + private void DrawBottomButtons(params ButtonSpec[] buttons) + { + var rect = new Rect(40f, DesignHeight - 170f, DesignWidth - 80f, 110f); + GUILayout.BeginArea(rect); + DrawInlineButtons(buttons); + GUILayout.EndArea(); + } + + private void DrawBottomNav(params NavSpec[] items) + { + var rect = new Rect(30f, DesignHeight - 170f, DesignWidth - 60f, 110f); + GUI.Box(new Rect(rect.x, rect.y, rect.width, rect.height), GUIContent.none, _cardStyle); + GUILayout.BeginArea(new Rect(rect.x + 10f, rect.y + 10f, rect.width - 20f, rect.height - 20f)); + GUILayout.BeginHorizontal(); + for (var i = 0; i < items.Length; i++) + { + var item = items[i]; + if (GUILayout.Button(item.Label, _primaryButtonStyle, GUILayout.Height(64f), GUILayout.Width(150f))) + { + _page = item.Page; + if (item.Page == SamplePage.Announcements) + { + RunAction("加载公告", LoadAnnouncementsAsync); + } + else if (item.Page == SamplePage.Leaderboard) + { + RunAction("加载排行榜", RefreshLeaderboardAsync); + } + else if (item.Page == SamplePage.Archive) + { + RunAction("加载存档", GetArchiveSlotsAsync); + } + else if (item.Page == SamplePage.Space) + { + RunAction("加载空间", LoadMySpaceAsync); + } + } + } + + GUILayout.EndHorizontal(); + GUILayout.EndArea(); + } + + private void DrawHeaderButton(string label, Action action) + { + if (GUILayout.Button(label, _buttonStyle, GUILayout.Width(150f), GUILayout.Height(42f))) + { + action(); + } + } + + private void DrawInlineButtons(params ButtonSpec[] buttons) + { + GUILayout.BeginHorizontal(); + for (var i = 0; i < buttons.Length; i++) + { + var button = buttons[i]; + var previous = GUI.enabled; + GUI.enabled = previous && !_isBusy && button.Enabled; + var style = button.Primary ? _primaryButtonStyle : _buttonStyle; + if (GUILayout.Button(button.Label, style, GUILayout.Height(56f))) + { + RunAction(button.Label, button.Action); + } + + GUI.enabled = previous; + } + + GUILayout.EndHorizontal(); + } + + private void DrawCard(string title, Action content) + { + GUILayout.BeginVertical(_cardStyle); + GUILayout.Label(title, _sectionTitleStyle); + GUILayout.Space(6f); + content(); + GUILayout.EndVertical(); + GUILayout.Space(12f); + } + + private string DrawInputRow(string label, string value) + { + GUILayout.Label(label, _hintStyle); + return GUILayout.TextField(value ?? string.Empty, _inputStyle, GUILayout.Height(48f)); + } + + private bool DrawToggleRow(string label, bool value) + { + GUILayout.Label(label, _hintStyle); + return GUILayout.Toggle(value, value ? "开启" : "关闭", GUILayout.Height(42f)); + } + + private void DrawValueRow(string label, string value) + { + GUILayout.BeginHorizontal(); + GUILayout.Label(label, _hintStyle, GUILayout.Width(180f)); + GUILayout.Label(value, _bodyStyle); + GUILayout.EndHorizontal(); } private void RunAction(string actionName, Func action) @@ -612,15 +911,15 @@ public sealed class BriskQuickStartSample : MonoBehaviour return; } - RunActionAsync(actionName, action); + RunActionInternal(actionName, action); } - private async void RunActionAsync(string actionName, Func action) + private async void RunActionInternal(string actionName, Func action) { _isBusy = true; _busyAction = actionName; _statusText = "执行中: " + actionName; - _lastErrorText = string.Empty; + _errorText = string.Empty; Log("开始执行: " + actionName); try @@ -632,8 +931,8 @@ public sealed class BriskQuickStartSample : MonoBehaviour catch (Exception exception) { _statusText = "执行失败: " + actionName; - _lastErrorText = FormatException(exception); - Log("执行失败: " + actionName + " | " + exception.GetType().Name + " | " + exception.Message); + _errorText = FormatException(exception); + Log("执行失败: " + actionName + " | " + exception.Message); Debug.LogException(exception, this); } finally @@ -643,9 +942,67 @@ public sealed class BriskQuickStartSample : MonoBehaviour } } - private void SetResult(string title, object value) + private void SyncPageBySdkState() { - _resultText = title + "\n" + new string('=', title.Length) + "\n" + FormatValue(value); + if (!Brisk.IsInitialized) + { + _page = SamplePage.Initialize; + return; + } + + _page = Brisk.IsLoggedIn ? SamplePage.Home : SamplePage.Login; + } + + private void HandleInitialized() + { + SyncPageBySdkState(); + Log("事件: 初始化完成"); + } + + private void HandleLoggedIn() + { + SyncPageBySdkState(); + Log("事件: 登录完成"); + } + + private void HandleLoggedOut() + { + SyncPageBySdkState(); + Log("事件: 登出完成"); + } + + private void HandleAuthExpired(BriskAuthExpiredException exception) + { + _page = SamplePage.Login; + Log("事件: 登录态失效 | " + exception.Message); + } + + private void HandleBlockingError(BriskBlockingException exception) + { + Log("事件: 阻断错误 | " + exception.Message); + } + + private void HandleExitRequested() + { + Log("Brisk 请求退出。"); + } + + private void Log(string message) + { + _logs.Add("[" + DateTime.Now.ToString("HH:mm:ss") + "] " + message); + if (_logs.Count > 80) + { + _logs.RemoveAt(0); + } + } + + private BriskProfile CreateProfile() + { + return new BriskProfile + { + Nickname = Nickname, + AvatarUrl = AvatarUrl + }; } private void ApplyIdentity(string playerId, string loginProvider, string loginUserId) @@ -668,70 +1025,69 @@ public sealed class BriskQuickStartSample : MonoBehaviour } } - private void ApplyCurrentIdentityToSpace() + private void EnsureStyles() { - if (Brisk.Identity != null) + if (_pageStyle != null) { - ApplyIdentity(Brisk.Identity.PlayerId, Brisk.Identity.LoginProvider, Brisk.Identity.LoginUserId); return; } - if (Brisk.IsInitialized) + _pageStyle = new GUIStyle(GUI.skin.box) { - ApplyIdentity(Brisk.PlayerId, LoginProvider, LoginUserId); - } - } - - private BriskProfile CreateProfile() - { - return new BriskProfile - { - Nickname = Nickname, - AvatarUrl = AvatarUrl + padding = new RectOffset(18, 18, 18, 18) }; - } - 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) + _cardStyle = new GUIStyle(GUI.skin.box) { - _eventLogs.RemoveAt(0); - } + padding = new RectOffset(16, 16, 14, 14), + margin = new RectOffset(0, 0, 0, 0) + }; - _logScroll.y = float.MaxValue; + _titleStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 28, + fontStyle = FontStyle.Bold, + alignment = TextAnchor.MiddleCenter, + wordWrap = true + }; + + _sectionTitleStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 18, + fontStyle = FontStyle.Bold, + wordWrap = true + }; + + _bodyStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 16, + wordWrap = true + }; + + _hintStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 14, + wordWrap = true + }; + + _statusStyle = new GUIStyle(_bodyStyle) + { + fontStyle = FontStyle.Bold + }; + + _buttonStyle = new GUIStyle(GUI.skin.button) + { + fontSize = 16, + fontStyle = FontStyle.Bold, + wordWrap = true + }; + + _primaryButtonStyle = new GUIStyle(_buttonStyle); + _dangerButtonStyle = new GUIStyle(_buttonStyle); + _inputStyle = new GUIStyle(GUI.skin.textField) { fontSize = 16 }; + _textAreaStyle = new GUIStyle(GUI.skin.textArea) { fontSize = 15, wordWrap = true }; + _listButtonStyle = new GUIStyle(GUI.skin.button) { fontSize = 16, alignment = TextAnchor.MiddleLeft, wordWrap = true }; + _modalStyle = new GUIStyle(GUI.skin.box) { padding = new RectOffset(20, 20, 20, 20) }; } private static int ParseRequiredInt(string value, string fieldName) @@ -744,17 +1100,7 @@ public sealed class BriskQuickStartSample : MonoBehaviour 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) + private static int ParseOrDefault(string value, int fallback) { return int.TryParse(value, out var result) ? result : fallback; } @@ -769,11 +1115,32 @@ public sealed class BriskQuickStartSample : MonoBehaviour return int.TryParse(value, out var result) ? result : (int?)null; } + private static string NullToDash(string value) + { + return string.IsNullOrWhiteSpace(value) ? "-" : value; + } + + private static async Task DownloadSpaceTextAsync(BriskSpaceView view, Func> downloadContent) + { + if (view == null || !view.ContentExists || downloadContent == null) + { + return string.Empty; + } + + var content = await downloadContent(); + if (content == null || content.Bytes == null || content.Bytes.Length == 0) + { + return string.Empty; + } + + return Encoding.UTF8.GetString(content.Bytes); + } + private static string FormatException(Exception exception) { if (exception == null) { - return "未知错误。"; + return "未知错误"; } var builder = new StringBuilder(); @@ -791,39 +1158,26 @@ public sealed class BriskQuickStartSample : MonoBehaviour { var builder = new StringBuilder(); AppendValue(builder, value, 0, 0); - return builder.ToString().TrimEnd(); + return builder.Length == 0 ? "-" : builder.ToString().TrimEnd(); } private static void AppendValue(StringBuilder builder, object value, int indent, int depth) { if (depth > 5) { - builder.AppendLine(Indent(indent) + "..."); + builder.AppendLine(new string(' ', indent) + "..."); return; } if (value == null) { - builder.AppendLine(Indent(indent) + "null"); + builder.AppendLine(new string(' ', 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); + builder.AppendLine(new string(' ', indent) + stringValue); return; } @@ -831,92 +1185,64 @@ public sealed class BriskQuickStartSample : MonoBehaviour { foreach (DictionaryEntry entry in dictionary) { - builder.AppendLine(Indent(indent) + entry.Key + ":"); + builder.AppendLine(new string(' ', indent) + entry.Key + ":"); AppendValue(builder, entry.Value, indent + 2, depth + 1); } - return; } - if (value is IEnumerable enumerable) + if (value is IEnumerable enumerable && !(value is byte[])) { var index = 0; foreach (var item in enumerable) { - builder.AppendLine(Indent(indent) + "[" + index + "]"); + builder.AppendLine(new string(' ', indent) + "[" + index + "]"); AppendValue(builder, item, indent + 2, depth + 1); index++; } + return; + } - if (index == 0) - { - builder.AppendLine(Indent(indent) + "(empty)"); - } - + var type = value.GetType(); + if (type.IsPrimitive || value is decimal) + { + builder.AppendLine(new string(' ', indent) + value); 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 + ":"); + builder.AppendLine(new string(' ', indent) + field.Name + ":"); AppendValue(builder, field.GetValue(value), indent + 2, depth + 1); } } - private static string Indent(int indent) + private readonly struct ButtonSpec { - return new string(' ', indent); + public ButtonSpec(string label, Func action, bool primary, bool enabled) + { + Label = label; + Action = action; + Primary = primary; + Enabled = enabled; + } + + public string Label { get; } + public Func Action { get; } + public bool Primary { get; } + public bool Enabled { get; } } - private static void BeginSection(string title) + private readonly struct NavSpec { - GUILayout.Space(10f); - GUILayout.BeginVertical(GUI.skin.box); - GUILayout.Label(title, GUI.skin.label); - GUILayout.Space(4f); - } + public NavSpec(string label, SamplePage page) + { + Label = label; + Page = page; + } - 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(); + public string Label { get; } + public SamplePage Page { get; } } } diff --git a/Assets/Scenes/BriskQuickStartScene.unity b/Assets/Scenes/BriskQuickStartScene.unity index 38bad70..3230947 100644 --- a/Assets/Scenes/BriskQuickStartScene.unity +++ b/Assets/Scenes/BriskQuickStartScene.unity @@ -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} diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/CHANGELOG.md b/PackageSource/com.foldcc.cc-framework.BriskGameServer/CHANGELOG.md index 101f4c2..2e4f21b 100644 --- a/PackageSource/com.foldcc.cc-framework.BriskGameServer/CHANGELOG.md +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/CHANGELOG.md @@ -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 diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Documentation~/QuickStart.md b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Documentation~/QuickStart.md index 2ce083d..8e0d493 100644 --- a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Documentation~/QuickStart.md +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Documentation~/QuickStart.md @@ -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: diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/README.md b/PackageSource/com.foldcc.cc-framework.BriskGameServer/README.md index 2542a57..a17da0f 100644 --- a/PackageSource/com.foldcc.cc-framework.BriskGameServer/README.md +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/README.md @@ -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` diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Archive/BriskArchiveModule.cs b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Archive/BriskArchiveModule.cs index de9ed05..2d1bdff 100644 --- a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Archive/BriskArchiveModule.cs +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Archive/BriskArchiveModule.cs @@ -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 { new MultipartFormDataSection("base_version", (baseVersion ?? 0).ToString()), @@ -63,6 +63,24 @@ public sealed class BriskArchiveModule }); } + /// + /// 以 UTF-8 文本形式上传指定槽位的存档。 + /// + public Task UploadTextAsync(int slotNo, string text, int? baseVersion = null, string checksum = null) + { + RequireNotNull(text, nameof(text)); + return UploadAsync(slotNo, Encoding.UTF8.GetBytes(text), baseVersion, checksum); + } + + /// + /// 以 JSON 文本形式上传指定槽位的存档。 + /// + public Task 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); + } + /// /// 下载指定槽位的二进制存档。 /// @@ -82,6 +100,24 @@ public sealed class BriskArchiveModule }); } + /// + /// 以 UTF-8 文本形式下载指定槽位的存档。 + /// + public async Task DownloadTextAsync(int slotNo) + { + var result = await DownloadAsync(slotNo); + return result == null || result.Bytes == null ? string.Empty : Encoding.UTF8.GetString(result.Bytes); + } + + /// + /// 以 JSON 对象形式下载指定槽位的存档。 + /// + public async Task 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 headers, string key) { var value = ReadHeader(headers, key); diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Core/BriskHttpClient.cs b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Core/BriskHttpClient.cs index 8c87207..3baf9f2 100644 --- a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Core/BriskHttpClient.cs +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Core/BriskHttpClient.cs @@ -51,23 +51,26 @@ public sealed class BriskHttpClient public async Task> PostMultipartAsync(string path, List formSections, bool auth = false) { - using (var request = UnityWebRequest.Post(BuildUrl(path, null), formSections)) + return await SendMultipartAsync(UnityWebRequest.kHttpVerbPOST, path, formSections, auth); + } + + public async Task> PutMultipartAsync(string path, List formSections, bool auth = false) + { + return await SendMultipartAsync(UnityWebRequest.kHttpVerbPUT, path, formSections, auth); + } + + public async Task GetBytesAsync(string path, Dictionary 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 ?? new Dictionary(); - } - } - - public async Task GetBytesAsync(string path, Dictionary 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> SendMultipartAsync(string method, string path, List 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 ?? new Dictionary(); + } + } + private UnityWebRequest BuildRequest(string method, string path, Dictionary query, object body, bool auth) { var url = BuildUrl(path, query); diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Core/BriskModelMapper.cs b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Core/BriskModelMapper.cs index 6fd9cd3..20917da 100644 --- a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Core/BriskModelMapper.cs +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Core/BriskModelMapper.cs @@ -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 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 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 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 ToSpaceLikeItems(object data) + { + return ExtractList(data) + .Select(item => ToSpaceLikeItem(item as Dictionary)) + .Where(item => item != null) + .ToList(); + } + public static Dictionary ExtractObject(object data) { return data as Dictionary; diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceContentDownloadResult.cs b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceContentDownloadResult.cs new file mode 100644 index 0000000..a8a2a87 --- /dev/null +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceContentDownloadResult.cs @@ -0,0 +1,7 @@ +public sealed class BriskSpaceContentDownloadResult +{ + public byte[] Bytes; + public long Version; + public string Checksum; + public string ContentType; +} diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceContentDownloadResult.cs.meta b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceContentDownloadResult.cs.meta new file mode 100644 index 0000000..5b029c6 --- /dev/null +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceContentDownloadResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e6f27d1ba8b24f5e85362b9c6c9f4c01 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceContentUpdateResult.cs b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceContentUpdateResult.cs new file mode 100644 index 0000000..42fb550 --- /dev/null +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceContentUpdateResult.cs @@ -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; +} diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceContentUpdateResult.cs.meta b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceContentUpdateResult.cs.meta new file mode 100644 index 0000000..7332b34 --- /dev/null +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceContentUpdateResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4bd67f378d264cc69c71f1d03d5b564b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceLikeItem.cs b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceLikeItem.cs new file mode 100644 index 0000000..7ab87d1 --- /dev/null +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceLikeItem.cs @@ -0,0 +1,7 @@ +public sealed class BriskSpaceLikeItem +{ + public string PlayerId; + public string Nickname; + public string AvatarUrl; + public string CreatedAt; +} diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceLikeItem.cs.meta b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceLikeItem.cs.meta new file mode 100644 index 0000000..08c5e05 --- /dev/null +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceLikeItem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ef7d53b3f0ab43e7b74a4c23a76f84f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceLikeResult.cs b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceLikeResult.cs new file mode 100644 index 0000000..3637490 --- /dev/null +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceLikeResult.cs @@ -0,0 +1,5 @@ +public sealed class BriskSpaceLikeResult +{ + public bool Liked; + public long LikeCount; +} diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceLikeResult.cs.meta b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceLikeResult.cs.meta new file mode 100644 index 0000000..fa60d9c --- /dev/null +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceLikeResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 966f47c348db46dfb60120dba20f1ffb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceStats.cs b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceStats.cs index d59e7e1..48fb95b 100644 --- a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceStats.cs +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceStats.cs @@ -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; } diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceView.cs b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceView.cs index 9118e09..d6338e8 100644 --- a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceView.cs +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Models/BriskSpaceView.cs @@ -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; } diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Space/BriskSpaceModule.cs b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Space/BriskSpaceModule.cs index ebd61a8..8ef72e4 100644 --- a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Space/BriskSpaceModule.cs +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Runtime/Space/BriskSpaceModule.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; using System.Threading.Tasks; +using UnityEngine.Networking; /// /// 玩家空间模块。 @@ -65,77 +68,179 @@ public sealed class BriskSpaceModule } /// - /// 按玩家 ID 点赞空间。 + /// 按玩家 ID 获取最近点赞列表。 /// - public async Task LikeByPlayerIdAsync(string playerId) + public async Task> 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(), true); + var data = await context.HttpClient.GetRawDataAsync($"/spaces/{playerId}/likes", CreateLimitQuery(limit), true); + return (IReadOnlyList)BriskModelMapper.ToSpaceLikeItems(data); + }); + } + + /// + /// 按登录身份获取最近点赞列表。 + /// + public async Task> 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)BriskModelMapper.ToSpaceLikeItems(data); + }); + } + + /// + /// 按玩家 ID 点赞空间。 + /// + public async Task LikeByPlayerIdAsync(string playerId) + { + ValidatePlayerId(playerId); + + return await ExecuteAsync(async context => + { + var data = await context.HttpClient.PostJsonRawAsync($"/spaces/{playerId}/like", new Dictionary(), true); + return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data)); }); } /// /// 按玩家 ID 取消点赞空间。 /// - public async Task UnlikeByPlayerIdAsync(string playerId) + public async Task 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)); }); } /// /// 按登录身份点赞空间。 /// - public async Task LikeByLoginIdentityAsync(string loginProvider, string loginUserId) + public async Task 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(), true, CreateLoginIdentityQuery(loginProvider, loginUserId)); + var data = await context.HttpClient.PostJsonRawAsync("/spaces/by-login/like", new Dictionary(), true, CreateLoginIdentityQuery(loginProvider, loginUserId)); + return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data)); }); } /// /// 按登录身份取消点赞空间。 /// - public async Task UnlikeByLoginIdentityAsync(string loginProvider, string loginUserId) + public async Task 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)); + }); + } + + /// + /// 上传当前玩家自己的空间内容。 + /// + public async Task 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 + { + 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); }); } /// /// 更新当前玩家自己的空间内容。 /// - public async Task UpdateMyAsync(object payload) + public Task 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 { { "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)); + } + + /// + /// 按玩家 ID 下载空间内容。 + /// + public async Task DownloadContentByPlayerIdAsync(string playerId) + { + ValidatePlayerId(playerId); + + return await ExecuteAsync(async context => + { + var response = await context.HttpClient.GetBytesAsync($"/spaces/{playerId}/content", null, true); + return CreateDownloadResult(response); + }); + } + + /// + /// 按登录身份下载空间内容。 + /// + public async Task 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); }); } /// /// 获取我的访客列表。 /// - public async Task> GetMyVisitsAsync() + public async Task> 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)BriskModelMapper.ToSpaceVisits(data); }); } @@ -149,6 +254,14 @@ public sealed class BriskSpaceModule }; } + private static Dictionary CreateLimitQuery(int limit) + { + return new Dictionary + { + { "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 headers, string key) + { + var value = ReadHeader(headers, key); + return long.TryParse(value, out var result) ? result : 0L; + } + + private static string ReadHeader(Dictionary 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; + } } diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Samples~/QuickStart/BriskQuickStartSample.cs b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Samples~/QuickStart/BriskQuickStartSample.cs index b67477c..efc9d75 100644 --- a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Samples~/QuickStart/BriskQuickStartSample.cs +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Samples~/QuickStart/BriskQuickStartSample.cs @@ -8,6 +8,20 @@ using UnityEngine; public sealed class BriskQuickStartSample : MonoBehaviour { + private const float DesignWidth = 720f; + private const float DesignHeight = 1280f; + + private enum SamplePage + { + Initialize, + Login, + Home, + Announcements, + Leaderboard, + Archive, + Space + } + [Header("初始化")] public string BaseUrl = "https://brisk.lightyears.ltd"; public string GameKey = "demo-game"; @@ -24,7 +38,7 @@ public sealed class BriskQuickStartSample : MonoBehaviour [Header("排行榜")] public string RankKey = "season-score"; - public string SubmitScoreValue = "128"; + public string SubmitScoreValue = "0"; public string LeaderboardLimit = "10"; public string AroundMeRange = "5"; public string SeasonId = string.Empty; @@ -44,23 +58,60 @@ public sealed class BriskQuickStartSample : MonoBehaviour 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}"; + public string SpacePayloadText = "这里是 unity 测试账户的空间内容。\n\n0.o"; [Header("演示")] public bool AutoRunOnStart; - private readonly List _eventLogs = new List(); + private readonly List _logs = new List(); - private Vector2 _pageScroll; - private Vector2 _resultScroll; - private Vector2 _logScroll; + private Vector2 _initScroll; + private Vector2 _loginScroll; + private Vector2 _homeScroll; + private Vector2 _announcementScroll; + private Vector2 _leaderboardScroll; + private Vector2 _archiveScroll; + private Vector2 _spaceScroll; + private Vector2 _modalScroll; + + private SamplePage _page = SamplePage.Initialize; private bool _isBusy; private string _busyAction = string.Empty; - private string _statusText = "就绪"; - private string _resultText = "尚未执行请求。"; - private string _lastErrorText = string.Empty; - private IReadOnlyList _announcementCache = Array.Empty(); - private IReadOnlyList _seasonHistoryCache = Array.Empty(); + private string _statusText = "等待开始"; + private string _errorText = string.Empty; + + private BriskPlayerMe _me; + private BriskConfigCurrent _config; + private IReadOnlyList _announcements = Array.Empty(); + private BriskAnnouncementItem _selectedAnnouncement; + private IReadOnlyList _leaderboardEntries = Array.Empty(); + private BriskLeaderboardPlayerRank _leaderboardMe; + private IReadOnlyList _archiveSlots = Array.Empty(); + private BriskArchiveMeta _archiveMeta; + private BriskSpaceView _mySpaceView; + private BriskSpaceStats _mySpaceStats; + private IReadOnlyList _mySpaceLikes = Array.Empty(); + private BriskSpaceView _targetSpaceView; + private BriskSpaceStats _targetSpaceStats; + private string _targetSpaceContentText = string.Empty; + private bool _viewAroundMe; + private bool _showAnnouncementModal; + private bool _viewingTargetSpace; + + private GUIStyle _pageStyle; + private GUIStyle _cardStyle; + private GUIStyle _titleStyle; + private GUIStyle _sectionTitleStyle; + private GUIStyle _bodyStyle; + private GUIStyle _hintStyle; + private GUIStyle _buttonStyle; + private GUIStyle _primaryButtonStyle; + private GUIStyle _dangerButtonStyle; + private GUIStyle _inputStyle; + private GUIStyle _textAreaStyle; + private GUIStyle _statusStyle; + private GUIStyle _listButtonStyle; + private GUIStyle _modalStyle; private void OnEnable() { @@ -82,239 +133,399 @@ public sealed class BriskQuickStartSample : MonoBehaviour private void Start() { + SyncPageBySdkState(); if (AutoRunOnStart) { - RunAction("自动冒烟流程", RunSmokeFlowAsync); + 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); + EnsureStyles(); - DrawHeader(); - DrawStatusPanel(); - DrawInitSection(); - DrawLoginSection(); - DrawPlayerAndConfigSection(); - DrawAnnouncementsSection(); - DrawLeaderboardSection(); - DrawArchiveSection(); - DrawSpaceSection(); - DrawOutputSection(); + var scale = Mathf.Min(Screen.width / DesignWidth, Screen.height / DesignHeight); + var offsetX = (Screen.width - (DesignWidth * scale)) * 0.5f; + var offsetY = (Screen.height - (DesignHeight * scale)) * 0.5f; + + var previousMatrix = GUI.matrix; + GUI.matrix = Matrix4x4.TRS(new Vector3(offsetX, offsetY, 0f), Quaternion.identity, new Vector3(scale, scale, 1f)); + + GUI.Box(new Rect(0f, 0f, DesignWidth, DesignHeight), GUIContent.none); + GUILayout.BeginArea(new Rect(0f, 0f, DesignWidth, DesignHeight)); + DrawCurrentPage(); + GUILayout.EndArea(); + + if (_showAnnouncementModal && _selectedAnnouncement != null) + { + DrawAnnouncementModal(); + } + + if (_isBusy) + { + DrawBusyOverlay(); + } + + GUI.matrix = previousMatrix; + } + + private void DrawCurrentPage() + { + switch (_page) + { + case SamplePage.Initialize: + DrawInitializePage(); + break; + case SamplePage.Login: + DrawLoginPage(); + break; + case SamplePage.Home: + DrawHomePage(); + break; + case SamplePage.Announcements: + DrawAnnouncementsPage(); + break; + case SamplePage.Leaderboard: + DrawLeaderboardPage(); + break; + case SamplePage.Archive: + DrawArchivePage(); + break; + default: + DrawSpacePage(); + break; + } + } + + private void DrawInitializePage() + { + var contentRect = DrawPageFrame("初始化", null, null); + GUILayout.BeginArea(contentRect); + _initScroll = GUILayout.BeginScrollView(_initScroll); + + DrawCard("当前配置", () => + { + BaseUrl = DrawInputRow("服务地址", BaseUrl); + GameKey = DrawInputRow("游戏 Key", GameKey); + ClientVersion = DrawInputRow("客户端版本", ClientVersion); + DeviceId = DrawInputRow("设备标识", DeviceId); + ValidateSessionOnInitialize = DrawToggleRow("初始化时校验旧会话", ValidateSessionOnInitialize); + }); + + DrawCard("说明", () => + { + GUILayout.Label("点击“初始化”后会直接调用 Brisk.InitializeAsync。", _bodyStyle); + GUILayout.Label("成功时自动进入登录页;如果已恢复登录态,则直接进入主页。", _bodyStyle); + GUILayout.Label("失败会停留在当前页,并把错误展示在底部状态栏。", _bodyStyle); + }); + + GUILayout.FlexibleSpace(); + GUILayout.EndScrollView(); + GUILayout.EndArea(); + + DrawBottomButtons( + new ButtonSpec("初始化", InitializeFlowAsync, true, true), + new ButtonSpec("重新填写", ResetTransientStatusAsync, false, true)); + } + + private void DrawLoginPage() + { + var contentRect = DrawPageFrame("登录", "返回初始化", () => _page = SamplePage.Initialize); + GUILayout.BeginArea(contentRect); + _loginScroll = GUILayout.BeginScrollView(_loginScroll); + + DrawCard("登录信息", () => + { + LoginProvider = DrawInputRow("登录提供方", LoginProvider); + LoginUserId = DrawInputRow("用户 ID", LoginUserId); + LoginCode = DrawInputRow("登录 Code(可选)", LoginCode); + Nickname = DrawInputRow("昵称", Nickname); + AvatarUrl = DrawInputRow("头像地址", AvatarUrl); + }); + + DrawCard("说明", () => + { + GUILayout.Label("点击“登录”时:如果填写了 LoginCode,则优先走 code 登录;否则走用户 ID 登录。", _bodyStyle); + GUILayout.Label("登录成功后自动拉取主页数据,并进入主页。", _bodyStyle); + }); + + GUILayout.EndScrollView(); + GUILayout.EndArea(); + + DrawBottomButtons(new ButtonSpec("登录", LoginFlowAsync, true, true)); + } + + private void DrawHomePage() + { + var contentRect = DrawPageFrame("主页", "退出登录", () => RunAction("退出登录", LogoutFlowAsync)); + GUILayout.BeginArea(contentRect); + _homeScroll = GUILayout.BeginScrollView(_homeScroll); + + DrawCard("当前用户状态", () => + { + DrawValueRow("已初始化", Brisk.IsInitialized ? "是" : "否"); + DrawValueRow("已登录", Brisk.IsLoggedIn ? "是" : "否"); + DrawValueRow("PlayerId", NullToDash(Brisk.PlayerId)); + DrawValueRow("登录身份", Brisk.Identity == null ? "-" : Brisk.Identity.LoginProvider + " / " + Brisk.Identity.LoginUserId); + DrawValueRow("昵称", _me == null ? "-" : NullToDash(_me.Nickname)); + DrawValueRow("项目账号", _me == null ? "-" : NullToDash(_me.ProjectAccountId)); + }); + + DrawCard("动态配置", () => + { + GUILayout.Label("FeatureFlags", _sectionTitleStyle); + GUILayout.TextArea(FormatValue(_config == null ? null : _config.FeatureFlags), _textAreaStyle, GUILayout.Height(120f)); + GUILayout.Space(8f); + GUILayout.Label("DynamicConfig", _sectionTitleStyle); + GUILayout.TextArea(FormatValue(_config == null ? null : _config.DynamicConfig), _textAreaStyle, GUILayout.Height(160f)); + }); + + DrawCard("快捷操作", () => + { + DrawInlineButtons( + new ButtonSpec("刷新主页数据", RefreshHomeAsync, false, true), + new ButtonSpec("查看日志", ShowHomeLogsAsync, false, true)); + }); + + GUILayout.EndScrollView(); + GUILayout.EndArea(); + + DrawBottomNav( + new NavSpec("公告", SamplePage.Announcements), + new NavSpec("排行榜", SamplePage.Leaderboard), + new NavSpec("云存档", SamplePage.Archive), + new NavSpec("空间", SamplePage.Space)); + } + + private void DrawAnnouncementsPage() + { + var contentRect = DrawPageFrame("公告", "返回主页", () => _page = SamplePage.Home, "刷新", LoadAnnouncementsAsync); + GUILayout.BeginArea(contentRect); + _announcementScroll = GUILayout.BeginScrollView(_announcementScroll); + + if (_announcements.Count == 0) + { + DrawCard("公告列表", () => GUILayout.Label("暂无公告,点击右上角“刷新”获取。", _bodyStyle)); + } + else + { + DrawCard("公告列表", () => + { + for (var i = 0; i < _announcements.Count; i++) + { + var item = _announcements[i]; + var prefix = item.IsRead ? "[已读] " : "[未读] "; + if (GUILayout.Button(prefix + item.Title, _listButtonStyle, GUILayout.Height(58f))) + { + _selectedAnnouncement = item; + _showAnnouncementModal = true; + } + } + }); + } GUILayout.EndScrollView(); GUILayout.EndArea(); } - private void DrawHeader() + private void DrawLeaderboardPage() { - GUILayout.Label("Brisk IMGUI 测试面板", GUI.skin.box); - GUILayout.Label("这个场景用于在一个页面内测试 SDK 的完整流程。", GUI.skin.label); - } + var contentRect = DrawPageFrame("排行榜", "返回主页", () => _page = SamplePage.Home); + GUILayout.BeginArea(contentRect); + _leaderboardScroll = GUILayout.BeginScrollView(_leaderboardScroll); - 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 缓存", () => + DrawCard("排行榜配置", () => { - SetResult("Bootstrap 缓存", Brisk.Bootstrap); - return Task.CompletedTask; - }, Brisk.IsInitialized); - DrawButton("关闭 SDK", () => - { - Brisk.Shutdown(); - Log("SDK 已关闭。"); - _statusText = "SDK 已关闭"; - return Task.CompletedTask; + RankKey = DrawInputRow("排行榜 Key", RankKey); + LeaderboardLimit = DrawInputRow("Top 数量", LeaderboardLimit); + AroundMeRange = DrawInputRow("附近范围", AroundMeRange); + DrawInlineButtons( + new ButtonSpec("更新列表", RefreshLeaderboardAsync, false, true), + new ButtonSpec(_viewAroundMe ? "切换到 Top" : "切换到我附近", ToggleLeaderboardModeAsync, false, true)); }); - DrawButton("清空输出", () => + + DrawCard("我的排名", () => { - _resultText = "输出已清空。"; - _lastErrorText = string.Empty; - _eventLogs.Clear(); - _statusText = "输出已清空"; - return Task.CompletedTask; + DrawValueRow("当前模式", _viewAroundMe ? "我附近的排名" : "Top 排行榜"); + DrawValueRow("我的名次", _leaderboardMe == null ? "-" : _leaderboardMe.Rank.ToString()); + DrawValueRow("我的分数", _leaderboardMe == null ? "-" : _leaderboardMe.Score.ToString()); + DrawValueRow("当前待提交分数", SubmitScoreValue); + DrawInlineButtons(new ButtonSpec("分数 +1 并提交", IncreaseAndSubmitScoreAsync, true, true)); }); - 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("同步当前身份到空间查询", () => + DrawCard("排行榜列表", () => { - ApplyCurrentIdentityToSpace(); - SetResult("空间查询身份", new Dictionary + if (_leaderboardEntries.Count == 0) { - { "space_player_id", SpacePlayerId }, - { "space_login_provider", SpaceLoginProvider }, - { "space_login_user_id", SpaceLoginUserId } - }); - return Task.CompletedTask; - }, Brisk.IsInitialized); - GUILayout.EndHorizontal(); - EndSection(); - } + GUILayout.Label("暂无数据,请先点击“更新列表”。", _bodyStyle); + return; + } - private void DrawAnnouncementsSection() - { - BeginSection("公告"); - AnnouncementId = DrawEditableRow("公告 ID", AnnouncementId); + for (var i = 0; i < _leaderboardEntries.Count; i++) + { + var entry = _leaderboardEntries[i]; + GUILayout.BeginVertical(_cardStyle); + GUILayout.Label("#" + entry.Rank + " " + NullToDash(entry.Nickname), _sectionTitleStyle); + GUILayout.Label("PlayerId: " + NullToDash(entry.PlayerId), _bodyStyle); + GUILayout.Label("分数: " + entry.Score, _bodyStyle); + GUILayout.EndVertical(); + } + }); - 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(); + GUILayout.EndArea(); } - private async Task InitializeAsync() + private void DrawArchivePage() + { + var contentRect = DrawPageFrame("云存档", "返回主页", () => _page = SamplePage.Home); + GUILayout.BeginArea(contentRect); + _archiveScroll = GUILayout.BeginScrollView(_archiveScroll); + + DrawCard("槽位与操作", () => + { + ArchiveSlotNo = DrawInputRow("槽位号", ArchiveSlotNo); + ArchiveBaseVersion = DrawInputRow("基准版本", ArchiveBaseVersion); + DrawInlineButtons( + new ButtonSpec("获取槽位列表", GetArchiveSlotsAsync, false, true), + new ButtonSpec("获取元信息", GetArchiveMetaAsync, false, true), + new ButtonSpec("下载存档", DownloadArchiveAsync, false, true), + new ButtonSpec("上传存档", UploadArchiveAsync, true, true)); + }); + + DrawCard("文本存档内容", () => + { + ArchiveContent = GUILayout.TextArea(ArchiveContent ?? string.Empty, _textAreaStyle, GUILayout.Height(260f)); + }); + + DrawCard("槽位列表", () => + { + if (_archiveSlots.Count == 0) + { + GUILayout.Label("暂无槽位数据。", _bodyStyle); + } + else + { + for (var i = 0; i < _archiveSlots.Count; i++) + { + var slot = _archiveSlots[i]; + GUILayout.Label("槽位 " + slot.SlotNo + " | version " + slot.Version + " | " + slot.SizeBytes + " bytes", _bodyStyle); + } + } + + GUILayout.Space(8f); + GUILayout.Label("当前元信息", _sectionTitleStyle); + GUILayout.TextArea(FormatValue(_archiveMeta), _textAreaStyle, GUILayout.Height(120f)); + }); + + GUILayout.EndScrollView(); + GUILayout.EndArea(); + } + + private void DrawSpacePage() + { + var title = _viewingTargetSpace ? "TA 的空间" : "我的空间"; + var backLabel = _viewingTargetSpace ? "返回我的空间" : "返回主页"; + Action backAction = () => + { + if (_viewingTargetSpace) + { + _viewingTargetSpace = false; + _targetSpaceView = null; + _targetSpaceStats = null; + _targetSpaceContentText = string.Empty; + return; + } + + _page = SamplePage.Home; + }; + + var contentRect = DrawPageFrame(title, backLabel, backAction, "刷新", RefreshSpacePageAsync); + GUILayout.BeginArea(contentRect); + _spaceScroll = GUILayout.BeginScrollView(_spaceScroll); + + if (_viewingTargetSpace) + { + DrawTargetSpaceContent(); + } + else + { + DrawMySpaceContent(); + } + + GUILayout.EndScrollView(); + GUILayout.EndArea(); + } + + private void DrawMySpaceContent() + { + DrawCard("我的空间内容", () => + { + GUILayout.Label("当前空间文本", _sectionTitleStyle); + SpacePayloadText = GUILayout.TextArea(SpacePayloadText ?? string.Empty, _textAreaStyle, GUILayout.Height(240f)); + DrawInlineButtons( + new ButtonSpec("更新我的空间", UpdateMySpaceAsync, true, true), + new ButtonSpec("重新拉取我的空间", LoadMySpaceAsync, false, true)); + }); + + DrawCard("我的空间状态", () => + { + DrawValueRow("我的 PlayerId", NullToDash(Brisk.PlayerId)); + DrawValueRow("昵称", _mySpaceView == null ? "-" : NullToDash(_mySpaceView.Nickname)); + DrawValueRow("内容版本", _mySpaceView == null ? "-" : _mySpaceView.ContentVersion.ToString()); + DrawValueRow("内容类型", _mySpaceView == null ? "-" : NullToDash(_mySpaceView.ContentType)); + DrawValueRow("内容大小", _mySpaceView == null ? "-" : _mySpaceView.ContentSizeBytes + " bytes"); + DrawValueRow("点赞数", _mySpaceStats == null ? "-" : _mySpaceStats.LikeCount.ToString()); + DrawValueRow("访问数", _mySpaceStats == null ? "-" : _mySpaceStats.VisitCount.ToString()); + DrawValueRow("更新时间", _mySpaceView == null ? "-" : NullToDash(_mySpaceView.UpdatedAt)); + GUILayout.Space(8f); + GUILayout.Label("最近给我点赞的用户", _sectionTitleStyle); + if (_mySpaceLikes.Count == 0) + { + GUILayout.Label("暂无点赞记录。", _bodyStyle); + } + else + { + for (var i = 0; i < _mySpaceLikes.Count; i++) + { + var item = _mySpaceLikes[i]; + GUILayout.Label(NullToDash(item.Nickname) + " | " + NullToDash(item.PlayerId), _bodyStyle); + } + } + }); + + DrawCard("访问其他用户空间", () => + { + SpacePlayerId = DrawInputRow("目标用户 PlayerId", SpacePlayerId); + DrawInlineButtons(new ButtonSpec("前往该用户空间", VisitTargetSpaceAsync, true, true)); + }); + } + + private void DrawTargetSpaceContent() + { + DrawCard("TA 的空间内容", () => + { + DrawValueRow("昵称", _targetSpaceView == null ? "-" : NullToDash(_targetSpaceView.Nickname)); + DrawValueRow("PlayerId", _targetSpaceView == null ? "-" : NullToDash(_targetSpaceView.PlayerId)); + DrawValueRow("内容类型", _targetSpaceView == null ? "-" : NullToDash(_targetSpaceView.ContentType)); + DrawValueRow("内容大小", _targetSpaceView == null ? "-" : _targetSpaceView.ContentSizeBytes + " bytes"); + DrawValueRow("点赞数", _targetSpaceStats == null ? "-" : _targetSpaceStats.LikeCount.ToString()); + DrawValueRow("访问数", _targetSpaceStats == null ? "-" : _targetSpaceStats.VisitCount.ToString()); + DrawValueRow("我的点赞状态", _targetSpaceView != null && _targetSpaceView.LikedByMe ? "已点赞" : "未点赞"); + GUILayout.Space(8f); + GUILayout.Label("空间内容", _sectionTitleStyle); + GUILayout.TextArea(_targetSpaceContentText ?? string.Empty, _textAreaStyle, GUILayout.Height(240f)); + }); + + DrawCard("操作", () => + { + DrawInlineButtons( + new ButtonSpec("点赞", LikeTargetSpaceAsync, false, true), + new ButtonSpec("取消点赞", UnlikeTargetSpaceAsync, false, true)); + }); + } + + private async Task InitializeFlowAsync() { await Brisk.InitializeAsync(new BriskOptions { @@ -326,283 +537,371 @@ public sealed class BriskQuickStartSample : MonoBehaviour ExitHandler = HandleExitRequested }); - SetResult("初始化结果", Brisk.Bootstrap); + _page = Brisk.IsLoggedIn ? SamplePage.Home : SamplePage.Login; + if (Brisk.IsLoggedIn) + { + await RefreshHomeAsync(); + } } - private async Task ReinitializeAsync() + private async Task LoginFlowAsync() { - if (Brisk.IsInitialized) + if (string.IsNullOrWhiteSpace(LoginCode)) { - Brisk.Shutdown(); - Log("重新初始化前已先关闭 SDK。"); + await Brisk.Auth.LoginWithUserIdAsync(LoginProvider, LoginUserId, CreateProfile()); + } + else + { + await Brisk.Auth.LoginWithCodeAsync(LoginProvider, LoginCode, CreateProfile()); } - await InitializeAsync(); + await RefreshHomeAsync(); + _page = SamplePage.Home; } - private async Task LoginWithUserIdAsync() + private async Task RefreshHomeAsync() { - var result = await Brisk.Auth.LoginWithUserIdAsync(LoginProvider, LoginUserId, CreateProfile()); - ApplyIdentity(result.PlayerId, result.LoginProvider, result.LoginUserId); - SetResult("按用户 ID 登录结果", result); + _me = await Brisk.Player.GetMeAsync(); + _config = await Brisk.Config.GetCurrentAsync(); + ApplyIdentity(_me.PlayerId, _me.LoginProvider, _me.LoginUserId); + await LoadMySpaceAsync(); } - private async Task LoginWithCodeAsync() + private async Task LoadAnnouncementsAsync() { - 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 + _announcements = await Brisk.Announcements.GetListAsync(); + if (_announcements.Count > 0) { - { "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(); + AnnouncementId = _announcements[0].Id.ToString(); } - - SetResult("公告列表", _announcementCache); } - private async Task MarkAnnouncementAsync() + private async Task RefreshLeaderboardAsync() { - var id = ParseRequiredLong(AnnouncementId, nameof(AnnouncementId)); - await Brisk.Announcements.MarkReadAsync(id); - SetResult("公告已标记已读", new Dictionary { { "announcement_id", id } }); + _leaderboardMe = await Brisk.Leaderboard.GetMeAsync(RankKey); + SubmitScoreValue = (_leaderboardMe == null ? 0L : _leaderboardMe.Score).ToString(); + _leaderboardEntries = _viewAroundMe + ? await Brisk.Leaderboard.GetAroundMeAsync(RankKey, ParseOrDefault(AroundMeRange, 5)) + : await Brisk.Leaderboard.GetTopAsync(RankKey, ParseOrDefault(LeaderboardLimit, 10)); } - private async Task MarkFirstCachedAnnouncementAsync() + private async Task ToggleLeaderboardModeAsync() { - if (_announcementCache.Count == 0) - { - throw new InvalidOperationException("公告缓存为空,请先执行“获取公告列表”。"); - } - - var id = _announcementCache[0].Id; - AnnouncementId = id.ToString(); - await Brisk.Announcements.MarkReadAsync(id); - SetResult("首条缓存公告已标记已读", _announcementCache[0]); + _viewAroundMe = !_viewAroundMe; + await RefreshLeaderboardAsync(); } - private async Task GetTopAsync() + private async Task IncreaseAndSubmitScoreAsync() { - 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)); + var score = ParseOrDefault(SubmitScoreValue, 0) + 1; + SubmitScoreValue = score.ToString(); await Brisk.Leaderboard.SubmitScoreAsync(RankKey, score); - SetResult("分数提交结果", new Dictionary - { - { "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); + await RefreshLeaderboardAsync(); } private async Task GetArchiveSlotsAsync() { - var result = await Brisk.Archive.GetSlotsAsync(); - SetResult("存档槽位列表", result); + _archiveSlots = await Brisk.Archive.GetSlotsAsync(); } 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); + _archiveMeta = await Brisk.Archive.GetMetaAsync(ParseRequiredInt(ArchiveSlotNo, nameof(ArchiveSlotNo))); + ArchiveBaseVersion = _archiveMeta == null ? ArchiveBaseVersion : _archiveMeta.Version.ToString(); } private async Task DownloadArchiveAsync() { var result = await Brisk.Archive.DownloadAsync(ParseRequiredInt(ArchiveSlotNo, nameof(ArchiveSlotNo))); ArchiveBaseVersion = result == null ? ArchiveBaseVersion : result.Version.ToString(); + ArchiveContent = await Brisk.Archive.DownloadTextAsync(ParseRequiredInt(ArchiveSlotNo, nameof(ArchiveSlotNo))); + await GetArchiveMetaAsync(); + } - var output = new Dictionary + private async Task UploadArchiveAsync() + { + var baseVersion = ParseNullableInt(ArchiveBaseVersion); + var result = await Brisk.Archive.UploadTextAsync(ParseRequiredInt(ArchiveSlotNo, nameof(ArchiveSlotNo)), ArchiveContent ?? string.Empty, baseVersion); + ArchiveBaseVersion = result == null ? ArchiveBaseVersion : result.Version.ToString(); + await GetArchiveSlotsAsync(); + await GetArchiveMetaAsync(); + } + + private async Task LoadMySpaceAsync() + { + if (string.IsNullOrWhiteSpace(Brisk.PlayerId)) { - { "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) } - }; + return; + } - SetResult("存档下载结果", output); + _mySpaceView = await Brisk.Space.GetByPlayerIdAsync(Brisk.PlayerId); + _mySpaceStats = await Brisk.Space.GetStatsByPlayerIdAsync(Brisk.PlayerId); + _mySpaceLikes = await Brisk.Space.GetLikesByPlayerIdAsync(Brisk.PlayerId, 10); + SpacePayloadText = await DownloadSpaceTextAsync(_mySpaceView, () => Brisk.Space.DownloadContentByPlayerIdAsync(Brisk.PlayerId)); } - private async Task GetSpaceByPlayerIdAsync() + private async Task RefreshSpacePageAsync() { - 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 { { "player_id", SpacePlayerId } }); - } - - private async Task UnlikeByPlayerIdAsync() - { - await Brisk.Space.UnlikeByPlayerIdAsync(SpacePlayerId); - SetResult("按 PlayerId 取消点赞结果", new Dictionary { { "player_id", SpacePlayerId } }); - } - - private async Task LikeByLoginAsync() - { - await Brisk.Space.LikeByLoginIdentityAsync(SpaceLoginProvider, SpaceLoginUserId); - SetResult("按登录身份点赞结果", new Dictionary + if (_viewingTargetSpace) { - { "login_provider", SpaceLoginProvider }, - { "login_user_id", SpaceLoginUserId } - }); - } + await LoadTargetSpaceAsync(); + return; + } - private async Task UnlikeByLoginAsync() - { - await Brisk.Space.UnlikeByLoginIdentityAsync(SpaceLoginProvider, SpaceLoginUserId); - SetResult("按登录身份取消点赞结果", new Dictionary - { - { "login_provider", SpaceLoginProvider }, - { "login_user_id", SpaceLoginUserId } - }); + await LoadMySpaceAsync(); } private async Task UpdateMySpaceAsync() { - var payload = new Dictionary - { - { "sample_text", SpacePayloadText }, - { "updated_at", DateTimeOffset.UtcNow.ToString("O") }, - { "player_id", Brisk.PlayerId }, - { "rank_key", RankKey } - }; - - await Brisk.Space.UpdateMyAsync(payload); - SetResult("更新我的空间结果", payload); + var baseVersion = _mySpaceView != null && _mySpaceView.ContentExists ? _mySpaceView.ContentVersion : 0L; + await Brisk.Space.UpdateMyAsync(SpacePayloadText ?? string.Empty, baseVersion, "text/plain"); + await LoadMySpaceAsync(); } - private async Task GetMyVisitsAsync() + private async Task VisitTargetSpaceAsync() { - var result = await Brisk.Space.GetMyVisitsAsync(); - SetResult("我的访客列表", result); + await LoadTargetSpaceAsync(); + _viewingTargetSpace = true; + } + + private async Task LoadTargetSpaceAsync() + { + _targetSpaceView = await Brisk.Space.GetByPlayerIdAsync(SpacePlayerId); + _targetSpaceStats = await Brisk.Space.GetStatsByPlayerIdAsync(SpacePlayerId); + _targetSpaceContentText = await DownloadSpaceTextAsync(_targetSpaceView, () => Brisk.Space.DownloadContentByPlayerIdAsync(SpacePlayerId)); + } + + private async Task LikeTargetSpaceAsync() + { + await Brisk.Space.LikeByPlayerIdAsync(SpacePlayerId); + await LoadTargetSpaceAsync(); + } + + private async Task UnlikeTargetSpaceAsync() + { + await Brisk.Space.UnlikeByPlayerIdAsync(SpacePlayerId); + await LoadTargetSpaceAsync(); + } + + private async Task LogoutFlowAsync() + { + await Brisk.Auth.LogoutAsync(); + _page = SamplePage.Login; + } + + private Task ResetTransientStatusAsync() + { + _statusText = "等待开始"; + _errorText = string.Empty; + return Task.CompletedTask; + } + + private Task ShowHomeLogsAsync() + { + _errorText = string.Join("\n", _logs.ToArray()); + return Task.CompletedTask; } private async Task RunSmokeFlowAsync() { - await InitializeAsync(); - + await InitializeFlowAsync(); if (!Brisk.IsLoggedIn) { - await LoginWithUserIdAsync(); + await LoginFlowAsync(); } - - await GetMeAsync(); - await GetConfigAsync(); - await GetTopAsync(); - await GetAnnouncementsAsync(); - await GetArchiveSlotsAsync(); - await GetMyVisitsAsync(); } - private void DrawButton(string label, Func action, bool enabled = true) + private void DrawAnnouncementModal() { - var previous = GUI.enabled; - GUI.enabled = previous && !_isBusy && enabled; - if (GUILayout.Button(label, GUILayout.Height(30f))) + GUI.color = new Color(0f, 0f, 0f, 0.55f); + GUI.Box(new Rect(0f, 0f, DesignWidth, DesignHeight), GUIContent.none); + GUI.color = Color.white; + + var modalRect = new Rect(48f, 180f, DesignWidth - 96f, DesignHeight - 360f); + GUILayout.BeginArea(modalRect, GUI.skin.box); + GUILayout.Label(_selectedAnnouncement.Title, _titleStyle); + GUILayout.Label("开始: " + NullToDash(_selectedAnnouncement.StartAt), _bodyStyle); + GUILayout.Label("结束: " + NullToDash(_selectedAnnouncement.EndAt), _bodyStyle); + GUILayout.Label("状态: " + (_selectedAnnouncement.IsRead ? "已读" : "未读"), _bodyStyle); + GUILayout.Space(8f); + _modalScroll = GUILayout.BeginScrollView(_modalScroll, GUILayout.Height(520f)); + GUILayout.TextArea(NullToDash(_selectedAnnouncement.Content), _textAreaStyle, GUILayout.ExpandHeight(true)); + GUILayout.EndScrollView(); + GUILayout.Space(10f); + DrawInlineButtons( + new ButtonSpec("标记已读", MarkSelectedAnnouncementAsync, false, true), + new ButtonSpec("关闭", CloseAnnouncementModalAsync, false, true)); + GUILayout.EndArea(); + } + + private Task CloseAnnouncementModalAsync() + { + _showAnnouncementModal = false; + return Task.CompletedTask; + } + + private async Task MarkSelectedAnnouncementAsync() + { + if (_selectedAnnouncement == null) { - RunAction(label, action); + return; } - GUI.enabled = previous; + await Brisk.Announcements.MarkReadAsync(_selectedAnnouncement.Id); + _selectedAnnouncement.IsRead = true; + await LoadAnnouncementsAsync(); + _showAnnouncementModal = false; + } + + private void DrawBusyOverlay() + { + GUI.color = new Color(0f, 0f, 0f, 0.35f); + GUI.Box(new Rect(0f, 0f, DesignWidth, DesignHeight), GUIContent.none); + GUI.color = Color.white; + GUI.Box(new Rect(180f, 580f, 360f, 120f), "正在处理: " + _busyAction); + } + + private Rect DrawPageFrame(string title, string backLabel, Action backAction, string rightLabel = null, Func rightAction = null) + { + var outerRect = new Rect(20f, 20f, DesignWidth - 40f, DesignHeight - 40f); + GUI.Box(outerRect, GUIContent.none, _pageStyle); + + var headerRect = new Rect(outerRect.x + 20f, outerRect.y + 16f, outerRect.width - 40f, 70f); + GUILayout.BeginArea(headerRect); + GUILayout.BeginHorizontal(); + if (!string.IsNullOrWhiteSpace(backLabel)) + { + DrawHeaderButton(backLabel, backAction); + } + else + { + GUILayout.Space(150f); + } + + GUILayout.FlexibleSpace(); + GUILayout.Label(title, _titleStyle, GUILayout.Height(48f)); + GUILayout.FlexibleSpace(); + if (!string.IsNullOrWhiteSpace(rightLabel) && rightAction != null) + { + DrawHeaderButton(rightLabel, () => RunAction(rightLabel, rightAction)); + } + else + { + GUILayout.Space(150f); + } + + GUILayout.EndHorizontal(); + GUILayout.EndArea(); + + var statusRect = new Rect(outerRect.x + 20f, headerRect.yMax + 6f, outerRect.width - 40f, 74f); + GUI.Box(statusRect, GUIContent.none, _cardStyle); + GUILayout.BeginArea(new Rect(statusRect.x + 12f, statusRect.y + 10f, statusRect.width - 24f, statusRect.height - 20f)); + GUILayout.Label("状态: " + _statusText, _statusStyle); + GUILayout.Label("错误: " + (string.IsNullOrWhiteSpace(_errorText) ? "无" : _errorText), _hintStyle); + GUILayout.EndArea(); + + return new Rect(outerRect.x + 20f, statusRect.yMax + 12f, outerRect.width - 40f, outerRect.height - 250f); + } + + private void DrawBottomButtons(params ButtonSpec[] buttons) + { + var rect = new Rect(40f, DesignHeight - 170f, DesignWidth - 80f, 110f); + GUILayout.BeginArea(rect); + DrawInlineButtons(buttons); + GUILayout.EndArea(); + } + + private void DrawBottomNav(params NavSpec[] items) + { + var rect = new Rect(30f, DesignHeight - 170f, DesignWidth - 60f, 110f); + GUI.Box(new Rect(rect.x, rect.y, rect.width, rect.height), GUIContent.none, _cardStyle); + GUILayout.BeginArea(new Rect(rect.x + 10f, rect.y + 10f, rect.width - 20f, rect.height - 20f)); + GUILayout.BeginHorizontal(); + for (var i = 0; i < items.Length; i++) + { + var item = items[i]; + if (GUILayout.Button(item.Label, _primaryButtonStyle, GUILayout.Height(64f), GUILayout.Width(150f))) + { + _page = item.Page; + if (item.Page == SamplePage.Announcements) + { + RunAction("加载公告", LoadAnnouncementsAsync); + } + else if (item.Page == SamplePage.Leaderboard) + { + RunAction("加载排行榜", RefreshLeaderboardAsync); + } + else if (item.Page == SamplePage.Archive) + { + RunAction("加载存档", GetArchiveSlotsAsync); + } + else if (item.Page == SamplePage.Space) + { + RunAction("加载空间", LoadMySpaceAsync); + } + } + } + + GUILayout.EndHorizontal(); + GUILayout.EndArea(); + } + + private void DrawHeaderButton(string label, Action action) + { + if (GUILayout.Button(label, _buttonStyle, GUILayout.Width(150f), GUILayout.Height(42f))) + { + action(); + } + } + + private void DrawInlineButtons(params ButtonSpec[] buttons) + { + GUILayout.BeginHorizontal(); + for (var i = 0; i < buttons.Length; i++) + { + var button = buttons[i]; + var previous = GUI.enabled; + GUI.enabled = previous && !_isBusy && button.Enabled; + var style = button.Primary ? _primaryButtonStyle : _buttonStyle; + if (GUILayout.Button(button.Label, style, GUILayout.Height(56f))) + { + RunAction(button.Label, button.Action); + } + + GUI.enabled = previous; + } + + GUILayout.EndHorizontal(); + } + + private void DrawCard(string title, Action content) + { + GUILayout.BeginVertical(_cardStyle); + GUILayout.Label(title, _sectionTitleStyle); + GUILayout.Space(6f); + content(); + GUILayout.EndVertical(); + GUILayout.Space(12f); + } + + private string DrawInputRow(string label, string value) + { + GUILayout.Label(label, _hintStyle); + return GUILayout.TextField(value ?? string.Empty, _inputStyle, GUILayout.Height(48f)); + } + + private bool DrawToggleRow(string label, bool value) + { + GUILayout.Label(label, _hintStyle); + return GUILayout.Toggle(value, value ? "开启" : "关闭", GUILayout.Height(42f)); + } + + private void DrawValueRow(string label, string value) + { + GUILayout.BeginHorizontal(); + GUILayout.Label(label, _hintStyle, GUILayout.Width(180f)); + GUILayout.Label(value, _bodyStyle); + GUILayout.EndHorizontal(); } private void RunAction(string actionName, Func action) @@ -612,15 +911,15 @@ public sealed class BriskQuickStartSample : MonoBehaviour return; } - RunActionAsync(actionName, action); + RunActionInternal(actionName, action); } - private async void RunActionAsync(string actionName, Func action) + private async void RunActionInternal(string actionName, Func action) { _isBusy = true; _busyAction = actionName; _statusText = "执行中: " + actionName; - _lastErrorText = string.Empty; + _errorText = string.Empty; Log("开始执行: " + actionName); try @@ -632,8 +931,8 @@ public sealed class BriskQuickStartSample : MonoBehaviour catch (Exception exception) { _statusText = "执行失败: " + actionName; - _lastErrorText = FormatException(exception); - Log("执行失败: " + actionName + " | " + exception.GetType().Name + " | " + exception.Message); + _errorText = FormatException(exception); + Log("执行失败: " + actionName + " | " + exception.Message); Debug.LogException(exception, this); } finally @@ -643,9 +942,67 @@ public sealed class BriskQuickStartSample : MonoBehaviour } } - private void SetResult(string title, object value) + private void SyncPageBySdkState() { - _resultText = title + "\n" + new string('=', title.Length) + "\n" + FormatValue(value); + if (!Brisk.IsInitialized) + { + _page = SamplePage.Initialize; + return; + } + + _page = Brisk.IsLoggedIn ? SamplePage.Home : SamplePage.Login; + } + + private void HandleInitialized() + { + SyncPageBySdkState(); + Log("事件: 初始化完成"); + } + + private void HandleLoggedIn() + { + SyncPageBySdkState(); + Log("事件: 登录完成"); + } + + private void HandleLoggedOut() + { + SyncPageBySdkState(); + Log("事件: 登出完成"); + } + + private void HandleAuthExpired(BriskAuthExpiredException exception) + { + _page = SamplePage.Login; + Log("事件: 登录态失效 | " + exception.Message); + } + + private void HandleBlockingError(BriskBlockingException exception) + { + Log("事件: 阻断错误 | " + exception.Message); + } + + private void HandleExitRequested() + { + Log("Brisk 请求退出。"); + } + + private void Log(string message) + { + _logs.Add("[" + DateTime.Now.ToString("HH:mm:ss") + "] " + message); + if (_logs.Count > 80) + { + _logs.RemoveAt(0); + } + } + + private BriskProfile CreateProfile() + { + return new BriskProfile + { + Nickname = Nickname, + AvatarUrl = AvatarUrl + }; } private void ApplyIdentity(string playerId, string loginProvider, string loginUserId) @@ -668,70 +1025,69 @@ public sealed class BriskQuickStartSample : MonoBehaviour } } - private void ApplyCurrentIdentityToSpace() + private void EnsureStyles() { - if (Brisk.Identity != null) + if (_pageStyle != null) { - ApplyIdentity(Brisk.Identity.PlayerId, Brisk.Identity.LoginProvider, Brisk.Identity.LoginUserId); return; } - if (Brisk.IsInitialized) + _pageStyle = new GUIStyle(GUI.skin.box) { - ApplyIdentity(Brisk.PlayerId, LoginProvider, LoginUserId); - } - } - - private BriskProfile CreateProfile() - { - return new BriskProfile - { - Nickname = Nickname, - AvatarUrl = AvatarUrl + padding = new RectOffset(18, 18, 18, 18) }; - } - 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) + _cardStyle = new GUIStyle(GUI.skin.box) { - _eventLogs.RemoveAt(0); - } + padding = new RectOffset(16, 16, 14, 14), + margin = new RectOffset(0, 0, 0, 0) + }; - _logScroll.y = float.MaxValue; + _titleStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 28, + fontStyle = FontStyle.Bold, + alignment = TextAnchor.MiddleCenter, + wordWrap = true + }; + + _sectionTitleStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 18, + fontStyle = FontStyle.Bold, + wordWrap = true + }; + + _bodyStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 16, + wordWrap = true + }; + + _hintStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 14, + wordWrap = true + }; + + _statusStyle = new GUIStyle(_bodyStyle) + { + fontStyle = FontStyle.Bold + }; + + _buttonStyle = new GUIStyle(GUI.skin.button) + { + fontSize = 16, + fontStyle = FontStyle.Bold, + wordWrap = true + }; + + _primaryButtonStyle = new GUIStyle(_buttonStyle); + _dangerButtonStyle = new GUIStyle(_buttonStyle); + _inputStyle = new GUIStyle(GUI.skin.textField) { fontSize = 16 }; + _textAreaStyle = new GUIStyle(GUI.skin.textArea) { fontSize = 15, wordWrap = true }; + _listButtonStyle = new GUIStyle(GUI.skin.button) { fontSize = 16, alignment = TextAnchor.MiddleLeft, wordWrap = true }; + _modalStyle = new GUIStyle(GUI.skin.box) { padding = new RectOffset(20, 20, 20, 20) }; } private static int ParseRequiredInt(string value, string fieldName) @@ -744,17 +1100,7 @@ public sealed class BriskQuickStartSample : MonoBehaviour 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) + private static int ParseOrDefault(string value, int fallback) { return int.TryParse(value, out var result) ? result : fallback; } @@ -769,11 +1115,32 @@ public sealed class BriskQuickStartSample : MonoBehaviour return int.TryParse(value, out var result) ? result : (int?)null; } + private static string NullToDash(string value) + { + return string.IsNullOrWhiteSpace(value) ? "-" : value; + } + + private static async Task DownloadSpaceTextAsync(BriskSpaceView view, Func> downloadContent) + { + if (view == null || !view.ContentExists || downloadContent == null) + { + return string.Empty; + } + + var content = await downloadContent(); + if (content == null || content.Bytes == null || content.Bytes.Length == 0) + { + return string.Empty; + } + + return Encoding.UTF8.GetString(content.Bytes); + } + private static string FormatException(Exception exception) { if (exception == null) { - return "未知错误。"; + return "未知错误"; } var builder = new StringBuilder(); @@ -791,39 +1158,26 @@ public sealed class BriskQuickStartSample : MonoBehaviour { var builder = new StringBuilder(); AppendValue(builder, value, 0, 0); - return builder.ToString().TrimEnd(); + return builder.Length == 0 ? "-" : builder.ToString().TrimEnd(); } private static void AppendValue(StringBuilder builder, object value, int indent, int depth) { if (depth > 5) { - builder.AppendLine(Indent(indent) + "..."); + builder.AppendLine(new string(' ', indent) + "..."); return; } if (value == null) { - builder.AppendLine(Indent(indent) + "null"); + builder.AppendLine(new string(' ', 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); + builder.AppendLine(new string(' ', indent) + stringValue); return; } @@ -831,92 +1185,64 @@ public sealed class BriskQuickStartSample : MonoBehaviour { foreach (DictionaryEntry entry in dictionary) { - builder.AppendLine(Indent(indent) + entry.Key + ":"); + builder.AppendLine(new string(' ', indent) + entry.Key + ":"); AppendValue(builder, entry.Value, indent + 2, depth + 1); } - return; } - if (value is IEnumerable enumerable) + if (value is IEnumerable enumerable && !(value is byte[])) { var index = 0; foreach (var item in enumerable) { - builder.AppendLine(Indent(indent) + "[" + index + "]"); + builder.AppendLine(new string(' ', indent) + "[" + index + "]"); AppendValue(builder, item, indent + 2, depth + 1); index++; } + return; + } - if (index == 0) - { - builder.AppendLine(Indent(indent) + "(empty)"); - } - + var type = value.GetType(); + if (type.IsPrimitive || value is decimal) + { + builder.AppendLine(new string(' ', indent) + value); 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 + ":"); + builder.AppendLine(new string(' ', indent) + field.Name + ":"); AppendValue(builder, field.GetValue(value), indent + 2, depth + 1); } } - private static string Indent(int indent) + private readonly struct ButtonSpec { - return new string(' ', indent); + public ButtonSpec(string label, Func action, bool primary, bool enabled) + { + Label = label; + Action = action; + Primary = primary; + Enabled = enabled; + } + + public string Label { get; } + public Func Action { get; } + public bool Primary { get; } + public bool Enabled { get; } } - private static void BeginSection(string title) + private readonly struct NavSpec { - GUILayout.Space(10f); - GUILayout.BeginVertical(GUI.skin.box); - GUILayout.Label(title, GUI.skin.label); - GUILayout.Space(4f); - } + public NavSpec(string label, SamplePage page) + { + Label = label; + Page = page; + } - 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(); + public string Label { get; } + public SamplePage Page { get; } } } diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Samples~/QuickStart/BriskQuickStartScene.unity b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Samples~/QuickStart/BriskQuickStartScene.unity index 38bad70..3230947 100644 --- a/PackageSource/com.foldcc.cc-framework.BriskGameServer/Samples~/QuickStart/BriskQuickStartScene.unity +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/Samples~/QuickStart/BriskQuickStartScene.unity @@ -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} diff --git a/PackageSource/com.foldcc.cc-framework.BriskGameServer/package.json b/PackageSource/com.foldcc.cc-framework.BriskGameServer/package.json index 0b27bff..a080264 100644 --- a/PackageSource/com.foldcc.cc-framework.BriskGameServer/package.json +++ b/PackageSource/com.foldcc.cc-framework.BriskGameServer/package.json @@ -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": [ diff --git a/README.md b/README.md index de8f10a..e80dd62 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,28 @@ Brisk Unity SDK 原始开发工程。 - 玩家空间 - 严重错误默认 IMGUI 提示 +内容型接口当前默认推荐用法: + +- 云存档: + - `Brisk.Archive.UploadAsync(slotNo, bytes)` 直接传二进制 + - `Brisk.Archive.UploadTextAsync(slotNo, text)` 直接传文本 + - `Brisk.Archive.UploadJsonAsync(slotNo, payload)` 直接传对象 + - `Brisk.Archive.DownloadAsync(slotNo)` 获取原始 bytes + 元信息 + - `Brisk.Archive.DownloadTextAsync(slotNo)` 直接取文本 + - `Brisk.Archive.DownloadJsonAsync(slotNo)` 直接取 JSON 对象 +- 玩家空间: + - `Brisk.Space.UpdateMyAsync(string)` 可直接更新文本内容 + - `Brisk.Space.UpdateMyAsync(byte[])` 可直接更新二进制内容 + - `Brisk.Space.UpdateMyAsync(object)` 会自动转成 JSON 上传 + - `Brisk.Space.DownloadContentByPlayerIdAsync(playerId)` 获取原始 bytes + +云存档上传补充约定: + +- SDK 默认会自动计算上传文件的 `checksum` +- 当前服务端要求的是纯 SHA256 十六进制字符串 +- 不应传 `sha256:xxxx` 这类带算法前缀的值 +- 如果手动传入 `checksum`,SDK 也会自动去掉 `sha256:` 前缀再发送 + 静态入口保持为: - `Brisk.xxx` @@ -62,6 +84,8 @@ Brisk Unity SDK 原始开发工程。 - 玩家空间读取、点赞、更新、访客 - 全局事件日志与最近一次结果输出 +当前样例已经直接使用 `UploadTextAsync` / `DownloadTextAsync` 这类快捷接口;如果业务需要,也可以继续走原始 bytes 接口。 + ## 发布结构 对外 package 发布目录位于: @@ -99,7 +123,7 @@ Brisk Unity SDK 原始开发工程。 - 开发分支:`feature/...`、`fix/...` - 发包分支:`release/upm-vX.Y.Z` -- 发包 tag:`upm/vX.Y.Z` +- 发包 tag:`vX.Y.Z` 推荐发布步骤: @@ -108,13 +132,13 @@ Brisk Unity SDK 原始开发工程。 3. 检查 `PackageSource/com.foldcc.cc-framework.BriskGameServer` 4. 更新 `package.json` 版本号与 `CHANGELOG.md` 5. 创建发布分支 `release/upm-vX.Y.Z` -6. 打 tag `upm/vX.Y.Z` +6. 打 tag `vX.Y.Z` 7. 推送发布分支与 tag 外部项目 Git Package 接入示例: ```text -http://private.lightyears.ltd:18650/foldcc/CC-Framework.BriskGameServer.git?path=/PackageSource/com.foldcc.cc-framework.BriskGameServer#upm/v0.1.0 +http://private.lightyears.ltd:18650/foldcc/CC-Framework.BriskGameServer.git?path=/PackageSource/com.foldcc.cc-framework.BriskGameServer#v0.2.0 ``` ## 文档位置 diff --git a/服务参考_临时/Brisk API接口与示例文档.md b/服务参考_临时/Brisk API接口与示例文档.md index f3b96b5..7f9b60e 100644 --- a/服务参考_临时/Brisk API接口与示例文档.md +++ b/服务参考_临时/Brisk API接口与示例文档.md @@ -234,25 +234,66 @@ curl -X POST "https://brisk.lightyears.ltd/api/archives/slot/1/upload" \ 说明: -- 空间数据当前为 `payload_json` -- 适合公开资料、展示文案、轻量个性化内容 -- 如果是游戏主存档,请优先走云存档模块 +- 空间主信息接口只返回元数据,不再内嵌内容本体 +- 空间内容本体通过独立 `/content` 接口上传和下载 +- 空间内容支持任意 bytes +- 可以上传二进制、文本、JSON、protobuf、msgpack、压缩包等格式 +- 如果是多槽位私有主存档,仍优先走云存档模块;如果是空间自定义内容,可直接走空间内容接口 接口: -- `PUT /api/spaces/me` +- `PUT /api/spaces/me/content` - `GET /api/spaces/{player_id}` +- `GET /api/spaces/{player_id}/content` - `GET /api/spaces/{player_id}/stats` - `GET /api/spaces/{player_id}/likes` - `POST /api/spaces/{player_id}/like` - `DELETE /api/spaces/{player_id}/like` - `GET /api/spaces/me/visits` - `GET /api/spaces/by-login` +- `GET /api/spaces/by-login/content` - `GET /api/spaces/by-login/stats` - `GET /api/spaces/by-login/likes` - `POST /api/spaces/by-login/like` - `DELETE /api/spaces/by-login/like` +主信息接口返回重点: + +- `content_exists` +- `content_version` +- `content_type` +- `content_size_bytes` +- `content_checksum` +- `like_count` +- `visit_count` +- `updated_at` + +上传表单字段: + +- `base_version`:用于乐观锁;首次上传可传 `0` +- `content_type`:可选,建议传,例如 `application/octet-stream`、`text/plain`、`application/json` +- `checksum`:可选;如果传入,服务端会校验 SHA-256 +- `file`:必传,内容本体 + +上传空间内容示例: + +```bash +curl -X PUT "https://brisk.lightyears.ltd/api/spaces/me/content" \ + -H "Authorization: Bearer " \ + -F "base_version=0" \ + -F "content_type=application/octet-stream" \ + -F "checksum=" \ + -F "file=@space-content.bin" +``` + +下载空间内容示例: + +```bash +curl "https://brisk.lightyears.ltd/api/spaces/by-login/content?login_provider=tap&login_user_id=tap_user_10001" \ + -H "Authorization: Bearer " \ + --output space-content.bin +``` + 按第三方身份查看空间示例: ```bash @@ -312,6 +353,24 @@ curl -X POST "https://brisk.lightyears.ltd/api/spaces/by-login/like?login_provid - `login_provider` - `login_user_id` +### 10.5 空间管理 + +- `GET /api/admin/spaces/overview?project_id=...` +- `GET /api/admin/spaces?project_id=...` +- `GET /api/admin/spaces/{player_id}?project_id=...` +- `GET /api/admin/spaces/{player_id}/content?project_id=...` +- `GET /api/admin/spaces/{player_id}/likes?project_id=...` +- `GET /api/admin/spaces/{player_id}/visits?project_id=...` + +说明: + +- 后台空间详情返回内容元数据 +- 如果需要查看空间内容本体,使用独立 `/content` 下载接口 +- 下载响应会返回: + - `Content-Type` + - `X-Space-Version` + - `X-Space-Checksum` + ## 11. OpenAPI 完整 OpenAPI 导出: diff --git a/服务参考_临时/Brisk Unity SDK接入文档.md b/服务参考_临时/Brisk Unity SDK接入文档.md index f75cc81..686107b 100644 --- a/服务参考_临时/Brisk Unity SDK接入文档.md +++ b/服务参考_临时/Brisk Unity SDK接入文档.md @@ -2,17 +2,18 @@ ## 1. 这份文档适合做什么 -这份文档用于在 Unity 侧封装 Brisk SDK。 +这份文档用于说明当前 Brisk Unity SDK 的接入结构与推荐调用方式。 -建议把 SDK 拆成以下层次: +当前 SDK 已采用静态总入口: -- `BriskClient` -- `BriskAuth` -- `BriskConfig` -- `BriskAnnouncements` -- `BriskRanks` -- `BriskArchives` -- `BriskSpaces` +- `Brisk.InitializeAsync(...)` +- `Brisk.Auth.xxx` +- `Brisk.Player.xxx` +- `Brisk.Config.xxx` +- `Brisk.Announcements.xxx` +- `Brisk.Leaderboard.xxx` +- `Brisk.Archive.xxx` +- `Brisk.Space.xxx` ## 2. 初始化模型 @@ -141,16 +142,31 @@ Task LikeSpaceByLoginIdentityAsync(string loginProvider, string loginUserId) - msgpack bytes - 压缩后的存档二进制 -Unity SDK 建议提供: +当前 SDK 已提供: - `GetArchiveSlotsAsync()` - `GetArchiveMetaAsync(slotNo)` -- `UploadArchiveAsync(slotNo, baseVersion, checksum, byte[])` +- `UploadAsync(slotNo, byte[], baseVersion, checksum)` +- `UploadTextAsync(slotNo, text, baseVersion, checksum)` +- `UploadJsonAsync(slotNo, payload, baseVersion, checksum)` - `DownloadArchiveAsync(slotNo)` +- `DownloadTextAsync(slotNo)` +- `DownloadJsonAsync(slotNo)` + +推荐上层使用策略: + +- 文本存档:优先直接用 `UploadTextAsync / DownloadTextAsync` +- JSON 存档:优先直接用 `UploadJsonAsync / DownloadJsonAsync` +- 二进制存档:继续用 `UploadAsync / DownloadAsync` + +其中: + +- SDK 会自动计算上传用 `checksum` +- 手动传 `checksum` 时也可以带 `sha256:` 前缀,SDK 会自动归一化 ## 9. 玩家空间模块建议 -玩家空间适合承载公开展示数据,不建议放主存档。 +玩家空间适合承载公开展示数据或空间自定义内容,不再限制为 JSON。 推荐放: @@ -158,16 +174,48 @@ Unity SDK 建议提供: - 头像展示字段 - 成就展示摘要 - 公开资料卡 +- 小型二进制空间内容 +- 文本、JSON、protobuf、msgpack 等自定义格式 -当前接口核心是: +当前 SDK / 接口核心是: -- `UpdateMySpaceAsync(payloadJson)` +- `UploadMySpaceContentAsync(baseVersion, contentType, checksum, byte[])` +- `DownloadSpaceContentByPlayerIdAsync(playerId)` +- `DownloadSpaceContentByLoginIdentityAsync(loginProvider, loginUserId)` - `GetSpaceByPlayerIdAsync(playerId)` - `GetSpaceByLoginIdentityAsync(loginProvider, loginUserId)` - `LikeSpaceAsync(playerId)` - `LikeSpaceByLoginIdentityAsync(loginProvider, loginUserId)` - `GetMyVisitsAsync(limit)` +当前 SDK 额外提供了一个极简便捷入口: + +- `Brisk.Space.UpdateMyAsync(payload, baseVersion, contentType, checksum)` + +它的行为是: + +- `payload` 为 `string` 时,自动按 `text/plain` 上传 +- `payload` 为 `byte[]` 时,自动按 `application/octet-stream` 上传 +- `payload` 为其他对象时,自动序列化为 JSON 并按 `application/json` 上传 + +建议 SDK 暴露两层模型: + +- 空间主信息: + - `ContentExists` + - `ContentVersion` + - `ContentType` + - `ContentSizeBytes` + - `ContentChecksum` + - `LikeCount` + - `VisitCount` +- 空间内容本体: + - 原始 `byte[]` + - 下载响应头中的 `X-Space-Version` + - 下载响应头中的 `X-Space-Checksum` + +如果上层业务追求最省事,可以直接用 `UpdateMyAsync(string)` 或 `UpdateMyAsync(object)`; +如果业务自己管理编码、压缩、protobuf、msgpack 等格式,则继续走 `byte[]` 上传下载接口。 + ## 10. 动态配置建议 动态配置拆为两层: diff --git a/服务参考_临时/Brisk 错误码文档(内部实施版).md b/服务参考_临时/Brisk 错误码文档(内部实施版).md index 1c850f4..e915f76 100644 --- a/服务参考_临时/Brisk 错误码文档(内部实施版).md +++ b/服务参考_临时/Brisk 错误码文档(内部实施版).md @@ -36,6 +36,7 @@ | `41000 - 41999` | 后台公告 | | `50000 - 50999` | 排行榜 | | `51000 - 51999` | 后台榜单 | +| `52000 - 52999` | 后台空间 | | `60000 - 60999` | 存档 | | `70000 - 70999` | 玩家空间 | | `80000 - 80999` | 限流 | @@ -146,6 +147,18 @@ | `51013` | `admin/ranks` | 缺少榜单必填字段 | | `51014` | `admin/ranks` | 创建榜单失败 | +| 错误码 | 接口/模块 | 含义 | +|---|---|---| +| `52010` | `admin/spaces` | project_id 非法 | +| `52011` | `admin/spaces` | 空间列表查询失败 | +| `52012` | `admin/spaces/overview` | 空间总览查询失败 | +| `52013` | `admin/spaces/{player_id}` | player_id 非法 | +| `52014` | `admin/spaces/{player_id}` | 空间不存在 | +| `52015` | `admin/spaces/{player_id}` | 空间详情查询失败 | +| `52016` | `admin/spaces/{player_id}/likes` | 空间点赞列表查询失败 | +| `52017` | `admin/spaces/{player_id}/visits` | 空间访客列表查询失败 | +| `52018` | `admin/spaces/{player_id}/content` | 空间内容下载失败 | + | 错误码 | 接口/模块 | 含义 | |---|---|---| | `60010` | `archives/meta` | 缺少会话 | @@ -167,17 +180,36 @@ | `70010` | `spaces/{player_id}` | 缺少会话 | | `70011` | `spaces/{player_id}` | 玩家不存在 | | `70012` | `spaces/{player_id}` | 空间读取失败 | -| `70013` | `spaces/me` | 缺少会话 | -| `70014` | `spaces/me` | 请求体错误 | -| `70015` | `spaces/me` | 空间更新失败 | -| `70016` | `spaces/{player_id}/like` | 缺少会话 | -| `70017` | `spaces/{player_id}/like` | 玩家不存在 | -| `70018` | `spaces/{player_id}/like` | 点赞失败 | -| `70019` | `spaces/{player_id}/like` | 缺少会话 | -| `70020` | `spaces/{player_id}/like` | 玩家不存在 | -| `70021` | `spaces/{player_id}/like` | 取消点赞失败 | -| `70022` | `spaces/me/visits` | 缺少会话 | -| `70023` | `spaces/me/visits` | 访客列表查询失败 | +| `70013` | `spaces/me/content` | 缺少会话 | +| `70014` | `spaces/me/content` | multipart 表单错误 | +| `70015` | `spaces/me/content` | 缺少文件 | +| `70016` | `spaces/me/content` | 空间内容版本冲突 | +| `70017` | `spaces/me/content` | checksum 不匹配 | +| `70018` | `spaces/me/content` | 空间内容更新失败 | +| `70019` | `spaces/*/content` | 缺少会话 | +| `70020` | `spaces/*/content` | 玩家不存在 | +| `70021` | `spaces/*/content` | 空间内容不存在 | +| `70022` | `spaces/*/content` | 空间内容下载失败 | +| `70023` | `spaces/{player_id}/like` | 缺少会话 | +| `70024` | `spaces/{player_id}/like` | 玩家不存在 | +| `70025` | `spaces/{player_id}/like` | 点赞失败 | +| `70026` | `spaces/{player_id}/like` | 缺少会话 | +| `70027` | `spaces/{player_id}/like` | 玩家不存在 | +| `70028` | `spaces/{player_id}/like` | 取消点赞失败 | +| `70029` | `spaces/me/visits` | 缺少会话 | +| `70030` | `spaces` | 缺少空间目标参数 | +| `70031` | `spaces/*/content` | 缺少空间目标参数 | +| `70032` | `spaces/{player_id}/like` | 缺少空间目标参数 | +| `70033` | `spaces/{player_id}/like` | 缺少空间目标参数 | +| `70034` | `spaces/me/visits` | 访客列表查询失败 | +| `70035` | `spaces/{player_id}/stats` | 缺少会话 | +| `70036` | `spaces/{player_id}/stats` | 缺少空间目标参数 | +| `70037` | `spaces/{player_id}/stats` | 玩家不存在 | +| `70038` | `spaces/{player_id}/stats` | 空间统计读取失败 | +| `70039` | `spaces/{player_id}/likes` | 缺少会话 | +| `70040` | `spaces/{player_id}/likes` | 缺少空间目标参数 | +| `70041` | `spaces/{player_id}/likes` | 玩家不存在 | +| `70042` | `spaces/{player_id}/likes` | 点赞列表查询失败 | | 错误码 | 接口/模块 | 含义 | |---|---|---| diff --git a/服务参考_临时/README.md b/服务参考_临时/README.md index 5604711..4147d61 100644 --- a/服务参考_临时/README.md +++ b/服务参考_临时/README.md @@ -66,7 +66,9 @@ docker compose up --build - 云存档: - 支持二进制文件上传/下载,适合直接存游戏存档 bytes - 玩家空间: - - 当前为 `payload_json` 结构,适合资料卡、展示文本等轻量公开数据 + - 主信息接口返回空间元数据 + - 自定义空间内容通过独立二进制接口上传/下载 + - 支持任意 bytes,兼容二进制、文本、JSON、protobuf、msgpack 等格式 ## 测试脚本 @@ -83,6 +85,7 @@ Windows PowerShell: - [0001_init.sql](/F:/OtherWork/BriskGameSerivce/internal/platform/db/migrations/0001_init.sql) - [0008_identity_and_config_simplification.sql](/F:/OtherWork/BriskGameSerivce/internal/platform/db/migrations/0008_identity_and_config_simplification.sql) +- [0009_space_binary_content.sql](/F:/OtherWork/BriskGameSerivce/internal/platform/db/migrations/0009_space_binary_content.sql) 服务启动时会自动执行未应用迁移。 diff --git a/服务参考_临时/openapi.yaml b/服务参考_临时/openapi.yaml index 91423a1..a7be799 100644 --- a/服务参考_临时/openapi.yaml +++ b/服务参考_临时/openapi.yaml @@ -350,13 +350,149 @@ components: score: type: integer format: int64 - SpaceUpdateRequest: + SpaceContentUploadRequest: type: object - required: [payload_json] + required: [file] properties: - payload_json: - type: object - additionalProperties: true + base_version: + type: integer + format: int64 + description: Optimistic lock base version. Use 0 for the first upload. + content_type: + type: string + description: Optional MIME type. Defaults to application/octet-stream when omitted. + checksum: + type: string + description: Optional SHA-256 hex checksum. When provided, the server validates it before saving. + file: + type: string + format: binary + SpaceMetadata: + type: object + properties: + project_account_id: + type: string + player_id: + type: string + login_provider: + type: string + login_user_id: + type: string + nickname: + type: string + avatar_url: + type: string + content_exists: + type: boolean + content_version: + type: integer + format: int64 + content_type: + type: string + content_size_bytes: + type: integer + format: int64 + content_checksum: + type: string + like_count: + type: integer + format: int64 + visit_count: + type: integer + format: int64 + liked_by_me: + type: boolean + updated_at: + type: string + format: date-time + nullable: true + SpaceStats: + type: object + properties: + project_account_id: + type: string + player_id: + type: string + login_provider: + type: string + login_user_id: + type: string + nickname: + type: string + avatar_url: + type: string + content_exists: + type: boolean + content_version: + type: integer + format: int64 + content_type: + type: string + content_size_bytes: + type: integer + format: int64 + content_checksum: + type: string + like_count: + type: integer + format: int64 + visit_count: + type: integer + format: int64 + updated_at: + type: string + format: date-time + nullable: true + SpaceContentUpdateResult: + type: object + properties: + player_id: + type: string + content_version: + type: integer + format: int64 + content_type: + type: string + content_size_bytes: + type: integer + format: int64 + content_checksum: + type: string + updated_at: + type: string + format: date-time + SpaceLikeResult: + type: object + properties: + liked: + type: boolean + like_count: + type: integer + format: int64 + SpaceVisitItem: + type: object + properties: + visitor_player_id: + type: string + nickname: + type: string + avatar_url: + type: string + visited_at: + type: string + format: date-time + SpaceLikeItem: + type: object + properties: + player_id: + type: string + nickname: + type: string + avatar_url: + type: string + created_at: + type: string + format: date-time paths: /client/bootstrap: get: @@ -644,7 +780,7 @@ paths: get: tags: [spaces] security: [{ bearerAuth: [] }] - summary: Get player space + summary: Get player space metadata parameters: - in: path name: player_id @@ -659,7 +795,46 @@ paths: schema: { type: string } description: Optional target login user id used with login provider lookup. responses: - '200': { description: Space payload } + '200': + description: Space metadata + content: + application/json: + schema: + $ref: '#/components/schemas/SpaceMetadata' + /spaces/{player_id}/content: + get: + tags: [spaces] + security: [{ bearerAuth: [] }] + summary: Download player space content + parameters: + - in: path + name: player_id + required: true + schema: { type: string } + - in: query + name: login_provider + schema: { type: string } + description: Optional target login provider. When present, login_user_id is also required and overrides player_id lookup. + - in: query + name: login_user_id + schema: { type: string } + description: Optional target login user id used with login provider lookup. + responses: + '200': + description: Binary space content stream + headers: + X-Space-Version: + schema: + type: integer + format: int64 + X-Space-Checksum: + schema: + type: string + content: + application/octet-stream: + schema: + type: string + format: binary /spaces/{player_id}/stats: get: tags: [spaces] @@ -677,7 +852,12 @@ paths: name: login_user_id schema: { type: string } responses: - '200': { description: Space stats } + '200': + description: Space stats + content: + application/json: + schema: + $ref: '#/components/schemas/SpaceStats' /spaces/{player_id}/likes: get: tags: [spaces] @@ -698,20 +878,33 @@ paths: name: limit schema: { type: integer } responses: - '200': { description: Space likes } - /spaces/me: + '200': + description: Space likes + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SpaceLikeItem' + /spaces/me/content: put: tags: [spaces] security: [{ bearerAuth: [] }] - summary: Update my space + summary: Upload my space content requestBody: required: true content: - application/json: + multipart/form-data: schema: - $ref: '#/components/schemas/SpaceUpdateRequest' + $ref: '#/components/schemas/SpaceContentUploadRequest' responses: - '200': { description: Space updated } + '200': + description: Space content updated + content: + application/json: + schema: + $ref: '#/components/schemas/SpaceContentUpdateResult' + '409': { description: Version conflict } /spaces/{player_id}/like: post: tags: [spaces] @@ -729,7 +922,12 @@ paths: name: login_user_id schema: { type: string } responses: - '200': { description: Liked } + '200': + description: Liked + content: + application/json: + schema: + $ref: '#/components/schemas/SpaceLikeResult' '429': { description: Rate limited } delete: tags: [spaces] @@ -747,13 +945,18 @@ paths: name: login_user_id schema: { type: string } responses: - '200': { description: Unliked } + '200': + description: Unliked + content: + application/json: + schema: + $ref: '#/components/schemas/SpaceLikeResult' '429': { description: Rate limited } /spaces/by-login: get: tags: [spaces] security: [{ bearerAuth: [] }] - summary: Get player space by login identity + summary: Get player space metadata by login identity parameters: - in: query name: login_provider @@ -764,7 +967,42 @@ paths: required: true schema: { type: string } responses: - '200': { description: Space payload } + '200': + description: Space metadata + content: + application/json: + schema: + $ref: '#/components/schemas/SpaceMetadata' + /spaces/by-login/content: + get: + tags: [spaces] + security: [{ bearerAuth: [] }] + summary: Download player space content by login identity + parameters: + - in: query + name: login_provider + required: true + schema: { type: string } + - in: query + name: login_user_id + required: true + schema: { type: string } + responses: + '200': + description: Binary space content stream + headers: + X-Space-Version: + schema: + type: integer + format: int64 + X-Space-Checksum: + schema: + type: string + content: + application/octet-stream: + schema: + type: string + format: binary /spaces/by-login/stats: get: tags: [spaces] @@ -780,7 +1018,12 @@ paths: required: true schema: { type: string } responses: - '200': { description: Space stats } + '200': + description: Space stats + content: + application/json: + schema: + $ref: '#/components/schemas/SpaceStats' /spaces/by-login/likes: get: tags: [spaces] @@ -799,7 +1042,14 @@ paths: name: limit schema: { type: integer } responses: - '200': { description: Space likes } + '200': + description: Space likes + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SpaceLikeItem' /spaces/by-login/like: post: tags: [spaces] @@ -815,7 +1065,12 @@ paths: required: true schema: { type: string } responses: - '200': { description: Liked } + '200': + description: Liked + content: + application/json: + schema: + $ref: '#/components/schemas/SpaceLikeResult' '429': { description: Rate limited } delete: tags: [spaces] @@ -831,7 +1086,12 @@ paths: required: true schema: { type: string } responses: - '200': { description: Unliked } + '200': + description: Unliked + content: + application/json: + schema: + $ref: '#/components/schemas/SpaceLikeResult' '429': { description: Rate limited } /spaces/me/visits: get: @@ -843,7 +1103,14 @@ paths: name: limit schema: { type: integer } responses: - '200': { description: Visitor list } + '200': + description: Visitor list + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SpaceVisitItem' /admin/auth/login: post: tags: [admin-auth] @@ -1499,7 +1766,7 @@ paths: get: tags: [admin-spaces] security: [{ bearerAuth: [] }] - summary: Get player space detail + summary: Get player space metadata detail parameters: - in: path name: player_id @@ -1511,6 +1778,36 @@ paths: schema: { type: integer, format: int64 } responses: '200': { description: Space detail } + /admin/spaces/{player_id}/content: + get: + tags: [admin-spaces] + security: [{ bearerAuth: [] }] + summary: Download player space content + parameters: + - in: path + name: player_id + required: true + schema: { type: string } + - in: query + name: project_id + required: true + schema: { type: integer, format: int64 } + responses: + '200': + description: Binary space content stream + headers: + X-Space-Version: + schema: + type: integer + format: int64 + X-Space-Checksum: + schema: + type: string + content: + application/octet-stream: + schema: + type: string + format: binary /admin/spaces/{player_id}/likes: get: tags: [admin-spaces]