You've already forked CC-Framework.BriskGameServer
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88b7d9c757 | |||
| 9c57faa16c | |||
| 3a4f2f6652 | |||
| 4fd2222d6d | |||
| 0e5cab4f27 | |||
| a257a8a912 | |||
| 7a8dd4cea0 | |||
| 07fc690e67 |
@@ -50,7 +50,7 @@ public sealed class BriskArchiveModule
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : checksum;
|
||||
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : NormalizeChecksum(checksum);
|
||||
var sections = new List<IMultipartFormSection>
|
||||
{
|
||||
new MultipartFormDataSection("base_version", (baseVersion ?? 0).ToString()),
|
||||
@@ -63,6 +63,24 @@ public sealed class BriskArchiveModule
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 UTF-8 文本形式上传指定槽位的存档。
|
||||
/// </summary>
|
||||
public Task<BriskArchiveUploadResult> UploadTextAsync(int slotNo, string text, int? baseVersion = null, string checksum = null)
|
||||
{
|
||||
RequireNotNull(text, nameof(text));
|
||||
return UploadAsync(slotNo, Encoding.UTF8.GetBytes(text), baseVersion, checksum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 JSON 文本形式上传指定槽位的存档。
|
||||
/// </summary>
|
||||
public Task<BriskArchiveUploadResult> UploadJsonAsync(int slotNo, object payload, int? baseVersion = null, string checksum = null)
|
||||
{
|
||||
RequireNotNull(payload, nameof(payload));
|
||||
return UploadAsync(slotNo, Encoding.UTF8.GetBytes(BriskJson.Serialize(payload)), baseVersion, checksum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载指定槽位的二进制存档。
|
||||
/// </summary>
|
||||
@@ -82,6 +100,24 @@ public sealed class BriskArchiveModule
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 UTF-8 文本形式下载指定槽位的存档。
|
||||
/// </summary>
|
||||
public async Task<string> DownloadTextAsync(int slotNo)
|
||||
{
|
||||
var result = await DownloadAsync(slotNo);
|
||||
return result == null || result.Bytes == null ? string.Empty : Encoding.UTF8.GetString(result.Bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 JSON 对象形式下载指定槽位的存档。
|
||||
/// </summary>
|
||||
public async Task<object> DownloadJsonAsync(int slotNo)
|
||||
{
|
||||
var text = await DownloadTextAsync(slotNo);
|
||||
return string.IsNullOrWhiteSpace(text) ? null : BriskJson.Deserialize(text);
|
||||
}
|
||||
|
||||
private static void ValidateSlotNo(int slotNo)
|
||||
{
|
||||
RequirePositive(slotNo, nameof(slotNo), "slotNo must be greater than 0.");
|
||||
@@ -92,8 +128,7 @@ public sealed class BriskArchiveModule
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
var builder = new StringBuilder(hash.Length * 2 + 7);
|
||||
builder.Append("sha256:");
|
||||
var builder = new StringBuilder(hash.Length * 2);
|
||||
for (var i = 0; i < hash.Length; i++)
|
||||
{
|
||||
builder.Append(hash[i].ToString("x2"));
|
||||
@@ -103,6 +138,19 @@ public sealed class BriskArchiveModule
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeChecksum(string checksum)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(checksum))
|
||||
{
|
||||
return checksum;
|
||||
}
|
||||
|
||||
var value = checksum.Trim();
|
||||
return value.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)
|
||||
? value.Substring("sha256:".Length)
|
||||
: value;
|
||||
}
|
||||
|
||||
private static int ReadHeaderInt(Dictionary<string, string> headers, string key)
|
||||
{
|
||||
var value = ReadHeader(headers, key);
|
||||
|
||||
@@ -197,8 +197,17 @@ public static class Brisk
|
||||
}
|
||||
|
||||
internal static void NotifyAuthExpired(BriskAuthExpiredException exception)
|
||||
{
|
||||
NotifyAuthExpired(exception, true);
|
||||
}
|
||||
|
||||
internal static void NotifyAuthExpired(BriskAuthExpiredException exception, bool showDefaultPresenter)
|
||||
{
|
||||
if (showDefaultPresenter)
|
||||
{
|
||||
s_context?.ErrorPresenter?.ShowAuthExpired(exception);
|
||||
}
|
||||
|
||||
OnAuthExpired?.Invoke(exception);
|
||||
}
|
||||
|
||||
@@ -241,6 +250,15 @@ public static class Brisk
|
||||
return;
|
||||
}
|
||||
|
||||
if (storedSession.ExpiresAt.HasValue && storedSession.ExpiresAt.Value <= DateTimeOffset.UtcNow)
|
||||
{
|
||||
await HandleExpiredStartupSessionAsync(
|
||||
context,
|
||||
storedSession,
|
||||
new BriskAuthExpiredException("Stored session expired before initialization."));
|
||||
return;
|
||||
}
|
||||
|
||||
context.Session.Update(
|
||||
storedSession.AccessToken,
|
||||
storedSession.ExpiresAt,
|
||||
@@ -268,9 +286,7 @@ public static class Brisk
|
||||
}
|
||||
catch (BriskAuthExpiredException exception)
|
||||
{
|
||||
context.Session.Clear();
|
||||
await context.TokenStore.ClearAsync();
|
||||
NotifyAuthExpired(exception);
|
||||
await HandleExpiredStartupSessionAsync(context, storedSession, exception);
|
||||
}
|
||||
catch (BriskBlockingException exception)
|
||||
{
|
||||
@@ -278,4 +294,36 @@ public static class Brisk
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task HandleExpiredStartupSessionAsync(BriskContext context, BriskStoredSession storedSession, BriskAuthExpiredException exception)
|
||||
{
|
||||
context.Session.Clear();
|
||||
await context.TokenStore.ClearAsync();
|
||||
NotifyAuthExpired(exception, false);
|
||||
|
||||
if (!context.Options.AutoReloginOnInitialize || !HasStoredLoginIdentity(storedSession))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Auth.LoginWithUserIdAsync(storedSession.LoginProvider, storedSession.LoginUserId);
|
||||
}
|
||||
catch (BriskBlockingException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 初始化期静默重登失败时,交给业务层后续第三方登录流程重新调用 Brisk.Auth。
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasStoredLoginIdentity(BriskStoredSession storedSession)
|
||||
{
|
||||
return storedSession != null
|
||||
&& !string.IsNullOrWhiteSpace(storedSession.LoginProvider)
|
||||
&& !string.IsNullOrWhiteSpace(storedSession.LoginUserId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,74 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
[DefaultExecutionOrder(10000)]
|
||||
internal sealed class BriskDefaultErrorDialog : MonoBehaviour
|
||||
{
|
||||
private const string HostName = "BriskDefaultErrorDialog";
|
||||
private const float OpenDuration = 0.24f;
|
||||
private const float CloseDuration = 0.2f;
|
||||
|
||||
private static readonly Color Ink = ColorFromHex(0x05060a);
|
||||
private static readonly Color InkSoft = ColorFromHex(0x1b1d24);
|
||||
private static readonly Color Muted = ColorFromHex(0x5f6572);
|
||||
private static readonly Color Line = ColorFromHex(0xdde2ea);
|
||||
private static readonly Color Paper = ColorFromHex(0xffffff);
|
||||
private static readonly Color Soft = ColorFromHex(0xf7f8fb);
|
||||
private static readonly Color Blue = ColorFromHex(0x2563ff);
|
||||
private static readonly Color Teal = ColorFromHex(0x23d6c8);
|
||||
private static readonly Color Violet = ColorFromHex(0x8b5cff);
|
||||
private static readonly Color Dark = ColorFromHex(0x050506);
|
||||
|
||||
private static BriskDefaultErrorDialog s_instance;
|
||||
|
||||
private string _title;
|
||||
private string _message;
|
||||
private string _confirmText;
|
||||
private UIDocument _document;
|
||||
private PanelSettings _panelSettings;
|
||||
private VisualElement _root;
|
||||
private VisualElement _stage;
|
||||
private VisualElement _panel;
|
||||
private Label _eyebrowLabel;
|
||||
private Label _titleLabel;
|
||||
private Label _messageLabel;
|
||||
private VisualElement _spacer;
|
||||
private VisualElement _divider;
|
||||
private VisualElement _confirmButton;
|
||||
private Label _confirmLabel;
|
||||
private Action _onConfirm;
|
||||
private Coroutine _animation;
|
||||
private bool _visible;
|
||||
private bool _closeOnBackdrop;
|
||||
private bool _closeOnEscape;
|
||||
private GUIStyle _legacyTitleStyle;
|
||||
private GUIStyle _legacyMessageStyle;
|
||||
private GUIStyle _legacyEyebrowStyle;
|
||||
private GUIStyle _legacyButtonStyle;
|
||||
private Texture2D _legacyButtonTexture;
|
||||
private Texture2D _legacyButtonHoverTexture;
|
||||
|
||||
public static void Show(string title, string message, string confirmText, Action onConfirm = null)
|
||||
internal static bool IsVisible => s_instance != null && s_instance._visible;
|
||||
|
||||
internal static void RenderTopmostLegacyOverlay()
|
||||
{
|
||||
if (s_instance == null || !s_instance._visible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
s_instance.DrawLegacyOverlay();
|
||||
}
|
||||
|
||||
public static void Show(string title, string message, string confirmText, Action onConfirm = null, bool closeOnBackdrop = false, bool closeOnEscape = false)
|
||||
{
|
||||
var host = EnsureInstance();
|
||||
host._title = string.IsNullOrWhiteSpace(title) ? "Brisk" : title;
|
||||
host._message = string.IsNullOrWhiteSpace(message) ? "Unknown error." : message;
|
||||
host._confirmText = string.IsNullOrWhiteSpace(confirmText) ? "OK" : confirmText;
|
||||
host._onConfirm = onConfirm;
|
||||
host._visible = true;
|
||||
host.ShowInternal(
|
||||
string.IsNullOrWhiteSpace(title) ? "Brisk" : title,
|
||||
string.IsNullOrWhiteSpace(message) ? "服务暂时不可用,请稍后再试。" : message,
|
||||
string.IsNullOrWhiteSpace(confirmText) ? "知道了" : confirmText,
|
||||
onConfirm,
|
||||
closeOnBackdrop,
|
||||
closeOnEscape);
|
||||
}
|
||||
|
||||
private static BriskDefaultErrorDialog EnsureInstance()
|
||||
@@ -46,6 +94,249 @@ internal sealed class BriskDefaultErrorDialog : MonoBehaviour
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
private void ShowInternal(string title, string message, string confirmText, Action onConfirm, bool closeOnBackdrop, bool closeOnEscape)
|
||||
{
|
||||
EnsureDocument();
|
||||
ApplySafeAreaPadding();
|
||||
|
||||
_titleLabel.text = title;
|
||||
_messageLabel.text = message;
|
||||
_confirmLabel.text = confirmText;
|
||||
_onConfirm = onConfirm;
|
||||
_closeOnBackdrop = closeOnBackdrop;
|
||||
_closeOnEscape = closeOnEscape;
|
||||
_visible = true;
|
||||
|
||||
StartDialogAnimation(PlayOpenAnimation());
|
||||
}
|
||||
|
||||
private void EnsureDocument()
|
||||
{
|
||||
if (_document != null)
|
||||
{
|
||||
ApplyResponsiveLayout();
|
||||
return;
|
||||
}
|
||||
|
||||
_panelSettings = ScriptableObject.CreateInstance<PanelSettings>();
|
||||
_panelSettings.name = HostName + "PanelSettings";
|
||||
_panelSettings.scaleMode = PanelScaleMode.ConstantPixelSize;
|
||||
_panelSettings.scale = 1f;
|
||||
_panelSettings.sortingOrder = 32767f;
|
||||
|
||||
_document = gameObject.AddComponent<UIDocument>();
|
||||
_document.panelSettings = _panelSettings;
|
||||
_document.sortingOrder = 32767f;
|
||||
|
||||
BuildVisualTree(_document.rootVisualElement);
|
||||
ApplyResponsiveLayout();
|
||||
}
|
||||
|
||||
private void BuildVisualTree(VisualElement documentRoot)
|
||||
{
|
||||
documentRoot.Clear();
|
||||
documentRoot.pickingMode = PickingMode.Ignore;
|
||||
documentRoot.style.position = Position.Absolute;
|
||||
documentRoot.style.left = 0f;
|
||||
documentRoot.style.top = 0f;
|
||||
documentRoot.style.right = 0f;
|
||||
documentRoot.style.bottom = 0f;
|
||||
documentRoot.style.width = Length.Percent(100f);
|
||||
documentRoot.style.height = Length.Percent(100f);
|
||||
documentRoot.style.flexGrow = 1f;
|
||||
|
||||
_root = new VisualElement
|
||||
{
|
||||
pickingMode = PickingMode.Position,
|
||||
focusable = true
|
||||
};
|
||||
_root.style.position = Position.Absolute;
|
||||
_root.style.left = 0;
|
||||
_root.style.top = 0;
|
||||
_root.style.right = 0;
|
||||
_root.style.bottom = 0;
|
||||
_root.style.display = DisplayStyle.None;
|
||||
_root.style.opacity = 0f;
|
||||
_root.style.backgroundColor = new Color(0.02f, 0.02f, 0.024f, 0.58f);
|
||||
_root.style.justifyContent = Justify.Center;
|
||||
_root.style.alignItems = Align.Center;
|
||||
_root.RegisterCallback<KeyDownEvent>(HandleRootKeyDown);
|
||||
_root.RegisterCallback<PointerDownEvent>(evt =>
|
||||
{
|
||||
if (_closeOnBackdrop && evt.target == _root)
|
||||
{
|
||||
BeginClose();
|
||||
}
|
||||
|
||||
evt.StopPropagation();
|
||||
});
|
||||
documentRoot.Add(_root);
|
||||
|
||||
_stage = new VisualElement();
|
||||
_stage.pickingMode = PickingMode.Ignore;
|
||||
_stage.style.position = Position.Relative;
|
||||
_stage.style.width = Length.Percent(82f);
|
||||
_stage.style.maxWidth = 900f;
|
||||
_stage.style.opacity = 0f;
|
||||
_stage.style.marginTop = 14f;
|
||||
_root.Add(_stage);
|
||||
|
||||
var shadow = new VisualElement();
|
||||
shadow.pickingMode = PickingMode.Ignore;
|
||||
shadow.style.position = Position.Absolute;
|
||||
shadow.style.left = 0f;
|
||||
shadow.style.right = 0f;
|
||||
shadow.style.top = 10f;
|
||||
shadow.style.bottom = -10f;
|
||||
shadow.style.backgroundColor = new Color(0f, 0f, 0f, 0.09f);
|
||||
shadow.style.borderTopLeftRadius = 18f;
|
||||
shadow.style.borderTopRightRadius = 18f;
|
||||
shadow.style.borderBottomLeftRadius = 18f;
|
||||
shadow.style.borderBottomRightRadius = 18f;
|
||||
_stage.Add(shadow);
|
||||
|
||||
_panel = new VisualElement
|
||||
{
|
||||
pickingMode = PickingMode.Position
|
||||
};
|
||||
_panel.style.width = Length.Percent(100f);
|
||||
_panel.style.minHeight = 360f;
|
||||
_panel.style.paddingTop = 36f;
|
||||
_panel.style.paddingRight = 36f;
|
||||
_panel.style.paddingBottom = 36f;
|
||||
_panel.style.paddingLeft = 36f;
|
||||
_panel.style.backgroundColor = Paper;
|
||||
_panel.style.borderTopWidth = 1f;
|
||||
_panel.style.borderRightWidth = 1f;
|
||||
_panel.style.borderBottomWidth = 1f;
|
||||
_panel.style.borderLeftWidth = 1f;
|
||||
_panel.style.borderTopColor = Line;
|
||||
_panel.style.borderRightColor = Line;
|
||||
_panel.style.borderBottomColor = Line;
|
||||
_panel.style.borderLeftColor = Line;
|
||||
_panel.style.borderTopLeftRadius = 18f;
|
||||
_panel.style.borderTopRightRadius = 18f;
|
||||
_panel.style.borderBottomLeftRadius = 18f;
|
||||
_panel.style.borderBottomRightRadius = 18f;
|
||||
_panel.style.overflow = Overflow.Hidden;
|
||||
_stage.Add(_panel);
|
||||
|
||||
var accent = new VisualElement();
|
||||
accent.pickingMode = PickingMode.Ignore;
|
||||
accent.style.position = Position.Absolute;
|
||||
accent.style.top = 0f;
|
||||
accent.style.left = 0f;
|
||||
accent.style.right = 0f;
|
||||
accent.style.height = 2f;
|
||||
accent.style.flexDirection = FlexDirection.Row;
|
||||
_panel.Add(accent);
|
||||
AddAccentSegment(accent, Blue);
|
||||
AddAccentSegment(accent, Teal);
|
||||
AddAccentSegment(accent, Violet);
|
||||
|
||||
_eyebrowLabel = new Label("BRISK SERVICE");
|
||||
_eyebrowLabel.style.color = Muted;
|
||||
_eyebrowLabel.style.fontSize = 13f;
|
||||
_eyebrowLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
_eyebrowLabel.style.letterSpacing = 1.4f;
|
||||
_eyebrowLabel.style.height = 18f;
|
||||
_eyebrowLabel.style.flexShrink = 0f;
|
||||
_eyebrowLabel.style.marginBottom = 12f;
|
||||
_panel.Add(_eyebrowLabel);
|
||||
|
||||
_titleLabel = new Label();
|
||||
_titleLabel.style.color = Ink;
|
||||
_titleLabel.style.fontSize = 30f;
|
||||
_titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
_titleLabel.style.whiteSpace = WhiteSpace.Normal;
|
||||
_titleLabel.style.unityTextAlign = TextAnchor.MiddleLeft;
|
||||
_titleLabel.style.minHeight = 40f;
|
||||
_titleLabel.style.flexShrink = 0f;
|
||||
_titleLabel.style.marginBottom = 16f;
|
||||
_panel.Add(_titleLabel);
|
||||
|
||||
_messageLabel = new Label();
|
||||
_messageLabel.style.color = Muted;
|
||||
_messageLabel.style.fontSize = 21f;
|
||||
_messageLabel.style.whiteSpace = WhiteSpace.Normal;
|
||||
_messageLabel.style.unityTextAlign = TextAnchor.UpperLeft;
|
||||
_messageLabel.style.minHeight = 64f;
|
||||
_messageLabel.style.flexShrink = 0f;
|
||||
_messageLabel.style.marginBottom = 22f;
|
||||
_panel.Add(_messageLabel);
|
||||
|
||||
_spacer = new VisualElement();
|
||||
_spacer.pickingMode = PickingMode.Ignore;
|
||||
_spacer.style.flexGrow = 1f;
|
||||
_spacer.style.minHeight = 18f;
|
||||
_panel.Add(_spacer);
|
||||
|
||||
_divider = new VisualElement();
|
||||
_divider.style.height = 1f;
|
||||
_divider.style.flexShrink = 0f;
|
||||
_divider.style.backgroundColor = Line;
|
||||
_divider.style.marginBottom = 22f;
|
||||
_panel.Add(_divider);
|
||||
|
||||
_confirmButton = new VisualElement
|
||||
{
|
||||
focusable = true,
|
||||
pickingMode = PickingMode.Position
|
||||
};
|
||||
_confirmButton.style.height = 58f;
|
||||
_confirmButton.style.minHeight = 58f;
|
||||
_confirmButton.style.flexShrink = 0f;
|
||||
_confirmButton.style.borderTopLeftRadius = 24f;
|
||||
_confirmButton.style.borderTopRightRadius = 24f;
|
||||
_confirmButton.style.borderBottomLeftRadius = 24f;
|
||||
_confirmButton.style.borderBottomRightRadius = 24f;
|
||||
_confirmButton.style.backgroundColor = Dark;
|
||||
_confirmButton.style.justifyContent = Justify.Center;
|
||||
_confirmButton.style.alignItems = Align.Center;
|
||||
_confirmButton.RegisterCallback<PointerEnterEvent>(_ => _confirmButton.style.backgroundColor = InkSoft);
|
||||
_confirmButton.RegisterCallback<PointerLeaveEvent>(_ => _confirmButton.style.backgroundColor = Dark);
|
||||
_confirmButton.RegisterCallback<PointerDownEvent>(_ =>
|
||||
{
|
||||
_confirmButton.style.backgroundColor = Ink;
|
||||
_confirmButton.style.marginTop = 1f;
|
||||
});
|
||||
_confirmButton.RegisterCallback<PointerUpEvent>(_ =>
|
||||
{
|
||||
_confirmButton.style.backgroundColor = InkSoft;
|
||||
_confirmButton.style.marginTop = 0f;
|
||||
});
|
||||
_confirmButton.RegisterCallback<ClickEvent>(_ => BeginClose());
|
||||
_confirmButton.RegisterCallback<KeyDownEvent>(HandleButtonKeyDown);
|
||||
_panel.Add(_confirmButton);
|
||||
|
||||
_confirmLabel = new Label();
|
||||
_confirmLabel.style.color = Color.white;
|
||||
_confirmLabel.style.fontSize = 18f;
|
||||
_confirmLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
_confirmLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||
_confirmLabel.style.height = 58f;
|
||||
_confirmButton.Add(_confirmLabel);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_visible && _root != null)
|
||||
{
|
||||
ApplySafeAreaPadding();
|
||||
ApplyResponsiveLayout();
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginClose()
|
||||
{
|
||||
if (!_visible || _root == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StartDialogAnimation(PlayCloseAnimation());
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
if (!_visible)
|
||||
@@ -53,36 +344,379 @@ internal sealed class BriskDefaultErrorDialog : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
var overlayRect = new Rect(0, 0, Screen.width, Screen.height);
|
||||
DrawLegacyOverlay();
|
||||
}
|
||||
|
||||
private void DrawLegacyOverlay()
|
||||
{
|
||||
EnsureLegacyStyles();
|
||||
|
||||
var previousDepth = GUI.depth;
|
||||
var previousMatrix = GUI.matrix;
|
||||
var previousColor = GUI.color;
|
||||
GUI.color = new Color(0f, 0f, 0f, 0.65f);
|
||||
GUI.Box(overlayRect, GUIContent.none);
|
||||
GUI.depth = -10000;
|
||||
GUI.matrix = Matrix4x4.identity;
|
||||
|
||||
DrawLegacyRect(new Rect(0f, 0f, Screen.width, Screen.height), new Color(0.02f, 0.02f, 0.024f, 0.58f));
|
||||
|
||||
var scale = GetReadableScale();
|
||||
ApplyLegacyScale(scale);
|
||||
|
||||
var margin = GetDialogMargin(scale);
|
||||
var padding = GetDialogPadding(scale);
|
||||
var buttonHeight = GetDialogButtonHeight(scale);
|
||||
var width = GetDialogWidth(margin);
|
||||
var contentWidth = width - padding * 2f;
|
||||
var titleHeight = Mathf.Max(34f, _legacyTitleStyle.CalcHeight(new GUIContent(_titleLabel.text), contentWidth));
|
||||
var messageHeight = Mathf.Max(48f, _legacyMessageStyle.CalcHeight(new GUIContent(_messageLabel.text), contentWidth));
|
||||
var minHeight = GetDialogMinHeight(scale, margin);
|
||||
var contentHeight = padding * 2f + 18f * scale + 12f * scale + titleHeight + 16f * scale + messageHeight + 24f * scale + 1f + 22f * scale + buttonHeight;
|
||||
var height = Mathf.Clamp(contentHeight, minHeight, Mathf.Max(minHeight, Screen.height - margin * 2f));
|
||||
var rect = new Rect((Screen.width - width) * 0.5f, (Screen.height - height) * 0.5f, width, height);
|
||||
|
||||
DrawLegacyPanel(rect);
|
||||
|
||||
var x = rect.x + padding;
|
||||
var y = rect.y + padding;
|
||||
GUI.Label(new Rect(x, y, contentWidth, 18f * scale), "BRISK SERVICE", _legacyEyebrowStyle);
|
||||
y += 30f * scale;
|
||||
GUI.Label(new Rect(x, y, contentWidth, titleHeight), _titleLabel.text, _legacyTitleStyle);
|
||||
y += titleHeight + 16f * scale;
|
||||
GUI.Label(new Rect(x, y, contentWidth, messageHeight), _messageLabel.text, _legacyMessageStyle);
|
||||
|
||||
DrawLegacyRect(new Rect(x, rect.yMax - padding - buttonHeight - 22f * scale, contentWidth, 1f), Line);
|
||||
var buttonRect = new Rect(x, rect.yMax - padding - buttonHeight, contentWidth, buttonHeight);
|
||||
if (GUI.Button(buttonRect, _confirmLabel.text, _legacyButtonStyle))
|
||||
{
|
||||
BeginClose();
|
||||
}
|
||||
|
||||
ConsumeBlockingEvent();
|
||||
|
||||
GUI.color = previousColor;
|
||||
GUI.matrix = previousMatrix;
|
||||
GUI.depth = previousDepth;
|
||||
}
|
||||
|
||||
var width = Mathf.Min(460f, Screen.width - 40f);
|
||||
var height = 220f;
|
||||
var dialogRect = new Rect(
|
||||
(Screen.width - width) * 0.5f,
|
||||
(Screen.height - height) * 0.5f,
|
||||
width,
|
||||
height);
|
||||
private IEnumerator PlayOpenAnimation()
|
||||
{
|
||||
_root.style.display = DisplayStyle.Flex;
|
||||
_root.style.opacity = 0f;
|
||||
_stage.style.opacity = 0f;
|
||||
_stage.style.marginTop = 14f;
|
||||
|
||||
GUILayout.BeginArea(dialogRect, GUI.skin.window);
|
||||
GUILayout.Space(8f);
|
||||
GUILayout.Label(_title, GUI.skin.label);
|
||||
GUILayout.Space(12f);
|
||||
GUILayout.Label(_message, GUI.skin.label);
|
||||
GUILayout.FlexibleSpace();
|
||||
yield return null;
|
||||
_root.Focus();
|
||||
|
||||
if (GUILayout.Button(_confirmText, GUILayout.Height(36f)))
|
||||
var elapsed = 0f;
|
||||
while (elapsed < OpenDuration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
var eased = EaseOutCubic(Mathf.Clamp01(elapsed / OpenDuration));
|
||||
_root.style.opacity = eased;
|
||||
_stage.style.opacity = eased;
|
||||
_stage.style.marginTop = Mathf.Lerp(14f, 0f, eased);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
_root.style.opacity = 1f;
|
||||
_stage.style.opacity = 1f;
|
||||
_stage.style.marginTop = 0f;
|
||||
}
|
||||
|
||||
private IEnumerator PlayCloseAnimation()
|
||||
{
|
||||
var callback = _onConfirm;
|
||||
_visible = false;
|
||||
_onConfirm = null;
|
||||
|
||||
var elapsed = 0f;
|
||||
while (elapsed < CloseDuration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
var eased = EaseInCubic(Mathf.Clamp01(elapsed / CloseDuration));
|
||||
var alpha = 1f - eased;
|
||||
_root.style.opacity = alpha;
|
||||
_stage.style.opacity = alpha;
|
||||
_stage.style.marginTop = Mathf.Lerp(0f, 10f, eased);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
_root.style.display = DisplayStyle.None;
|
||||
_visible = false;
|
||||
callback?.Invoke();
|
||||
}
|
||||
|
||||
GUILayout.Space(8f);
|
||||
GUILayout.EndArea();
|
||||
private void StartDialogAnimation(IEnumerator animation)
|
||||
{
|
||||
if (_animation != null)
|
||||
{
|
||||
StopCoroutine(_animation);
|
||||
}
|
||||
|
||||
_animation = StartCoroutine(animation);
|
||||
}
|
||||
|
||||
private void ApplySafeAreaPadding()
|
||||
{
|
||||
var safeArea = Screen.safeArea;
|
||||
var basePadding = Mathf.Max(20f, Mathf.Min(Screen.width, Screen.height) * 0.04f);
|
||||
_root.style.paddingLeft = Mathf.Max(basePadding, safeArea.xMin + basePadding);
|
||||
_root.style.paddingRight = Mathf.Max(basePadding, Screen.width - safeArea.xMax + basePadding);
|
||||
_root.style.paddingTop = Mathf.Max(basePadding, Screen.height - safeArea.yMax + basePadding);
|
||||
_root.style.paddingBottom = Mathf.Max(basePadding, safeArea.yMin + basePadding);
|
||||
}
|
||||
|
||||
private void ApplyResponsiveLayout()
|
||||
{
|
||||
if (_panel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var scale = GetReadableScale();
|
||||
var margin = GetDialogMargin(scale);
|
||||
var maxWidth = Mathf.Min(900f, Mathf.Max(320f, Screen.width - margin * 2f));
|
||||
var minHeight = GetDialogMinHeight(scale, margin);
|
||||
var padding = GetDialogPadding(scale);
|
||||
var buttonHeight = GetDialogButtonHeight(scale);
|
||||
|
||||
_stage.style.width = Length.Percent(82f);
|
||||
_stage.style.maxWidth = maxWidth;
|
||||
_panel.style.minHeight = minHeight;
|
||||
_panel.style.paddingTop = padding;
|
||||
_panel.style.paddingRight = padding;
|
||||
_panel.style.paddingBottom = padding;
|
||||
_panel.style.paddingLeft = padding;
|
||||
_panel.style.borderTopLeftRadius = 20f * scale;
|
||||
_panel.style.borderTopRightRadius = 20f * scale;
|
||||
_panel.style.borderBottomLeftRadius = 20f * scale;
|
||||
_panel.style.borderBottomRightRadius = 20f * scale;
|
||||
|
||||
_eyebrowLabel.style.fontSize = 12f * scale;
|
||||
_eyebrowLabel.style.height = 18f * scale;
|
||||
_eyebrowLabel.style.marginBottom = 12f * scale;
|
||||
_titleLabel.style.fontSize = 28f * scale;
|
||||
_titleLabel.style.minHeight = 40f * scale;
|
||||
_titleLabel.style.marginBottom = 16f * scale;
|
||||
_messageLabel.style.fontSize = 20f * scale;
|
||||
_messageLabel.style.minHeight = 64f * scale;
|
||||
_messageLabel.style.marginBottom = 22f * scale;
|
||||
_spacer.style.minHeight = 18f * scale;
|
||||
_divider.style.marginBottom = 22f * scale;
|
||||
_confirmButton.style.height = buttonHeight;
|
||||
_confirmButton.style.minHeight = buttonHeight;
|
||||
_confirmButton.style.borderTopLeftRadius = buttonHeight * 0.5f;
|
||||
_confirmButton.style.borderTopRightRadius = buttonHeight * 0.5f;
|
||||
_confirmButton.style.borderBottomLeftRadius = buttonHeight * 0.5f;
|
||||
_confirmButton.style.borderBottomRightRadius = buttonHeight * 0.5f;
|
||||
_confirmLabel.style.fontSize = 17f * scale;
|
||||
_confirmLabel.style.height = buttonHeight;
|
||||
}
|
||||
|
||||
private void HandleRootKeyDown(KeyDownEvent evt)
|
||||
{
|
||||
if (_closeOnEscape && evt.keyCode == KeyCode.Escape)
|
||||
{
|
||||
BeginClose();
|
||||
evt.StopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
private void HandleButtonKeyDown(KeyDownEvent evt)
|
||||
{
|
||||
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter || evt.keyCode == KeyCode.Space)
|
||||
{
|
||||
BeginClose();
|
||||
evt.StopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (s_instance == this)
|
||||
{
|
||||
s_instance = null;
|
||||
}
|
||||
|
||||
if (_panelSettings != null)
|
||||
{
|
||||
Destroy(_panelSettings);
|
||||
_panelSettings = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureLegacyStyles()
|
||||
{
|
||||
if (_legacyTitleStyle != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_legacyButtonTexture = CreateTexture(Dark);
|
||||
_legacyButtonHoverTexture = CreateTexture(InkSoft);
|
||||
|
||||
_legacyEyebrowStyle = new GUIStyle
|
||||
{
|
||||
fontStyle = FontStyle.Bold,
|
||||
normal = { textColor = Muted }
|
||||
};
|
||||
|
||||
_legacyTitleStyle = new GUIStyle
|
||||
{
|
||||
fontStyle = FontStyle.Bold,
|
||||
wordWrap = true,
|
||||
normal = { textColor = Ink }
|
||||
};
|
||||
|
||||
_legacyMessageStyle = new GUIStyle
|
||||
{
|
||||
wordWrap = true,
|
||||
normal = { textColor = Muted }
|
||||
};
|
||||
|
||||
_legacyButtonStyle = new GUIStyle
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontStyle = FontStyle.Bold,
|
||||
border = new RectOffset(24, 24, 24, 24),
|
||||
normal =
|
||||
{
|
||||
background = _legacyButtonTexture,
|
||||
textColor = Color.white
|
||||
},
|
||||
hover =
|
||||
{
|
||||
background = _legacyButtonHoverTexture,
|
||||
textColor = Color.white
|
||||
},
|
||||
active =
|
||||
{
|
||||
background = _legacyButtonHoverTexture,
|
||||
textColor = Color.white
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void ApplyLegacyScale(float scale)
|
||||
{
|
||||
_legacyEyebrowStyle.fontSize = Mathf.RoundToInt(12f * scale);
|
||||
_legacyTitleStyle.fontSize = Mathf.RoundToInt(28f * scale);
|
||||
_legacyMessageStyle.fontSize = Mathf.RoundToInt(20f * scale);
|
||||
_legacyButtonStyle.fontSize = Mathf.RoundToInt(17f * scale);
|
||||
}
|
||||
|
||||
private void DrawLegacyPanel(Rect rect)
|
||||
{
|
||||
DrawLegacyRect(new Rect(rect.x, rect.y + 12f, rect.width, rect.height), new Color(0f, 0f, 0f, 0.08f));
|
||||
DrawLegacyRect(rect, Paper);
|
||||
DrawLegacyRect(new Rect(rect.x, rect.y, rect.width, 1f), Line);
|
||||
DrawLegacyRect(new Rect(rect.x, rect.yMax - 1f, rect.width, 1f), Line);
|
||||
DrawLegacyRect(new Rect(rect.x, rect.y, 1f, rect.height), Line);
|
||||
DrawLegacyRect(new Rect(rect.xMax - 1f, rect.y, 1f, rect.height), Line);
|
||||
|
||||
var segment = rect.width / 3f;
|
||||
DrawLegacyRect(new Rect(rect.x, rect.y, segment, 2f), Blue);
|
||||
DrawLegacyRect(new Rect(rect.x + segment, rect.y, segment, 2f), Teal);
|
||||
DrawLegacyRect(new Rect(rect.x + segment * 2f, rect.y, rect.width - segment * 2f, 2f), Violet);
|
||||
}
|
||||
|
||||
private static void ConsumeBlockingEvent()
|
||||
{
|
||||
var evt = Event.current;
|
||||
if (evt == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (evt.type)
|
||||
{
|
||||
case EventType.MouseDown:
|
||||
case EventType.MouseUp:
|
||||
case EventType.MouseDrag:
|
||||
case EventType.ScrollWheel:
|
||||
case EventType.KeyDown:
|
||||
case EventType.KeyUp:
|
||||
evt.Use();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static Texture2D CreateTexture(Color color)
|
||||
{
|
||||
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false)
|
||||
{
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
texture.SetPixel(0, 0, color);
|
||||
texture.Apply();
|
||||
return texture;
|
||||
}
|
||||
|
||||
private static void DrawLegacyRect(Rect rect, Color color)
|
||||
{
|
||||
var previousColor = GUI.color;
|
||||
GUI.color = color;
|
||||
GUI.DrawTexture(rect, Texture2D.whiteTexture);
|
||||
GUI.color = previousColor;
|
||||
}
|
||||
|
||||
private static void AddAccentSegment(VisualElement parent, Color color)
|
||||
{
|
||||
var segment = new VisualElement();
|
||||
segment.style.flexGrow = 1f;
|
||||
segment.style.backgroundColor = color;
|
||||
parent.Add(segment);
|
||||
}
|
||||
|
||||
private static float GetReadableScale()
|
||||
{
|
||||
return Mathf.Clamp(Mathf.Min(Screen.width, Screen.height) / 720f, 1f, 1.22f);
|
||||
}
|
||||
|
||||
private static float GetDialogMargin(float scale)
|
||||
{
|
||||
return Mathf.Max(24f * scale, Mathf.Min(Screen.width, Screen.height) * 0.06f);
|
||||
}
|
||||
|
||||
private static float GetDialogPadding(float scale)
|
||||
{
|
||||
return 34f * scale;
|
||||
}
|
||||
|
||||
private static float GetDialogButtonHeight(float scale)
|
||||
{
|
||||
return 56f * scale;
|
||||
}
|
||||
|
||||
private static float GetDialogWidth(float margin)
|
||||
{
|
||||
return Mathf.Clamp(Screen.width * 0.82f, 320f, Mathf.Max(320f, Mathf.Min(900f, Screen.width - margin * 2f)));
|
||||
}
|
||||
|
||||
private static float GetDialogMinHeight(float scale, float margin)
|
||||
{
|
||||
var availableHeight = Mathf.Max(320f, Screen.height - margin * 2f);
|
||||
return Mathf.Min(availableHeight, Mathf.Clamp(Screen.height * 0.24f, 360f * scale, 620f));
|
||||
}
|
||||
|
||||
private static float EaseOutCubic(float t)
|
||||
{
|
||||
return 1f - Mathf.Pow(1f - t, 3f);
|
||||
}
|
||||
|
||||
private static float EaseInCubic(float t)
|
||||
{
|
||||
return t * t * t;
|
||||
}
|
||||
|
||||
private static Color ColorFromHex(int rgb)
|
||||
{
|
||||
var r = ((rgb >> 16) & 0xff) / 255f;
|
||||
var g = ((rgb >> 8) & 0xff) / 255f;
|
||||
var b = (rgb & 0xff) / 255f;
|
||||
return new Color(r, g, b, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,18 +13,22 @@ public sealed class BriskDefaultErrorPresenter : IBriskErrorPresenter
|
||||
|
||||
public void ShowBlockingError(BriskBlockingException exception)
|
||||
{
|
||||
var message = exception != null && !string.IsNullOrWhiteSpace(exception.Message)
|
||||
? exception.Message
|
||||
: "A blocking error occurred.";
|
||||
var message = GetBlockingMessage(exception);
|
||||
|
||||
BriskDefaultErrorDialog.Show(
|
||||
title: GetBlockingTitle(exception),
|
||||
message: message,
|
||||
confirmText: "OK",
|
||||
confirmText: "知道了",
|
||||
onConfirm: () =>
|
||||
{
|
||||
var exitHandler = Brisk.IsInitialized ? Brisk.Options?.ExitHandler : null;
|
||||
exitHandler?.Invoke();
|
||||
var exitHandler = Brisk.IsInitialized ? Brisk.GetRequiredContext().ExitHandler : null;
|
||||
if (exitHandler != null)
|
||||
{
|
||||
exitHandler.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
Application.Quit();
|
||||
});
|
||||
|
||||
Debug.LogError("[Brisk] Blocking error: " + message);
|
||||
@@ -32,35 +36,63 @@ public sealed class BriskDefaultErrorPresenter : IBriskErrorPresenter
|
||||
|
||||
public void ShowAuthExpired(BriskAuthExpiredException exception)
|
||||
{
|
||||
var message = exception != null && !string.IsNullOrWhiteSpace(exception.Message)
|
||||
? exception.Message
|
||||
: "Login expired. Please sign in again.";
|
||||
var message = GetAuthExpiredMessage(exception);
|
||||
|
||||
BriskDefaultErrorDialog.Show(
|
||||
title: "Login Expired",
|
||||
title: "登录已过期",
|
||||
message: message,
|
||||
confirmText: "OK");
|
||||
confirmText: "知道了");
|
||||
|
||||
Debug.LogWarning("[Brisk] Auth expired: " + message);
|
||||
Debug.LogWarning("[Brisk] Auth expired: " + (exception != null ? exception.Message : message));
|
||||
}
|
||||
|
||||
private static string GetBlockingTitle(BriskBlockingException exception)
|
||||
{
|
||||
if (exception is BriskMaintenanceException)
|
||||
{
|
||||
return "Maintenance";
|
||||
return "服务维护中";
|
||||
}
|
||||
|
||||
if (exception is BriskAccountBannedException)
|
||||
{
|
||||
return "Account Restricted";
|
||||
return "账号受限";
|
||||
}
|
||||
|
||||
if (exception is BriskClientUpdateRequiredException)
|
||||
{
|
||||
return "Update Required";
|
||||
return "需要更新";
|
||||
}
|
||||
|
||||
return "Service Unavailable";
|
||||
return "服务暂不可用";
|
||||
}
|
||||
|
||||
private static string GetBlockingMessage(BriskBlockingException exception)
|
||||
{
|
||||
if (exception is BriskMaintenanceException)
|
||||
{
|
||||
return "服务正在维护,请稍后再试。";
|
||||
}
|
||||
|
||||
if (exception is BriskAccountBannedException)
|
||||
{
|
||||
return "当前账号暂时无法使用该服务。";
|
||||
}
|
||||
|
||||
if (exception is BriskClientUpdateRequiredException)
|
||||
{
|
||||
return "当前版本过低,请更新后继续。";
|
||||
}
|
||||
|
||||
return "服务暂时不可用,请稍后再试。";
|
||||
}
|
||||
|
||||
private static string GetAuthExpiredMessage(BriskAuthExpiredException exception)
|
||||
{
|
||||
if (exception != null && exception.Message != null && exception.Message.IndexOf("missing", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return "当前登录状态不可用,请重新登录。";
|
||||
}
|
||||
|
||||
return "登录状态已失效,请重新登录后继续。";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,23 +51,26 @@ public sealed class BriskHttpClient
|
||||
|
||||
public async Task<Dictionary<string, object>> PostMultipartAsync(string path, List<IMultipartFormSection> formSections, bool auth = false)
|
||||
{
|
||||
using (var request = UnityWebRequest.Post(BuildUrl(path, null), formSections))
|
||||
return await SendMultipartAsync(UnityWebRequest.kHttpVerbPOST, path, formSections, auth);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, object>> PutMultipartAsync(string path, List<IMultipartFormSection> formSections, bool auth = false)
|
||||
{
|
||||
return await SendMultipartAsync(UnityWebRequest.kHttpVerbPUT, path, formSections, auth);
|
||||
}
|
||||
|
||||
public async Task<BriskBinaryResponse> GetBytesAsync(string path, Dictionary<string, string> query = null, bool auth = false)
|
||||
{
|
||||
using (var request = new UnityWebRequest(BuildUrl(path, query), UnityWebRequest.kHttpVerbGET))
|
||||
{
|
||||
request.downloadHandler = new DownloadHandlerBuffer();
|
||||
|
||||
if (auth)
|
||||
{
|
||||
AddAuthorizationHeader(request);
|
||||
}
|
||||
|
||||
request.SetRequestHeader("Accept", "application/json");
|
||||
await SendRequestAsync(request);
|
||||
return ParseEnvelope(request) as Dictionary<string, object> ?? new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BriskBinaryResponse> GetBytesAsync(string path, Dictionary<string, string> query = null, bool auth = false)
|
||||
{
|
||||
using (var request = BuildRequest(UnityWebRequest.kHttpVerbGET, path, query, null, auth))
|
||||
{
|
||||
request.SetRequestHeader("Accept", "*/*");
|
||||
await SendRequestAsync(request);
|
||||
EnsureSuccessOrThrow(request);
|
||||
return new BriskBinaryResponse
|
||||
@@ -93,6 +96,23 @@ public sealed class BriskHttpClient
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, object>> SendMultipartAsync(string method, string path, List<IMultipartFormSection> formSections, bool auth)
|
||||
{
|
||||
using (var request = UnityWebRequest.Post(BuildUrl(path, null), formSections))
|
||||
{
|
||||
request.method = method;
|
||||
|
||||
if (auth)
|
||||
{
|
||||
AddAuthorizationHeader(request);
|
||||
}
|
||||
|
||||
request.SetRequestHeader("Accept", "application/json");
|
||||
await SendRequestAsync(request);
|
||||
return ParseEnvelope(request) as Dictionary<string, object> ?? new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
private UnityWebRequest BuildRequest(string method, string path, Dictionary<string, string> query, object body, bool auth)
|
||||
{
|
||||
var url = BuildUrl(path, query);
|
||||
|
||||
@@ -237,10 +237,23 @@ internal static class BriskModelMapper
|
||||
|
||||
return new BriskSpaceView
|
||||
{
|
||||
ProjectAccountId = BriskValueReader.GetString(data, "project_account_id"),
|
||||
PlayerId = BriskValueReader.GetString(data, "player_id"),
|
||||
LoginProvider = BriskValueReader.GetString(data, "login_provider"),
|
||||
LoginUserId = BriskValueReader.GetString(data, "login_user_id"),
|
||||
Payload = BriskValueReader.GetDictionary(data, "payload_json") ?? BriskValueReader.GetDictionary(data, "payload")
|
||||
Nickname = BriskValueReader.GetString(data, "nickname"),
|
||||
AvatarUrl = BriskValueReader.GetString(data, "avatar_url"),
|
||||
ContentExists = BriskValueReader.GetBool(data, "content_exists"),
|
||||
ContentVersion = BriskValueReader.GetLong(data, "content_version"),
|
||||
ContentType = BriskValueReader.GetString(data, "content_type"),
|
||||
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
|
||||
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
|
||||
LikeCount = BriskValueReader.GetLong(data, "like_count"),
|
||||
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
|
||||
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
|
||||
LikedByMe = BriskValueReader.GetBool(data, "liked_by_me"),
|
||||
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at"),
|
||||
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -253,8 +266,58 @@ internal static class BriskModelMapper
|
||||
|
||||
return new BriskSpaceStats
|
||||
{
|
||||
LikeCount = BriskValueReader.GetInt(data, "like_count"),
|
||||
VisitCount = BriskValueReader.GetInt(data, "visit_count")
|
||||
ProjectAccountId = BriskValueReader.GetString(data, "project_account_id"),
|
||||
PlayerId = BriskValueReader.GetString(data, "player_id"),
|
||||
LoginProvider = BriskValueReader.GetString(data, "login_provider"),
|
||||
LoginUserId = BriskValueReader.GetString(data, "login_user_id"),
|
||||
Nickname = BriskValueReader.GetString(data, "nickname"),
|
||||
AvatarUrl = BriskValueReader.GetString(data, "avatar_url"),
|
||||
ContentExists = BriskValueReader.GetBool(data, "content_exists"),
|
||||
ContentVersion = BriskValueReader.GetLong(data, "content_version"),
|
||||
ContentType = BriskValueReader.GetString(data, "content_type"),
|
||||
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
|
||||
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
|
||||
LikeCount = BriskValueReader.GetLong(data, "like_count"),
|
||||
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
|
||||
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
|
||||
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at"),
|
||||
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
|
||||
};
|
||||
}
|
||||
|
||||
public static BriskSpaceContentUpdateResult ToSpaceContentUpdateResult(Dictionary<string, object> data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BriskSpaceContentUpdateResult
|
||||
{
|
||||
PlayerId = BriskValueReader.GetString(data, "player_id"),
|
||||
ContentVersion = BriskValueReader.GetLong(data, "content_version"),
|
||||
ContentType = BriskValueReader.GetString(data, "content_type"),
|
||||
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
|
||||
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
|
||||
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
|
||||
};
|
||||
}
|
||||
|
||||
public static BriskSpaceLikeResult ToSpaceLikeResult(Dictionary<string, object> data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BriskSpaceLikeResult
|
||||
{
|
||||
Liked = BriskValueReader.GetBool(data, "liked"),
|
||||
Created = BriskValueReader.GetBool(data, "created"),
|
||||
LikedByMe = BriskValueReader.GetBool(data, "liked_by_me"),
|
||||
LikeCount = BriskValueReader.GetLong(data, "like_count"),
|
||||
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
|
||||
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -284,6 +347,30 @@ internal static class BriskModelMapper
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static BriskSpaceLikeItem ToSpaceLikeItem(Dictionary<string, object> data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BriskSpaceLikeItem
|
||||
{
|
||||
PlayerId = BriskValueReader.GetString(data, "player_id"),
|
||||
Nickname = BriskValueReader.GetString(data, "nickname"),
|
||||
AvatarUrl = BriskValueReader.GetString(data, "avatar_url"),
|
||||
CreatedAt = BriskValueReader.GetString(data, "created_at")
|
||||
};
|
||||
}
|
||||
|
||||
public static List<BriskSpaceLikeItem> ToSpaceLikeItems(object data)
|
||||
{
|
||||
return ExtractList(data)
|
||||
.Select(item => ToSpaceLikeItem(item as Dictionary<string, object>))
|
||||
.Where(item => item != null)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static Dictionary<string, object> ExtractObject(object data)
|
||||
{
|
||||
return data as Dictionary<string, object>;
|
||||
|
||||
@@ -30,6 +30,10 @@ public sealed class BriskOptions
|
||||
/// </summary>
|
||||
public bool ValidateSessionOnInitialize = true;
|
||||
/// <summary>
|
||||
/// 初始化恢复的本地会话已过期或失效时,是否使用本地保存的登录身份静默换取新会话。
|
||||
/// </summary>
|
||||
public bool AutoReloginOnInitialize = true;
|
||||
/// <summary>
|
||||
/// 自定义登录态持久化实现。
|
||||
/// </summary>
|
||||
public IBriskTokenStore TokenStore;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
public sealed class BriskSpaceContentDownloadResult
|
||||
{
|
||||
public byte[] Bytes;
|
||||
public long Version;
|
||||
public string Checksum;
|
||||
public string ContentType;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6f27d1ba8b24f5e85362b9c6c9f4c01
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4bd67f378d264cc69c71f1d03d5b564b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
7
Assets/BriskSdk/Runtime/Models/BriskSpaceLikeItem.cs
Normal file
7
Assets/BriskSdk/Runtime/Models/BriskSpaceLikeItem.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
public sealed class BriskSpaceLikeItem
|
||||
{
|
||||
public string PlayerId;
|
||||
public string Nickname;
|
||||
public string AvatarUrl;
|
||||
public string CreatedAt;
|
||||
}
|
||||
11
Assets/BriskSdk/Runtime/Models/BriskSpaceLikeItem.cs.meta
Normal file
11
Assets/BriskSdk/Runtime/Models/BriskSpaceLikeItem.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef7d53b3f0ab43e7b74a4c23a76f84f5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
9
Assets/BriskSdk/Runtime/Models/BriskSpaceLikeResult.cs
Normal file
9
Assets/BriskSdk/Runtime/Models/BriskSpaceLikeResult.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
public sealed class BriskSpaceLikeResult
|
||||
{
|
||||
public bool Liked;
|
||||
public bool Created;
|
||||
public bool LikedByMe;
|
||||
public long LikeCount;
|
||||
public long TodayLikeCount;
|
||||
public string LikeResetAt;
|
||||
}
|
||||
11
Assets/BriskSdk/Runtime/Models/BriskSpaceLikeResult.cs.meta
Normal file
11
Assets/BriskSdk/Runtime/Models/BriskSpaceLikeResult.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 966f47c348db46dfb60120dba20f1ffb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,5 +1,19 @@
|
||||
public sealed class BriskSpaceStats
|
||||
{
|
||||
public int LikeCount;
|
||||
public int VisitCount;
|
||||
public string ProjectAccountId;
|
||||
public string PlayerId;
|
||||
public string LoginProvider;
|
||||
public string LoginUserId;
|
||||
public string Nickname;
|
||||
public string AvatarUrl;
|
||||
public bool ContentExists;
|
||||
public long ContentVersion;
|
||||
public string ContentType;
|
||||
public long ContentSizeBytes;
|
||||
public string ContentChecksum;
|
||||
public long LikeCount;
|
||||
public long TodayLikeCount;
|
||||
public long VisitCount;
|
||||
public string LikeResetAt;
|
||||
public string UpdatedAt;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
public sealed class BriskSpaceView
|
||||
{
|
||||
public string ProjectAccountId;
|
||||
public string PlayerId;
|
||||
public string LoginProvider;
|
||||
public string LoginUserId;
|
||||
public object Payload;
|
||||
public string Nickname;
|
||||
public string AvatarUrl;
|
||||
public bool ContentExists;
|
||||
public long ContentVersion;
|
||||
public string ContentType;
|
||||
public long ContentSizeBytes;
|
||||
public string ContentChecksum;
|
||||
public long LikeCount;
|
||||
public long TodayLikeCount;
|
||||
public long VisitCount;
|
||||
public bool LikedByMe;
|
||||
public string LikeResetAt;
|
||||
public string UpdatedAt;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
/// <summary>
|
||||
/// 玩家空间模块。
|
||||
@@ -65,77 +68,183 @@ public sealed class BriskSpaceModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按玩家 ID 点赞空间。
|
||||
/// 按玩家 ID 获取点赞列表。
|
||||
/// </summary>
|
||||
public async Task LikeByPlayerIdAsync(string playerId)
|
||||
public async Task<IReadOnlyList<BriskSpaceLikeItem>> GetLikesByPlayerIdAsync(string playerId, int limit = 20, bool currentCycleOnly = false)
|
||||
{
|
||||
ValidatePlayerId(playerId);
|
||||
await ExecuteAsync(async context =>
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.PostJsonRawAsync($"/spaces/{playerId}/like", new Dictionary<string, object>(), true);
|
||||
var data = await context.HttpClient.GetRawDataAsync($"/spaces/{playerId}/likes", CreateLikesQuery(limit, currentCycleOnly), true);
|
||||
return (IReadOnlyList<BriskSpaceLikeItem>)BriskModelMapper.ToSpaceLikeItems(data);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按登录身份获取点赞列表。
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<BriskSpaceLikeItem>> GetLikesByLoginIdentityAsync(string loginProvider, string loginUserId, int limit = 20, bool currentCycleOnly = false)
|
||||
{
|
||||
ValidateLoginIdentity(loginProvider, loginUserId);
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var query = CreateLoginIdentityQuery(loginProvider, loginUserId);
|
||||
query["limit"] = NormalizeLimit(limit);
|
||||
if (currentCycleOnly)
|
||||
{
|
||||
query["scope"] = "cycle";
|
||||
}
|
||||
var data = await context.HttpClient.GetRawDataAsync("/spaces/by-login/likes", query, true);
|
||||
return (IReadOnlyList<BriskSpaceLikeItem>)BriskModelMapper.ToSpaceLikeItems(data);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按玩家 ID 点赞空间。
|
||||
/// </summary>
|
||||
public async Task<BriskSpaceLikeResult> LikeByPlayerIdAsync(string playerId)
|
||||
{
|
||||
ValidatePlayerId(playerId);
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var data = await context.HttpClient.PostJsonRawAsync($"/spaces/{playerId}/like", new Dictionary<string, object>(), true);
|
||||
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按玩家 ID 取消点赞空间。
|
||||
/// </summary>
|
||||
public async Task UnlikeByPlayerIdAsync(string playerId)
|
||||
public async Task<BriskSpaceLikeResult> UnlikeByPlayerIdAsync(string playerId)
|
||||
{
|
||||
ValidatePlayerId(playerId);
|
||||
await ExecuteAsync(async context =>
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.SendDeleteJsonRawAsync($"/spaces/{playerId}/like", null, true);
|
||||
var data = await context.HttpClient.SendDeleteJsonRawAsync($"/spaces/{playerId}/like", null, true);
|
||||
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按登录身份点赞空间。
|
||||
/// </summary>
|
||||
public async Task LikeByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
public async Task<BriskSpaceLikeResult> LikeByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
{
|
||||
ValidateLoginIdentity(loginProvider, loginUserId);
|
||||
await ExecuteAsync(async context =>
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.PostJsonRawAsync("/spaces/by-login/like", new Dictionary<string, object>(), true, CreateLoginIdentityQuery(loginProvider, loginUserId));
|
||||
var data = await context.HttpClient.PostJsonRawAsync("/spaces/by-login/like", new Dictionary<string, object>(), true, CreateLoginIdentityQuery(loginProvider, loginUserId));
|
||||
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按登录身份取消点赞空间。
|
||||
/// </summary>
|
||||
public async Task UnlikeByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
public async Task<BriskSpaceLikeResult> UnlikeByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
{
|
||||
ValidateLoginIdentity(loginProvider, loginUserId);
|
||||
await ExecuteAsync(async context =>
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.SendDeleteJsonRawAsync("/spaces/by-login/like", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
|
||||
var data = await context.HttpClient.SendDeleteJsonRawAsync("/spaces/by-login/like", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
|
||||
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传当前玩家自己的空间内容。
|
||||
/// </summary>
|
||||
public async Task<BriskSpaceContentUpdateResult> UploadMyContentAsync(long? baseVersion, string contentType, string checksum, byte[] bytes)
|
||||
{
|
||||
RequireNotNull(bytes, nameof(bytes));
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : NormalizeChecksum(checksum);
|
||||
var sections = new List<IMultipartFormSection>
|
||||
{
|
||||
new MultipartFormDataSection("base_version", (baseVersion ?? 0L).ToString()),
|
||||
new MultipartFormFileSection("file", bytes, "space-content.bin", string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(contentType))
|
||||
{
|
||||
sections.Insert(1, new MultipartFormDataSection("content_type", contentType));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(finalChecksum))
|
||||
{
|
||||
sections.Insert(string.IsNullOrWhiteSpace(contentType) ? 1 : 2, new MultipartFormDataSection("checksum", finalChecksum));
|
||||
}
|
||||
|
||||
var data = await context.HttpClient.PutMultipartAsync("/spaces/me/content", sections, true);
|
||||
return BriskModelMapper.ToSpaceContentUpdateResult(data);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新当前玩家自己的空间内容。
|
||||
/// </summary>
|
||||
public async Task UpdateMyAsync(object payload)
|
||||
public Task<BriskSpaceContentUpdateResult> UpdateMyAsync(object payload, long? baseVersion = null, string contentType = null, string checksum = null)
|
||||
{
|
||||
RequireNotNull(payload, nameof(payload));
|
||||
|
||||
await ExecuteAsync(async context =>
|
||||
if (payload is byte[] bytes)
|
||||
{
|
||||
await context.HttpClient.SendPutJsonRawAsync(
|
||||
"/spaces/me",
|
||||
new Dictionary<string, object> { { "payload_json", payload } },
|
||||
true);
|
||||
return UploadMyContentAsync(baseVersion, contentType ?? "application/octet-stream", checksum, bytes);
|
||||
}
|
||||
|
||||
if (payload is string text)
|
||||
{
|
||||
return UploadMyContentAsync(baseVersion, contentType ?? "text/plain", checksum, Encoding.UTF8.GetBytes(text));
|
||||
}
|
||||
|
||||
var json = BriskJson.Serialize(payload);
|
||||
return UploadMyContentAsync(baseVersion, contentType ?? "application/json", checksum, Encoding.UTF8.GetBytes(json));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按玩家 ID 下载空间内容。
|
||||
/// </summary>
|
||||
public async Task<BriskSpaceContentDownloadResult> DownloadContentByPlayerIdAsync(string playerId)
|
||||
{
|
||||
ValidatePlayerId(playerId);
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var response = await context.HttpClient.GetBytesAsync($"/spaces/{playerId}/content", null, true);
|
||||
return CreateDownloadResult(response);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按登录身份下载空间内容。
|
||||
/// </summary>
|
||||
public async Task<BriskSpaceContentDownloadResult> DownloadContentByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
{
|
||||
ValidateLoginIdentity(loginProvider, loginUserId);
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var response = await context.HttpClient.GetBytesAsync("/spaces/by-login/content", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
|
||||
return CreateDownloadResult(response);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取我的访客列表。
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<BriskSpaceVisit>> GetMyVisitsAsync()
|
||||
public async Task<IReadOnlyList<BriskSpaceVisit>> GetMyVisitsAsync(int limit = 50)
|
||||
{
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var data = await context.HttpClient.GetRawDataAsync("/spaces/me/visits", null, true);
|
||||
var data = await context.HttpClient.GetRawDataAsync("/spaces/me/visits", CreateLimitQuery(limit), true);
|
||||
return (IReadOnlyList<BriskSpaceVisit>)BriskModelMapper.ToSpaceVisits(data);
|
||||
});
|
||||
}
|
||||
@@ -149,6 +258,25 @@ public sealed class BriskSpaceModule
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> CreateLimitQuery(int limit)
|
||||
{
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
{ "limit", NormalizeLimit(limit) }
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> CreateLikesQuery(int limit, bool currentCycleOnly)
|
||||
{
|
||||
var query = CreateLimitQuery(limit);
|
||||
if (currentCycleOnly)
|
||||
{
|
||||
query["scope"] = "cycle";
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
private static void ValidatePlayerId(string playerId)
|
||||
{
|
||||
RequireNotEmpty(playerId, nameof(playerId));
|
||||
@@ -159,4 +287,72 @@ public sealed class BriskSpaceModule
|
||||
RequireNotEmpty(loginProvider, nameof(loginProvider));
|
||||
RequireNotEmpty(loginUserId, nameof(loginUserId));
|
||||
}
|
||||
|
||||
private static string NormalizeLimit(int limit)
|
||||
{
|
||||
return (limit > 0 ? limit : 20).ToString();
|
||||
}
|
||||
|
||||
private static BriskSpaceContentDownloadResult CreateDownloadResult(BriskBinaryResponse response)
|
||||
{
|
||||
return new BriskSpaceContentDownloadResult
|
||||
{
|
||||
Bytes = response.Bytes,
|
||||
Version = ReadHeaderLong(response.Headers, "X-Space-Version"),
|
||||
Checksum = ReadHeader(response.Headers, "X-Space-Checksum"),
|
||||
ContentType = ReadHeader(response.Headers, "Content-Type")
|
||||
};
|
||||
}
|
||||
|
||||
private static string ComputeSha256(byte[] bytes)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
var builder = new StringBuilder(hash.Length * 2);
|
||||
for (var i = 0; i < hash.Length; i++)
|
||||
{
|
||||
builder.Append(hash[i].ToString("x2"));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeChecksum(string checksum)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(checksum))
|
||||
{
|
||||
return checksum;
|
||||
}
|
||||
|
||||
var value = checksum.Trim();
|
||||
return value.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)
|
||||
? value.Substring("sha256:".Length)
|
||||
: value;
|
||||
}
|
||||
|
||||
private static long ReadHeaderLong(Dictionary<string, string> headers, string key)
|
||||
{
|
||||
var value = ReadHeader(headers, key);
|
||||
return long.TryParse(value, out var result) ? result : 0L;
|
||||
}
|
||||
|
||||
private static string ReadHeader(Dictionary<string, string> headers, string key)
|
||||
{
|
||||
if (headers == null || string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var pair in headers)
|
||||
{
|
||||
if (string.Equals(pair.Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,6 @@ RenderSettings:
|
||||
m_ReflectionIntensity: 1
|
||||
m_CustomReflection: {fileID: 0}
|
||||
m_Sun: {fileID: 0}
|
||||
m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_UseRadianceAmbientProbe: 0
|
||||
--- !u!157 &3
|
||||
LightmapSettings:
|
||||
@@ -104,7 +103,7 @@ NavMeshSettings:
|
||||
serializedVersion: 2
|
||||
m_ObjectHideFlags: 0
|
||||
m_BuildSettings:
|
||||
serializedVersion: 2
|
||||
serializedVersion: 3
|
||||
agentTypeID: 0
|
||||
agentRadius: 0.5
|
||||
agentHeight: 2
|
||||
@@ -117,7 +116,7 @@ NavMeshSettings:
|
||||
cellSize: 0.16666667
|
||||
manualTileSize: 0
|
||||
tileSize: 256
|
||||
accuratePlacement: 0
|
||||
buildHeightMesh: 0
|
||||
maxJobWorkers: 0
|
||||
preserveTilesOutsideBounds: 0
|
||||
debug:
|
||||
@@ -163,9 +162,17 @@ Camera:
|
||||
m_projectionMatrixMode: 1
|
||||
m_GateFitMode: 2
|
||||
m_FOVAxisMode: 0
|
||||
m_Iso: 200
|
||||
m_ShutterSpeed: 0.005
|
||||
m_Aperture: 16
|
||||
m_FocusDistance: 10
|
||||
m_FocalLength: 50
|
||||
m_BladeCount: 5
|
||||
m_Curvature: {x: 2, y: 11}
|
||||
m_BarrelClipping: 0.25
|
||||
m_Anamorphism: 0
|
||||
m_SensorSize: {x: 36, y: 24}
|
||||
m_LensShift: {x: 0, y: 0}
|
||||
m_FocalLength: 50
|
||||
m_NormalizedViewPortRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
@@ -199,12 +206,13 @@ Transform:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 519420028}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: -10}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1600000000
|
||||
GameObject:
|
||||
@@ -230,12 +238,13 @@ Transform:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1600000000}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 1
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &1600000002
|
||||
MonoBehaviour:
|
||||
@@ -250,16 +259,16 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
BaseUrl: https://brisk.lightyears.ltd
|
||||
GameKey: demo-game
|
||||
GameKey: game_a1faf5ee93d0
|
||||
ClientVersion: 1.0.0
|
||||
DeviceId: editor-device
|
||||
ValidateSessionOnInitialize: 1
|
||||
LoginProvider: tap
|
||||
LoginUserId: tap_user_10001
|
||||
LoginCode:
|
||||
Nickname: Unity示例玩家
|
||||
Nickname: "Unity\u793A\u4F8B\u73A9\u5BB6"
|
||||
AvatarUrl:
|
||||
RankKey: season-score
|
||||
RankKey: rank-20260411062004-2bce
|
||||
SubmitScoreValue: 128
|
||||
LeaderboardLimit: 10
|
||||
AroundMeRange: 5
|
||||
@@ -267,10 +276,18 @@ MonoBehaviour:
|
||||
SeasonHistoryLimit: 20
|
||||
ArchiveSlotNo: 1
|
||||
ArchiveBaseVersion:
|
||||
ArchiveContent: '{"save":1,"coins":128,"hero":"mage","title":"中文测试存档"}'
|
||||
ArchiveContent: "{\n \"save\": 1,\n \"coins\": 128,\n \"hero\": \"mage\",\n
|
||||
\"title\": \"\u4E2D\u6587\u6D4B\u8BD5\u5B58\u6863\"\n}"
|
||||
AnnouncementId:
|
||||
SpacePlayerId:
|
||||
SpaceLoginProvider: tap
|
||||
SpaceLoginUserId: tap_user_10001
|
||||
SpacePayloadText: '{"mood":"ready","title":"你好 Brisk","desc":"这是中文测试空间数据"}'
|
||||
SpacePayloadText: "{\n \"mood\": \"ready\",\n \"title\": \"\u4F60\u597D Brisk\",\n
|
||||
\"desc\": \"\u8FD9\u662F\u4E2D\u6587\u6D4B\u8BD5\u7A7A\u95F4\u6570\u636E\"\n}"
|
||||
AutoRunOnStart: 0
|
||||
--- !u!1660057539 &9223372036854775807
|
||||
SceneRoots:
|
||||
m_ObjectHideFlags: 0
|
||||
m_Roots:
|
||||
- {fileID: 519420032}
|
||||
- {fileID: 1600000001}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Initial embedded package structure
|
||||
- Added Brisk runtime facade and service modules
|
||||
- Added default error presenter
|
||||
- Added quick start sample script and sample scene
|
||||
@@ -1,43 +0,0 @@
|
||||
# Quick Start
|
||||
|
||||
## Initialize
|
||||
|
||||
```csharp
|
||||
await Brisk.InitializeAsync(new BriskOptions
|
||||
{
|
||||
BaseUrl = "https://brisk.lightyears.ltd",
|
||||
GameKey = "demo-game",
|
||||
ClientVersion = Application.version,
|
||||
DeviceId = SystemInfo.deviceUniqueIdentifier
|
||||
});
|
||||
```
|
||||
|
||||
## Login
|
||||
|
||||
```csharp
|
||||
await Brisk.Auth.LoginWithUserIdAsync("tap", "tap_user_10001");
|
||||
```
|
||||
|
||||
## Common calls
|
||||
|
||||
```csharp
|
||||
var me = await Brisk.Player.GetMeAsync();
|
||||
var config = await Brisk.Config.GetCurrentAsync();
|
||||
var top = await Brisk.Leaderboard.GetTopAsync("season-score", 20);
|
||||
await Brisk.Leaderboard.SubmitScoreAsync("season-score", 128);
|
||||
```
|
||||
|
||||
## Sample
|
||||
|
||||
For the current source project, open directly:
|
||||
|
||||
- `Assets/BriskSdk/Samples/QuickStart/BriskQuickStartSample.cs`
|
||||
- `Assets/Scenes/BriskQuickStartScene.unity`
|
||||
|
||||
The sample scene uses an IMGUI debug panel and can directly test:
|
||||
|
||||
- initialize and restore
|
||||
- login by `login_user_id`
|
||||
- login by `code`
|
||||
- player, config, announcement, leaderboard, archive, and space flows
|
||||
- global event logs and request results
|
||||
@@ -1,42 +0,0 @@
|
||||
# FoldCC Brisk Game Server SDK
|
||||
|
||||
This directory is the package publishing skeleton for Brisk.
|
||||
|
||||
The active Unity project source currently lives under:
|
||||
|
||||
- `Assets/BriskSdk/Runtime`
|
||||
- `Assets/BriskSdk/Samples/QuickStart`
|
||||
- `Assets/Scenes/BriskQuickStartScene.unity`
|
||||
|
||||
Sync package content from the Unity source project with:
|
||||
|
||||
- `Tools/Sync-BriskPackage.ps1`
|
||||
|
||||
## Included runtime modules
|
||||
|
||||
- Bootstrap and initialization
|
||||
- Auth and session restore
|
||||
- Player profile
|
||||
- Dynamic config
|
||||
- Announcements
|
||||
- Leaderboard
|
||||
- Archive upload and download
|
||||
- Player space
|
||||
- Default blocking error UI
|
||||
|
||||
## Package layout
|
||||
|
||||
- `Runtime`
|
||||
- `Samples~`
|
||||
- `Documentation~`
|
||||
|
||||
## Quick start
|
||||
|
||||
See:
|
||||
|
||||
- `Documentation~/QuickStart.md`
|
||||
- `Samples~/QuickStart`
|
||||
|
||||
This sample uses an IMGUI test panel for end-to-end SDK flow verification.
|
||||
|
||||
When preparing a publish branch, place the package-ready runtime, samples, and docs into this package directory and tag that branch for external consumption.
|
||||
@@ -1,88 +0,0 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
internal sealed class BriskDefaultErrorDialog : MonoBehaviour
|
||||
{
|
||||
private const string HostName = "BriskDefaultErrorDialog";
|
||||
|
||||
private static BriskDefaultErrorDialog s_instance;
|
||||
|
||||
private string _title;
|
||||
private string _message;
|
||||
private string _confirmText;
|
||||
private Action _onConfirm;
|
||||
private bool _visible;
|
||||
|
||||
public static void Show(string title, string message, string confirmText, Action onConfirm = null)
|
||||
{
|
||||
var host = EnsureInstance();
|
||||
host._title = string.IsNullOrWhiteSpace(title) ? "Brisk" : title;
|
||||
host._message = string.IsNullOrWhiteSpace(message) ? "Unknown error." : message;
|
||||
host._confirmText = string.IsNullOrWhiteSpace(confirmText) ? "OK" : confirmText;
|
||||
host._onConfirm = onConfirm;
|
||||
host._visible = true;
|
||||
}
|
||||
|
||||
private static BriskDefaultErrorDialog EnsureInstance()
|
||||
{
|
||||
if (s_instance != null)
|
||||
{
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
var existing = GameObject.Find(HostName);
|
||||
if (existing != null)
|
||||
{
|
||||
s_instance = existing.GetComponent<BriskDefaultErrorDialog>();
|
||||
if (s_instance != null)
|
||||
{
|
||||
return s_instance;
|
||||
}
|
||||
}
|
||||
|
||||
var gameObject = new GameObject(HostName);
|
||||
DontDestroyOnLoad(gameObject);
|
||||
s_instance = gameObject.AddComponent<BriskDefaultErrorDialog>();
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
if (!_visible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var overlayRect = new Rect(0, 0, Screen.width, Screen.height);
|
||||
var previousColor = GUI.color;
|
||||
GUI.color = new Color(0f, 0f, 0f, 0.65f);
|
||||
GUI.Box(overlayRect, GUIContent.none);
|
||||
GUI.color = previousColor;
|
||||
|
||||
var width = Mathf.Min(460f, Screen.width - 40f);
|
||||
var height = 220f;
|
||||
var dialogRect = new Rect(
|
||||
(Screen.width - width) * 0.5f,
|
||||
(Screen.height - height) * 0.5f,
|
||||
width,
|
||||
height);
|
||||
|
||||
GUILayout.BeginArea(dialogRect, GUI.skin.window);
|
||||
GUILayout.Space(8f);
|
||||
GUILayout.Label(_title, GUI.skin.label);
|
||||
GUILayout.Space(12f);
|
||||
GUILayout.Label(_message, GUI.skin.label);
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
if (GUILayout.Button(_confirmText, GUILayout.Height(36f)))
|
||||
{
|
||||
var callback = _onConfirm;
|
||||
_visible = false;
|
||||
_onConfirm = null;
|
||||
callback?.Invoke();
|
||||
}
|
||||
|
||||
GUILayout.Space(8f);
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
public sealed class BriskDefaultErrorPresenter : IBriskErrorPresenter
|
||||
{
|
||||
private static readonly BriskDefaultErrorPresenter InstanceValue = new BriskDefaultErrorPresenter();
|
||||
|
||||
private BriskDefaultErrorPresenter()
|
||||
{
|
||||
}
|
||||
|
||||
public static BriskDefaultErrorPresenter Instance => InstanceValue;
|
||||
|
||||
public void ShowBlockingError(BriskBlockingException exception)
|
||||
{
|
||||
var message = exception != null && !string.IsNullOrWhiteSpace(exception.Message)
|
||||
? exception.Message
|
||||
: "A blocking error occurred.";
|
||||
|
||||
BriskDefaultErrorDialog.Show(
|
||||
title: GetBlockingTitle(exception),
|
||||
message: message,
|
||||
confirmText: "OK",
|
||||
onConfirm: () =>
|
||||
{
|
||||
var exitHandler = Brisk.IsInitialized ? Brisk.Options?.ExitHandler : null;
|
||||
exitHandler?.Invoke();
|
||||
});
|
||||
|
||||
Debug.LogError("[Brisk] Blocking error: " + message);
|
||||
}
|
||||
|
||||
public void ShowAuthExpired(BriskAuthExpiredException exception)
|
||||
{
|
||||
var message = exception != null && !string.IsNullOrWhiteSpace(exception.Message)
|
||||
? exception.Message
|
||||
: "Login expired. Please sign in again.";
|
||||
|
||||
BriskDefaultErrorDialog.Show(
|
||||
title: "Login Expired",
|
||||
message: message,
|
||||
confirmText: "OK");
|
||||
|
||||
Debug.LogWarning("[Brisk] Auth expired: " + message);
|
||||
}
|
||||
|
||||
private static string GetBlockingTitle(BriskBlockingException exception)
|
||||
{
|
||||
if (exception is BriskMaintenanceException)
|
||||
{
|
||||
return "Maintenance";
|
||||
}
|
||||
|
||||
if (exception is BriskAccountBannedException)
|
||||
{
|
||||
return "Account Restricted";
|
||||
}
|
||||
|
||||
if (exception is BriskClientUpdateRequiredException)
|
||||
{
|
||||
return "Update Required";
|
||||
}
|
||||
|
||||
return "Service Unavailable";
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
public sealed class BriskSpaceStats
|
||||
{
|
||||
public int LikeCount;
|
||||
public int VisitCount;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
public sealed class BriskSpaceView
|
||||
{
|
||||
public string PlayerId;
|
||||
public string LoginProvider;
|
||||
public string LoginUserId;
|
||||
public object Payload;
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// 玩家空间模块。
|
||||
/// </summary>
|
||||
public sealed class BriskSpaceModule
|
||||
: BriskModuleBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 按玩家 ID 获取空间数据。
|
||||
/// </summary>
|
||||
public async Task<BriskSpaceView> GetByPlayerIdAsync(string playerId)
|
||||
{
|
||||
ValidatePlayerId(playerId);
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var data = await context.HttpClient.GetDataAsync($"/spaces/{playerId}", null, true);
|
||||
return BriskModelMapper.ToSpaceView(data);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按登录身份获取空间数据。
|
||||
/// </summary>
|
||||
public async Task<BriskSpaceView> GetByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
{
|
||||
ValidateLoginIdentity(loginProvider, loginUserId);
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var data = await context.HttpClient.GetDataAsync("/spaces/by-login", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
|
||||
return BriskModelMapper.ToSpaceView(data);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按玩家 ID 获取空间统计。
|
||||
/// </summary>
|
||||
public async Task<BriskSpaceStats> GetStatsByPlayerIdAsync(string playerId)
|
||||
{
|
||||
ValidatePlayerId(playerId);
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var data = await context.HttpClient.GetDataAsync($"/spaces/{playerId}/stats", null, true);
|
||||
return BriskModelMapper.ToSpaceStats(data);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按登录身份获取空间统计。
|
||||
/// </summary>
|
||||
public async Task<BriskSpaceStats> GetStatsByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
{
|
||||
ValidateLoginIdentity(loginProvider, loginUserId);
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var data = await context.HttpClient.GetDataAsync("/spaces/by-login/stats", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
|
||||
return BriskModelMapper.ToSpaceStats(data);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按玩家 ID 点赞空间。
|
||||
/// </summary>
|
||||
public async Task LikeByPlayerIdAsync(string playerId)
|
||||
{
|
||||
ValidatePlayerId(playerId);
|
||||
await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.PostJsonRawAsync($"/spaces/{playerId}/like", new Dictionary<string, object>(), true);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按玩家 ID 取消点赞空间。
|
||||
/// </summary>
|
||||
public async Task UnlikeByPlayerIdAsync(string playerId)
|
||||
{
|
||||
ValidatePlayerId(playerId);
|
||||
await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.SendDeleteJsonRawAsync($"/spaces/{playerId}/like", null, true);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按登录身份点赞空间。
|
||||
/// </summary>
|
||||
public async Task LikeByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
{
|
||||
ValidateLoginIdentity(loginProvider, loginUserId);
|
||||
await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.PostJsonRawAsync("/spaces/by-login/like", new Dictionary<string, object>(), true, CreateLoginIdentityQuery(loginProvider, loginUserId));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按登录身份取消点赞空间。
|
||||
/// </summary>
|
||||
public async Task UnlikeByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
{
|
||||
ValidateLoginIdentity(loginProvider, loginUserId);
|
||||
await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.SendDeleteJsonRawAsync("/spaces/by-login/like", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新当前玩家自己的空间内容。
|
||||
/// </summary>
|
||||
public async Task UpdateMyAsync(object payload)
|
||||
{
|
||||
RequireNotNull(payload, nameof(payload));
|
||||
|
||||
await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.SendPutJsonRawAsync(
|
||||
"/spaces/me",
|
||||
new Dictionary<string, object> { { "payload_json", payload } },
|
||||
true);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取我的访客列表。
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<BriskSpaceVisit>> GetMyVisitsAsync()
|
||||
{
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var data = await context.HttpClient.GetRawDataAsync("/spaces/me/visits", null, true);
|
||||
return (IReadOnlyList<BriskSpaceVisit>)BriskModelMapper.ToSpaceVisits(data);
|
||||
});
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> CreateLoginIdentityQuery(string loginProvider, string loginUserId)
|
||||
{
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
{ "login_provider", loginProvider },
|
||||
{ "login_user_id", loginUserId }
|
||||
};
|
||||
}
|
||||
|
||||
private static void ValidatePlayerId(string playerId)
|
||||
{
|
||||
RequireNotEmpty(playerId, nameof(playerId));
|
||||
}
|
||||
|
||||
private static void ValidateLoginIdentity(string loginProvider, string loginUserId)
|
||||
{
|
||||
RequireNotEmpty(loginProvider, nameof(loginProvider));
|
||||
RequireNotEmpty(loginUserId, nameof(loginUserId));
|
||||
}
|
||||
}
|
||||
@@ -1,922 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
public sealed class BriskQuickStartSample : MonoBehaviour
|
||||
{
|
||||
[Header("初始化")]
|
||||
public string BaseUrl = "https://brisk.lightyears.ltd";
|
||||
public string GameKey = "demo-game";
|
||||
public string ClientVersion = "1.0.0";
|
||||
public string DeviceId = "editor-device";
|
||||
public bool ValidateSessionOnInitialize = true;
|
||||
|
||||
[Header("登录")]
|
||||
public string LoginProvider = "tap";
|
||||
public string LoginUserId = "tap_user_10001";
|
||||
public string LoginCode = string.Empty;
|
||||
public string Nickname = "Unity示例玩家";
|
||||
public string AvatarUrl = string.Empty;
|
||||
|
||||
[Header("排行榜")]
|
||||
public string RankKey = "season-score";
|
||||
public string SubmitScoreValue = "128";
|
||||
public string LeaderboardLimit = "10";
|
||||
public string AroundMeRange = "5";
|
||||
public string SeasonId = string.Empty;
|
||||
public string SeasonHistoryLimit = "20";
|
||||
|
||||
[Header("云存档")]
|
||||
public string ArchiveSlotNo = "1";
|
||||
public string ArchiveBaseVersion = string.Empty;
|
||||
[TextArea(3, 6)]
|
||||
public string ArchiveContent = "{\n \"save\": 1,\n \"coins\": 128,\n \"hero\": \"mage\",\n \"title\": \"中文测试存档\"\n}";
|
||||
|
||||
[Header("公告")]
|
||||
public string AnnouncementId = string.Empty;
|
||||
|
||||
[Header("玩家空间")]
|
||||
public string SpacePlayerId = string.Empty;
|
||||
public string SpaceLoginProvider = "tap";
|
||||
public string SpaceLoginUserId = "tap_user_10001";
|
||||
[TextArea(3, 6)]
|
||||
public string SpacePayloadText = "{\n \"mood\": \"ready\",\n \"title\": \"你好 Brisk\",\n \"desc\": \"这是中文测试空间数据\"\n}";
|
||||
|
||||
[Header("演示")]
|
||||
public bool AutoRunOnStart;
|
||||
|
||||
private readonly List<string> _eventLogs = new List<string>();
|
||||
|
||||
private Vector2 _pageScroll;
|
||||
private Vector2 _resultScroll;
|
||||
private Vector2 _logScroll;
|
||||
private bool _isBusy;
|
||||
private string _busyAction = string.Empty;
|
||||
private string _statusText = "就绪";
|
||||
private string _resultText = "尚未执行请求。";
|
||||
private string _lastErrorText = string.Empty;
|
||||
private IReadOnlyList<BriskAnnouncementItem> _announcementCache = Array.Empty<BriskAnnouncementItem>();
|
||||
private IReadOnlyList<BriskRankSeasonInfo> _seasonHistoryCache = Array.Empty<BriskRankSeasonInfo>();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
Brisk.OnInitialized += HandleInitialized;
|
||||
Brisk.OnLoggedIn += HandleLoggedIn;
|
||||
Brisk.OnLoggedOut += HandleLoggedOut;
|
||||
Brisk.OnAuthExpired += HandleAuthExpired;
|
||||
Brisk.OnBlockingError += HandleBlockingError;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
Brisk.OnInitialized -= HandleInitialized;
|
||||
Brisk.OnLoggedIn -= HandleLoggedIn;
|
||||
Brisk.OnLoggedOut -= HandleLoggedOut;
|
||||
Brisk.OnAuthExpired -= HandleAuthExpired;
|
||||
Brisk.OnBlockingError -= HandleBlockingError;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (AutoRunOnStart)
|
||||
{
|
||||
RunAction("自动冒烟流程", RunSmokeFlowAsync);
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("运行 Brisk 示例")]
|
||||
public void RunFromContextMenu()
|
||||
{
|
||||
RunAction("右键菜单冒烟流程", RunSmokeFlowAsync);
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
var area = new Rect(12f, 12f, Screen.width - 24f, Screen.height - 24f);
|
||||
GUILayout.BeginArea(area, GUI.skin.box);
|
||||
_pageScroll = GUILayout.BeginScrollView(_pageScroll);
|
||||
|
||||
DrawHeader();
|
||||
DrawStatusPanel();
|
||||
DrawInitSection();
|
||||
DrawLoginSection();
|
||||
DrawPlayerAndConfigSection();
|
||||
DrawAnnouncementsSection();
|
||||
DrawLeaderboardSection();
|
||||
DrawArchiveSection();
|
||||
DrawSpaceSection();
|
||||
DrawOutputSection();
|
||||
|
||||
GUILayout.EndScrollView();
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
private void DrawHeader()
|
||||
{
|
||||
GUILayout.Label("Brisk IMGUI 测试面板", GUI.skin.box);
|
||||
GUILayout.Label("这个场景用于在一个页面内测试 SDK 的完整流程。", GUI.skin.label);
|
||||
}
|
||||
|
||||
private void DrawStatusPanel()
|
||||
{
|
||||
BeginSection("运行状态");
|
||||
DrawReadOnlyRow("已初始化", Brisk.IsInitialized ? "是" : "否");
|
||||
DrawReadOnlyRow("已登录", Brisk.IsLoggedIn ? "是" : "否");
|
||||
DrawReadOnlyRow("PlayerId", Brisk.PlayerId);
|
||||
DrawReadOnlyRow("当前身份", Brisk.Identity == null ? string.Empty : Brisk.Identity.LoginProvider + " / " + Brisk.Identity.LoginUserId);
|
||||
DrawReadOnlyRow("当前动作", _isBusy ? _busyAction : "空闲");
|
||||
DrawReadOnlyRow("状态", _statusText);
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
DrawButton("执行冒烟流程", RunSmokeFlowAsync);
|
||||
DrawButton("查看 Bootstrap 缓存", () =>
|
||||
{
|
||||
SetResult("Bootstrap 缓存", Brisk.Bootstrap);
|
||||
return Task.CompletedTask;
|
||||
}, Brisk.IsInitialized);
|
||||
DrawButton("关闭 SDK", () =>
|
||||
{
|
||||
Brisk.Shutdown();
|
||||
Log("SDK 已关闭。");
|
||||
_statusText = "SDK 已关闭";
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
DrawButton("清空输出", () =>
|
||||
{
|
||||
_resultText = "输出已清空。";
|
||||
_lastErrorText = string.Empty;
|
||||
_eventLogs.Clear();
|
||||
_statusText = "输出已清空";
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
GUILayout.EndHorizontal();
|
||||
EndSection();
|
||||
}
|
||||
|
||||
private void DrawInitSection()
|
||||
{
|
||||
BeginSection("初始化");
|
||||
BaseUrl = DrawEditableRow("服务地址", BaseUrl);
|
||||
GameKey = DrawEditableRow("游戏 Key", GameKey);
|
||||
ClientVersion = DrawEditableRow("客户端版本", ClientVersion);
|
||||
DeviceId = DrawEditableRow("设备标识", DeviceId);
|
||||
ValidateSessionOnInitialize = DrawToggleRow("初始化时校验旧会话", ValidateSessionOnInitialize);
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
DrawButton("初始化", InitializeAsync);
|
||||
DrawButton("重新初始化", ReinitializeAsync);
|
||||
GUILayout.EndHorizontal();
|
||||
EndSection();
|
||||
}
|
||||
|
||||
private void DrawLoginSection()
|
||||
{
|
||||
BeginSection("登录");
|
||||
LoginProvider = DrawEditableRow("登录提供方", LoginProvider);
|
||||
LoginUserId = DrawEditableRow("登录用户 ID", LoginUserId);
|
||||
LoginCode = DrawEditableRow("登录 Code", LoginCode);
|
||||
Nickname = DrawEditableRow("昵称", Nickname);
|
||||
AvatarUrl = DrawEditableRow("头像地址", AvatarUrl);
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
DrawButton("按用户 ID 登录", LoginWithUserIdAsync, Brisk.IsInitialized);
|
||||
DrawButton("按 Code 登录", LoginWithCodeAsync, Brisk.IsInitialized);
|
||||
DrawButton("登出", LogoutAsync, Brisk.IsInitialized);
|
||||
GUILayout.EndHorizontal();
|
||||
EndSection();
|
||||
}
|
||||
|
||||
private void DrawPlayerAndConfigSection()
|
||||
{
|
||||
BeginSection("玩家与配置");
|
||||
GUILayout.BeginHorizontal();
|
||||
DrawButton("获取当前玩家", GetMeAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("获取当前配置", GetConfigAsync, Brisk.IsInitialized);
|
||||
DrawButton("同步当前身份到空间查询", () =>
|
||||
{
|
||||
ApplyCurrentIdentityToSpace();
|
||||
SetResult("空间查询身份", new Dictionary<string, object>
|
||||
{
|
||||
{ "space_player_id", SpacePlayerId },
|
||||
{ "space_login_provider", SpaceLoginProvider },
|
||||
{ "space_login_user_id", SpaceLoginUserId }
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}, Brisk.IsInitialized);
|
||||
GUILayout.EndHorizontal();
|
||||
EndSection();
|
||||
}
|
||||
|
||||
private void DrawAnnouncementsSection()
|
||||
{
|
||||
BeginSection("公告");
|
||||
AnnouncementId = DrawEditableRow("公告 ID", AnnouncementId);
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
DrawButton("获取公告列表", GetAnnouncementsAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("标记已读", MarkAnnouncementAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("标记首条缓存公告已读", MarkFirstCachedAnnouncementAsync, Brisk.IsLoggedIn && _announcementCache.Count > 0);
|
||||
GUILayout.EndHorizontal();
|
||||
EndSection();
|
||||
}
|
||||
|
||||
private void DrawLeaderboardSection()
|
||||
{
|
||||
BeginSection("排行榜");
|
||||
RankKey = DrawEditableRow("排行榜 Key", RankKey);
|
||||
SubmitScoreValue = DrawEditableRow("提交分数", SubmitScoreValue);
|
||||
LeaderboardLimit = DrawEditableRow("Top 数量", LeaderboardLimit);
|
||||
AroundMeRange = DrawEditableRow("附近范围", AroundMeRange);
|
||||
SeasonId = DrawEditableRow("赛季 ID", SeasonId);
|
||||
SeasonHistoryLimit = DrawEditableRow("历史条数", SeasonHistoryLimit);
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
DrawButton("获取 Top", GetTopAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("获取我的排名", GetMyRankAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("获取我附近的排名", GetAroundMeAsync, Brisk.IsLoggedIn);
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
DrawButton("提交分数", SubmitScoreAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("获取当前赛季", GetCurrentSeasonAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("获取赛季历史", GetSeasonHistoryAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("获取赛季详情", GetSeasonDetailAsync, Brisk.IsLoggedIn);
|
||||
GUILayout.EndHorizontal();
|
||||
EndSection();
|
||||
}
|
||||
|
||||
private void DrawArchiveSection()
|
||||
{
|
||||
BeginSection("云存档");
|
||||
ArchiveSlotNo = DrawEditableRow("槽位号", ArchiveSlotNo);
|
||||
ArchiveBaseVersion = DrawEditableRow("基准版本", ArchiveBaseVersion);
|
||||
ArchiveContent = DrawTextAreaRow("存档内容", ArchiveContent, 90f);
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
DrawButton("获取槽位列表", GetArchiveSlotsAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("获取存档元信息", GetArchiveMetaAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("上传文本存档", UploadArchiveAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("下载存档", DownloadArchiveAsync, Brisk.IsLoggedIn);
|
||||
GUILayout.EndHorizontal();
|
||||
EndSection();
|
||||
}
|
||||
|
||||
private void DrawSpaceSection()
|
||||
{
|
||||
BeginSection("玩家空间");
|
||||
SpacePlayerId = DrawEditableRow("空间 PlayerId", SpacePlayerId);
|
||||
SpaceLoginProvider = DrawEditableRow("空间登录提供方", SpaceLoginProvider);
|
||||
SpaceLoginUserId = DrawEditableRow("空间登录用户 ID", SpaceLoginUserId);
|
||||
SpacePayloadText = DrawTextAreaRow("空间 Payload 文本", SpacePayloadText, 90f);
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
DrawButton("按 PlayerId 获取空间", GetSpaceByPlayerIdAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("按登录身份获取空间", GetSpaceByLoginAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("按 PlayerId 获取统计", GetSpaceStatsByPlayerIdAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("按登录身份获取统计", GetSpaceStatsByLoginAsync, Brisk.IsLoggedIn);
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
DrawButton("按 PlayerId 点赞", LikeByPlayerIdAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("按 PlayerId 取消点赞", UnlikeByPlayerIdAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("按登录身份点赞", LikeByLoginAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("按登录身份取消点赞", UnlikeByLoginAsync, Brisk.IsLoggedIn);
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
DrawButton("更新我的空间", UpdateMySpaceAsync, Brisk.IsLoggedIn);
|
||||
DrawButton("获取我的访客", GetMyVisitsAsync, Brisk.IsLoggedIn);
|
||||
GUILayout.EndHorizontal();
|
||||
EndSection();
|
||||
}
|
||||
|
||||
private void DrawOutputSection()
|
||||
{
|
||||
BeginSection("输出");
|
||||
GUILayout.Label("最近一次结果", GUI.skin.label);
|
||||
_resultScroll = GUILayout.BeginScrollView(_resultScroll, GUILayout.Height(240f));
|
||||
GUILayout.TextArea(_resultText, GUILayout.ExpandHeight(true));
|
||||
GUILayout.EndScrollView();
|
||||
|
||||
GUILayout.Space(8f);
|
||||
GUILayout.Label("最近一次错误", GUI.skin.label);
|
||||
GUILayout.TextArea(string.IsNullOrWhiteSpace(_lastErrorText) ? "暂无错误。" : _lastErrorText, GUILayout.Height(90f));
|
||||
|
||||
GUILayout.Space(8f);
|
||||
GUILayout.Label("事件日志", GUI.skin.label);
|
||||
_logScroll = GUILayout.BeginScrollView(_logScroll, GUILayout.Height(220f));
|
||||
GUILayout.TextArea(_eventLogs.Count == 0 ? "暂无事件。" : string.Join("\n", _eventLogs.ToArray()), GUILayout.ExpandHeight(true));
|
||||
GUILayout.EndScrollView();
|
||||
EndSection();
|
||||
}
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
await Brisk.InitializeAsync(new BriskOptions
|
||||
{
|
||||
BaseUrl = BaseUrl,
|
||||
GameKey = GameKey,
|
||||
ClientVersion = ClientVersion,
|
||||
DeviceId = DeviceId,
|
||||
ValidateSessionOnInitialize = ValidateSessionOnInitialize,
|
||||
ExitHandler = HandleExitRequested
|
||||
});
|
||||
|
||||
SetResult("初始化结果", Brisk.Bootstrap);
|
||||
}
|
||||
|
||||
private async Task ReinitializeAsync()
|
||||
{
|
||||
if (Brisk.IsInitialized)
|
||||
{
|
||||
Brisk.Shutdown();
|
||||
Log("重新初始化前已先关闭 SDK。");
|
||||
}
|
||||
|
||||
await InitializeAsync();
|
||||
}
|
||||
|
||||
private async Task LoginWithUserIdAsync()
|
||||
{
|
||||
var result = await Brisk.Auth.LoginWithUserIdAsync(LoginProvider, LoginUserId, CreateProfile());
|
||||
ApplyIdentity(result.PlayerId, result.LoginProvider, result.LoginUserId);
|
||||
SetResult("按用户 ID 登录结果", result);
|
||||
}
|
||||
|
||||
private async Task LoginWithCodeAsync()
|
||||
{
|
||||
var result = await Brisk.Auth.LoginWithCodeAsync(LoginProvider, LoginCode, CreateProfile());
|
||||
ApplyIdentity(result.PlayerId, result.LoginProvider, result.LoginUserId);
|
||||
SetResult("按 Code 登录结果", result);
|
||||
}
|
||||
|
||||
private async Task LogoutAsync()
|
||||
{
|
||||
await Brisk.Auth.LogoutAsync();
|
||||
SetResult("登出结果", new Dictionary<string, object>
|
||||
{
|
||||
{ "logged_in", Brisk.IsLoggedIn },
|
||||
{ "player_id", Brisk.PlayerId }
|
||||
});
|
||||
}
|
||||
|
||||
private async Task GetMeAsync()
|
||||
{
|
||||
var me = await Brisk.Player.GetMeAsync();
|
||||
ApplyIdentity(me.PlayerId, me.LoginProvider, me.LoginUserId);
|
||||
SetResult("当前玩家信息", me);
|
||||
}
|
||||
|
||||
private async Task GetConfigAsync()
|
||||
{
|
||||
var config = await Brisk.Config.GetCurrentAsync();
|
||||
SetResult("当前配置", config);
|
||||
}
|
||||
|
||||
private async Task GetAnnouncementsAsync()
|
||||
{
|
||||
_announcementCache = await Brisk.Announcements.GetListAsync();
|
||||
if (_announcementCache.Count > 0 && string.IsNullOrWhiteSpace(AnnouncementId))
|
||||
{
|
||||
AnnouncementId = _announcementCache[0].Id.ToString();
|
||||
}
|
||||
|
||||
SetResult("公告列表", _announcementCache);
|
||||
}
|
||||
|
||||
private async Task MarkAnnouncementAsync()
|
||||
{
|
||||
var id = ParseRequiredLong(AnnouncementId, nameof(AnnouncementId));
|
||||
await Brisk.Announcements.MarkReadAsync(id);
|
||||
SetResult("公告已标记已读", new Dictionary<string, object> { { "announcement_id", id } });
|
||||
}
|
||||
|
||||
private async Task MarkFirstCachedAnnouncementAsync()
|
||||
{
|
||||
if (_announcementCache.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("公告缓存为空,请先执行“获取公告列表”。");
|
||||
}
|
||||
|
||||
var id = _announcementCache[0].Id;
|
||||
AnnouncementId = id.ToString();
|
||||
await Brisk.Announcements.MarkReadAsync(id);
|
||||
SetResult("首条缓存公告已标记已读", _announcementCache[0]);
|
||||
}
|
||||
|
||||
private async Task GetTopAsync()
|
||||
{
|
||||
var result = await Brisk.Leaderboard.GetTopAsync(RankKey, ParseOptionalInt(LeaderboardLimit, 10));
|
||||
SetResult("排行榜 Top", result);
|
||||
}
|
||||
|
||||
private async Task GetMyRankAsync()
|
||||
{
|
||||
var result = await Brisk.Leaderboard.GetMeAsync(RankKey);
|
||||
SetResult("我的排行榜信息", result);
|
||||
}
|
||||
|
||||
private async Task GetAroundMeAsync()
|
||||
{
|
||||
var result = await Brisk.Leaderboard.GetAroundMeAsync(RankKey, ParseOptionalInt(AroundMeRange, 5));
|
||||
SetResult("我附近的排行榜", result);
|
||||
}
|
||||
|
||||
private async Task SubmitScoreAsync()
|
||||
{
|
||||
var score = ParseRequiredLong(SubmitScoreValue, nameof(SubmitScoreValue));
|
||||
await Brisk.Leaderboard.SubmitScoreAsync(RankKey, score);
|
||||
SetResult("分数提交结果", new Dictionary<string, object>
|
||||
{
|
||||
{ "rank_key", RankKey },
|
||||
{ "score", score }
|
||||
});
|
||||
}
|
||||
|
||||
private async Task GetCurrentSeasonAsync()
|
||||
{
|
||||
var result = await Brisk.Leaderboard.GetCurrentSeasonAsync(RankKey);
|
||||
SeasonId = result == null ? SeasonId : result.SeasonId;
|
||||
SetResult("当前赛季", result);
|
||||
}
|
||||
|
||||
private async Task GetSeasonHistoryAsync()
|
||||
{
|
||||
_seasonHistoryCache = await Brisk.Leaderboard.GetSeasonHistoryAsync(RankKey, ParseOptionalInt(SeasonHistoryLimit, 20));
|
||||
if (_seasonHistoryCache.Count > 0 && string.IsNullOrWhiteSpace(SeasonId))
|
||||
{
|
||||
SeasonId = _seasonHistoryCache[0].SeasonId;
|
||||
}
|
||||
|
||||
SetResult("赛季历史", _seasonHistoryCache);
|
||||
}
|
||||
|
||||
private async Task GetSeasonDetailAsync()
|
||||
{
|
||||
var result = await Brisk.Leaderboard.GetSeasonHistoryDetailAsync(RankKey, SeasonId, ParseOptionalInt(SeasonHistoryLimit, 20));
|
||||
SetResult("赛季详情", result);
|
||||
}
|
||||
|
||||
private async Task GetArchiveSlotsAsync()
|
||||
{
|
||||
var result = await Brisk.Archive.GetSlotsAsync();
|
||||
SetResult("存档槽位列表", result);
|
||||
}
|
||||
|
||||
private async Task GetArchiveMetaAsync()
|
||||
{
|
||||
var result = await Brisk.Archive.GetMetaAsync(ParseRequiredInt(ArchiveSlotNo, nameof(ArchiveSlotNo)));
|
||||
ArchiveBaseVersion = result == null ? ArchiveBaseVersion : result.Version.ToString();
|
||||
SetResult("存档元信息", result);
|
||||
}
|
||||
|
||||
private async Task UploadArchiveAsync()
|
||||
{
|
||||
var slotNo = ParseRequiredInt(ArchiveSlotNo, nameof(ArchiveSlotNo));
|
||||
var baseVersion = ParseNullableInt(ArchiveBaseVersion);
|
||||
var bytes = Encoding.UTF8.GetBytes(ArchiveContent ?? string.Empty);
|
||||
var result = await Brisk.Archive.UploadAsync(slotNo, bytes, baseVersion);
|
||||
ArchiveBaseVersion = result == null ? ArchiveBaseVersion : result.Version.ToString();
|
||||
SetResult("存档上传结果", result);
|
||||
}
|
||||
|
||||
private async Task DownloadArchiveAsync()
|
||||
{
|
||||
var result = await Brisk.Archive.DownloadAsync(ParseRequiredInt(ArchiveSlotNo, nameof(ArchiveSlotNo)));
|
||||
ArchiveBaseVersion = result == null ? ArchiveBaseVersion : result.Version.ToString();
|
||||
|
||||
var output = new Dictionary<string, object>
|
||||
{
|
||||
{ "version", result.Version },
|
||||
{ "checksum", result.Checksum },
|
||||
{ "byte_length", result.Bytes == null ? 0 : result.Bytes.Length },
|
||||
{ "text_preview", result.Bytes == null ? string.Empty : Encoding.UTF8.GetString(result.Bytes) }
|
||||
};
|
||||
|
||||
SetResult("存档下载结果", output);
|
||||
}
|
||||
|
||||
private async Task GetSpaceByPlayerIdAsync()
|
||||
{
|
||||
var result = await Brisk.Space.GetByPlayerIdAsync(SpacePlayerId);
|
||||
SetResult("按 PlayerId 获取空间", result);
|
||||
}
|
||||
|
||||
private async Task GetSpaceByLoginAsync()
|
||||
{
|
||||
var result = await Brisk.Space.GetByLoginIdentityAsync(SpaceLoginProvider, SpaceLoginUserId);
|
||||
SetResult("按登录身份获取空间", result);
|
||||
}
|
||||
|
||||
private async Task GetSpaceStatsByPlayerIdAsync()
|
||||
{
|
||||
var result = await Brisk.Space.GetStatsByPlayerIdAsync(SpacePlayerId);
|
||||
SetResult("按 PlayerId 获取空间统计", result);
|
||||
}
|
||||
|
||||
private async Task GetSpaceStatsByLoginAsync()
|
||||
{
|
||||
var result = await Brisk.Space.GetStatsByLoginIdentityAsync(SpaceLoginProvider, SpaceLoginUserId);
|
||||
SetResult("按登录身份获取空间统计", result);
|
||||
}
|
||||
|
||||
private async Task LikeByPlayerIdAsync()
|
||||
{
|
||||
await Brisk.Space.LikeByPlayerIdAsync(SpacePlayerId);
|
||||
SetResult("按 PlayerId 点赞结果", new Dictionary<string, object> { { "player_id", SpacePlayerId } });
|
||||
}
|
||||
|
||||
private async Task UnlikeByPlayerIdAsync()
|
||||
{
|
||||
await Brisk.Space.UnlikeByPlayerIdAsync(SpacePlayerId);
|
||||
SetResult("按 PlayerId 取消点赞结果", new Dictionary<string, object> { { "player_id", SpacePlayerId } });
|
||||
}
|
||||
|
||||
private async Task LikeByLoginAsync()
|
||||
{
|
||||
await Brisk.Space.LikeByLoginIdentityAsync(SpaceLoginProvider, SpaceLoginUserId);
|
||||
SetResult("按登录身份点赞结果", new Dictionary<string, object>
|
||||
{
|
||||
{ "login_provider", SpaceLoginProvider },
|
||||
{ "login_user_id", SpaceLoginUserId }
|
||||
});
|
||||
}
|
||||
|
||||
private async Task UnlikeByLoginAsync()
|
||||
{
|
||||
await Brisk.Space.UnlikeByLoginIdentityAsync(SpaceLoginProvider, SpaceLoginUserId);
|
||||
SetResult("按登录身份取消点赞结果", new Dictionary<string, object>
|
||||
{
|
||||
{ "login_provider", SpaceLoginProvider },
|
||||
{ "login_user_id", SpaceLoginUserId }
|
||||
});
|
||||
}
|
||||
|
||||
private async Task UpdateMySpaceAsync()
|
||||
{
|
||||
var payload = new Dictionary<string, object>
|
||||
{
|
||||
{ "sample_text", SpacePayloadText },
|
||||
{ "updated_at", DateTimeOffset.UtcNow.ToString("O") },
|
||||
{ "player_id", Brisk.PlayerId },
|
||||
{ "rank_key", RankKey }
|
||||
};
|
||||
|
||||
await Brisk.Space.UpdateMyAsync(payload);
|
||||
SetResult("更新我的空间结果", payload);
|
||||
}
|
||||
|
||||
private async Task GetMyVisitsAsync()
|
||||
{
|
||||
var result = await Brisk.Space.GetMyVisitsAsync();
|
||||
SetResult("我的访客列表", result);
|
||||
}
|
||||
|
||||
private async Task RunSmokeFlowAsync()
|
||||
{
|
||||
await InitializeAsync();
|
||||
|
||||
if (!Brisk.IsLoggedIn)
|
||||
{
|
||||
await LoginWithUserIdAsync();
|
||||
}
|
||||
|
||||
await GetMeAsync();
|
||||
await GetConfigAsync();
|
||||
await GetTopAsync();
|
||||
await GetAnnouncementsAsync();
|
||||
await GetArchiveSlotsAsync();
|
||||
await GetMyVisitsAsync();
|
||||
}
|
||||
|
||||
private void DrawButton(string label, Func<Task> action, bool enabled = true)
|
||||
{
|
||||
var previous = GUI.enabled;
|
||||
GUI.enabled = previous && !_isBusy && enabled;
|
||||
if (GUILayout.Button(label, GUILayout.Height(30f)))
|
||||
{
|
||||
RunAction(label, action);
|
||||
}
|
||||
|
||||
GUI.enabled = previous;
|
||||
}
|
||||
|
||||
private void RunAction(string actionName, Func<Task> action)
|
||||
{
|
||||
if (_isBusy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RunActionAsync(actionName, action);
|
||||
}
|
||||
|
||||
private async void RunActionAsync(string actionName, Func<Task> action)
|
||||
{
|
||||
_isBusy = true;
|
||||
_busyAction = actionName;
|
||||
_statusText = "执行中: " + actionName;
|
||||
_lastErrorText = string.Empty;
|
||||
Log("开始执行: " + actionName);
|
||||
|
||||
try
|
||||
{
|
||||
await action();
|
||||
_statusText = "执行成功: " + actionName;
|
||||
Log("执行成功: " + actionName);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_statusText = "执行失败: " + actionName;
|
||||
_lastErrorText = FormatException(exception);
|
||||
Log("执行失败: " + actionName + " | " + exception.GetType().Name + " | " + exception.Message);
|
||||
Debug.LogException(exception, this);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busyAction = string.Empty;
|
||||
_isBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetResult(string title, object value)
|
||||
{
|
||||
_resultText = title + "\n" + new string('=', title.Length) + "\n" + FormatValue(value);
|
||||
}
|
||||
|
||||
private void ApplyIdentity(string playerId, string loginProvider, string loginUserId)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(playerId))
|
||||
{
|
||||
SpacePlayerId = playerId;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(loginProvider))
|
||||
{
|
||||
SpaceLoginProvider = loginProvider;
|
||||
LoginProvider = loginProvider;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(loginUserId))
|
||||
{
|
||||
SpaceLoginUserId = loginUserId;
|
||||
LoginUserId = loginUserId;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyCurrentIdentityToSpace()
|
||||
{
|
||||
if (Brisk.Identity != null)
|
||||
{
|
||||
ApplyIdentity(Brisk.Identity.PlayerId, Brisk.Identity.LoginProvider, Brisk.Identity.LoginUserId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Brisk.IsInitialized)
|
||||
{
|
||||
ApplyIdentity(Brisk.PlayerId, LoginProvider, LoginUserId);
|
||||
}
|
||||
}
|
||||
|
||||
private BriskProfile CreateProfile()
|
||||
{
|
||||
return new BriskProfile
|
||||
{
|
||||
Nickname = Nickname,
|
||||
AvatarUrl = AvatarUrl
|
||||
};
|
||||
}
|
||||
|
||||
private void HandleInitialized()
|
||||
{
|
||||
Log("事件: 初始化完成");
|
||||
}
|
||||
|
||||
private void HandleLoggedIn()
|
||||
{
|
||||
Log("事件: 登录完成");
|
||||
ApplyCurrentIdentityToSpace();
|
||||
}
|
||||
|
||||
private void HandleLoggedOut()
|
||||
{
|
||||
Log("事件: 登出完成");
|
||||
}
|
||||
|
||||
private void HandleAuthExpired(BriskAuthExpiredException exception)
|
||||
{
|
||||
Log("事件: 登录态失效 | " + exception.Message);
|
||||
}
|
||||
|
||||
private void HandleBlockingError(BriskBlockingException exception)
|
||||
{
|
||||
Log("事件: 严重阻断错误 | " + exception.Message);
|
||||
}
|
||||
|
||||
private void HandleExitRequested()
|
||||
{
|
||||
Log("Brisk 阻断流程请求退出。");
|
||||
}
|
||||
|
||||
private void Log(string message)
|
||||
{
|
||||
var line = "[" + DateTime.Now.ToString("HH:mm:ss") + "] " + message;
|
||||
_eventLogs.Add(line);
|
||||
if (_eventLogs.Count > 120)
|
||||
{
|
||||
_eventLogs.RemoveAt(0);
|
||||
}
|
||||
|
||||
_logScroll.y = float.MaxValue;
|
||||
}
|
||||
|
||||
private static int ParseRequiredInt(string value, string fieldName)
|
||||
{
|
||||
if (!int.TryParse(value, out var result))
|
||||
{
|
||||
throw new InvalidOperationException(fieldName + " 必须是合法整数。");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static long ParseRequiredLong(string value, string fieldName)
|
||||
{
|
||||
if (!long.TryParse(value, out var result))
|
||||
{
|
||||
throw new InvalidOperationException(fieldName + " 必须是合法整数。");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int ParseOptionalInt(string value, int fallback)
|
||||
{
|
||||
return int.TryParse(value, out var result) ? result : fallback;
|
||||
}
|
||||
|
||||
private static int? ParseNullableInt(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return int.TryParse(value, out var result) ? result : (int?)null;
|
||||
}
|
||||
|
||||
private static string FormatException(Exception exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
return "未知错误。";
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
var current = exception;
|
||||
while (current != null)
|
||||
{
|
||||
builder.AppendLine(current.GetType().Name + ": " + current.Message);
|
||||
current = current.InnerException;
|
||||
}
|
||||
|
||||
return builder.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
private static string FormatValue(object value)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
AppendValue(builder, value, 0, 0);
|
||||
return builder.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
private static void AppendValue(StringBuilder builder, object value, int indent, int depth)
|
||||
{
|
||||
if (depth > 5)
|
||||
{
|
||||
builder.AppendLine(Indent(indent) + "...");
|
||||
return;
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
builder.AppendLine(Indent(indent) + "null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is string stringValue)
|
||||
{
|
||||
builder.AppendLine(Indent(indent) + stringValue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is byte[] bytes)
|
||||
{
|
||||
builder.AppendLine(Indent(indent) + "byte[" + bytes.Length + "]");
|
||||
return;
|
||||
}
|
||||
|
||||
var type = value.GetType();
|
||||
if (type.IsPrimitive || value is decimal)
|
||||
{
|
||||
builder.AppendLine(Indent(indent) + value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is IDictionary dictionary)
|
||||
{
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
{
|
||||
builder.AppendLine(Indent(indent) + entry.Key + ":");
|
||||
AppendValue(builder, entry.Value, indent + 2, depth + 1);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is IEnumerable enumerable)
|
||||
{
|
||||
var index = 0;
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
builder.AppendLine(Indent(indent) + "[" + index + "]");
|
||||
AppendValue(builder, item, indent + 2, depth + 1);
|
||||
index++;
|
||||
}
|
||||
|
||||
if (index == 0)
|
||||
{
|
||||
builder.AppendLine(Indent(indent) + "(empty)");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public);
|
||||
if (fields.Length == 0)
|
||||
{
|
||||
builder.AppendLine(Indent(indent) + value);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var field in fields)
|
||||
{
|
||||
builder.AppendLine(Indent(indent) + field.Name + ":");
|
||||
AppendValue(builder, field.GetValue(value), indent + 2, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Indent(int indent)
|
||||
{
|
||||
return new string(' ', indent);
|
||||
}
|
||||
|
||||
private static void BeginSection(string title)
|
||||
{
|
||||
GUILayout.Space(10f);
|
||||
GUILayout.BeginVertical(GUI.skin.box);
|
||||
GUILayout.Label(title, GUI.skin.label);
|
||||
GUILayout.Space(4f);
|
||||
}
|
||||
|
||||
private static void EndSection()
|
||||
{
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
private static string DrawEditableRow(string label, string value)
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label(label, GUILayout.Width(180f));
|
||||
var next = GUILayout.TextField(value ?? string.Empty);
|
||||
GUILayout.EndHorizontal();
|
||||
return next;
|
||||
}
|
||||
|
||||
private static string DrawTextAreaRow(string label, string value, float height)
|
||||
{
|
||||
GUILayout.Label(label, GUILayout.Width(180f));
|
||||
return GUILayout.TextArea(value ?? string.Empty, GUILayout.Height(height));
|
||||
}
|
||||
|
||||
private static bool DrawToggleRow(string label, bool value)
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label(label, GUILayout.Width(180f));
|
||||
var next = GUILayout.Toggle(value, value ? "Enabled" : "Disabled");
|
||||
GUILayout.EndHorizontal();
|
||||
return next;
|
||||
}
|
||||
|
||||
private static void DrawReadOnlyRow(string label, string value)
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label(label, GUILayout.Width(180f));
|
||||
GUILayout.Label(string.IsNullOrWhiteSpace(value) ? "-" : value, GUI.skin.box);
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# Changelog
|
||||
|
||||
## 0.4.1
|
||||
|
||||
- Startup restore now silently clears locally expired or server-rejected sessions without showing the default auth-expired dialog
|
||||
- Added silent initialization relogin from the stored `login_provider` and `login_user_id` when available
|
||||
- Localized the default blocking/auth-expired dialogs to Chinese and stopped showing raw server messages to players
|
||||
- Rebuilt the default dialog with a larger mobile-readable Runtime UI Toolkit layout and topmost legacy IMGUI bridge
|
||||
- Default dialogs no longer close from backdrop or Escape; blocking dialogs now invoke the configured exit handler or fall back to `Application.Quit()`
|
||||
|
||||
## 0.4.0
|
||||
|
||||
- Added daily-cycle space like fields and result semantics to the Unity SDK models
|
||||
- Added current-cycle filtering support for space like list APIs
|
||||
- Updated the quick start sample to display total likes, today likes, like reset time, and current-cycle like actions
|
||||
- Changed `GetMyVisitsAsync()` default page size to `50` while preserving the optional `limit` parameter
|
||||
- Refreshed package and integration documentation to match the latest production API behavior
|
||||
|
||||
## 0.3.0
|
||||
|
||||
- Renamed the UPM package identifier to `com.foldcc.cc-framework.brisk-game-server`
|
||||
- Renamed the publish directory to the all-lowercase package path
|
||||
- Updated sync tooling and integration documentation for Git-based package import
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Refined archive APIs with direct text and JSON upload/download helpers
|
||||
- Refactored player space to the latest metadata plus binary content architecture
|
||||
- Added space content download/update result models and like list models
|
||||
- Updated quick start sample to use the latest archive and space flows
|
||||
- Updated package and integration docs to match the current API surface
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Initial embedded package structure
|
||||
- Added Brisk runtime facade and service modules
|
||||
- Added default error presenter
|
||||
- Added quick start sample script and sample scene
|
||||
@@ -0,0 +1,100 @@
|
||||
# Quick Start
|
||||
|
||||
## Initialize
|
||||
|
||||
```csharp
|
||||
await Brisk.InitializeAsync(new BriskOptions
|
||||
{
|
||||
BaseUrl = "https://brisk.lightyears.ltd",
|
||||
GameKey = "demo-game",
|
||||
ClientVersion = Application.version,
|
||||
DeviceId = SystemInfo.deviceUniqueIdentifier
|
||||
});
|
||||
```
|
||||
|
||||
## Login
|
||||
|
||||
```csharp
|
||||
await Brisk.Auth.LoginWithUserIdAsync("tap", "tap_user_10001");
|
||||
```
|
||||
|
||||
## Common calls
|
||||
|
||||
```csharp
|
||||
var me = await Brisk.Player.GetMeAsync();
|
||||
var config = await Brisk.Config.GetCurrentAsync();
|
||||
var top = await Brisk.Leaderboard.GetTopAsync("season-score", 20);
|
||||
await Brisk.Leaderboard.SubmitScoreAsync("season-score", 128);
|
||||
```
|
||||
|
||||
## Archive upload
|
||||
|
||||
```csharp
|
||||
await Brisk.Archive.UploadTextAsync(1, "{\"save\":1}");
|
||||
await Brisk.Archive.UploadJsonAsync(2, new
|
||||
{
|
||||
save = 1,
|
||||
coins = 128
|
||||
});
|
||||
|
||||
var text = await Brisk.Archive.DownloadTextAsync(1);
|
||||
var json = await Brisk.Archive.DownloadJsonAsync(2);
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- if you already have binary data, keep using `Brisk.Archive.UploadAsync(slotNo, bytes)`
|
||||
- if you need version and checksum, keep using `Brisk.Archive.DownloadAsync(slotNo)`
|
||||
- `checksum` is optional in normal use
|
||||
- the SDK computes SHA256 for you automatically
|
||||
- if you pass a manual checksum, use plain SHA256 hex
|
||||
- values like `sha256:abcd...` will be normalized by the SDK before upload
|
||||
|
||||
## Space content
|
||||
|
||||
```csharp
|
||||
await Brisk.Space.UpdateMyAsync("Hello Brisk Space");
|
||||
|
||||
await Brisk.Space.UpdateMyAsync(new
|
||||
{
|
||||
mood = "ready",
|
||||
title = "hello"
|
||||
});
|
||||
|
||||
var mySpace = await Brisk.Space.GetByPlayerIdAsync(Brisk.PlayerId);
|
||||
var myContent = await Brisk.Space.DownloadContentByPlayerIdAsync(Brisk.PlayerId);
|
||||
var text = Encoding.UTF8.GetString(myContent.Bytes);
|
||||
|
||||
var likeResult = await Brisk.Space.LikeByPlayerIdAsync("target-player-id");
|
||||
Debug.Log($"total={likeResult.LikeCount}, today={likeResult.TodayLikeCount}, created={likeResult.Created}");
|
||||
|
||||
var todayLikes = await Brisk.Space.GetLikesByPlayerIdAsync(Brisk.PlayerId, 20, true);
|
||||
var visits = await Brisk.Space.GetMyVisitsAsync();
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- space metadata and space content are now separated
|
||||
- `GetByPlayerIdAsync(...)` returns metadata only
|
||||
- use `DownloadContentByPlayerIdAsync(...)` to read the actual content bytes
|
||||
- `UpdateMyAsync(...)` automatically picks text / binary / json behavior from the payload type
|
||||
- space likes are now tracked by natural day in `Asia/Shanghai`
|
||||
- `BriskSpaceView` / `BriskSpaceStats` include both `LikeCount` and `TodayLikeCount`
|
||||
- `BriskSpaceView` also includes `LikedByMe` and `LikeResetAt`
|
||||
- `GetLikesByPlayerIdAsync(..., currentCycleOnly: true)` returns current-cycle likes only
|
||||
- `GetMyVisitsAsync()` now defaults to the latest `50` visits, with `100` as the server-side max
|
||||
|
||||
## Sample
|
||||
|
||||
For the current source project, open directly:
|
||||
|
||||
- `Assets/BriskSdk/Samples/QuickStart/BriskQuickStartSample.cs`
|
||||
- `Assets/Scenes/BriskQuickStartScene.unity`
|
||||
|
||||
The sample scene uses an IMGUI debug panel and can directly test:
|
||||
|
||||
- initialize and restore
|
||||
- login by `login_user_id`
|
||||
- login by `code`
|
||||
- player, config, announcement, leaderboard, archive, and space flows
|
||||
- global event logs and request results
|
||||
@@ -0,0 +1,105 @@
|
||||
# FoldCC Brisk Game Server SDK
|
||||
|
||||
Brisk Unity SDK 的 UPM 发布目录。
|
||||
|
||||
当前仓库地址:
|
||||
|
||||
- `http://private.lightyears.ltd:18650/foldcc/CC-Framework.BriskGameServer`
|
||||
|
||||
当前包名:
|
||||
|
||||
- `com.foldcc.cc-framework.brisk-game-server`
|
||||
|
||||
## 如何引入到项目
|
||||
|
||||
推荐通过 Unity Package Manager 的 Git URL 方式引入。
|
||||
|
||||
### 方式一:Package Manager
|
||||
|
||||
1. 打开 `Window > Package Manager`
|
||||
2. 点击左上角 `+`
|
||||
3. 选择 `Add package from git URL...`
|
||||
4. 输入:
|
||||
|
||||
```text
|
||||
http://private.lightyears.ltd:18650/foldcc/CC-Framework.BriskGameServer.git?path=/PackageSource/com.foldcc.cc-framework.brisk-game-server#v0.4.1
|
||||
```
|
||||
|
||||
### 方式二:修改 `Packages/manifest.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"com.foldcc.cc-framework.brisk-game-server": "http://private.lightyears.ltd:18650/foldcc/CC-Framework.BriskGameServer.git?path=/PackageSource/com.foldcc.cc-framework.brisk-game-server#v0.4.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果需要跟随主分支最新代码,可将末尾的 `#v0.4.1` 改成 `#main`;正式环境建议固定到发布 tag。
|
||||
|
||||
## 开发态源码位置
|
||||
|
||||
开发工程中的活代码位于:
|
||||
|
||||
- `Assets/BriskSdk/Runtime`
|
||||
- `Assets/BriskSdk/Samples/QuickStart`
|
||||
- `Assets/Scenes/BriskQuickStartScene.unity`
|
||||
|
||||
同步 package 内容时执行:
|
||||
|
||||
```powershell
|
||||
./Tools/Sync-BriskPackage.ps1
|
||||
```
|
||||
|
||||
## 已包含模块
|
||||
|
||||
- Bootstrap 与初始化
|
||||
- Auth 与会话恢复
|
||||
- 玩家信息
|
||||
- 动态配置
|
||||
- 公告
|
||||
- 排行榜
|
||||
- 云存档上传下载
|
||||
- 玩家空间
|
||||
- 默认阻断式错误 UI
|
||||
|
||||
## Archive checksum
|
||||
|
||||
云存档上传时,SDK 默认自动处理 checksum。
|
||||
|
||||
- `UploadAsync(slotNo, bytes)` 用于二进制
|
||||
- `UploadTextAsync(slotNo, text)` 用于 UTF-8 文本
|
||||
- `UploadJsonAsync(slotNo, payload)` 用于 JSON 对象
|
||||
- `DownloadAsync(slotNo)` 返回原始 bytes 和元信息
|
||||
- `DownloadTextAsync(slotNo)` 返回 UTF-8 文本
|
||||
- `DownloadJsonAsync(slotNo)` 返回 JSON 结果
|
||||
- SDK 会自动计算 SHA256
|
||||
- 当前 Brisk archive API 要求纯小写 SHA256 十六进制字符串
|
||||
- 不要传 `sha256:` 前缀
|
||||
- 若手动传入带前缀的 checksum,SDK 会自动归一化
|
||||
|
||||
## Space content
|
||||
|
||||
玩家空间当前采用 metadata + binary content 模型。
|
||||
|
||||
- `GetByPlayerIdAsync(...)` 和 `GetByLoginIdentityAsync(...)` 返回元数据
|
||||
- `DownloadContentByPlayerIdAsync(...)` 和 `DownloadContentByLoginIdentityAsync(...)` 返回原始 bytes
|
||||
- `UpdateMyAsync(string)` 直接上传文本
|
||||
- `UpdateMyAsync(byte[])` 直接上传二进制
|
||||
- `UpdateMyAsync(object)` 自动序列化为 JSON
|
||||
- `LikeByPlayerIdAsync(...)` / `UnlikeByPlayerIdAsync(...)` 返回累计点赞数、今日点赞数、当前周期是否创建新点赞、重置时间
|
||||
- `GetLikesByPlayerIdAsync(playerId, limit, true)` 和 `GetLikesByLoginIdentityAsync(..., true)` 可只读取当前周期点赞记录
|
||||
- `GetMyVisitsAsync()` 默认读取最近 `50` 条访问记录,也可手动传入 `limit`
|
||||
|
||||
## 目录结构
|
||||
|
||||
- `Runtime`
|
||||
- `Samples~`
|
||||
- `Documentation~`
|
||||
|
||||
## 快速开始
|
||||
|
||||
查看:
|
||||
|
||||
- `Documentation~/QuickStart.md`
|
||||
- `Samples~/QuickStart`
|
||||
@@ -50,7 +50,7 @@ public sealed class BriskArchiveModule
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : checksum;
|
||||
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : NormalizeChecksum(checksum);
|
||||
var sections = new List<IMultipartFormSection>
|
||||
{
|
||||
new MultipartFormDataSection("base_version", (baseVersion ?? 0).ToString()),
|
||||
@@ -63,6 +63,24 @@ public sealed class BriskArchiveModule
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 UTF-8 文本形式上传指定槽位的存档。
|
||||
/// </summary>
|
||||
public Task<BriskArchiveUploadResult> UploadTextAsync(int slotNo, string text, int? baseVersion = null, string checksum = null)
|
||||
{
|
||||
RequireNotNull(text, nameof(text));
|
||||
return UploadAsync(slotNo, Encoding.UTF8.GetBytes(text), baseVersion, checksum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 JSON 文本形式上传指定槽位的存档。
|
||||
/// </summary>
|
||||
public Task<BriskArchiveUploadResult> UploadJsonAsync(int slotNo, object payload, int? baseVersion = null, string checksum = null)
|
||||
{
|
||||
RequireNotNull(payload, nameof(payload));
|
||||
return UploadAsync(slotNo, Encoding.UTF8.GetBytes(BriskJson.Serialize(payload)), baseVersion, checksum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载指定槽位的二进制存档。
|
||||
/// </summary>
|
||||
@@ -82,6 +100,24 @@ public sealed class BriskArchiveModule
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 UTF-8 文本形式下载指定槽位的存档。
|
||||
/// </summary>
|
||||
public async Task<string> DownloadTextAsync(int slotNo)
|
||||
{
|
||||
var result = await DownloadAsync(slotNo);
|
||||
return result == null || result.Bytes == null ? string.Empty : Encoding.UTF8.GetString(result.Bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 JSON 对象形式下载指定槽位的存档。
|
||||
/// </summary>
|
||||
public async Task<object> DownloadJsonAsync(int slotNo)
|
||||
{
|
||||
var text = await DownloadTextAsync(slotNo);
|
||||
return string.IsNullOrWhiteSpace(text) ? null : BriskJson.Deserialize(text);
|
||||
}
|
||||
|
||||
private static void ValidateSlotNo(int slotNo)
|
||||
{
|
||||
RequirePositive(slotNo, nameof(slotNo), "slotNo must be greater than 0.");
|
||||
@@ -92,8 +128,7 @@ public sealed class BriskArchiveModule
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
var builder = new StringBuilder(hash.Length * 2 + 7);
|
||||
builder.Append("sha256:");
|
||||
var builder = new StringBuilder(hash.Length * 2);
|
||||
for (var i = 0; i < hash.Length; i++)
|
||||
{
|
||||
builder.Append(hash[i].ToString("x2"));
|
||||
@@ -103,6 +138,19 @@ public sealed class BriskArchiveModule
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeChecksum(string checksum)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(checksum))
|
||||
{
|
||||
return checksum;
|
||||
}
|
||||
|
||||
var value = checksum.Trim();
|
||||
return value.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)
|
||||
? value.Substring("sha256:".Length)
|
||||
: value;
|
||||
}
|
||||
|
||||
private static int ReadHeaderInt(Dictionary<string, string> headers, string key)
|
||||
{
|
||||
var value = ReadHeader(headers, key);
|
||||
@@ -197,8 +197,17 @@ public static class Brisk
|
||||
}
|
||||
|
||||
internal static void NotifyAuthExpired(BriskAuthExpiredException exception)
|
||||
{
|
||||
NotifyAuthExpired(exception, true);
|
||||
}
|
||||
|
||||
internal static void NotifyAuthExpired(BriskAuthExpiredException exception, bool showDefaultPresenter)
|
||||
{
|
||||
if (showDefaultPresenter)
|
||||
{
|
||||
s_context?.ErrorPresenter?.ShowAuthExpired(exception);
|
||||
}
|
||||
|
||||
OnAuthExpired?.Invoke(exception);
|
||||
}
|
||||
|
||||
@@ -241,6 +250,15 @@ public static class Brisk
|
||||
return;
|
||||
}
|
||||
|
||||
if (storedSession.ExpiresAt.HasValue && storedSession.ExpiresAt.Value <= DateTimeOffset.UtcNow)
|
||||
{
|
||||
await HandleExpiredStartupSessionAsync(
|
||||
context,
|
||||
storedSession,
|
||||
new BriskAuthExpiredException("Stored session expired before initialization."));
|
||||
return;
|
||||
}
|
||||
|
||||
context.Session.Update(
|
||||
storedSession.AccessToken,
|
||||
storedSession.ExpiresAt,
|
||||
@@ -268,9 +286,7 @@ public static class Brisk
|
||||
}
|
||||
catch (BriskAuthExpiredException exception)
|
||||
{
|
||||
context.Session.Clear();
|
||||
await context.TokenStore.ClearAsync();
|
||||
NotifyAuthExpired(exception);
|
||||
await HandleExpiredStartupSessionAsync(context, storedSession, exception);
|
||||
}
|
||||
catch (BriskBlockingException exception)
|
||||
{
|
||||
@@ -278,4 +294,36 @@ public static class Brisk
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task HandleExpiredStartupSessionAsync(BriskContext context, BriskStoredSession storedSession, BriskAuthExpiredException exception)
|
||||
{
|
||||
context.Session.Clear();
|
||||
await context.TokenStore.ClearAsync();
|
||||
NotifyAuthExpired(exception, false);
|
||||
|
||||
if (!context.Options.AutoReloginOnInitialize || !HasStoredLoginIdentity(storedSession))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Auth.LoginWithUserIdAsync(storedSession.LoginProvider, storedSession.LoginUserId);
|
||||
}
|
||||
catch (BriskBlockingException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 初始化期静默重登失败时,交给业务层后续第三方登录流程重新调用 Brisk.Auth。
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasStoredLoginIdentity(BriskStoredSession storedSession)
|
||||
{
|
||||
return storedSession != null
|
||||
&& !string.IsNullOrWhiteSpace(storedSession.LoginProvider)
|
||||
&& !string.IsNullOrWhiteSpace(storedSession.LoginUserId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,722 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
[DefaultExecutionOrder(10000)]
|
||||
internal sealed class BriskDefaultErrorDialog : MonoBehaviour
|
||||
{
|
||||
private const string HostName = "BriskDefaultErrorDialog";
|
||||
private const float OpenDuration = 0.24f;
|
||||
private const float CloseDuration = 0.2f;
|
||||
|
||||
private static readonly Color Ink = ColorFromHex(0x05060a);
|
||||
private static readonly Color InkSoft = ColorFromHex(0x1b1d24);
|
||||
private static readonly Color Muted = ColorFromHex(0x5f6572);
|
||||
private static readonly Color Line = ColorFromHex(0xdde2ea);
|
||||
private static readonly Color Paper = ColorFromHex(0xffffff);
|
||||
private static readonly Color Soft = ColorFromHex(0xf7f8fb);
|
||||
private static readonly Color Blue = ColorFromHex(0x2563ff);
|
||||
private static readonly Color Teal = ColorFromHex(0x23d6c8);
|
||||
private static readonly Color Violet = ColorFromHex(0x8b5cff);
|
||||
private static readonly Color Dark = ColorFromHex(0x050506);
|
||||
|
||||
private static BriskDefaultErrorDialog s_instance;
|
||||
|
||||
private UIDocument _document;
|
||||
private PanelSettings _panelSettings;
|
||||
private VisualElement _root;
|
||||
private VisualElement _stage;
|
||||
private VisualElement _panel;
|
||||
private Label _eyebrowLabel;
|
||||
private Label _titleLabel;
|
||||
private Label _messageLabel;
|
||||
private VisualElement _spacer;
|
||||
private VisualElement _divider;
|
||||
private VisualElement _confirmButton;
|
||||
private Label _confirmLabel;
|
||||
private Action _onConfirm;
|
||||
private Coroutine _animation;
|
||||
private bool _visible;
|
||||
private bool _closeOnBackdrop;
|
||||
private bool _closeOnEscape;
|
||||
private GUIStyle _legacyTitleStyle;
|
||||
private GUIStyle _legacyMessageStyle;
|
||||
private GUIStyle _legacyEyebrowStyle;
|
||||
private GUIStyle _legacyButtonStyle;
|
||||
private Texture2D _legacyButtonTexture;
|
||||
private Texture2D _legacyButtonHoverTexture;
|
||||
|
||||
internal static bool IsVisible => s_instance != null && s_instance._visible;
|
||||
|
||||
internal static void RenderTopmostLegacyOverlay()
|
||||
{
|
||||
if (s_instance == null || !s_instance._visible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
s_instance.DrawLegacyOverlay();
|
||||
}
|
||||
|
||||
public static void Show(string title, string message, string confirmText, Action onConfirm = null, bool closeOnBackdrop = false, bool closeOnEscape = false)
|
||||
{
|
||||
var host = EnsureInstance();
|
||||
host.ShowInternal(
|
||||
string.IsNullOrWhiteSpace(title) ? "Brisk" : title,
|
||||
string.IsNullOrWhiteSpace(message) ? "服务暂时不可用,请稍后再试。" : message,
|
||||
string.IsNullOrWhiteSpace(confirmText) ? "知道了" : confirmText,
|
||||
onConfirm,
|
||||
closeOnBackdrop,
|
||||
closeOnEscape);
|
||||
}
|
||||
|
||||
private static BriskDefaultErrorDialog EnsureInstance()
|
||||
{
|
||||
if (s_instance != null)
|
||||
{
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
var existing = GameObject.Find(HostName);
|
||||
if (existing != null)
|
||||
{
|
||||
s_instance = existing.GetComponent<BriskDefaultErrorDialog>();
|
||||
if (s_instance != null)
|
||||
{
|
||||
return s_instance;
|
||||
}
|
||||
}
|
||||
|
||||
var gameObject = new GameObject(HostName);
|
||||
DontDestroyOnLoad(gameObject);
|
||||
s_instance = gameObject.AddComponent<BriskDefaultErrorDialog>();
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
private void ShowInternal(string title, string message, string confirmText, Action onConfirm, bool closeOnBackdrop, bool closeOnEscape)
|
||||
{
|
||||
EnsureDocument();
|
||||
ApplySafeAreaPadding();
|
||||
|
||||
_titleLabel.text = title;
|
||||
_messageLabel.text = message;
|
||||
_confirmLabel.text = confirmText;
|
||||
_onConfirm = onConfirm;
|
||||
_closeOnBackdrop = closeOnBackdrop;
|
||||
_closeOnEscape = closeOnEscape;
|
||||
_visible = true;
|
||||
|
||||
StartDialogAnimation(PlayOpenAnimation());
|
||||
}
|
||||
|
||||
private void EnsureDocument()
|
||||
{
|
||||
if (_document != null)
|
||||
{
|
||||
ApplyResponsiveLayout();
|
||||
return;
|
||||
}
|
||||
|
||||
_panelSettings = ScriptableObject.CreateInstance<PanelSettings>();
|
||||
_panelSettings.name = HostName + "PanelSettings";
|
||||
_panelSettings.scaleMode = PanelScaleMode.ConstantPixelSize;
|
||||
_panelSettings.scale = 1f;
|
||||
_panelSettings.sortingOrder = 32767f;
|
||||
|
||||
_document = gameObject.AddComponent<UIDocument>();
|
||||
_document.panelSettings = _panelSettings;
|
||||
_document.sortingOrder = 32767f;
|
||||
|
||||
BuildVisualTree(_document.rootVisualElement);
|
||||
ApplyResponsiveLayout();
|
||||
}
|
||||
|
||||
private void BuildVisualTree(VisualElement documentRoot)
|
||||
{
|
||||
documentRoot.Clear();
|
||||
documentRoot.pickingMode = PickingMode.Ignore;
|
||||
documentRoot.style.position = Position.Absolute;
|
||||
documentRoot.style.left = 0f;
|
||||
documentRoot.style.top = 0f;
|
||||
documentRoot.style.right = 0f;
|
||||
documentRoot.style.bottom = 0f;
|
||||
documentRoot.style.width = Length.Percent(100f);
|
||||
documentRoot.style.height = Length.Percent(100f);
|
||||
documentRoot.style.flexGrow = 1f;
|
||||
|
||||
_root = new VisualElement
|
||||
{
|
||||
pickingMode = PickingMode.Position,
|
||||
focusable = true
|
||||
};
|
||||
_root.style.position = Position.Absolute;
|
||||
_root.style.left = 0;
|
||||
_root.style.top = 0;
|
||||
_root.style.right = 0;
|
||||
_root.style.bottom = 0;
|
||||
_root.style.display = DisplayStyle.None;
|
||||
_root.style.opacity = 0f;
|
||||
_root.style.backgroundColor = new Color(0.02f, 0.02f, 0.024f, 0.58f);
|
||||
_root.style.justifyContent = Justify.Center;
|
||||
_root.style.alignItems = Align.Center;
|
||||
_root.RegisterCallback<KeyDownEvent>(HandleRootKeyDown);
|
||||
_root.RegisterCallback<PointerDownEvent>(evt =>
|
||||
{
|
||||
if (_closeOnBackdrop && evt.target == _root)
|
||||
{
|
||||
BeginClose();
|
||||
}
|
||||
|
||||
evt.StopPropagation();
|
||||
});
|
||||
documentRoot.Add(_root);
|
||||
|
||||
_stage = new VisualElement();
|
||||
_stage.pickingMode = PickingMode.Ignore;
|
||||
_stage.style.position = Position.Relative;
|
||||
_stage.style.width = Length.Percent(82f);
|
||||
_stage.style.maxWidth = 900f;
|
||||
_stage.style.opacity = 0f;
|
||||
_stage.style.marginTop = 14f;
|
||||
_root.Add(_stage);
|
||||
|
||||
var shadow = new VisualElement();
|
||||
shadow.pickingMode = PickingMode.Ignore;
|
||||
shadow.style.position = Position.Absolute;
|
||||
shadow.style.left = 0f;
|
||||
shadow.style.right = 0f;
|
||||
shadow.style.top = 10f;
|
||||
shadow.style.bottom = -10f;
|
||||
shadow.style.backgroundColor = new Color(0f, 0f, 0f, 0.09f);
|
||||
shadow.style.borderTopLeftRadius = 18f;
|
||||
shadow.style.borderTopRightRadius = 18f;
|
||||
shadow.style.borderBottomLeftRadius = 18f;
|
||||
shadow.style.borderBottomRightRadius = 18f;
|
||||
_stage.Add(shadow);
|
||||
|
||||
_panel = new VisualElement
|
||||
{
|
||||
pickingMode = PickingMode.Position
|
||||
};
|
||||
_panel.style.width = Length.Percent(100f);
|
||||
_panel.style.minHeight = 360f;
|
||||
_panel.style.paddingTop = 36f;
|
||||
_panel.style.paddingRight = 36f;
|
||||
_panel.style.paddingBottom = 36f;
|
||||
_panel.style.paddingLeft = 36f;
|
||||
_panel.style.backgroundColor = Paper;
|
||||
_panel.style.borderTopWidth = 1f;
|
||||
_panel.style.borderRightWidth = 1f;
|
||||
_panel.style.borderBottomWidth = 1f;
|
||||
_panel.style.borderLeftWidth = 1f;
|
||||
_panel.style.borderTopColor = Line;
|
||||
_panel.style.borderRightColor = Line;
|
||||
_panel.style.borderBottomColor = Line;
|
||||
_panel.style.borderLeftColor = Line;
|
||||
_panel.style.borderTopLeftRadius = 18f;
|
||||
_panel.style.borderTopRightRadius = 18f;
|
||||
_panel.style.borderBottomLeftRadius = 18f;
|
||||
_panel.style.borderBottomRightRadius = 18f;
|
||||
_panel.style.overflow = Overflow.Hidden;
|
||||
_stage.Add(_panel);
|
||||
|
||||
var accent = new VisualElement();
|
||||
accent.pickingMode = PickingMode.Ignore;
|
||||
accent.style.position = Position.Absolute;
|
||||
accent.style.top = 0f;
|
||||
accent.style.left = 0f;
|
||||
accent.style.right = 0f;
|
||||
accent.style.height = 2f;
|
||||
accent.style.flexDirection = FlexDirection.Row;
|
||||
_panel.Add(accent);
|
||||
AddAccentSegment(accent, Blue);
|
||||
AddAccentSegment(accent, Teal);
|
||||
AddAccentSegment(accent, Violet);
|
||||
|
||||
_eyebrowLabel = new Label("BRISK SERVICE");
|
||||
_eyebrowLabel.style.color = Muted;
|
||||
_eyebrowLabel.style.fontSize = 13f;
|
||||
_eyebrowLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
_eyebrowLabel.style.letterSpacing = 1.4f;
|
||||
_eyebrowLabel.style.height = 18f;
|
||||
_eyebrowLabel.style.flexShrink = 0f;
|
||||
_eyebrowLabel.style.marginBottom = 12f;
|
||||
_panel.Add(_eyebrowLabel);
|
||||
|
||||
_titleLabel = new Label();
|
||||
_titleLabel.style.color = Ink;
|
||||
_titleLabel.style.fontSize = 30f;
|
||||
_titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
_titleLabel.style.whiteSpace = WhiteSpace.Normal;
|
||||
_titleLabel.style.unityTextAlign = TextAnchor.MiddleLeft;
|
||||
_titleLabel.style.minHeight = 40f;
|
||||
_titleLabel.style.flexShrink = 0f;
|
||||
_titleLabel.style.marginBottom = 16f;
|
||||
_panel.Add(_titleLabel);
|
||||
|
||||
_messageLabel = new Label();
|
||||
_messageLabel.style.color = Muted;
|
||||
_messageLabel.style.fontSize = 21f;
|
||||
_messageLabel.style.whiteSpace = WhiteSpace.Normal;
|
||||
_messageLabel.style.unityTextAlign = TextAnchor.UpperLeft;
|
||||
_messageLabel.style.minHeight = 64f;
|
||||
_messageLabel.style.flexShrink = 0f;
|
||||
_messageLabel.style.marginBottom = 22f;
|
||||
_panel.Add(_messageLabel);
|
||||
|
||||
_spacer = new VisualElement();
|
||||
_spacer.pickingMode = PickingMode.Ignore;
|
||||
_spacer.style.flexGrow = 1f;
|
||||
_spacer.style.minHeight = 18f;
|
||||
_panel.Add(_spacer);
|
||||
|
||||
_divider = new VisualElement();
|
||||
_divider.style.height = 1f;
|
||||
_divider.style.flexShrink = 0f;
|
||||
_divider.style.backgroundColor = Line;
|
||||
_divider.style.marginBottom = 22f;
|
||||
_panel.Add(_divider);
|
||||
|
||||
_confirmButton = new VisualElement
|
||||
{
|
||||
focusable = true,
|
||||
pickingMode = PickingMode.Position
|
||||
};
|
||||
_confirmButton.style.height = 58f;
|
||||
_confirmButton.style.minHeight = 58f;
|
||||
_confirmButton.style.flexShrink = 0f;
|
||||
_confirmButton.style.borderTopLeftRadius = 24f;
|
||||
_confirmButton.style.borderTopRightRadius = 24f;
|
||||
_confirmButton.style.borderBottomLeftRadius = 24f;
|
||||
_confirmButton.style.borderBottomRightRadius = 24f;
|
||||
_confirmButton.style.backgroundColor = Dark;
|
||||
_confirmButton.style.justifyContent = Justify.Center;
|
||||
_confirmButton.style.alignItems = Align.Center;
|
||||
_confirmButton.RegisterCallback<PointerEnterEvent>(_ => _confirmButton.style.backgroundColor = InkSoft);
|
||||
_confirmButton.RegisterCallback<PointerLeaveEvent>(_ => _confirmButton.style.backgroundColor = Dark);
|
||||
_confirmButton.RegisterCallback<PointerDownEvent>(_ =>
|
||||
{
|
||||
_confirmButton.style.backgroundColor = Ink;
|
||||
_confirmButton.style.marginTop = 1f;
|
||||
});
|
||||
_confirmButton.RegisterCallback<PointerUpEvent>(_ =>
|
||||
{
|
||||
_confirmButton.style.backgroundColor = InkSoft;
|
||||
_confirmButton.style.marginTop = 0f;
|
||||
});
|
||||
_confirmButton.RegisterCallback<ClickEvent>(_ => BeginClose());
|
||||
_confirmButton.RegisterCallback<KeyDownEvent>(HandleButtonKeyDown);
|
||||
_panel.Add(_confirmButton);
|
||||
|
||||
_confirmLabel = new Label();
|
||||
_confirmLabel.style.color = Color.white;
|
||||
_confirmLabel.style.fontSize = 18f;
|
||||
_confirmLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
_confirmLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||
_confirmLabel.style.height = 58f;
|
||||
_confirmButton.Add(_confirmLabel);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_visible && _root != null)
|
||||
{
|
||||
ApplySafeAreaPadding();
|
||||
ApplyResponsiveLayout();
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginClose()
|
||||
{
|
||||
if (!_visible || _root == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StartDialogAnimation(PlayCloseAnimation());
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
if (!_visible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DrawLegacyOverlay();
|
||||
}
|
||||
|
||||
private void DrawLegacyOverlay()
|
||||
{
|
||||
EnsureLegacyStyles();
|
||||
|
||||
var previousDepth = GUI.depth;
|
||||
var previousMatrix = GUI.matrix;
|
||||
var previousColor = GUI.color;
|
||||
GUI.depth = -10000;
|
||||
GUI.matrix = Matrix4x4.identity;
|
||||
|
||||
DrawLegacyRect(new Rect(0f, 0f, Screen.width, Screen.height), new Color(0.02f, 0.02f, 0.024f, 0.58f));
|
||||
|
||||
var scale = GetReadableScale();
|
||||
ApplyLegacyScale(scale);
|
||||
|
||||
var margin = GetDialogMargin(scale);
|
||||
var padding = GetDialogPadding(scale);
|
||||
var buttonHeight = GetDialogButtonHeight(scale);
|
||||
var width = GetDialogWidth(margin);
|
||||
var contentWidth = width - padding * 2f;
|
||||
var titleHeight = Mathf.Max(34f, _legacyTitleStyle.CalcHeight(new GUIContent(_titleLabel.text), contentWidth));
|
||||
var messageHeight = Mathf.Max(48f, _legacyMessageStyle.CalcHeight(new GUIContent(_messageLabel.text), contentWidth));
|
||||
var minHeight = GetDialogMinHeight(scale, margin);
|
||||
var contentHeight = padding * 2f + 18f * scale + 12f * scale + titleHeight + 16f * scale + messageHeight + 24f * scale + 1f + 22f * scale + buttonHeight;
|
||||
var height = Mathf.Clamp(contentHeight, minHeight, Mathf.Max(minHeight, Screen.height - margin * 2f));
|
||||
var rect = new Rect((Screen.width - width) * 0.5f, (Screen.height - height) * 0.5f, width, height);
|
||||
|
||||
DrawLegacyPanel(rect);
|
||||
|
||||
var x = rect.x + padding;
|
||||
var y = rect.y + padding;
|
||||
GUI.Label(new Rect(x, y, contentWidth, 18f * scale), "BRISK SERVICE", _legacyEyebrowStyle);
|
||||
y += 30f * scale;
|
||||
GUI.Label(new Rect(x, y, contentWidth, titleHeight), _titleLabel.text, _legacyTitleStyle);
|
||||
y += titleHeight + 16f * scale;
|
||||
GUI.Label(new Rect(x, y, contentWidth, messageHeight), _messageLabel.text, _legacyMessageStyle);
|
||||
|
||||
DrawLegacyRect(new Rect(x, rect.yMax - padding - buttonHeight - 22f * scale, contentWidth, 1f), Line);
|
||||
var buttonRect = new Rect(x, rect.yMax - padding - buttonHeight, contentWidth, buttonHeight);
|
||||
if (GUI.Button(buttonRect, _confirmLabel.text, _legacyButtonStyle))
|
||||
{
|
||||
BeginClose();
|
||||
}
|
||||
|
||||
ConsumeBlockingEvent();
|
||||
|
||||
GUI.color = previousColor;
|
||||
GUI.matrix = previousMatrix;
|
||||
GUI.depth = previousDepth;
|
||||
}
|
||||
|
||||
private IEnumerator PlayOpenAnimation()
|
||||
{
|
||||
_root.style.display = DisplayStyle.Flex;
|
||||
_root.style.opacity = 0f;
|
||||
_stage.style.opacity = 0f;
|
||||
_stage.style.marginTop = 14f;
|
||||
|
||||
yield return null;
|
||||
_root.Focus();
|
||||
|
||||
var elapsed = 0f;
|
||||
while (elapsed < OpenDuration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
var eased = EaseOutCubic(Mathf.Clamp01(elapsed / OpenDuration));
|
||||
_root.style.opacity = eased;
|
||||
_stage.style.opacity = eased;
|
||||
_stage.style.marginTop = Mathf.Lerp(14f, 0f, eased);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
_root.style.opacity = 1f;
|
||||
_stage.style.opacity = 1f;
|
||||
_stage.style.marginTop = 0f;
|
||||
}
|
||||
|
||||
private IEnumerator PlayCloseAnimation()
|
||||
{
|
||||
var callback = _onConfirm;
|
||||
_onConfirm = null;
|
||||
|
||||
var elapsed = 0f;
|
||||
while (elapsed < CloseDuration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
var eased = EaseInCubic(Mathf.Clamp01(elapsed / CloseDuration));
|
||||
var alpha = 1f - eased;
|
||||
_root.style.opacity = alpha;
|
||||
_stage.style.opacity = alpha;
|
||||
_stage.style.marginTop = Mathf.Lerp(0f, 10f, eased);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
_root.style.display = DisplayStyle.None;
|
||||
_visible = false;
|
||||
callback?.Invoke();
|
||||
}
|
||||
|
||||
private void StartDialogAnimation(IEnumerator animation)
|
||||
{
|
||||
if (_animation != null)
|
||||
{
|
||||
StopCoroutine(_animation);
|
||||
}
|
||||
|
||||
_animation = StartCoroutine(animation);
|
||||
}
|
||||
|
||||
private void ApplySafeAreaPadding()
|
||||
{
|
||||
var safeArea = Screen.safeArea;
|
||||
var basePadding = Mathf.Max(20f, Mathf.Min(Screen.width, Screen.height) * 0.04f);
|
||||
_root.style.paddingLeft = Mathf.Max(basePadding, safeArea.xMin + basePadding);
|
||||
_root.style.paddingRight = Mathf.Max(basePadding, Screen.width - safeArea.xMax + basePadding);
|
||||
_root.style.paddingTop = Mathf.Max(basePadding, Screen.height - safeArea.yMax + basePadding);
|
||||
_root.style.paddingBottom = Mathf.Max(basePadding, safeArea.yMin + basePadding);
|
||||
}
|
||||
|
||||
private void ApplyResponsiveLayout()
|
||||
{
|
||||
if (_panel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var scale = GetReadableScale();
|
||||
var margin = GetDialogMargin(scale);
|
||||
var maxWidth = Mathf.Min(900f, Mathf.Max(320f, Screen.width - margin * 2f));
|
||||
var minHeight = GetDialogMinHeight(scale, margin);
|
||||
var padding = GetDialogPadding(scale);
|
||||
var buttonHeight = GetDialogButtonHeight(scale);
|
||||
|
||||
_stage.style.width = Length.Percent(82f);
|
||||
_stage.style.maxWidth = maxWidth;
|
||||
_panel.style.minHeight = minHeight;
|
||||
_panel.style.paddingTop = padding;
|
||||
_panel.style.paddingRight = padding;
|
||||
_panel.style.paddingBottom = padding;
|
||||
_panel.style.paddingLeft = padding;
|
||||
_panel.style.borderTopLeftRadius = 20f * scale;
|
||||
_panel.style.borderTopRightRadius = 20f * scale;
|
||||
_panel.style.borderBottomLeftRadius = 20f * scale;
|
||||
_panel.style.borderBottomRightRadius = 20f * scale;
|
||||
|
||||
_eyebrowLabel.style.fontSize = 12f * scale;
|
||||
_eyebrowLabel.style.height = 18f * scale;
|
||||
_eyebrowLabel.style.marginBottom = 12f * scale;
|
||||
_titleLabel.style.fontSize = 28f * scale;
|
||||
_titleLabel.style.minHeight = 40f * scale;
|
||||
_titleLabel.style.marginBottom = 16f * scale;
|
||||
_messageLabel.style.fontSize = 20f * scale;
|
||||
_messageLabel.style.minHeight = 64f * scale;
|
||||
_messageLabel.style.marginBottom = 22f * scale;
|
||||
_spacer.style.minHeight = 18f * scale;
|
||||
_divider.style.marginBottom = 22f * scale;
|
||||
_confirmButton.style.height = buttonHeight;
|
||||
_confirmButton.style.minHeight = buttonHeight;
|
||||
_confirmButton.style.borderTopLeftRadius = buttonHeight * 0.5f;
|
||||
_confirmButton.style.borderTopRightRadius = buttonHeight * 0.5f;
|
||||
_confirmButton.style.borderBottomLeftRadius = buttonHeight * 0.5f;
|
||||
_confirmButton.style.borderBottomRightRadius = buttonHeight * 0.5f;
|
||||
_confirmLabel.style.fontSize = 17f * scale;
|
||||
_confirmLabel.style.height = buttonHeight;
|
||||
}
|
||||
|
||||
private void HandleRootKeyDown(KeyDownEvent evt)
|
||||
{
|
||||
if (_closeOnEscape && evt.keyCode == KeyCode.Escape)
|
||||
{
|
||||
BeginClose();
|
||||
evt.StopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
private void HandleButtonKeyDown(KeyDownEvent evt)
|
||||
{
|
||||
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter || evt.keyCode == KeyCode.Space)
|
||||
{
|
||||
BeginClose();
|
||||
evt.StopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (s_instance == this)
|
||||
{
|
||||
s_instance = null;
|
||||
}
|
||||
|
||||
if (_panelSettings != null)
|
||||
{
|
||||
Destroy(_panelSettings);
|
||||
_panelSettings = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureLegacyStyles()
|
||||
{
|
||||
if (_legacyTitleStyle != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_legacyButtonTexture = CreateTexture(Dark);
|
||||
_legacyButtonHoverTexture = CreateTexture(InkSoft);
|
||||
|
||||
_legacyEyebrowStyle = new GUIStyle
|
||||
{
|
||||
fontStyle = FontStyle.Bold,
|
||||
normal = { textColor = Muted }
|
||||
};
|
||||
|
||||
_legacyTitleStyle = new GUIStyle
|
||||
{
|
||||
fontStyle = FontStyle.Bold,
|
||||
wordWrap = true,
|
||||
normal = { textColor = Ink }
|
||||
};
|
||||
|
||||
_legacyMessageStyle = new GUIStyle
|
||||
{
|
||||
wordWrap = true,
|
||||
normal = { textColor = Muted }
|
||||
};
|
||||
|
||||
_legacyButtonStyle = new GUIStyle
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontStyle = FontStyle.Bold,
|
||||
border = new RectOffset(24, 24, 24, 24),
|
||||
normal =
|
||||
{
|
||||
background = _legacyButtonTexture,
|
||||
textColor = Color.white
|
||||
},
|
||||
hover =
|
||||
{
|
||||
background = _legacyButtonHoverTexture,
|
||||
textColor = Color.white
|
||||
},
|
||||
active =
|
||||
{
|
||||
background = _legacyButtonHoverTexture,
|
||||
textColor = Color.white
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void ApplyLegacyScale(float scale)
|
||||
{
|
||||
_legacyEyebrowStyle.fontSize = Mathf.RoundToInt(12f * scale);
|
||||
_legacyTitleStyle.fontSize = Mathf.RoundToInt(28f * scale);
|
||||
_legacyMessageStyle.fontSize = Mathf.RoundToInt(20f * scale);
|
||||
_legacyButtonStyle.fontSize = Mathf.RoundToInt(17f * scale);
|
||||
}
|
||||
|
||||
private void DrawLegacyPanel(Rect rect)
|
||||
{
|
||||
DrawLegacyRect(new Rect(rect.x, rect.y + 12f, rect.width, rect.height), new Color(0f, 0f, 0f, 0.08f));
|
||||
DrawLegacyRect(rect, Paper);
|
||||
DrawLegacyRect(new Rect(rect.x, rect.y, rect.width, 1f), Line);
|
||||
DrawLegacyRect(new Rect(rect.x, rect.yMax - 1f, rect.width, 1f), Line);
|
||||
DrawLegacyRect(new Rect(rect.x, rect.y, 1f, rect.height), Line);
|
||||
DrawLegacyRect(new Rect(rect.xMax - 1f, rect.y, 1f, rect.height), Line);
|
||||
|
||||
var segment = rect.width / 3f;
|
||||
DrawLegacyRect(new Rect(rect.x, rect.y, segment, 2f), Blue);
|
||||
DrawLegacyRect(new Rect(rect.x + segment, rect.y, segment, 2f), Teal);
|
||||
DrawLegacyRect(new Rect(rect.x + segment * 2f, rect.y, rect.width - segment * 2f, 2f), Violet);
|
||||
}
|
||||
|
||||
private static void ConsumeBlockingEvent()
|
||||
{
|
||||
var evt = Event.current;
|
||||
if (evt == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (evt.type)
|
||||
{
|
||||
case EventType.MouseDown:
|
||||
case EventType.MouseUp:
|
||||
case EventType.MouseDrag:
|
||||
case EventType.ScrollWheel:
|
||||
case EventType.KeyDown:
|
||||
case EventType.KeyUp:
|
||||
evt.Use();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static Texture2D CreateTexture(Color color)
|
||||
{
|
||||
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false)
|
||||
{
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
texture.SetPixel(0, 0, color);
|
||||
texture.Apply();
|
||||
return texture;
|
||||
}
|
||||
|
||||
private static void DrawLegacyRect(Rect rect, Color color)
|
||||
{
|
||||
var previousColor = GUI.color;
|
||||
GUI.color = color;
|
||||
GUI.DrawTexture(rect, Texture2D.whiteTexture);
|
||||
GUI.color = previousColor;
|
||||
}
|
||||
|
||||
private static void AddAccentSegment(VisualElement parent, Color color)
|
||||
{
|
||||
var segment = new VisualElement();
|
||||
segment.style.flexGrow = 1f;
|
||||
segment.style.backgroundColor = color;
|
||||
parent.Add(segment);
|
||||
}
|
||||
|
||||
private static float GetReadableScale()
|
||||
{
|
||||
return Mathf.Clamp(Mathf.Min(Screen.width, Screen.height) / 720f, 1f, 1.22f);
|
||||
}
|
||||
|
||||
private static float GetDialogMargin(float scale)
|
||||
{
|
||||
return Mathf.Max(24f * scale, Mathf.Min(Screen.width, Screen.height) * 0.06f);
|
||||
}
|
||||
|
||||
private static float GetDialogPadding(float scale)
|
||||
{
|
||||
return 34f * scale;
|
||||
}
|
||||
|
||||
private static float GetDialogButtonHeight(float scale)
|
||||
{
|
||||
return 56f * scale;
|
||||
}
|
||||
|
||||
private static float GetDialogWidth(float margin)
|
||||
{
|
||||
return Mathf.Clamp(Screen.width * 0.82f, 320f, Mathf.Max(320f, Mathf.Min(900f, Screen.width - margin * 2f)));
|
||||
}
|
||||
|
||||
private static float GetDialogMinHeight(float scale, float margin)
|
||||
{
|
||||
var availableHeight = Mathf.Max(320f, Screen.height - margin * 2f);
|
||||
return Mathf.Min(availableHeight, Mathf.Clamp(Screen.height * 0.24f, 360f * scale, 620f));
|
||||
}
|
||||
|
||||
private static float EaseOutCubic(float t)
|
||||
{
|
||||
return 1f - Mathf.Pow(1f - t, 3f);
|
||||
}
|
||||
|
||||
private static float EaseInCubic(float t)
|
||||
{
|
||||
return t * t * t;
|
||||
}
|
||||
|
||||
private static Color ColorFromHex(int rgb)
|
||||
{
|
||||
var r = ((rgb >> 16) & 0xff) / 255f;
|
||||
var g = ((rgb >> 8) & 0xff) / 255f;
|
||||
var b = (rgb & 0xff) / 255f;
|
||||
return new Color(r, g, b, 1f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
public sealed class BriskDefaultErrorPresenter : IBriskErrorPresenter
|
||||
{
|
||||
private static readonly BriskDefaultErrorPresenter InstanceValue = new BriskDefaultErrorPresenter();
|
||||
|
||||
private BriskDefaultErrorPresenter()
|
||||
{
|
||||
}
|
||||
|
||||
public static BriskDefaultErrorPresenter Instance => InstanceValue;
|
||||
|
||||
public void ShowBlockingError(BriskBlockingException exception)
|
||||
{
|
||||
var message = GetBlockingMessage(exception);
|
||||
|
||||
BriskDefaultErrorDialog.Show(
|
||||
title: GetBlockingTitle(exception),
|
||||
message: message,
|
||||
confirmText: "知道了",
|
||||
onConfirm: () =>
|
||||
{
|
||||
var exitHandler = Brisk.IsInitialized ? Brisk.GetRequiredContext().ExitHandler : null;
|
||||
if (exitHandler != null)
|
||||
{
|
||||
exitHandler.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
Application.Quit();
|
||||
});
|
||||
|
||||
Debug.LogError("[Brisk] Blocking error: " + message);
|
||||
}
|
||||
|
||||
public void ShowAuthExpired(BriskAuthExpiredException exception)
|
||||
{
|
||||
var message = GetAuthExpiredMessage(exception);
|
||||
|
||||
BriskDefaultErrorDialog.Show(
|
||||
title: "登录已过期",
|
||||
message: message,
|
||||
confirmText: "知道了");
|
||||
|
||||
Debug.LogWarning("[Brisk] Auth expired: " + (exception != null ? exception.Message : message));
|
||||
}
|
||||
|
||||
private static string GetBlockingTitle(BriskBlockingException exception)
|
||||
{
|
||||
if (exception is BriskMaintenanceException)
|
||||
{
|
||||
return "服务维护中";
|
||||
}
|
||||
|
||||
if (exception is BriskAccountBannedException)
|
||||
{
|
||||
return "账号受限";
|
||||
}
|
||||
|
||||
if (exception is BriskClientUpdateRequiredException)
|
||||
{
|
||||
return "需要更新";
|
||||
}
|
||||
|
||||
return "服务暂不可用";
|
||||
}
|
||||
|
||||
private static string GetBlockingMessage(BriskBlockingException exception)
|
||||
{
|
||||
if (exception is BriskMaintenanceException)
|
||||
{
|
||||
return "服务正在维护,请稍后再试。";
|
||||
}
|
||||
|
||||
if (exception is BriskAccountBannedException)
|
||||
{
|
||||
return "当前账号暂时无法使用该服务。";
|
||||
}
|
||||
|
||||
if (exception is BriskClientUpdateRequiredException)
|
||||
{
|
||||
return "当前版本过低,请更新后继续。";
|
||||
}
|
||||
|
||||
return "服务暂时不可用,请稍后再试。";
|
||||
}
|
||||
|
||||
private static string GetAuthExpiredMessage(BriskAuthExpiredException exception)
|
||||
{
|
||||
if (exception != null && exception.Message != null && exception.Message.IndexOf("missing", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return "当前登录状态不可用,请重新登录。";
|
||||
}
|
||||
|
||||
return "登录状态已失效,请重新登录后继续。";
|
||||
}
|
||||
}
|
||||
@@ -51,23 +51,26 @@ public sealed class BriskHttpClient
|
||||
|
||||
public async Task<Dictionary<string, object>> PostMultipartAsync(string path, List<IMultipartFormSection> formSections, bool auth = false)
|
||||
{
|
||||
using (var request = UnityWebRequest.Post(BuildUrl(path, null), formSections))
|
||||
return await SendMultipartAsync(UnityWebRequest.kHttpVerbPOST, path, formSections, auth);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, object>> PutMultipartAsync(string path, List<IMultipartFormSection> formSections, bool auth = false)
|
||||
{
|
||||
return await SendMultipartAsync(UnityWebRequest.kHttpVerbPUT, path, formSections, auth);
|
||||
}
|
||||
|
||||
public async Task<BriskBinaryResponse> GetBytesAsync(string path, Dictionary<string, string> query = null, bool auth = false)
|
||||
{
|
||||
using (var request = new UnityWebRequest(BuildUrl(path, query), UnityWebRequest.kHttpVerbGET))
|
||||
{
|
||||
request.downloadHandler = new DownloadHandlerBuffer();
|
||||
|
||||
if (auth)
|
||||
{
|
||||
AddAuthorizationHeader(request);
|
||||
}
|
||||
|
||||
request.SetRequestHeader("Accept", "application/json");
|
||||
await SendRequestAsync(request);
|
||||
return ParseEnvelope(request) as Dictionary<string, object> ?? new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BriskBinaryResponse> GetBytesAsync(string path, Dictionary<string, string> query = null, bool auth = false)
|
||||
{
|
||||
using (var request = BuildRequest(UnityWebRequest.kHttpVerbGET, path, query, null, auth))
|
||||
{
|
||||
request.SetRequestHeader("Accept", "*/*");
|
||||
await SendRequestAsync(request);
|
||||
EnsureSuccessOrThrow(request);
|
||||
return new BriskBinaryResponse
|
||||
@@ -93,6 +96,23 @@ public sealed class BriskHttpClient
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, object>> SendMultipartAsync(string method, string path, List<IMultipartFormSection> formSections, bool auth)
|
||||
{
|
||||
using (var request = UnityWebRequest.Post(BuildUrl(path, null), formSections))
|
||||
{
|
||||
request.method = method;
|
||||
|
||||
if (auth)
|
||||
{
|
||||
AddAuthorizationHeader(request);
|
||||
}
|
||||
|
||||
request.SetRequestHeader("Accept", "application/json");
|
||||
await SendRequestAsync(request);
|
||||
return ParseEnvelope(request) as Dictionary<string, object> ?? new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
private UnityWebRequest BuildRequest(string method, string path, Dictionary<string, string> query, object body, bool auth)
|
||||
{
|
||||
var url = BuildUrl(path, query);
|
||||
@@ -237,10 +237,23 @@ internal static class BriskModelMapper
|
||||
|
||||
return new BriskSpaceView
|
||||
{
|
||||
ProjectAccountId = BriskValueReader.GetString(data, "project_account_id"),
|
||||
PlayerId = BriskValueReader.GetString(data, "player_id"),
|
||||
LoginProvider = BriskValueReader.GetString(data, "login_provider"),
|
||||
LoginUserId = BriskValueReader.GetString(data, "login_user_id"),
|
||||
Payload = BriskValueReader.GetDictionary(data, "payload_json") ?? BriskValueReader.GetDictionary(data, "payload")
|
||||
Nickname = BriskValueReader.GetString(data, "nickname"),
|
||||
AvatarUrl = BriskValueReader.GetString(data, "avatar_url"),
|
||||
ContentExists = BriskValueReader.GetBool(data, "content_exists"),
|
||||
ContentVersion = BriskValueReader.GetLong(data, "content_version"),
|
||||
ContentType = BriskValueReader.GetString(data, "content_type"),
|
||||
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
|
||||
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
|
||||
LikeCount = BriskValueReader.GetLong(data, "like_count"),
|
||||
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
|
||||
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
|
||||
LikedByMe = BriskValueReader.GetBool(data, "liked_by_me"),
|
||||
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at"),
|
||||
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -253,8 +266,58 @@ internal static class BriskModelMapper
|
||||
|
||||
return new BriskSpaceStats
|
||||
{
|
||||
LikeCount = BriskValueReader.GetInt(data, "like_count"),
|
||||
VisitCount = BriskValueReader.GetInt(data, "visit_count")
|
||||
ProjectAccountId = BriskValueReader.GetString(data, "project_account_id"),
|
||||
PlayerId = BriskValueReader.GetString(data, "player_id"),
|
||||
LoginProvider = BriskValueReader.GetString(data, "login_provider"),
|
||||
LoginUserId = BriskValueReader.GetString(data, "login_user_id"),
|
||||
Nickname = BriskValueReader.GetString(data, "nickname"),
|
||||
AvatarUrl = BriskValueReader.GetString(data, "avatar_url"),
|
||||
ContentExists = BriskValueReader.GetBool(data, "content_exists"),
|
||||
ContentVersion = BriskValueReader.GetLong(data, "content_version"),
|
||||
ContentType = BriskValueReader.GetString(data, "content_type"),
|
||||
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
|
||||
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
|
||||
LikeCount = BriskValueReader.GetLong(data, "like_count"),
|
||||
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
|
||||
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
|
||||
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at"),
|
||||
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
|
||||
};
|
||||
}
|
||||
|
||||
public static BriskSpaceContentUpdateResult ToSpaceContentUpdateResult(Dictionary<string, object> data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BriskSpaceContentUpdateResult
|
||||
{
|
||||
PlayerId = BriskValueReader.GetString(data, "player_id"),
|
||||
ContentVersion = BriskValueReader.GetLong(data, "content_version"),
|
||||
ContentType = BriskValueReader.GetString(data, "content_type"),
|
||||
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
|
||||
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
|
||||
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
|
||||
};
|
||||
}
|
||||
|
||||
public static BriskSpaceLikeResult ToSpaceLikeResult(Dictionary<string, object> data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BriskSpaceLikeResult
|
||||
{
|
||||
Liked = BriskValueReader.GetBool(data, "liked"),
|
||||
Created = BriskValueReader.GetBool(data, "created"),
|
||||
LikedByMe = BriskValueReader.GetBool(data, "liked_by_me"),
|
||||
LikeCount = BriskValueReader.GetLong(data, "like_count"),
|
||||
TodayLikeCount = BriskValueReader.GetLong(data, "today_like_count"),
|
||||
LikeResetAt = BriskValueReader.GetString(data, "like_reset_at")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -284,6 +347,30 @@ internal static class BriskModelMapper
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static BriskSpaceLikeItem ToSpaceLikeItem(Dictionary<string, object> data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BriskSpaceLikeItem
|
||||
{
|
||||
PlayerId = BriskValueReader.GetString(data, "player_id"),
|
||||
Nickname = BriskValueReader.GetString(data, "nickname"),
|
||||
AvatarUrl = BriskValueReader.GetString(data, "avatar_url"),
|
||||
CreatedAt = BriskValueReader.GetString(data, "created_at")
|
||||
};
|
||||
}
|
||||
|
||||
public static List<BriskSpaceLikeItem> ToSpaceLikeItems(object data)
|
||||
{
|
||||
return ExtractList(data)
|
||||
.Select(item => ToSpaceLikeItem(item as Dictionary<string, object>))
|
||||
.Where(item => item != null)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static Dictionary<string, object> ExtractObject(object data)
|
||||
{
|
||||
return data as Dictionary<string, object>;
|
||||
@@ -30,6 +30,10 @@ public sealed class BriskOptions
|
||||
/// </summary>
|
||||
public bool ValidateSessionOnInitialize = true;
|
||||
/// <summary>
|
||||
/// 初始化恢复的本地会话已过期或失效时,是否使用本地保存的登录身份静默换取新会话。
|
||||
/// </summary>
|
||||
public bool AutoReloginOnInitialize = true;
|
||||
/// <summary>
|
||||
/// 自定义登录态持久化实现。
|
||||
/// </summary>
|
||||
public IBriskTokenStore TokenStore;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user