using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Text; using System.Threading.Tasks; using UnityEngine; public sealed class BriskQuickStartSample : MonoBehaviour { 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"; public string ClientVersion = "1.0.0"; public string DeviceId = "editor-device"; public bool ValidateSessionOnInitialize = true; [Header("登录")] public string LoginProvider = "tap"; public string LoginUserId = "tap_user_10001"; public string LoginCode = string.Empty; public string Nickname = "Unity示例玩家"; public string AvatarUrl = string.Empty; [Header("排行榜")] public string RankKey = "season-score"; public string SubmitScoreValue = "0"; public string LeaderboardLimit = "10"; public string AroundMeRange = "5"; public string SeasonId = string.Empty; public string SeasonHistoryLimit = "20"; [Header("云存档")] public string ArchiveSlotNo = "1"; public string ArchiveBaseVersion = string.Empty; [TextArea(3, 6)] public string ArchiveContent = "{\n \"save\": 1,\n \"coins\": 128,\n \"hero\": \"mage\",\n \"title\": \"中文测试存档\"\n}"; [Header("公告")] public string AnnouncementId = string.Empty; [Header("玩家空间")] public string SpacePlayerId = string.Empty; public string SpaceLoginProvider = "tap"; public string SpaceLoginUserId = "tap_user_10001"; [TextArea(3, 6)] public string SpacePayloadText = "这里是 unity 测试账户的空间内容。\n\n0.o"; [Header("演示")] public bool AutoRunOnStart; private readonly List _logs = new List(); 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 _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 PropertyInfo _defaultDialogVisibleProperty; private MethodInfo _defaultDialogRenderMethod; private void OnEnable() { Brisk.OnInitialized += HandleInitialized; Brisk.OnLoggedIn += HandleLoggedIn; Brisk.OnLoggedOut += HandleLoggedOut; Brisk.OnAuthExpired += HandleAuthExpired; Brisk.OnBlockingError += HandleBlockingError; } private void OnDisable() { Brisk.OnInitialized -= HandleInitialized; Brisk.OnLoggedIn -= HandleLoggedIn; Brisk.OnLoggedOut -= HandleLoggedOut; Brisk.OnAuthExpired -= HandleAuthExpired; Brisk.OnBlockingError -= HandleBlockingError; } private void Start() { SyncPageBySdkState(); if (AutoRunOnStart) { RunAction("自动冒烟", RunSmokeFlowAsync); } } private void OnGUI() { if (ShouldRouteInputToDefaultDialog()) { RenderDefaultErrorDialogTopmost(); return; } EnsureStyles(); 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; RenderDefaultErrorDialogTopmost(); } private bool ShouldRouteInputToDefaultDialog() { if (!IsDefaultErrorDialogVisible() || Event.current == null) { return false; } switch (Event.current.type) { case EventType.Layout: case EventType.Repaint: return false; default: return true; } } private bool IsDefaultErrorDialogVisible() { EnsureDefaultDialogReflection(); return _defaultDialogVisibleProperty != null && (bool)_defaultDialogVisibleProperty.GetValue(null, null); } private void RenderDefaultErrorDialogTopmost() { EnsureDefaultDialogReflection(); _defaultDialogRenderMethod?.Invoke(null, null); } private void EnsureDefaultDialogReflection() { if (_defaultDialogVisibleProperty != null && _defaultDialogRenderMethod != null) { return; } var dialogType = typeof(Brisk).Assembly.GetType("BriskDefaultErrorDialog"); if (dialogType == null) { return; } _defaultDialogVisibleProperty = dialogType.GetProperty("IsVisible", BindingFlags.Static | BindingFlags.NonPublic); _defaultDialogRenderMethod = dialogType.GetMethod("RenderTopmostLegacyOverlay", BindingFlags.Static | BindingFlags.NonPublic); } 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); }); DrawCard("默认弹窗外观预览", () => { GUILayout.Label("这些按钮只展示 SDK 默认弹窗外观,不会修改登录状态,也不会请求服务器。", _bodyStyle); DrawInlineButtons( new ButtonSpec("登录过期", PreviewAuthExpiredDialogAsync, false, true), new ButtonSpec("缺少 Token", PreviewMissingTokenDialogAsync, false, true)); GUILayout.Space(8f); DrawInlineButtons( new ButtonSpec("服务维护", PreviewMaintenanceDialogAsync, false, true), new ButtonSpec("账号受限", PreviewAccountBannedDialogAsync, false, true), new ButtonSpec("需要更新", PreviewUpdateRequiredDialogAsync, false, true)); }); 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 DrawLeaderboardPage() { var contentRect = DrawPageFrame("排行榜", "返回主页", () => _page = SamplePage.Home); GUILayout.BeginArea(contentRect); _leaderboardScroll = GUILayout.BeginScrollView(_leaderboardScroll); DrawCard("排行榜配置", () => { RankKey = DrawInputRow("排行榜 Key", RankKey); LeaderboardLimit = DrawInputRow("Top 数量", LeaderboardLimit); AroundMeRange = DrawInputRow("附近范围", AroundMeRange); DrawInlineButtons( new ButtonSpec("更新列表", RefreshLeaderboardAsync, false, true), new ButtonSpec(_viewAroundMe ? "切换到 Top" : "切换到我附近", ToggleLeaderboardModeAsync, false, true)); }); DrawCard("我的排名", () => { DrawValueRow("当前模式", _viewAroundMe ? "我附近的排名" : "Top 排行榜"); DrawValueRow("我的名次", _leaderboardMe == null ? "-" : _leaderboardMe.Rank.ToString()); DrawValueRow("我的分数", _leaderboardMe == null ? "-" : _leaderboardMe.Score.ToString()); DrawValueRow("当前待提交分数", SubmitScoreValue); DrawInlineButtons(new ButtonSpec("分数 +1 并提交", IncreaseAndSubmitScoreAsync, true, true)); }); DrawCard("排行榜列表", () => { if (_leaderboardEntries.Count == 0) { GUILayout.Label("暂无数据,请先点击“更新列表”。", _bodyStyle); return; } 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.EndScrollView(); GUILayout.EndArea(); } 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.TodayLikeCount.ToString()); DrawValueRow("访问数", _mySpaceStats == null ? "-" : _mySpaceStats.VisitCount.ToString()); DrawValueRow("今日点赞重置", _mySpaceStats == null ? "-" : FormatLikeResetAt(_mySpaceStats.LikeResetAt)); 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.TodayLikeCount.ToString()); DrawValueRow("访问数", _targetSpaceStats == null ? "-" : _targetSpaceStats.VisitCount.ToString()); DrawValueRow("今日点赞状态", _targetSpaceView != null && _targetSpaceView.LikedByMe ? "今日已点赞" : "今日未点赞"); DrawValueRow("今日点赞重置", _targetSpaceView == null ? "-" : FormatLikeResetAt(_targetSpaceView.LikeResetAt)); GUILayout.Space(8f); GUILayout.Label("空间内容", _sectionTitleStyle); GUILayout.TextArea(_targetSpaceContentText ?? string.Empty, _textAreaStyle, GUILayout.Height(240f)); }); DrawCard("操作", () => { DrawInlineButtons( new ButtonSpec("今日点赞", LikeTargetSpaceAsync, false, _targetSpaceView != null && !_targetSpaceView.LikedByMe), new ButtonSpec("撤销今日点赞", UnlikeTargetSpaceAsync, false, _targetSpaceView != null && _targetSpaceView.LikedByMe)); }); } private async Task InitializeFlowAsync() { await Brisk.InitializeAsync(new BriskOptions { BaseUrl = BaseUrl, GameKey = GameKey, ClientVersion = ClientVersion, DeviceId = DeviceId, ValidateSessionOnInitialize = ValidateSessionOnInitialize, ExitHandler = HandleExitRequested }); _page = Brisk.IsLoggedIn ? SamplePage.Home : SamplePage.Login; if (Brisk.IsLoggedIn) { await RefreshHomeAsync(); } } private async Task LoginFlowAsync() { if (string.IsNullOrWhiteSpace(LoginCode)) { await Brisk.Auth.LoginWithUserIdAsync(LoginProvider, LoginUserId, CreateProfile()); } else { await Brisk.Auth.LoginWithCodeAsync(LoginProvider, LoginCode, CreateProfile()); } await RefreshHomeAsync(); _page = SamplePage.Home; } private Task PreviewAuthExpiredDialogAsync() { BriskDefaultErrorPresenter.Instance.ShowAuthExpired(new BriskAuthExpiredException("invalid token")); return Task.CompletedTask; } private Task PreviewMissingTokenDialogAsync() { BriskDefaultErrorPresenter.Instance.ShowAuthExpired(new BriskAuthExpiredException("Missing access token.")); return Task.CompletedTask; } private Task PreviewMaintenanceDialogAsync() { BriskDefaultErrorPresenter.Instance.ShowBlockingError(new BriskMaintenanceException("maintenance")); return Task.CompletedTask; } private Task PreviewAccountBannedDialogAsync() { BriskDefaultErrorPresenter.Instance.ShowBlockingError(new BriskAccountBannedException("account banned")); return Task.CompletedTask; } private Task PreviewUpdateRequiredDialogAsync() { BriskDefaultErrorPresenter.Instance.ShowBlockingError(new BriskClientUpdateRequiredException("client update required")); return Task.CompletedTask; } private async Task RefreshHomeAsync() { _me = await Brisk.Player.GetMeAsync(); _config = await Brisk.Config.GetCurrentAsync(); ApplyIdentity(_me.PlayerId, _me.LoginProvider, _me.LoginUserId); await LoadMySpaceAsync(); } private async Task LoadAnnouncementsAsync() { _announcements = await Brisk.Announcements.GetListAsync(); if (_announcements.Count > 0) { AnnouncementId = _announcements[0].Id.ToString(); } } private async Task RefreshLeaderboardAsync() { _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 ToggleLeaderboardModeAsync() { _viewAroundMe = !_viewAroundMe; await RefreshLeaderboardAsync(); } private async Task IncreaseAndSubmitScoreAsync() { var score = ParseOrDefault(SubmitScoreValue, 0) + 1; SubmitScoreValue = score.ToString(); await Brisk.Leaderboard.SubmitScoreAsync(RankKey, score); await RefreshLeaderboardAsync(); } private async Task GetArchiveSlotsAsync() { _archiveSlots = await Brisk.Archive.GetSlotsAsync(); } private async Task GetArchiveMetaAsync() { _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(); } 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)) { return; } _mySpaceView = await Brisk.Space.GetByPlayerIdAsync(Brisk.PlayerId); _mySpaceStats = await Brisk.Space.GetStatsByPlayerIdAsync(Brisk.PlayerId); _mySpaceLikes = await Brisk.Space.GetLikesByPlayerIdAsync(Brisk.PlayerId, 10, true); SpacePayloadText = await DownloadSpaceTextAsync(_mySpaceView, () => Brisk.Space.DownloadContentByPlayerIdAsync(Brisk.PlayerId)); } private async Task RefreshSpacePageAsync() { if (_viewingTargetSpace) { await LoadTargetSpaceAsync(); return; } await LoadMySpaceAsync(); } private async Task UpdateMySpaceAsync() { var baseVersion = _mySpaceView != null && _mySpaceView.ContentExists ? _mySpaceView.ContentVersion : 0L; await Brisk.Space.UpdateMyAsync(SpacePayloadText ?? string.Empty, baseVersion, "text/plain"); await LoadMySpaceAsync(); } private async Task VisitTargetSpaceAsync() { 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() { var result = await Brisk.Space.LikeByPlayerIdAsync(SpacePlayerId); Log("空间今日点赞结果: created=" + result.Created + ", total=" + result.LikeCount + ", today=" + result.TodayLikeCount); await LoadTargetSpaceAsync(); } private async Task UnlikeTargetSpaceAsync() { var result = await Brisk.Space.UnlikeByPlayerIdAsync(SpacePlayerId); Log("空间撤销今日点赞结果: total=" + result.LikeCount + ", today=" + result.TodayLikeCount); 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 InitializeFlowAsync(); if (!Brisk.IsLoggedIn) { await LoginFlowAsync(); } } private void DrawAnnouncementModal() { 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) { return; } 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) { if (_isBusy) { return; } RunActionInternal(actionName, action); } private async void RunActionInternal(string actionName, Func action) { _isBusy = true; _busyAction = actionName; _statusText = "执行中: " + actionName; _errorText = string.Empty; Log("开始执行: " + actionName); try { await action(); _statusText = "执行成功: " + actionName; Log("执行成功: " + actionName); } catch (Exception exception) { _statusText = "执行失败: " + actionName; _errorText = FormatException(exception); Log("执行失败: " + actionName + " | " + exception.Message); Debug.LogException(exception, this); } finally { _busyAction = string.Empty; _isBusy = false; } } private void SyncPageBySdkState() { 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) { if (!string.IsNullOrWhiteSpace(playerId)) { SpacePlayerId = playerId; } if (!string.IsNullOrWhiteSpace(loginProvider)) { SpaceLoginProvider = loginProvider; LoginProvider = loginProvider; } if (!string.IsNullOrWhiteSpace(loginUserId)) { SpaceLoginUserId = loginUserId; LoginUserId = loginUserId; } } private void EnsureStyles() { if (_pageStyle != null) { return; } _pageStyle = new GUIStyle(GUI.skin.box) { padding = new RectOffset(18, 18, 18, 18) }; _cardStyle = new GUIStyle(GUI.skin.box) { padding = new RectOffset(16, 16, 14, 14), margin = new RectOffset(0, 0, 0, 0) }; _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) { if (!int.TryParse(value, out var result)) { throw new InvalidOperationException(fieldName + " 必须是合法整数。"); } return result; } private static int ParseOrDefault(string value, int fallback) { return int.TryParse(value, out var result) ? result : fallback; } private static int? ParseNullableInt(string value) { if (string.IsNullOrWhiteSpace(value)) { return null; } return int.TryParse(value, out var result) ? result : (int?)null; } private static string NullToDash(string value) { return string.IsNullOrWhiteSpace(value) ? "-" : value; } private static string FormatLikeResetAt(string value) { if (string.IsNullOrWhiteSpace(value)) { return "-"; } DateTimeOffset parsed; if (!DateTimeOffset.TryParse(value, out parsed)) { return value; } return parsed.LocalDateTime.ToString("yyyy-MM-dd HH:mm:ss"); } 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 "未知错误"; } var builder = new StringBuilder(); var current = exception; while (current != null) { builder.AppendLine(current.GetType().Name + ": " + current.Message); current = current.InnerException; } return builder.ToString().TrimEnd(); } private static string FormatValue(object value) { var builder = new StringBuilder(); AppendValue(builder, value, 0, 0); return builder.Length == 0 ? "-" : builder.ToString().TrimEnd(); } private static void AppendValue(StringBuilder builder, object value, int indent, int depth) { if (depth > 5) { builder.AppendLine(new string(' ', indent) + "..."); return; } if (value == null) { builder.AppendLine(new string(' ', indent) + "null"); return; } if (value is string stringValue) { builder.AppendLine(new string(' ', indent) + stringValue); return; } if (value is IDictionary dictionary) { foreach (DictionaryEntry entry in dictionary) { builder.AppendLine(new string(' ', indent) + entry.Key + ":"); AppendValue(builder, entry.Value, indent + 2, depth + 1); } return; } if (value is IEnumerable enumerable && !(value is byte[])) { var index = 0; foreach (var item in enumerable) { builder.AppendLine(new string(' ', indent) + "[" + index + "]"); AppendValue(builder, item, indent + 2, depth + 1); index++; } return; } 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); foreach (var field in fields) { builder.AppendLine(new string(' ', indent) + field.Name + ":"); AppendValue(builder, field.GetValue(value), indent + 2, depth + 1); } } private readonly struct ButtonSpec { 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 readonly struct NavSpec { public NavSpec(string label, SamplePage page) { Label = label; Page = page; } public string Label { get; } public SamplePage Page { get; } } }