yyl
2026-05-07 01b5728a890315e4dbe9aabf7225a7957171e1a5
webgl2
10个文件已修改
6个文件已添加
597 ■■■■ 已修改文件
Main/Core/NetworkPackage/GameNetSystem.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Core/NetworkPackage/Socket/ClientSocket.cs 87 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/SDK/SDKAndroidImpl.cs 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/SDK/SDKAndroidImpl.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/SDK/SDKBaseImpl.cs 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/SDK/SDKBaseImpl.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/SDK/SDKUtils.cs 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/SDK/SDKWebGLImpl.cs 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/SDK/SDKWebGLImpl.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/KnapSack/PackManager.cs 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Login/LoginManager.cs 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Login/LoginWin.cs 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Message/MessageWin.cs 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Tip/ScrollTip.cs 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Tip/ScrollTipDetail.cs 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Tip/ScrollTipWin.cs 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Core/NetworkPackage/GameNetSystem.cs
@@ -118,6 +118,8 @@
            Debug.Log(ex);
        }
        Debug.unityLogger.logEnabled = true;
        mainSocket = new ClientSocket(ServerType.Main);
        //  websocket的断开链接需要处理一下
        mainSocket.OnDisconnected = () =>
Main/Core/NetworkPackage/Socket/ClientSocket.cs
@@ -76,11 +76,13 @@
    public void Connect(string _ip, int _port, Action<bool> _onConnected)
    {
        Debug.unityLogger.logEnabled = true;
        try
        {
            ip = _ip;
            port = _port;
            onConnected = _onConnected;
            Debug.Log($"[ClientSocket][Connect] 尝试连接: ip={_ip}, port={_port}");
            //目前测试到异步两个问题
            // 1. BeginGetHostAddresses 不明情况下会很久才回调,导致触发超时
            // 2. 超时的情况下多次尝试登录后,会触发多次OnGetHostAddresses,导致登录异常
@@ -96,29 +98,29 @@
            ipAddress = ipAddresses[0];
#endif
            Debug.Log($"[ClientSocket][Connect] 解析到ipAddress={ipAddress}, family={ipAddress.AddressFamily}");
            if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
            {
                Debug.Log("当前使用的网络: IPV6");
                Debug.Log("[ClientSocket][Connect] 当前使用的网络: IPV6");
                m_Socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
            }
            else
            {
                Debug.Log("当前使用的网络: IPV4");
                Debug.Log("[ClientSocket][Connect] 当前使用的网络: IPV4");
                m_Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            }
            var ipEndPoint = new IPEndPoint(ipAddress, port);
            if (ipEndPoint == null)
            {
                Debug.Log("IpEndPoint is null");
                Debug.LogError("[ClientSocket][Connect] IpEndPoint is null");
            }
            m_Socket.BeginConnect(ipEndPoint, new AsyncCallback(ConnectCallBack), null);
        }
        catch (Exception e)
        {
            Debug.LogError(e.Message);
            Debug.LogError($"[ClientSocket][Connect] 异常: {e.Message}");
        }
@@ -154,12 +156,14 @@
    /// <param name="_result"></param>
    private void ConnectCallBack(IAsyncResult _result)
    {
        Debug.unityLogger.logEnabled = true;
        if (!_result.IsCompleted)
        {
            Debug.Log("链接超时!");
            Debug.LogError("[ClientSocket][ConnectCallBack] 链接超时!");
            CloseConnect();
            if (onConnected != null)
            {
                Debug.LogError("[ClientSocket][ConnectCallBack] onConnected(false) 超时");
                onConnected(false);
                onConnected = null;
            }
@@ -170,11 +174,12 @@
            {
                if (m_Socket != null && m_Socket.Connected)
                {
                    Debug.Log("确认的链接实现");
                    Debug.Log("[ClientSocket][ConnectCallBack] 确认的链接实现");
                    OnConnectSuccess();
                }
                else
                {
                    Debug.LogError("[ClientSocket][ConnectCallBack] m_Socket为null或未连接");
                    if (m_Socket != null)
                    {
                        m_Socket.Disconnect(true);
@@ -183,12 +188,13 @@
            }
            catch (System.Exception ex)
            {
                Debug.Log(ex);
                Debug.LogError($"[ClientSocket][ConnectCallBack] 异常: {ex}");
            }
            finally
            {
                if (onConnected != null)
                {
                    Debug.Log($"[ClientSocket][ConnectCallBack] onConnected({{0}})", m_Socket != null && m_Socket.Connected);
                    onConnected(m_Socket != null && m_Socket.Connected);
                    onConnected = null;
                }
@@ -205,6 +211,8 @@
    public void CloseConnect()
    {
        Debug.Log("==== CloseConnect");
        Debug.unityLogger.logEnabled = true;
        Debug.Log("[ClientSocket][CloseConnect] ==== CloseConnect");
        try
        {
            isStopTreading = true;
@@ -242,6 +250,20 @@
    {
        if (m_packageThread != null)
        {
            m_packageThread.Abort();
            m_packageThread = null;
        }
        m_LastPackageTime = DateTime.Now;
        m_packageThread = new Thread(new ThreadStart(ReceiveInfo)); // 启动线程接收信息
        m_packageThread.IsBackground = true;
        m_packageThread.Start();
        isStopTreading = false;
        Debug.unityLogger.logEnabled = true;
        Debug.Log("[ClientSocket][OnConnectSuccess] 连接成功,启动接收线程");
        if (m_packageThread != null)
        {
            Debug.LogWarning("[ClientSocket][OnConnectSuccess] m_packageThread已存在,先Abort");
            m_packageThread.Abort();
            m_packageThread = null;
        }
@@ -296,6 +318,51 @@
                Debug.Log(e);
            }
        }
        Debug.unityLogger.logEnabled = true;
        Debug.Log("[ClientSocket][ReceiveInfo] 接收线程启动");
        while (!isStopTreading)
        {
            try
            {
                var shutdown = false;
                if (!m_Socket.Connected)
                {
                    Debug.LogWarning("[ClientSocket][ReceiveInfo] m_Socket 已断开");
                    shutdown = true;
                }
                if (!shutdown)
                {
                    var dataLength = m_Socket.Receive(bufferBytes);
                    Debug.Log($"[ClientSocket][ReceiveInfo] 收到数据长度: {dataLength}");
                    if (dataLength <= 0)
                    {
                        Debug.LogWarning("[ClientSocket][ReceiveInfo] dataLength <= 0,准备断开");
                        shutdown = true;
                    }
                    else
                    {
                        getBytesTotal += dataLength;
                        var bytes = new byte[dataLength];
                        Array.Copy(bufferBytes, 0, bytes, 0, dataLength);
                        ReadInfo(bytes);
                    }
                }
                if (shutdown)
                {
                    Debug.LogWarning("[ClientSocket][ReceiveInfo] shutdown=true,关闭Socket");
                    isStopTreading = true;
                    m_Socket.Shutdown(SocketShutdown.Both);
                    m_Socket.Close();
                }
            }
            catch (Exception e)
            {
                Debug.LogError($"[ClientSocket][ReceiveInfo] 异常: {e}");
            }
        }
        Debug.Log("[ClientSocket][ReceiveInfo] 接收线程退出");
    }
@@ -312,6 +379,7 @@
            if (fragmentBytes != null && fragmentBytes.Length > 0)
            {
                Array.Resize(ref fixBytes, vBytes.Length + fragmentBytes.Length);
                Debug.Log($"[ClientSocket][ReadInfo] 存在fragmentBytes, 长度: {fragmentBytes.Length}");
                Array.Copy(fragmentBytes, 0, fixBytes, 0, fragmentBytes.Length);
                Array.Copy(vBytes, 0, fixBytes, fragmentBytes.Length, vBytes.Length);
            }
@@ -439,6 +507,7 @@
    
    public async void Connect(string _ip, int _port, Action<bool> _onConnected)
    {
        Debug.unityLogger.logEnabled = true;
        ip = _ip;
        port = _port;
        onConnected = _onConnected;
@@ -597,7 +666,7 @@
    
    public async void CloseConnect()
    {
        Debug.Log("[ClientSocket-WebSocket] ==== CloseConnect");
        Debug.Log("[ClientSocket-WebSocket] ==== CloseConnect\n" + System.Environment.StackTrace);
        fragmentBytes = null;
        
        if (webSocket != null)
Main/SDK/SDKAndroidImpl.cs
New file
@@ -0,0 +1,100 @@
#if UNITY_ANDROID
using UnityEngine;
using LitJson;
using System;
/// <summary>
/// Android平台SDK实现,继承SDKBaseImpl。
/// 通过AndroidJavaClass与原生SDK交互。
/// </summary>
public class SDKAndroidImpl : SDKBaseImpl
{
    public SDKAndroidImpl(SDKUtils utils) : base(utils) { }
    // -------------------------------------------------------
    // 底层消息发送 —— 通过AndroidJavaClass调用原生SDK
    // -------------------------------------------------------
    public override void SendToNative(string jsonStr)
    {
        if (VersionConfig.Get().appId == "sghy")
        {
            using (var sdk = new AndroidJavaClass("com.xssg.sdk.UnityMsgHandler"))
                sdk.CallStatic("onUnityMessage", jsonStr);
        }
        else
        {
            using (var sdk = new AndroidJavaClass("com.wgyx.sdk.UnityMsgHandler"))
                sdk.CallStatic("onUnityMessage", jsonStr);
        }
    }
    // -------------------------------------------------------
    // 初始化 —— 检查内置资源拷贝状态 + 同步包ID
    // -------------------------------------------------------
    public override void InitPlatform()
    {
        var savedVer = LocalSave.GetString("BuiltInAssetCopyCompleted_Android");
        SDKUtils.builtinAssetCopyFinished = !string.IsNullOrEmpty(savedVer)
            && VersionConfig.Get().version == savedVer;
        SyncClientPackageID();
    }
    // -------------------------------------------------------
    // SDK登录 —— 走正常原生SDK流程
    // -------------------------------------------------------
    public override void FreePlatformLogin()
    {
        utils.SendSdkMessage(SDKUtils.CodeU2A.FreePlatformLogin);
    }
    // -------------------------------------------------------
    // 设备信息 —— Android特有:mac/imei/totalMemory
    // -------------------------------------------------------
    public override void FillDeviceInfo(JsonData json)
    {
        utils.Device.macAddress = json["mac"].ToString();
        utils.Device.imei = json["imei"] != null ? json["imei"].ToString() : utils.Device.uniqueID;
        utils.Device.totalMemory = (int)json["memoryTotal"];
    }
    // -------------------------------------------------------
    // 退出游戏
    // -------------------------------------------------------
    public override void ExitGame()
    {
        Application.Quit();
    }
    // -------------------------------------------------------
    // Android专属:权限 & 包ID
    // -------------------------------------------------------
    public override void SyncClientPackageID()
    {
        utils.SendSdkMessage(SDKUtils.CodeU2A.ClientPackage,
            json => json["clientPkgID"] = VersionConfig.Get().clientPackageFlag);
    }
    public override void RequestPermission(string permission, Action<string, int> callBack)
    {
        utils.RegisterPermissionCallback(callBack);
        utils.SendSdkMessage(SDKUtils.CodeU2A.RequestPermission,
            json => json["permission"] = permission);
    }
    public override void RequestPermissionStart()
    {
        utils.SendSdkMessage(SDKUtils.CodeU2A.RequestPermissionStart);
    }
    // -------------------------------------------------------
    // 静态工具 —— 获取Android应用上下文
    // -------------------------------------------------------
    public static AndroidJavaObject GetApplicationContext()
    {
        using (var jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
        using (var jo = jc.GetStatic<AndroidJavaObject>("currentActivity"))
            return jo.Call<AndroidJavaObject>("getApplicationContext");
    }
}
#endif
Main/SDK/SDKAndroidImpl.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fad8ebe1b08b656489826bafdefb356f
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/SDK/SDKBaseImpl.cs
New file
@@ -0,0 +1,50 @@
using UnityEngine;
using LitJson;
using System;
/// <summary>
/// SDK平台实现的抽象基类。
/// 每个目标平台(Android、WebGL等)继承此类并实现各自的逻辑。
/// SDKUtils持有一个SDKBaseImpl实例,所有平台差异均通过此基类委托。
/// </summary>
public abstract class SDKBaseImpl
{
    protected readonly SDKUtils utils;
    protected SDKBaseImpl(SDKUtils utils)
    {
        this.utils = utils;
    }
    // -------------------------------------------------------
    // 必须由子类实现的平台核心方法
    // -------------------------------------------------------
    /// <summary>向平台原生SDK发送JSON消息</summary>
    public abstract void SendToNative(string jsonStr);
    /// <summary>平台初始化(检查内置资源拷贝状态、平台特有设置等)</summary>
    public abstract void InitPlatform();
    /// <summary>触发SDK账号登录流程</summary>
    public abstract void FreePlatformLogin();
    /// <summary>填充平台特有的设备信息字段(如Android的mac/imei等)</summary>
    public abstract void FillDeviceInfo(JsonData json);
    /// <summary>退出游戏(各平台行为不同)</summary>
    public abstract void ExitGame();
    // -------------------------------------------------------
    // 可选重写(默认空实现,仅Android等平台需要)
    // -------------------------------------------------------
    /// <summary>同步客户端包ID到原生SDK</summary>
    public virtual void SyncClientPackageID() { }
    /// <summary>动态申请单个权限</summary>
    public virtual void RequestPermission(string permission, Action<string, int> callBack) { }
    /// <summary>启动时批量申请权限(由SDK决定策略)</summary>
    public virtual void RequestPermissionStart() { }
}
Main/SDK/SDKBaseImpl.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 75293dc3923823b408a427acffc9d6a8
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/SDK/SDKUtils.cs
@@ -66,7 +66,7 @@
    /// sdk初始化是否完成标识
    /// 客户端一般需要等待这个值为true才继续逻辑
    /// </summary>
    public bool InitFinished { get; private set; }
    public bool InitFinished { get; set; }
    #region 基础定义与回调
@@ -106,7 +106,7 @@
    /// </summary>
    public UnityAction<NetworkReachability> OnNetworkStatusChanged;
    public static bool builtinAssetCopyFinished { get; private set; }
    public static bool builtinAssetCopyFinished { get; internal set; }
    /// <summary>
    /// 是否已经将StreamingAsset拷贝至目标路径
@@ -125,11 +125,24 @@
    #endregion
    #region 自由sdk
    public FP_LoginOk FreePlatformInfo { get; private set; }
    public FP_LoginOk FreePlatformInfo { get; set; }
    #endregion
    private JsonData m_Json = new JsonData();
    // 当前平台SDK实现
    private SDKBaseImpl m_impl;
    private SDKBaseImpl CreateImpl()
    {
#if UNITY_ANDROID
        return new SDKAndroidImpl(this);
#elif UNITY_WEBGL
        return new SDKWebGLImpl(this);
#else
        return null; // Editor/Standalone: SendMessageToSDK在Editor下直接跳过
#endif
    }
    public void Init()
    {
@@ -155,22 +168,14 @@
        NetworkType = NetworkReachability.NotReachable;
        BatteryLevel = 100;
        m_impl = CreateImpl();
#if !UNITY_EDITOR
        if (InitFinished)
        {
            return;
        }
#if UNITY_ANDROID
        var builtinAssetsCopyFinishVersion = LocalSave.GetString("BuiltInAssetCopyCompleted_Android");
        if (string.IsNullOrEmpty(builtinAssetsCopyFinishVersion))
        {
            builtinAssetCopyFinished = false;
        }
        else
        {
            builtinAssetCopyFinished = VersionConfig.Get().version == builtinAssetsCopyFinishVersion;
        }
#endif
        m_impl?.InitPlatform();
#if UNITY_IOS || UNITY_STANDALONE
        var builtinAssetsCopyFinishVersion = LocalSave.GetString("BuiltInAssetCopyCompleted_IOSorStandalone");
        if (string.IsNullOrEmpty(builtinAssetsCopyFinishVersion))
@@ -192,8 +197,6 @@
            AssetCopyFinished = VersionConfig.Get().version == assetsCopyFinishVersion;
        }
#elif UNITY_ANDROID
        SyncClientPackageID();
#endif
        InitFinished = false;
@@ -320,41 +323,22 @@
        SendMessageToSDK(m_Json);
    }
    public void SyncClientPackageID()
    {
#if UNITY_ANDROID
        m_Json.Clear();
        m_Json["code"] = CodeU2A.ClientPackage;
        m_Json["clientPkgID"] = VersionConfig.Get().clientPackageFlag;
        SendMessageToSDK(m_Json);
#endif
    }
    //申请Android 权限
    public void RequestAndroidPermission(string permission, Action<string, int> callBack)
    {
        onPermissionCallBack += callBack;
        m_Json.Clear();
        m_Json["code"] = CodeU2A.RequestPermission;
        m_Json["permission"] = permission;
        SendMessageToSDK(m_Json);
    }
    //申请Android 权限 启动时
    public void RequestAndroidPermissionStart()
    {
        m_Json.Clear();
        m_Json["code"] = CodeU2A.RequestPermissionStart;
        SendMessageToSDK(m_Json);
    }
    public void RequestSecretRule()
    {
        m_Json.Clear();
        m_Json["code"] = CodeU2A.RequestSecretRule;
        SendMessageToSDK(m_Json);
    }
    /// <summary>同步客户端包ID(委托给平台实现)</summary>
    public void SyncClientPackageID() => m_impl?.SyncClientPackageID();
    /// <summary>动态申请Android权限(非Android平台为空操作)</summary>
    public void RequestAndroidPermission(string permission, Action<string, int> callBack)
        => m_impl?.RequestPermission(permission, callBack);
    /// <summary>启动时批量申请权限(非Android平台为空操作)</summary>
    public void RequestAndroidPermissionStart() => m_impl?.RequestPermissionStart();
@@ -379,17 +363,6 @@
    #region 处理与SDK交互的底层方法
    public static AndroidJavaObject GetApplicationContext()
    {
        using (AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
        {
            using (AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity"))
            {
                return jo.Call<AndroidJavaObject>("getApplicationContext");
            }
        }
    }
    public string GetSplicePackageID()
    {
        var _result = "default";
@@ -409,29 +382,28 @@
    private void SendMessageToSDK(JsonData json)
    {
#if !UNITY_EDITOR
#if UNITY_ANDROID
        if (VersionConfig.Get().appId == "sghy")
        {
            using (AndroidJavaClass H2engineSDK = new AndroidJavaClass("com.xssg.sdk.UnityMsgHandler"))
            {
                H2engineSDK.CallStatic("onUnityMessage", json.ToJson());
            }
        }
        else
        {
            using (AndroidJavaClass H2engineSDK = new AndroidJavaClass("com.wgyx.sdk.UnityMsgHandler"))
            {
                H2engineSDK.CallStatic("onUnityMessage", json.ToJson());
            }
        }
#elif UNITY_IOS
        AotSdkUtility.IOSUniyMessageHandle(json.ToJson());
#elif UNITY_STANDALONE || UNITY_WEBGL
        InitFinished=true;
        var _jsonStr = json.ToJson();
#if UNITY_IOS
        AotSdkUtility.IOSUniyMessageHandle(_jsonStr);
#else
        m_impl?.SendToNative(_jsonStr);
#endif
#endif
    }
    /// <summary>供平台实现类调用的便捷发包方法(带可选JSON填充)</summary>
    internal void SendSdkMessage(int code, Action<JsonData> fillJson = null)
    {
        m_Json.Clear();
        m_Json["code"] = code;
        fillJson?.Invoke(m_Json);
        SendMessageToSDK(m_Json);
    }
    /// <summary>供平台实现类注册权限回调</summary>
    internal void RegisterPermissionCallback(Action<string, int> callBack)
    {
        onPermissionCallBack += callBack;
    }
    public void HandleMsgWithSDK(string jsonString)
@@ -447,18 +419,7 @@
                //Device.uniqueID = _json["unique_id"].ToString();
                Device.androidID = _json["android_id"].ToString();// ios平台下为idfa
                Device.userAgent = _json["userAgent"].ToString();
#if UNITY_ANDROID
                Device.macAddress = _json["mac"].ToString();
                if (_json["imei"] != null)
                {
                    Device.imei = _json["imei"].ToString();
                }
                else
                {
                    Device.imei = Device.uniqueID;
                }
                Device.totalMemory = (int)_json["memoryTotal"];
#endif
                m_impl?.FillDeviceInfo(_json);
                if (OnDeviceInfoChanged != null)
                {
                    OnDeviceInfoChanged(Device);
@@ -644,11 +605,7 @@
                // else
                {
                    //默认都是退出游戏
#if UNITY_WEBGL
                    Application.OpenURL("about:blank");
#else
                    Application.Quit();
#endif
                    m_impl?.ExitGame();
                }
                break;
            case CodeA2U.GetAdAward:
@@ -746,7 +703,7 @@
        #endregion
    }
    private static class CodeU2A
    public static class CodeU2A
    {
        /**
         * 执行资源拷贝
@@ -930,9 +887,7 @@
    /// </summary>
    public void FreePlatformLogin()
    {
        m_Json.Clear();
        m_Json["code"] = CodeU2A.FreePlatformLogin;
        SendMessageToSDK(m_Json);
        m_impl?.FreePlatformLogin();
    }
Main/SDK/SDKWebGLImpl.cs
New file
@@ -0,0 +1,68 @@
#if UNITY_WEBGL
using UnityEngine;
using LitJson;
using System;
/// <summary>
/// WebGL平台SDK实现,继承SDKBaseImpl。
/// WebGL无原生SDK层,登录直接模拟回调,退出跳转空白页。
/// </summary>
public class SDKWebGLImpl : SDKBaseImpl
{
    public SDKWebGLImpl(SDKUtils utils) : base(utils) { }
    // -------------------------------------------------------
    // 消息发送 —— WebGL无原生SDK,标记初始化完成即可
    // -------------------------------------------------------
    public override void SendToNative(string jsonStr)
    {
        // WebGL无原生SDK Bridge,收到第一次SendToNative(Init消息)后标记完成
        utils.InitFinished = true;
    }
    // -------------------------------------------------------
    // 初始化 —— WebGL无内置资源拷贝流程
    // -------------------------------------------------------
    public override void InitPlatform()
    {
        // WebGL不需要内置资源拷贝状态检查
    }
    // -------------------------------------------------------
    // SDK登录 —— WebGL直接模拟登录成功回调
    // -------------------------------------------------------
    public override void FreePlatformLogin()
    {
        Debug.Log("[SDKWebGLImpl] FreePlatformLogin: WebGL模拟登录");
        var info = new SDKUtils.FP_LoginOk
        {
            account      = "webgl_user",
            token        = "111",          // 内测服免密约定,与 InterTest Password 一致
            qkUserName   = "webgl_user",
            tokenExpire  = "9999999999",
            accountID    = 0,
            phone        = 0,
        };
        utils.FreePlatformInfo = info;
        utils.onFreePlatformLoginOk?.Invoke(info);
    }
    // -------------------------------------------------------
    // 设备信息 —— WebGL无特有设备字段
    // -------------------------------------------------------
    public override void FillDeviceInfo(JsonData json)
    {
        // WebGL下无mac/imei等字段,不做额外填充
    }
    // -------------------------------------------------------
    // 退出游戏 —— WebGL跳转空白页
    // -------------------------------------------------------
    public override void ExitGame()
    {
        Application.OpenURL("about:blank");
    }
}
#endif
Main/SDK/SDKWebGLImpl.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e46364c334945194eae43e4524adeb08
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/KnapSack/PackManager.cs
@@ -559,17 +559,9 @@
        }
    #endif
        path = AssetVersionUtility.GetAssetFilePath($"Config/{name}.txt");
        using var req = UnityEngine.Networking.UnityWebRequest.Get(path);
        await req.SendWebRequest();
        if (req.result != UnityEngine.Networking.UnityWebRequest.Result.Success)
            throw new Exception($"LoadConfigIni failed: {req.error}");
        return req.downloadHandler.text
            .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
        return await ResManager.Instance.LoadConfigAsync(name + ".ini", false);
    }
    public int GetCanBuyPackGirdCount(PackType type)
    {
        if (!PackMaxCountDict.ContainsKey((int)type))
Main/System/Login/LoginManager.cs
@@ -127,10 +127,12 @@
    public void AccountLogin(string _account, string _ip, int _port, int _gamePort)
    {
        Debug.LogFormat("账号登录, account:{0} ; ip:{1} ; port:{2} ; gamePort:{3}", _account, _ip, _port, _gamePort);
        Debug.unityLogger.logEnabled = true;
        Debug.LogFormat("[LoginManager][AccountLogin] 账号登录, account:{0} ; ip:{1} ; port:{2} ; gamePort:{3}", _account, _ip, _port, _gamePort);
        isLogined = true;
        if (Application.internetReachability == NetworkReachability.NotReachable)
        {
            Debug.LogError("[LoginManager][AccountLogin] 网络不可达,弹出提示并return");
            ConfirmCancel.ShowPopConfirm(
                Language.Get("Mail101"),
                Language.Get("L1116"),
@@ -141,6 +143,7 @@
        if (busy)
        {
            Debug.LogWarning("[LoginManager][AccountLogin] busy为true,直接return");
            return;
        }
@@ -155,6 +158,7 @@
            portBuf = _port;
            gamePortBuf = _gamePort;
            Debug.Log("[LoginManager][AccountLogin] 调用ConnectGameServer");
            ConnectGameServer(ipBuf, gamePortBuf);
            GameNetSystem.Instance.OnAccountLogin();
@@ -162,7 +166,7 @@
        }
        catch (Exception ex)
        {
            Debug.Log(ex);
            Debug.LogError($"[LoginManager][AccountLogin] 异常: {ex}");
            busy = false;
        }
@@ -170,6 +174,8 @@
    public void AccountLogin(string _ip, int _port, int _gamePort)
    {
        Debug.unityLogger.logEnabled = true;
        Debug.LogError("[YLTEST] Account Login " + sdkLogined);
        if (sdkLogined)
        {
            AccountLogin(sdkLoginResult.account, _ip, _port, _gamePort);
@@ -204,10 +210,11 @@
    private void OnAccountLogin(bool _ok, string _result)
    {
        Debug.unityLogger.logEnabled = true;
        if (!_ok)
        {
            busy = false;
            Debug.LogError(_result);
            Debug.LogError($"[LoginManager][OnAccountLogin] 登录回调失败: {_result}");
            NetLinkWin.Hide();
            return;
        }
@@ -215,6 +222,7 @@
        if (string.IsNullOrEmpty(_result))
        {
            busy = false;
            Debug.LogError("[LoginManager][OnAccountLogin] 登录回调_result为空,弹出提示");
            ServerTipDetails.DisplayNormalTip(Language.Get("L1117"));
            NetLinkWin.Hide();
            return;
@@ -225,6 +233,7 @@
        {
            if (stringSet.Length > 1)
            {
                Debug.LogError($"[LoginManager][OnAccountLogin] 登录回调_result非OK,提示: {stringSet[1]}");
                ServerTipDetails.DisplayNormalTip(stringSet[1]);
            }
@@ -236,11 +245,13 @@
        localSaveAccountName = accountBuf;
        try
        {
            Debug.Log("[LoginManager][OnAccountLogin] 登录回调OK,准备ConnectGameServer");
            ConnectGameServer(ipBuf, gamePortBuf);
        }
        catch (Exception ex)
        {
            busy = false;
            Debug.LogError($"[LoginManager][OnAccountLogin] ConnectGameServer异常: {ex}");
            Debug.Log(ex);
        }
    }
@@ -248,11 +259,15 @@
    void ConnectGameServer(string _ip, int _port)
    {
        Debug.unityLogger.logEnabled = true;
        Debug.LogError("[YLTEST] Connect Server " + _ip + ":" + _port);
        GameNetSystem.Instance.BeginConnectGameServer(_ip, _port, OnGameServerConnected);
    }
    private void OnGameServerConnected(bool ok)
    {
        Debug.unityLogger.logEnabled = true;
        Debug.LogError("[YLTEST] Connect Server result " + ok);
        if (ok)
        {
            GameNetSystem.Instance.SetIsWaitLogin(false);
@@ -386,7 +401,12 @@
    {
        GameNetSystem.Instance.SetIsWaitLogin(false);
        var send = new C0101_tagCPlayerLogin();
        switch (VersionConfig.config.versionAuthority)
        var authority = VersionConfig.config.versionAuthority;
#if UNITY_WEBGL && !UNITY_EDITOR
        // WebGL 平台没有真实 SDK,强制走 InterTest 分支(账号+密码)登录
        authority = VersionAuthority.InterTest;
#endif
        switch (authority)
        {
            case VersionAuthority.InterTest:
                send.IDType = 1;
Main/System/Login/LoginWin.cs
@@ -182,7 +182,7 @@
            var appId = VersionConfig.config.appId;
            var branch = VersionConfig.config.branch;
             m_ContainerAccount.SetActive(isGetServerList
            && (VersionConfig.config.versionAuthority == VersionAuthority.InterTest || VersionConfig.config.isBanShu));
            && (VersionConfig.config.versionAuthority == VersionAuthority.InterTest || VersionConfig.config.isBanShu || Application.platform == RuntimePlatform.WebGLPlayer));
        }).Forget();
        // m_UserHelp.SetActive(ContactConfig.GetConfig(appId, branch) != null);
@@ -275,18 +275,22 @@
        m_WaitServerList.SetActive(!ServerListCenter.Instance.serverListGot);
        m_ContainerEnterGame.SetActive(ServerListCenter.Instance.serverListGot);
        m_ContainerAccount.SetActive(ServerListCenter.Instance.serverListGot
            && (VersionConfig.config.versionAuthority == VersionAuthority.InterTest || VersionConfig.config.isBanShu));
            && (VersionConfig.config.versionAuthority == VersionAuthority.InterTest || VersionConfig.config.isBanShu || Application.platform == RuntimePlatform.WebGLPlayer));
    }
    protected virtual void EnterGame()
    {
        Debug.unityLogger.logEnabled = true;
        Debug.Log("[LoginWin][EnterGame] 方法进入");
        if (!checkRead.isOn)
        {
            Debug.LogWarning("[LoginWin][EnterGame] 用户未勾选协议,弹出确认框");
            ConfirmCancel.ShowPopConfirm(Language.Get("agreementTitle"), Language.Get("agreementInfo"));
            Debug.LogWarning("[LoginWin][EnterGame] return: 未勾选协议");
            return;
        }
        Debug.Log("[LoginWin][EnterGame] 用户已勾选协议,准备隐藏浮窗并调用Login()");
        SDKUtils.Instance.SendHideFloatWin();
        Login();
@@ -339,10 +343,13 @@
    protected void Login()
    {
        Debug.unityLogger.logEnabled = true;
        Debug.Log("[LoginWin][Login] 方法进入");
        var allow = false;
        if (DebugUtility.Instance.isWhiteListAccount)
        {
            allow = true;
            Debug.Log("[LoginWin][Login] 白名单账号,允许登录");
        }
        else
        {
@@ -350,10 +357,12 @@
                || ServerListCenter.Instance.currentServer.running_status == (int)ServerState.Predicted)
            {
                allow = false;
                Debug.LogWarning($"[LoginWin][Login] 当前服务器状态为 {ServerListCenter.Instance.currentServer.running_status},不允许登录");
            }
            else
            {
                allow = true;
                Debug.Log("[LoginWin][Login] 服务器状态允许登录");
            }
        }
@@ -362,13 +371,15 @@
            switch ((ServerState)ServerListCenter.Instance.currentServer.running_status)
            {
                case ServerState.Maintain:
                    Debug.LogWarning("[LoginWin][Login] 服务器维护中,弹出维护提示");
                    SysNotifyMgr.Instance.ShowTip("ServerDown");
                    break;
                case ServerState.Predicted:
                    Debug.LogWarning($"[LoginWin][Login] 服务器未开服,开服时间: {ServerListCenter.Instance.currentServer.start_date:yyyy-MM-dd HH:mm}");
                    SysNotifyMgr.Instance.ShowTip("ServerOpen", ServerListCenter.Instance.currentServer.start_date.ToString("yyyy-MM-dd HH:mm"));
                    break;
            }
            Debug.LogWarning("[LoginWin][Login] return: 服务器状态不允许登录");
            return;
        }
@@ -380,14 +391,29 @@
                case VersionAuthority.InterTest:
                    if (string.IsNullOrEmpty(m_Account.text))
                    {
                        Debug.LogWarning("[LoginWin][Login] 测试服账号为空,弹出提示");
                        ServerTipDetails.DisplayNormalTip(Language.Get("L1095"));
                        Debug.LogWarning("[LoginWin][Login] return: 测试服账号为空");
                        return;
                    }
                    Debug.Log($"[LoginWin][Login] 测试服账号登录: {m_Account.text}, IP: {m_ServerIP}, Port: {m_Port}, GamePort: {m_GamePort}");
                    LoginManager.Instance.AccountLogin(m_Account.text, m_ServerIP, m_Port, m_GamePort);
                    break;
                case VersionAuthority.Release:
#if UNITY_WEBGL
                    if (string.IsNullOrEmpty(m_Account.text))
                    {
                        Debug.LogWarning("[LoginWin][Login] WebGL账号为空,弹出提示");
                        ServerTipDetails.DisplayNormalTip(Language.Get("L1095"));
                        return;
                    }
                    Debug.Log($"[LoginWin][Login] WebGL账号登录: {m_Account.text}, IP: {m_ServerIP}, Port: {m_Port}, GamePort: {m_GamePort}");
                    LoginManager.Instance.AccountLogin(m_Account.text, m_ServerIP, m_Port, m_GamePort);
#else
                    Debug.Log($"[LoginWin][Login] 正式服账号登录: IP: {m_ServerIP}, Port: {m_Port}, GamePort: {m_GamePort}");
                    LoginManager.Instance.AccountLogin(m_ServerIP, m_Port, m_GamePort);
#endif
                    break;
            }
Main/System/Message/MessageWin.cs
@@ -1,6 +1,7 @@
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;
using Cysharp.Threading.Tasks;
using System.Text.RegularExpressions;
@@ -109,14 +110,6 @@
            OnGMOpen();
            ServerTipDetails.requireOpenGM = false;
        }
    }
    private void OnDisable()
    {
        m_ContainerNormalHint.SetActive(false);
        m_ContainerChatHint.SetActive(false);
        DisableServerTip();
        StopAllCoroutines();
    }
    void CheckNormalHint()
Main/System/Tip/ScrollTip.cs
@@ -19,6 +19,9 @@
    
    public static void ShowTip(string tip, ArrayList infoList = null, int _order = 0)
    {
        if (string.IsNullOrWhiteSpace(tip))
            return;
        int range = Math.Min(m_Hints.Count, 10);
        int findCnt = 0;
        for (int i = 1; i <= range; i++)
Main/System/Tip/ScrollTipDetail.cs
@@ -19,6 +19,7 @@
    private float m_TipShowTime;
    private float m_TipHideTime;
    private float m_TipDistance;
    private Sequence _sequence;
    private void Awake()
    {
@@ -46,12 +47,12 @@
        pos = transform.localPosition;
        presentState = state;
        //LateUpdate 改成dotwweing
        Sequence tween = DOTween.Sequence();;
        tween.Append(transform.DOLocalMove(pos.SetY(pos.y + m_TipDistance), ScrollTip.tipMoveTime)).SetEase(Ease.Linear);
        tween.AppendInterval(m_TipShowTime);
        tween.Append(canvasGroup.DOFade(0, m_TipHideTime)).SetEase(Ease.Linear);
        tween.Play();
        tween.OnComplete(() =>
        _sequence = DOTween.Sequence();
        _sequence.Append(transform.DOLocalMove(pos.SetY(pos.y + m_TipDistance), ScrollTip.tipMoveTime).SetEase(Ease.Linear));
        _sequence.AppendInterval(m_TipShowTime);
        _sequence.Append(canvasGroup.DOFade(0, m_TipHideTime).SetEase(Ease.Linear));
        _sequence.Play();
        _sequence.OnComplete(() =>
        { 
            presentState = ScrollTip.ScrollTipState.None;
            ScrollTip.Release(this, false);
Main/System/Tip/ScrollTipWin.cs
@@ -113,6 +113,15 @@
            ScrollTipDetail tipDetail = await ScrollTip.Request();
            if (tipDetail != null)
            {
                // 首次登录时 pool 为 null,Request() 异步加载预制体耗时 >100ms。
                // LoopTipReceiveEvent 每 100ms 重入,可能已有另一次调用先消耗了 m_Hints[0]。
                // 若 await 返回时 m_Hints 已空,必须将 tipDetail 归还池,否则它会被插入 Canvas 但
                // 永远不会播放动画,导致提示条卡在屏幕上(第一次登录复现的根因)。
                if (ScrollTip.m_Hints.Count == 0)
                {
                    ScrollTip.Release(tipDetail, false);
                    return;
                }
                tipDetail.SetTipConfig(m_TipShowTime, m_TipHideTime, m_TipDistance);
                ScrollTip.m_ActiveTips.Add(tipDetail);
                var rt = tipDetail.transform;