using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Text; using System.Threading.Tasks; using UnityEngine; public sealed class BriskQuickStartSample : MonoBehaviour { [Header("Init")] 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("Login")] public string LoginProvider = "tap"; public string LoginUserId = "tap_user_10001"; public string LoginCode = string.Empty; public string Nickname = "Unity Sample User"; public string AvatarUrl = string.Empty; [Header("Leaderboard")] public string RankKey = "season-score"; public string SubmitScoreValue = "128"; public string LeaderboardLimit = "10"; public string AroundMeRange = "5"; public string SeasonId = string.Empty; public string SeasonHistoryLimit = "20"; [Header("Archive")] 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}"; [Header("Announcements")] public string AnnouncementId = string.Empty; [Header("Space")] public string SpacePlayerId = string.Empty; public string SpaceLoginProvider = "tap"; public string SpaceLoginUserId = "tap_user_10001"; [TextArea(3, 6)] public string SpacePayloadText = "{\n \"mood\": \"ready\",\n \"title\": \"hello brisk\"\n}"; [Header("Demo")] public bool AutoRunOnStart; private readonly List _eventLogs = new List(); private Vector2 _pageScroll; private Vector2 _resultScroll; private Vector2 _logScroll; private bool _isBusy; private string _busyAction = string.Empty; private string _statusText = "Ready."; private string _resultText = "No request yet."; private string _lastErrorText = string.Empty; private IReadOnlyList _announcementCache = Array.Empty(); private IReadOnlyList _seasonHistoryCache = Array.Empty(); private void OnEnable() { Brisk.OnInitialized += HandleInitialized; Brisk.OnLoggedIn += HandleLoggedIn; Brisk.OnLoggedOut += HandleLoggedOut; Brisk.OnAuthExpired += HandleAuthExpired; Brisk.OnBlockingError += HandleBlockingError; } private void OnDisable() { Brisk.OnInitialized -= HandleInitialized; Brisk.OnLoggedIn -= HandleLoggedIn; Brisk.OnLoggedOut -= HandleLoggedOut; Brisk.OnAuthExpired -= HandleAuthExpired; Brisk.OnBlockingError -= HandleBlockingError; } private void Start() { if (AutoRunOnStart) { RunAction("Auto Smoke Run", RunSmokeFlowAsync); } } [ContextMenu("Run Brisk Sample")] public void RunFromContextMenu() { RunAction("Context Smoke Run", RunSmokeFlowAsync); } private void OnGUI() { var area = new Rect(12f, 12f, Screen.width - 24f, Screen.height - 24f); GUILayout.BeginArea(area, GUI.skin.box); _pageScroll = GUILayout.BeginScrollView(_pageScroll); DrawHeader(); DrawStatusPanel(); DrawInitSection(); DrawLoginSection(); DrawPlayerAndConfigSection(); DrawAnnouncementsSection(); DrawLeaderboardSection(); DrawArchiveSection(); DrawSpaceSection(); DrawOutputSection(); GUILayout.EndScrollView(); GUILayout.EndArea(); } private void DrawHeader() { GUILayout.Label("Brisk IMGUI Sample", GUI.skin.box); GUILayout.Label("This sample is designed for full SDK flow testing inside a single scene.", GUI.skin.label); } private void DrawStatusPanel() { BeginSection("Runtime Status"); DrawReadOnlyRow("Initialized", Brisk.IsInitialized ? "Yes" : "No"); DrawReadOnlyRow("Logged In", Brisk.IsLoggedIn ? "Yes" : "No"); DrawReadOnlyRow("PlayerId", Brisk.PlayerId); DrawReadOnlyRow("Identity", Brisk.Identity == null ? string.Empty : Brisk.Identity.LoginProvider + " / " + Brisk.Identity.LoginUserId); DrawReadOnlyRow("Current Action", _isBusy ? _busyAction : "Idle"); DrawReadOnlyRow("Status", _statusText); GUILayout.BeginHorizontal(); DrawButton("Run Smoke Flow", RunSmokeFlowAsync); DrawButton("Show Bootstrap Cache", () => { SetResult("Bootstrap Cache", Brisk.Bootstrap); return Task.CompletedTask; }, Brisk.IsInitialized); DrawButton("Shutdown SDK", () => { Brisk.Shutdown(); Log("SDK shutdown."); _statusText = "SDK shutdown."; return Task.CompletedTask; }); DrawButton("Clear Output", () => { _resultText = "Output cleared."; _lastErrorText = string.Empty; _eventLogs.Clear(); _statusText = "Output cleared."; return Task.CompletedTask; }); GUILayout.EndHorizontal(); EndSection(); } private void DrawInitSection() { BeginSection("Initialize"); BaseUrl = DrawEditableRow("Base Url", BaseUrl); GameKey = DrawEditableRow("Game Key", GameKey); ClientVersion = DrawEditableRow("Client Version", ClientVersion); DeviceId = DrawEditableRow("Device Id", DeviceId); ValidateSessionOnInitialize = DrawToggleRow("Validate Session On Init", ValidateSessionOnInitialize); GUILayout.BeginHorizontal(); DrawButton("Initialize", InitializeAsync); DrawButton("Reinitialize", ReinitializeAsync); GUILayout.EndHorizontal(); EndSection(); } private void DrawLoginSection() { BeginSection("Login"); LoginProvider = DrawEditableRow("Login Provider", LoginProvider); LoginUserId = DrawEditableRow("Login User Id", LoginUserId); LoginCode = DrawEditableRow("Login Code", LoginCode); Nickname = DrawEditableRow("Nickname", Nickname); AvatarUrl = DrawEditableRow("Avatar Url", AvatarUrl); GUILayout.BeginHorizontal(); DrawButton("Login By UserId", LoginWithUserIdAsync, Brisk.IsInitialized); DrawButton("Login By Code", LoginWithCodeAsync, Brisk.IsInitialized); DrawButton("Logout", LogoutAsync, Brisk.IsInitialized); GUILayout.EndHorizontal(); EndSection(); } private void DrawPlayerAndConfigSection() { BeginSection("Player And Config"); GUILayout.BeginHorizontal(); DrawButton("Get Me", GetMeAsync, Brisk.IsLoggedIn); DrawButton("Get Config Current", GetConfigAsync, Brisk.IsInitialized); DrawButton("Apply Current Identity To Space", () => { ApplyCurrentIdentityToSpace(); SetResult("Space Lookup Identity", new Dictionary { { "space_player_id", SpacePlayerId }, { "space_login_provider", SpaceLoginProvider }, { "space_login_user_id", SpaceLoginUserId } }); return Task.CompletedTask; }, Brisk.IsInitialized); GUILayout.EndHorizontal(); EndSection(); } private void DrawAnnouncementsSection() { BeginSection("Announcements"); AnnouncementId = DrawEditableRow("Announcement Id", AnnouncementId); GUILayout.BeginHorizontal(); DrawButton("Get Announcement List", GetAnnouncementsAsync, Brisk.IsLoggedIn); DrawButton("Mark Read", MarkAnnouncementAsync, Brisk.IsLoggedIn); DrawButton("Mark First Cached", MarkFirstCachedAnnouncementAsync, Brisk.IsLoggedIn && _announcementCache.Count > 0); GUILayout.EndHorizontal(); EndSection(); } private void DrawLeaderboardSection() { BeginSection("Leaderboard"); RankKey = DrawEditableRow("Rank Key", RankKey); SubmitScoreValue = DrawEditableRow("Submit Score", SubmitScoreValue); LeaderboardLimit = DrawEditableRow("Top Limit", LeaderboardLimit); AroundMeRange = DrawEditableRow("Around Range", AroundMeRange); SeasonId = DrawEditableRow("Season Id", SeasonId); SeasonHistoryLimit = DrawEditableRow("History Limit", SeasonHistoryLimit); GUILayout.BeginHorizontal(); DrawButton("Get Top", GetTopAsync, Brisk.IsLoggedIn); DrawButton("Get My Rank", GetMyRankAsync, Brisk.IsLoggedIn); DrawButton("Get Around Me", GetAroundMeAsync, Brisk.IsLoggedIn); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); DrawButton("Submit Score", SubmitScoreAsync, Brisk.IsLoggedIn); DrawButton("Get Current Season", GetCurrentSeasonAsync, Brisk.IsLoggedIn); DrawButton("Get Season History", GetSeasonHistoryAsync, Brisk.IsLoggedIn); DrawButton("Get Season Detail", GetSeasonDetailAsync, Brisk.IsLoggedIn); GUILayout.EndHorizontal(); EndSection(); } private void DrawArchiveSection() { BeginSection("Archive"); ArchiveSlotNo = DrawEditableRow("Slot No", ArchiveSlotNo); ArchiveBaseVersion = DrawEditableRow("Base Version", ArchiveBaseVersion); ArchiveContent = DrawTextAreaRow("Archive Content", ArchiveContent, 90f); GUILayout.BeginHorizontal(); DrawButton("Get Slots", GetArchiveSlotsAsync, Brisk.IsLoggedIn); DrawButton("Get Meta", GetArchiveMetaAsync, Brisk.IsLoggedIn); DrawButton("Upload Text", UploadArchiveAsync, Brisk.IsLoggedIn); DrawButton("Download", DownloadArchiveAsync, Brisk.IsLoggedIn); GUILayout.EndHorizontal(); EndSection(); } private void DrawSpaceSection() { BeginSection("Space"); SpacePlayerId = DrawEditableRow("Space Player Id", SpacePlayerId); SpaceLoginProvider = DrawEditableRow("Space Login Provider", SpaceLoginProvider); SpaceLoginUserId = DrawEditableRow("Space Login User Id", SpaceLoginUserId); SpacePayloadText = DrawTextAreaRow("Space Payload Text", SpacePayloadText, 90f); GUILayout.BeginHorizontal(); DrawButton("Get By PlayerId", GetSpaceByPlayerIdAsync, Brisk.IsLoggedIn); DrawButton("Get By Login", GetSpaceByLoginAsync, Brisk.IsLoggedIn); DrawButton("Get Stats By PlayerId", GetSpaceStatsByPlayerIdAsync, Brisk.IsLoggedIn); DrawButton("Get Stats By Login", GetSpaceStatsByLoginAsync, Brisk.IsLoggedIn); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); DrawButton("Like PlayerId", LikeByPlayerIdAsync, Brisk.IsLoggedIn); DrawButton("Unlike PlayerId", UnlikeByPlayerIdAsync, Brisk.IsLoggedIn); DrawButton("Like Login", LikeByLoginAsync, Brisk.IsLoggedIn); DrawButton("Unlike Login", UnlikeByLoginAsync, Brisk.IsLoggedIn); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); DrawButton("Update My Space", UpdateMySpaceAsync, Brisk.IsLoggedIn); DrawButton("Get My Visits", GetMyVisitsAsync, Brisk.IsLoggedIn); GUILayout.EndHorizontal(); EndSection(); } private void DrawOutputSection() { BeginSection("Output"); GUILayout.Label("Latest Result", GUI.skin.label); _resultScroll = GUILayout.BeginScrollView(_resultScroll, GUILayout.Height(240f)); GUILayout.TextArea(_resultText, GUILayout.ExpandHeight(true)); GUILayout.EndScrollView(); GUILayout.Space(8f); GUILayout.Label("Latest Error", GUI.skin.label); GUILayout.TextArea(string.IsNullOrWhiteSpace(_lastErrorText) ? "No error." : _lastErrorText, GUILayout.Height(90f)); GUILayout.Space(8f); GUILayout.Label("Event Log", GUI.skin.label); _logScroll = GUILayout.BeginScrollView(_logScroll, GUILayout.Height(220f)); GUILayout.TextArea(_eventLogs.Count == 0 ? "No events yet." : string.Join("\n", _eventLogs.ToArray()), GUILayout.ExpandHeight(true)); GUILayout.EndScrollView(); EndSection(); } private async Task InitializeAsync() { await Brisk.InitializeAsync(new BriskOptions { BaseUrl = BaseUrl, GameKey = GameKey, ClientVersion = ClientVersion, DeviceId = DeviceId, ValidateSessionOnInitialize = ValidateSessionOnInitialize, ExitHandler = HandleExitRequested }); SetResult("Initialize Result", Brisk.Bootstrap); } private async Task ReinitializeAsync() { if (Brisk.IsInitialized) { Brisk.Shutdown(); Log("SDK shutdown before reinitialize."); } await InitializeAsync(); } private async Task LoginWithUserIdAsync() { var result = await Brisk.Auth.LoginWithUserIdAsync(LoginProvider, LoginUserId, CreateProfile()); ApplyIdentity(result.PlayerId, result.LoginProvider, result.LoginUserId); SetResult("Login By UserId Result", result); } private async Task LoginWithCodeAsync() { var result = await Brisk.Auth.LoginWithCodeAsync(LoginProvider, LoginCode, CreateProfile()); ApplyIdentity(result.PlayerId, result.LoginProvider, result.LoginUserId); SetResult("Login By Code Result", result); } private async Task LogoutAsync() { await Brisk.Auth.LogoutAsync(); SetResult("Logout Result", new Dictionary { { "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("Player Me", me); } private async Task GetConfigAsync() { var config = await Brisk.Config.GetCurrentAsync(); SetResult("Config Current", config); } private async Task GetAnnouncementsAsync() { _announcementCache = await Brisk.Announcements.GetListAsync(); if (_announcementCache.Count > 0 && string.IsNullOrWhiteSpace(AnnouncementId)) { AnnouncementId = _announcementCache[0].Id.ToString(); } SetResult("Announcements", _announcementCache); } private async Task MarkAnnouncementAsync() { var id = ParseRequiredLong(AnnouncementId, nameof(AnnouncementId)); await Brisk.Announcements.MarkReadAsync(id); SetResult("Announcement Marked Read", new Dictionary { { "announcement_id", id } }); } private async Task MarkFirstCachedAnnouncementAsync() { if (_announcementCache.Count == 0) { throw new InvalidOperationException("Announcement cache is empty. Run Get Announcement List first."); } var id = _announcementCache[0].Id; AnnouncementId = id.ToString(); await Brisk.Announcements.MarkReadAsync(id); SetResult("First Cached Announcement Marked Read", _announcementCache[0]); } private async Task GetTopAsync() { var result = await Brisk.Leaderboard.GetTopAsync(RankKey, ParseOptionalInt(LeaderboardLimit, 10)); SetResult("Leaderboard Top", result); } private async Task GetMyRankAsync() { var result = await Brisk.Leaderboard.GetMeAsync(RankKey); SetResult("Leaderboard My Rank", result); } private async Task GetAroundMeAsync() { var result = await Brisk.Leaderboard.GetAroundMeAsync(RankKey, ParseOptionalInt(AroundMeRange, 5)); SetResult("Leaderboard Around Me", result); } private async Task SubmitScoreAsync() { var score = ParseRequiredLong(SubmitScoreValue, nameof(SubmitScoreValue)); await Brisk.Leaderboard.SubmitScoreAsync(RankKey, score); SetResult("Score Submitted", 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("Current Season", 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("Season History", _seasonHistoryCache); } private async Task GetSeasonDetailAsync() { var result = await Brisk.Leaderboard.GetSeasonHistoryDetailAsync(RankKey, SeasonId, ParseOptionalInt(SeasonHistoryLimit, 20)); SetResult("Season Detail", result); } private async Task GetArchiveSlotsAsync() { var result = await Brisk.Archive.GetSlotsAsync(); SetResult("Archive Slots", result); } private async Task GetArchiveMetaAsync() { var result = await Brisk.Archive.GetMetaAsync(ParseRequiredInt(ArchiveSlotNo, nameof(ArchiveSlotNo))); ArchiveBaseVersion = result == null ? ArchiveBaseVersion : result.Version.ToString(); SetResult("Archive Meta", 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("Archive Upload", result); } private async Task DownloadArchiveAsync() { var result = await Brisk.Archive.DownloadAsync(ParseRequiredInt(ArchiveSlotNo, nameof(ArchiveSlotNo))); ArchiveBaseVersion = result == null ? ArchiveBaseVersion : result.Version.ToString(); var output = new Dictionary { { "version", result.Version }, { "checksum", result.Checksum }, { "byte_length", result.Bytes == null ? 0 : result.Bytes.Length }, { "text_preview", result.Bytes == null ? string.Empty : Encoding.UTF8.GetString(result.Bytes) } }; SetResult("Archive Download", output); } private async Task GetSpaceByPlayerIdAsync() { var result = await Brisk.Space.GetByPlayerIdAsync(SpacePlayerId); SetResult("Space By PlayerId", result); } private async Task GetSpaceByLoginAsync() { var result = await Brisk.Space.GetByLoginIdentityAsync(SpaceLoginProvider, SpaceLoginUserId); SetResult("Space By Login", result); } private async Task GetSpaceStatsByPlayerIdAsync() { var result = await Brisk.Space.GetStatsByPlayerIdAsync(SpacePlayerId); SetResult("Space Stats By PlayerId", result); } private async Task GetSpaceStatsByLoginAsync() { var result = await Brisk.Space.GetStatsByLoginIdentityAsync(SpaceLoginProvider, SpaceLoginUserId); SetResult("Space Stats By Login", result); } private async Task LikeByPlayerIdAsync() { await Brisk.Space.LikeByPlayerIdAsync(SpacePlayerId); SetResult("Like By PlayerId", new Dictionary { { "player_id", SpacePlayerId } }); } private async Task UnlikeByPlayerIdAsync() { await Brisk.Space.UnlikeByPlayerIdAsync(SpacePlayerId); SetResult("Unlike By PlayerId", new Dictionary { { "player_id", SpacePlayerId } }); } private async Task LikeByLoginAsync() { await Brisk.Space.LikeByLoginIdentityAsync(SpaceLoginProvider, SpaceLoginUserId); SetResult("Like By Login", new Dictionary { { "login_provider", SpaceLoginProvider }, { "login_user_id", SpaceLoginUserId } }); } private async Task UnlikeByLoginAsync() { await Brisk.Space.UnlikeByLoginIdentityAsync(SpaceLoginProvider, SpaceLoginUserId); SetResult("Unlike By Login", new Dictionary { { "login_provider", SpaceLoginProvider }, { "login_user_id", SpaceLoginUserId } }); } 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("Update My Space", payload); } private async Task GetMyVisitsAsync() { var result = await Brisk.Space.GetMyVisitsAsync(); SetResult("My Space Visits", result); } private async Task RunSmokeFlowAsync() { await InitializeAsync(); if (!Brisk.IsLoggedIn) { await LoginWithUserIdAsync(); } await GetMeAsync(); await GetConfigAsync(); await GetTopAsync(); await GetAnnouncementsAsync(); await GetArchiveSlotsAsync(); await GetMyVisitsAsync(); } private void DrawButton(string label, Func action, bool enabled = true) { var previous = GUI.enabled; GUI.enabled = previous && !_isBusy && enabled; if (GUILayout.Button(label, GUILayout.Height(30f))) { RunAction(label, action); } GUI.enabled = previous; } private void RunAction(string actionName, Func action) { if (_isBusy) { return; } RunActionAsync(actionName, action); } private async void RunActionAsync(string actionName, Func action) { _isBusy = true; _busyAction = actionName; _statusText = "Running: " + actionName; _lastErrorText = string.Empty; Log("Start: " + actionName); try { await action(); _statusText = "Success: " + actionName; Log("Success: " + actionName); } catch (Exception exception) { _statusText = "Failed: " + actionName; _lastErrorText = FormatException(exception); Log("Failed: " + actionName + " | " + exception.GetType().Name + " | " + exception.Message); Debug.LogException(exception, this); } finally { _busyAction = string.Empty; _isBusy = false; } } private void SetResult(string title, object value) { _resultText = title + "\n" + new string('=', title.Length) + "\n" + FormatValue(value); } private void ApplyIdentity(string playerId, string loginProvider, string loginUserId) { if (!string.IsNullOrWhiteSpace(playerId)) { SpacePlayerId = playerId; } if (!string.IsNullOrWhiteSpace(loginProvider)) { SpaceLoginProvider = loginProvider; LoginProvider = loginProvider; } if (!string.IsNullOrWhiteSpace(loginUserId)) { SpaceLoginUserId = loginUserId; LoginUserId = loginUserId; } } private void ApplyCurrentIdentityToSpace() { if (Brisk.Identity != null) { ApplyIdentity(Brisk.Identity.PlayerId, Brisk.Identity.LoginProvider, Brisk.Identity.LoginUserId); return; } if (Brisk.IsInitialized) { ApplyIdentity(Brisk.PlayerId, LoginProvider, LoginUserId); } } private BriskProfile CreateProfile() { return new BriskProfile { Nickname = Nickname, AvatarUrl = AvatarUrl }; } private void HandleInitialized() { Log("Event: OnInitialized"); } private void HandleLoggedIn() { Log("Event: OnLoggedIn"); ApplyCurrentIdentityToSpace(); } private void HandleLoggedOut() { Log("Event: OnLoggedOut"); } private void HandleAuthExpired(BriskAuthExpiredException exception) { Log("Event: OnAuthExpired | " + exception.Message); } private void HandleBlockingError(BriskBlockingException exception) { Log("Event: OnBlockingError | " + exception.Message); } private void HandleExitRequested() { Log("Exit requested by Brisk blocking flow."); } private void Log(string message) { var line = "[" + DateTime.Now.ToString("HH:mm:ss") + "] " + message; _eventLogs.Add(line); if (_eventLogs.Count > 120) { _eventLogs.RemoveAt(0); } _logScroll.y = float.MaxValue; } private static int ParseRequiredInt(string value, string fieldName) { if (!int.TryParse(value, out var result)) { throw new InvalidOperationException(fieldName + " must be a valid integer."); } return result; } private static long ParseRequiredLong(string value, string fieldName) { if (!long.TryParse(value, out var result)) { throw new InvalidOperationException(fieldName + " must be a valid integer."); } return result; } private static int ParseOptionalInt(string value, int fallback) { return int.TryParse(value, out var result) ? result : fallback; } private static int? ParseNullableInt(string value) { if (string.IsNullOrWhiteSpace(value)) { return null; } return int.TryParse(value, out var result) ? result : (int?)null; } private static string FormatException(Exception exception) { if (exception == null) { return "Unknown error."; } var builder = new StringBuilder(); var current = exception; while (current != null) { builder.AppendLine(current.GetType().Name + ": " + current.Message); current = current.InnerException; } return builder.ToString().TrimEnd(); } private static string FormatValue(object value) { var builder = new StringBuilder(); AppendValue(builder, value, 0, 0); return builder.ToString().TrimEnd(); } private static void AppendValue(StringBuilder builder, object value, int indent, int depth) { if (depth > 5) { builder.AppendLine(Indent(indent) + "..."); return; } if (value == null) { builder.AppendLine(Indent(indent) + "null"); return; } if (value is string stringValue) { builder.AppendLine(Indent(indent) + stringValue); return; } if (value is byte[] bytes) { builder.AppendLine(Indent(indent) + "byte[" + bytes.Length + "]"); return; } var type = value.GetType(); if (type.IsPrimitive || value is decimal) { builder.AppendLine(Indent(indent) + value); return; } if (value is IDictionary dictionary) { foreach (DictionaryEntry entry in dictionary) { builder.AppendLine(Indent(indent) + entry.Key + ":"); AppendValue(builder, entry.Value, indent + 2, depth + 1); } return; } if (value is IEnumerable enumerable) { var index = 0; foreach (var item in enumerable) { builder.AppendLine(Indent(indent) + "[" + index + "]"); AppendValue(builder, item, indent + 2, depth + 1); index++; } if (index == 0) { builder.AppendLine(Indent(indent) + "(empty)"); } return; } var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public); if (fields.Length == 0) { builder.AppendLine(Indent(indent) + value); return; } foreach (var field in fields) { builder.AppendLine(Indent(indent) + field.Name + ":"); AppendValue(builder, field.GetValue(value), indent + 2, depth + 1); } } private static string Indent(int indent) { return new string(' ', indent); } private static void BeginSection(string title) { GUILayout.Space(10f); GUILayout.BeginVertical(GUI.skin.box); GUILayout.Label(title, GUI.skin.label); GUILayout.Space(4f); } private static void EndSection() { GUILayout.EndVertical(); } private static string DrawEditableRow(string label, string value) { GUILayout.BeginHorizontal(); GUILayout.Label(label, GUILayout.Width(180f)); var next = GUILayout.TextField(value ?? string.Empty); GUILayout.EndHorizontal(); return next; } private static string DrawTextAreaRow(string label, string value, float height) { GUILayout.Label(label, GUILayout.Width(180f)); return GUILayout.TextArea(value ?? string.Empty, GUILayout.Height(height)); } private static bool DrawToggleRow(string label, bool value) { GUILayout.BeginHorizontal(); GUILayout.Label(label, GUILayout.Width(180f)); var next = GUILayout.Toggle(value, value ? "Enabled" : "Disabled"); GUILayout.EndHorizontal(); return next; } private static void DrawReadOnlyRow(string label, string value) { GUILayout.BeginHorizontal(); GUILayout.Label(label, GUILayout.Width(180f)); GUILayout.Label(string.IsNullOrWhiteSpace(value) ? "-" : value, GUI.skin.box); GUILayout.EndHorizontal(); } }