三国卡牌客户端基础资源仓库
hch
2026-09-28 50d2d5e5011358066b5e213ac3d755a12da05324
0312 星玩接口加载
3个文件已添加
496 ■■■■■ 已修改文件
Assets/Launch/SDK.meta 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Launch/SDK/XingwanTTSDK.cs 477 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Launch/SDK/XingwanTTSDK.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Launch/SDK.meta
New file
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 18bafc95bb2490c4693efdfbedd90ac0
folderAsset: yes
DefaultImporter:
  externalObjects: {}
  userData:
  assetBundleName:
  assetBundleVariant:
Assets/Launch/SDK/XingwanTTSDK.cs
New file
@@ -0,0 +1,477 @@
// ============================================================================
// XingwanTTSDK.cs — 星玩(xingwan001)抖音小游戏 SDK 封装(AOT 程序集 Launch)
//
// 【为什么整个放在 Launch(AOT)】
//   WebGL 下 wasm 的导入函数由 IL2CPP / Emscripten 在 AOT 期静态解析,热更 DLL 里
//   新增的 DllImport("__Internal") 运行时无法解析,会抛 DllNotFoundException;
//   JS 回调所需的 MonoPInvokeCallback 反向包装也必须由 AOT 侧生成。
//   因此 P/Invoke 与 SDK 封装一起放在 AOT,和抖音 TTSDK 的结构完全一致:
//     TTSDK → Plugins/ByteGame/.../WebGL/TTWebGL.asmdef(AOT 独立程序集)
//     星玩  → Assets/Launch/SDK/XingwanTTSDK.cs(AOT 程序集 Launch)
//   热更侧 SDKDouyinImpl 只做普通方法调用,等同于 DouyinPlatform 调 TT.InitSDK。
//
//
// 【不做平台宏裁剪】整个类始终编译(含 Editor)。
//   Editor 下 SDKDouyinImpl 仍会引用本类(SyncType / Enabled 等),裁掉会编译失败;
//   是否真的调 JS 由 Enabled(内含 IsJsRuntime)运行时判断,Editor 下天然为 false,
//   不会触及任何 extern,因此 DllImport 声明在 Editor 下无害。
// ============================================================================
using System;
using System.Runtime.InteropServices;
using Cysharp.Threading.Tasks;
using LitJson;
using UnityEngine;
using UnityEngine.Scripting;
/// <summary>星玩 JS 回调统一签名:回传一个 JSON 字符串。</summary>
public delegate void XingwanStringCallback(string json);
/// <summary>
/// 星玩(xingwan001)抖音小游戏 SDK 的 C# 封装。
/// 通过 Assets/Plugins/Xingwan/XingwanTTGameSdk.jslib 调用小游戏根目录下的 xingwanTTGameSdk.js。
/// 对接文档:https://docs.xingwan001.com/client/ttDoc.html
///
/// 生命周期:initGame(必须最先) -> login -> pay / syncData
/// 所有接口成功码固定为 20000(StatusCodeOk)。
/// </summary>
public static class XingwanTTSDK
{
    /// <summary>星玩返回的成功状态码。</summary>
    public const int StatusCodeOk = 20000;
    private const int InitTimeoutMs = 20000;
    private const int LoginTimeoutMs = 20000;
    private const int SyncTimeoutMs = 15000;
    // ================================================================
    // 渠道参数:由星玩后台下发,直接写死在这里
    // 两个值都非空才启用星玩渠道,否则 SDKDouyinImpl 拒绝登录/支付
    // ================================================================
    /// <summary>渠道游戏标识 accessKeyId(星玩后台下发)。</summary>
    public static string AccessKeyId = "dasv8gr9ao6f4ecjlijg";
    /// <summary>渠道客户端公钥 clientKeySecret(注意不是 ServerKeySecret,后者只给服务端用)。</summary>
    public static string ClientKeySecret = "0a707656d0b4ba5a473e4c250e9e25951823261411861c28d346ba2306bb9603";
    /// <summary>两个渠道参数都填了。只代表配置层面已接入,不代表当前环境能调 JS。</summary>
    public static bool Configured
    {
        get { return !string.IsNullOrEmpty(AccessKeyId) && !string.IsNullOrEmpty(ClientKeySecret); }
    }
    /// <summary>
    /// 当前是否真的能调用 jslib。
    ///
    /// 必须用运行时判断,不能依赖 UNITY_EDITOR 这类编译期宏:本程序集在 Editor 下同样会编译,
    /// 若只靠宏判断,Editor 里也会走进星玩通道去调不存在的 JS 而抛 DllNotFoundException。
    /// </summary>
    public static bool IsJsRuntime
    {
        get { return Application.platform == RuntimePlatform.WebGLPlayer; }
    }
    /// <summary>
    /// 星玩是否启用:参数已配置,且当前确实运行在 WebGL(真机或小游戏开发者工具)。
    /// 为 false 时 SDKDouyinImpl 走原有登录/支付路径(Editor 下即平台模拟实现)。
    /// </summary>
    public static bool Enabled
    {
        get { return Configured && IsJsRuntime; }
    }
    // ================================================================
    // jslib 声明:Assets/Plugins/Xingwan/XingwanTTGameSdk.jslib
    // ================================================================
    [DllImport("__Internal")]
    private static extern void Xingwan_Init(string accessKeyId, string clientKeySecret, XingwanStringCallback cb);
    [DllImport("__Internal")]
    private static extern void Xingwan_Login(XingwanStringCallback cb);
    [DllImport("__Internal")]
    private static extern void Xingwan_Pay(string payJson, XingwanStringCallback cb);
    [DllImport("__Internal")]
    private static extern void Xingwan_SyncData(string syncJson, XingwanStringCallback cb);
    [DllImport("__Internal")]
    private static extern int Xingwan_IsReady();
    // ================================================================
    // 反向 P/Invoke 包装:IL2CPP 为这些静态方法生成函数指针,jslib 才能 dynCall 回来
    // ================================================================
    [Preserve]
    [AOT.MonoPInvokeCallback(typeof(XingwanStringCallback))]
    private static void OnInitResultImpl(string json)
    {
        SettleInit(Parse(json));
    }
    [Preserve]
    [AOT.MonoPInvokeCallback(typeof(XingwanStringCallback))]
    private static void OnLoginResultImpl(string json)
    {
        s_loginTcs?.TrySetResult(Parse(json));
    }
    [Preserve]
    [AOT.MonoPInvokeCallback(typeof(XingwanStringCallback))]
    private static void OnPayResultImpl(string json)
    {
        s_payTcs?.TrySetResult(Parse(json));
    }
    [Preserve]
    [AOT.MonoPInvokeCallback(typeof(XingwanStringCallback))]
    private static void OnSyncResultImpl(string json)
    {
        s_syncTcs?.TrySetResult(Parse(json));
    }
    // ================================================================
    // 结果定义
    // ================================================================
    public class XingwanResult
    {
        public bool Ok;
        public int StatusCode;
        public string Status;
        public JsonData Data;
        public override string ToString()
        {
            return $"Ok={Ok}, statusCode={StatusCode}, status={Status}";
        }
    }
    public class XingwanLoginResult : XingwanResult
    {
        /// <summary>登录 token,需传给游戏服务端做校验(服务端用 ServerKeySecret 向星玩验签)。</summary>
        public string Token;
        /// <summary>星玩平台用户唯一标识 userId。</summary>
        public string UserId;
    }
    /// <summary>星玩 pay 接口参数(见文档 3.3)。</summary>
    public class PayParams
    {
        /// <summary>支付金额,单位:分。</summary>
        public int MoneyFen;
        /// <summary>开发者支付订单号(CP 侧唯一)。</summary>
        public string DeveloperOrder = string.Empty;
        public string ProductId = string.Empty;
        public string ProductName = string.Empty;
        public string ZoneId = string.Empty;
        public string ZoneName = string.Empty;
        public string RoleId = string.Empty;
        public string RoleName = string.Empty;
        public string RoleLevel = string.Empty;
        /// <summary>扩展参数,支付成功后原样回传给服务端。</summary>
        public string DeveloperPayload = string.Empty;
    }
    /// <summary>星玩 syncData 同步类型(见文档 3.4)。</summary>
    public static class SyncType
    {
        public const int CreateRole = 1;
        public const int EnterGame = 2;
        public const int LevelUp = 3;
        public const int ExitGame = 4;
    }
    // ================================================================
    // 对外接口
    // ================================================================
    /// <summary>初始化星玩 SDK,登录/支付前必须成功。可重复调用,已成功则直接返回。</summary>
    public static async UniTask<XingwanResult> InitAsync()
    {
        if (!Enabled)
            return Fail("未配置星玩 accessKeyId / clientKeySecret,或当前非 WebGL 运行环境", -4);
        if (s_initResult != null && s_initResult.Ok)
            return s_initResult;
        if (s_initTcs != null)
            return await s_initTcs.Task;
        s_initTcs = new UniTaskCompletionSource<XingwanResult>();
        try
        {
            Xingwan_Init(AccessKeyId, ClientKeySecret, OnInitResultImpl);
        }
        catch (Exception e)
        {
            return SettleInit(Fail("initGame 调用异常: " + e.Message, -1));
        }
        var (hasResult, result) = await UniTask.WhenAny(s_initTcs.Task, UniTask.Delay(InitTimeoutMs));
        if (!hasResult)
            result = SettleInit(Fail("星玩初始化超时", -5));
        if (!result.Ok)
            Debug.LogError($"[Xingwan] 初始化失败: {result}");
        else
            Debug.Log($"[Xingwan] 初始化成功: {result.Status}");
        return result;
    }
    /// <summary>确保初始化完成;未手动初始化时内部自动触发一次。</summary>
    public static async UniTask<XingwanResult> EnsureInitAsync()
    {
        if (s_initResult != null && s_initResult.Ok)
            return s_initResult;
        if (s_initTcs == null)
            return await InitAsync();
        return await s_initTcs.Task;
    }
    /// <summary>星玩登录,返回 token 与 userId。</summary>
    public static async UniTask<XingwanLoginResult> LoginAsync()
    {
        var init = await EnsureInitAsync();
        if (!init.Ok)
            return new XingwanLoginResult
            {
                Ok = false,
                StatusCode = init.StatusCode,
                Status = "星玩未初始化: " + init.Status
            };
        if (s_loginTcs != null)
        {
            Debug.LogWarning("[Xingwan] 上一次登录尚未结束,复用其结果");
            return ToLoginResult(await s_loginTcs.Task);
        }
        s_loginTcs = new UniTaskCompletionSource<XingwanResult>();
        try
        {
            Xingwan_Login(OnLoginResultImpl);
        }
        catch (Exception e)
        {
            s_loginTcs.TrySetResult(Fail("login 调用异常: " + e.Message, -1));
        }
        var (hasResult, result) = await UniTask.WhenAny(s_loginTcs.Task, UniTask.Delay(LoginTimeoutMs));
        s_loginTcs = null;
        if (!hasResult)
            result = Fail("星玩登录超时", -5);
        var login = ToLoginResult(result);
        if (!login.Ok)
            Debug.LogError($"[Xingwan] 登录失败: {login}");
        else
            Debug.Log($"[Xingwan] 登录成功 userId={login.UserId}, token长度={login.Token?.Length ?? 0}");
        return login;
    }
    /// <summary>发起星玩支付。返回 Ok 表示支付流程完成(服务端以回调为准)。</summary>
    public static async UniTask<XingwanResult> PayAsync(PayParams p)
    {
        if (p == null)
            return Fail("支付参数为空", -4);
        var init = await EnsureInitAsync();
        if (!init.Ok)
            return Fail("星玩未初始化: " + init.Status, init.StatusCode);
        if (string.IsNullOrEmpty(p.DeveloperOrder))
            return Fail("开发者订单号为空", -4);
        var json = new JsonData();
        json["money"] = p.MoneyFen;
        json["developerOrder"] = p.DeveloperOrder ?? string.Empty;
        json["productId"] = p.ProductId ?? string.Empty;
        json["productName"] = p.ProductName ?? string.Empty;
        json["zoneId"] = p.ZoneId ?? string.Empty;
        json["zoneName"] = p.ZoneName ?? string.Empty;
        json["roleId"] = p.RoleId ?? string.Empty;
        json["roleName"] = p.RoleName ?? string.Empty;
        json["roleLevel"] = p.RoleLevel ?? string.Empty;
        json["developerPayload"] = p.DeveloperPayload ?? string.Empty;
        // 支付要等用户操作完成,不设超时
        s_payTcs = new UniTaskCompletionSource<XingwanResult>();
        try
        {
            Xingwan_Pay(json.ToJson(), OnPayResultImpl);
        }
        catch (Exception e)
        {
            s_payTcs.TrySetResult(Fail("pay 调用异常: " + e.Message, -1));
        }
        var result = await s_payTcs.Task;
        s_payTcs = null;
        Debug.Log($"[Xingwan] 支付返回: {result}, order={p.DeveloperOrder}, money={p.MoneyFen}分");
        return result;
    }
    /// <summary>提交角色数据(创建角色/进入游戏/升级/退出游戏)。type 见 SyncType。</summary>
    public static async UniTask<XingwanResult> SyncDataAsync(int type, string zoneId, string zoneName,
        string roleId, string roleName, string roleLevel)
    {
        var init = await EnsureInitAsync();
        if (!init.Ok)
            return Fail("星玩未初始化: " + init.Status, init.StatusCode);
        var json = new JsonData();
        json["type"] = type;
        json["zoneId"] = zoneId ?? string.Empty;
        json["zoneName"] = zoneName ?? string.Empty;
        json["roleId"] = roleId ?? string.Empty;
        json["roleName"] = roleName ?? string.Empty;
        json["roleLevel"] = roleLevel ?? string.Empty;
        s_syncTcs = new UniTaskCompletionSource<XingwanResult>();
        try
        {
            Xingwan_SyncData(json.ToJson(), OnSyncResultImpl);
        }
        catch (Exception e)
        {
            s_syncTcs.TrySetResult(Fail("syncData 调用异常: " + e.Message, -1));
        }
        var (hasResult, result) = await UniTask.WhenAny(s_syncTcs.Task, UniTask.Delay(SyncTimeoutMs));
        s_syncTcs = null;
        if (!hasResult)
            result = Fail("syncData 超时", -5);
        if (!result.Ok)
            Debug.LogWarning($"[Xingwan] 角色数据同步失败 type={type}: {result}");
        else
            Debug.Log($"[Xingwan] 角色数据同步成功 type={type}");
        return result;
    }
    /// <summary>JS 层 SDK 是否已就绪(用于启动自检)。</summary>
    public static bool IsJsReady()
    {
        if (!IsJsRuntime)
            return false;
        try
        {
            return Xingwan_IsReady() == 1;
        }
        catch (Exception e)
        {
            Debug.LogWarning($"[Xingwan] JS 就绪检查异常: {e.Message}");
            return false;
        }
    }
    // ================================================================
    // 回调与解析
    // ================================================================
    private static UniTaskCompletionSource<XingwanResult> s_initTcs;
    private static UniTaskCompletionSource<XingwanResult> s_loginTcs;
    private static UniTaskCompletionSource<XingwanResult> s_payTcs;
    private static UniTaskCompletionSource<XingwanResult> s_syncTcs;
    private static XingwanResult s_initResult;
    private static XingwanResult SettleInit(XingwanResult result)
    {
        s_initResult = result;
        s_initTcs?.TrySetResult(result);
        // 初始化失败/超时后清空凭证,允许下次调用重试
        if (!result.Ok)
            s_initTcs = null;
        return result;
    }
    private static XingwanResult Parse(string json)
    {
        var result = new XingwanResult();
        if (string.IsNullOrEmpty(json))
        {
            result.StatusCode = -1;
            result.Status = "空回调";
            return result;
        }
        try
        {
            var data = JsonMapper.ToObject(json);
            if (data == null)
                return Fail("回调解析为空", -1);
            if (data.Keys.Contains("statusCode"))
                result.StatusCode = ReadInt(data["statusCode"]);
            if (data.Keys.Contains("status"))
                result.Status = data["status"].ToString();
            if (data.Keys.Contains("data") && data["data"] != null)
                result.Data = data["data"];
            result.Ok = result.StatusCode == StatusCodeOk;
            return result;
        }
        catch (Exception e)
        {
            Debug.LogError($"[Xingwan] 回调解析异常: {e.Message}, json={json}");
            return Fail("回调解析异常: " + e.Message, -1);
        }
    }
    private static XingwanLoginResult ToLoginResult(XingwanResult result)
    {
        var login = new XingwanLoginResult
        {
            Ok = result.Ok,
            StatusCode = result.StatusCode,
            Status = result.Status,
            Data = result.Data,
        };
        if (result.Data != null)
        {
            login.Token = ReadString(result.Data, "token");
            login.UserId = ReadString(result.Data, "userId");
            if (string.IsNullOrEmpty(login.UserId))
                login.UserId = ReadString(result.Data, "uid");
            if (string.IsNullOrEmpty(login.UserId))
                login.UserId = ReadString(result.Data, "user_id");
            if (string.IsNullOrEmpty(login.UserId))
                login.UserId = ReadString(result.Data, "openid");
        }
        return login;
    }
    private static int ReadInt(JsonData data)
    {
        int value;
        return int.TryParse(data.ToString(), out value) ? value : -1;
    }
    private static string ReadString(JsonData data, string key)
    {
        if (data == null || !data.IsObject || !data.Keys.Contains(key))
            return string.Empty;
        var value = data[key];
        return value == null ? string.Empty : value.ToString();
    }
    private static XingwanResult Fail(string message, int code)
    {
        return new XingwanResult { Ok = false, StatusCode = code, Status = message };
    }
}
Assets/Launch/SDK/XingwanTTSDK.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6ded8e91b571b104181e28d61599832e
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant: