三国卡牌客户端基础资源仓库
yyl
2026-05-22 833debe445bb7590bbb21802bfeec4af7f1199df
dll builtin区分开其他package的改动
3个文件已修改
361 ■■■■■ 已修改文件
Assets/Launch/Launch.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Launch/Manager/LocalResManager.cs 260 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Launch/Manager/YooAssetInitializer.cs 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Launch/Launch.cs
@@ -204,52 +204,11 @@
        System.Net.ServicePointManager.DefaultConnectionLimit = 100;
        StringUtility.WarmupPool();
#if !UNITY_WEBGL && UNITY_EDITOR
        //内网下载测试
        _hotUpdateAss = System.AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "Main");
        Type type = _hotUpdateAss.GetType("InGameDownTestUtility");
        LocalResManager.Instance.isPCTestDownLoad = (bool)type.GetMethod("GetReadVerionEx").Invoke(null, null);
        LocalResManager.Instance.isOpenDownLoad = (bool)type.GetMethod("GetDownloadEnable").Invoke(null, null);
#endif
        SDKInit();
        LocalResManager.Instance.Init();
#if UNITY_WEBGL
#if UNITY_EDITOR
        // Editor WebGL 模拟:ReadText 走物理文件(不需要 YooAsset),可直接 InitTable
        LocalResManager.Instance.InitTable(async () =>
        {
            LocalResManager.Instance.InitDefaultLanguage();
            launchExWin = await LaunchExWin.OpenWindow();
            if (AssetSource.simulateWebGL)
                LocalResManager.step = LocalResManager.LoadDllStep.RequestVersion;
            else
                StartGame();
        }).Forget();
#else
        // 非 Editor WebGL:ReadText 依赖 YooAsset(PrefabPackage),但此时 YooAsset 尚未初始化。
        // 必须先走 RequestVersion → OnVersionCheckResult → InitYooAssetEarlyAsync,
        // YooAsset 就绪后再由 OnVersionCheckResult 内的 InitTable 完成配置加载。
        // 任何都要去请求版本信息
        LocalResManager.step = LocalResManager.LoadDllStep.RequestVersion;
#endif
#else
#if UNITY_EDITOR
        if (!LocalResManager.Instance.isOpenDownLoad)
        {
            // Editor 不走下载流程:InitTable 直接读物理文件(不需要 YooAsset),然后启动游戏
            LocalResManager.Instance.InitTable(() =>
            {
                LocalResManager.Instance.InitDefaultLanguage();
                launchExWin = LaunchExWin.OpenWindow();
                StartGame();
            }).Forget();
        }
        else
#endif
        {
            // 正式包 / Editor 下载模式:先请求版本获取 cdnUrl,YooAsset 初始化和 InitTable 在 OnVersionCheckResult 中执行
            LocalResManager.step = LocalResManager.LoadDllStep.RequestVersion;
        }
#endif
    }
    private void InitPlugins()
Assets/Launch/Manager/LocalResManager.cs
@@ -62,11 +62,6 @@
        }
    }
#if UNITY_EDITOR && !UNITY_WEBGL
    public bool isPCTestDownLoad = false;   //开启下载并开启下载bytes
    public bool isOpenDownLoad = false; //只开启下载
#endif
    public static int downLoadCount = 0;
    public static readonly string[] VERSION_URL = new string[] 
@@ -144,7 +139,7 @@
    private const int YOO_MAX_RETRY = 3;
    private const int YOO_BASE_DELAY_MS = 500;
    // package 为 null 时默认使用 PrefabPackage(Config/Shader/Materials/ScriptableObject/Scenes 等)
    // package 为 null 时默认使用启动配置包(当前为 Builtin)
    // BuiltIn 路径下的资源(Sprites/Prefabs)应传入 DefaultPackage(= Builtin 包)
    private async UniTask<T> LoadAssetWithRetryAsync<T>(string location, ResourcePackage package = null) where T : UnityEngine.Object
    {
@@ -307,75 +302,182 @@
    /// </summary>
    private void OnVersionCheckResult(bool _ok, string _result)
    {
        if (_ok)
        if (!_ok)
        {
            versionUrlResult = NormalizeVersionJson(_result);
            if (string.IsNullOrEmpty(versionUrlResult))
            RetryVersionCheck($"[LocalResManager] 版本接口请求失败,url={versionUrl},error={_result}");
            return;
        }
        versionUrlResult = _result;
        // 版本接口是启动链路入口:先拿到可解析的版本数据,再允许后续资源初始化。
        if (!TryUpdateVersionInfo(_result))
        {
            return;
        }
        Debug.Log("OK OnVersionCheckResult " + versionUrlResult);
        // resource_url 会决定 YooAsset HostPlayMode 的远端根路径,必须在初始化 YooAsset 前写入。
        TryApplyCdnUrlFromVersionInfo();
        // InitialFunction 是启动期功能开关和语言配置;CDN 失败时会回落到包内表,避免启动卡死。
        LoadInitialFunctionAndStartAsync(BuildInitialFunctionUrl()).Forget();
    }
    private bool TryUpdateVersionInfo(string rawResult)
    {
        versionUrlResult = NormalizeVersionJson(rawResult);
        if (string.IsNullOrEmpty(versionUrlResult))
        {
            RetryVersionCheck("[LocalResManager] 服务端返回空版本数据,请检查服务端是否已配置该渠道。");
            return false;
        }
        try
        {
            versionInfo = JsonMapper.ToObject<VersionInfo>(versionUrlResult);
            return true;
        }
        catch (Exception ex)
        {
            var firstChar = versionUrlResult.Length > 0 ? ((int)versionUrlResult[0]).ToString() : "<empty>";
            var preview = versionUrlResult.Length > 32 ? versionUrlResult.Substring(0, 32) : versionUrlResult;
            RetryVersionCheck($"[LocalResManager] Version json parse failed. firstChar={firstChar}, preview='{preview}', error={ex}");
            return false;
        }
    }
    private void TryApplyCdnUrlFromVersionInfo()
    {
        var versionConfig = VersionConfigEx.config;
        if (versionInfo == null || versionConfig == null)
        {
            Debug.LogWarning("[LocalResManager] VersionInfo 或 VersionConfigEx 为空,将使用本地/离线资源模式。");
            return;
        }
        try
        {
            string resourceUrl = versionInfo.GetResourcesURL(versionConfig.branch);
            if (string.IsNullOrEmpty(resourceUrl))
            {
                Debug.LogError("[LocalResManager] 服务端返回空版本数据,请检查服务端是否已配置该渠道。将在1秒后重试...");
                Clock.AlarmAt(DateTime.Now + new TimeSpan(TimeSpan.TicksPerSecond), RequestVersionCheck);
                return;
            }
            try
            {
                versionInfo = JsonMapper.ToObject<VersionInfo>(versionUrlResult);
            }
            catch (Exception ex)
            {
                var firstChar = versionUrlResult.Length > 0 ? ((int)versionUrlResult[0]).ToString() : "<empty>";
                var preview = versionUrlResult.Length > 32 ? versionUrlResult.Substring(0, 32) : versionUrlResult;
                Debug.LogError($"[LocalResManager] Version json parse failed. firstChar={firstChar}, preview='{preview}', error={ex}");
                Debug.LogWarning($"[LocalResManager] resource_url 为空,将使用本地/离线资源模式。branch={versionConfig.branch}");
                return;
            }
            Debug.Log("OK OnVersionCheckResult " + _result);
            versionConfig.cdnUrl = resourceUrl.TrimEnd('/');
            Debug.Log($"[LocalResManager] 已从 resource_url 设置 cdnUrl={versionConfig.cdnUrl} (branch={versionConfig.branch})");
        }
        catch (Exception urlEx)
        {
            Debug.LogWarning($"[LocalResManager] 获取 resource_url 失败,将使用本地/离线资源模式: {urlEx.Message}");
        }
    }
            // 从服务器返回的 resource_url 中提取 CDN 地址,写入 VersionConfigEx 供 YooAsset 初始化使用
            if (versionInfo != null && VersionConfigEx.config != null)
    private string BuildInitialFunctionUrl()
    {
        var cdnUrl = VersionConfigEx.config?.cdnUrl;
        if (string.IsNullOrEmpty(cdnUrl))
        {
            return string.Empty;
        }
        return cdnUrl.TrimEnd('/') + "/InitialFunction.txt";
    }
    private async UniTaskVoid LoadInitialFunctionAndStartAsync(string initFuncUrl)
    {
        bool configReady = await TryLoadInitialFunctionFromCdnAsync(initFuncUrl);
        if (!configReady)
        {
            await TryLoadInitialFunctionFromResourcesAsync();
        }
        await InitializeYooAssetAndEnterReadBytesAsync();
    }
    private async UniTask<bool> TryLoadInitialFunctionFromCdnAsync(string initFuncUrl)
    {
        if (string.IsNullOrEmpty(initFuncUrl))
        {
            Debug.LogWarning("[LocalResManager] cdnUrl 为空,跳过 CDN InitialFunction.txt,尝试读取包内配置。");
            return false;
        }
        var response = await HttpRequest.Instance.UnityWebRequestGetAsync(initFuncUrl, HttpRequest.defaultHttpContentType, 10);
        if (!response.ok)
        {
            Debug.LogWarning($"[LocalResManager] 从 CDN 加载 InitialFunction.txt 失败,url={initFuncUrl},error={response.message}");
            return false;
        }
        if (string.IsNullOrEmpty(response.message))
        {
            Debug.LogWarning($"[LocalResManager] CDN InitialFunction.txt 内容为空,url={initFuncUrl}");
            return false;
        }
        try
        {
            InitialFunctionConfig.Init(response.message);
            Debug.Log($"[LocalResManager] 成功从 CDN 加载 InitialFunction.txt,url={initFuncUrl}");
            return true;
        }
        catch (Exception ex)
        {
            Debug.LogError($"[LocalResManager] 解析 CDN InitialFunction.txt 失败,url={initFuncUrl},error={ex}");
            return false;
        }
    }
    private async UniTask<bool> TryLoadInitialFunctionFromResourcesAsync()
    {
        try
        {
            TextAsset textAsset = await Resources.LoadAsync<TextAsset>("Config/InitialFunction") as TextAsset;
            if (textAsset == null || string.IsNullOrEmpty(textAsset.text))
            {
// #if TEST_BUILD && UNITY_WEBGL
                // 本地测试:强制使用本地 CDN,忽略服务器返回的 resource_url
                // VersionConfigEx.config.cdnUrl = "http://localhost:8081/";
                // Debug.Log("[LocalResManager] TEST_BUILD+WebGL: 强制使用本地 CDN http://localhost:8081/");
// #else
                try
                {
                    string resourceUrl = versionInfo.GetResourcesURL(VersionConfigEx.config.branch);
                    if (!string.IsNullOrEmpty(resourceUrl))
                    {
                        VersionConfigEx.config.cdnUrl = resourceUrl;
                        Debug.Log($"[LocalResManager] 已从 resource_url 设置 cdnUrl={resourceUrl} (branch={VersionConfigEx.config.branch})");
                    }
                }
                catch (Exception urlEx)
                {
                    Debug.LogWarning($"[LocalResManager] 获取 resource_url 失败,将使用 OfflinePlayMode: {urlEx.Message}");
                }
// #endif
                Debug.LogWarning("[LocalResManager] 包内 InitialFunction.txt 不存在或内容为空,将继续启动。");
                return false;
            }
            Launch.Instance.InitYooAssetEarlyAsync().ContinueWith(() =>
            {
                if (YooAssetInitializer.Instance.State != YooAssetInitializer.InitState.Ready)
                {
                    Debug.LogError($"[LocalResManager] YooAsset 初始化失败(State={YooAssetInitializer.Instance.State}),无法继续加载资源");
                    return;
                }
                // YooAsset 就绪后,加载配置表并显示加载界面
                InitTable(() =>
                {
                    InitDefaultLanguage();
                    Launch.Instance.ShowLaunchUI();
                    step = LoadDllStep.ReadBytes;
                }).Forget();
            }).Forget();
            InitialFunctionConfig.Init(textAsset.text);
            Debug.Log("[LocalResManager] 已使用包内 InitialFunction.txt 初始化启动配置。");
            return true;
        }
        else
        catch (Exception ex)
        {
            Debug.Log("http 数据通讯: VersionUtility:" + versionUrl + "  result:" + versionUrlResult);
            Clock.AlarmAt(DateTime.Now + new TimeSpan(TimeSpan.TicksPerSecond), RequestVersionCheck);
            Debug.LogError($"[LocalResManager] 读取包内 InitialFunction.txt 失败,将继续启动。error={ex}");
            return false;
        }
    }
    private async UniTask InitializeYooAssetAndEnterReadBytesAsync()
    {
        if (Launch.Instance == null)
        {
            Debug.LogError("[LocalResManager] Launch.Instance 为空,无法继续启动流程。");
            return;
        }
        await Launch.Instance.InitYooAssetEarlyAsync();
        if (YooAssetInitializer.Instance.State != YooAssetInitializer.InitState.Ready)
        {
            Debug.LogError($"[LocalResManager] YooAsset 初始化失败(State={YooAssetInitializer.Instance.State}),无法继续加载资源");
            return;
        }
        // YooAsset 的 Manifest 就绪后,才可以显示加载界面并进入 DLL 读取阶段。
        InitDefaultLanguage();
        Launch.Instance.ShowLaunchUI();
        step = LoadDllStep.ReadBytes;
    }
    private void RetryVersionCheck(string message)
    {
        Debug.LogWarning(message + " 将在1秒后重试...");
        Clock.AlarmAt(DateTime.Now + TimeSpan.FromSeconds(1), RequestVersionCheck);
    }
    private static string NormalizeVersionJson(string raw)
@@ -596,36 +698,24 @@
    // 通过 YooAsset 加载 TextAsset 类型的文本文件。
    // location: 完整的 YooAsset 路径,如 "Assets/ResourcesOut/Config/InitialFunction.txt"
    // Editor 下直接读物理文件(剥离 Assets/ 前缀后拼 Application.dataPath)
    private async UniTask ReadText(string location, Action<bool, string> OnComplete = null)
    private async UniTask ReadText(string fileName, Action<bool, string> OnComplete = null)
    {
        string content = string.Empty;
        bool result = false;
#if UNITY_EDITOR
        // location 形如 "Assets/ResourcesOut/Config/Foo.txt" → 物理路径
        var editorPath = Application.dataPath + "/" + location.Substring("Assets/".Length);
        if (File.Exists(editorPath))
        {
            content = File.ReadAllText(editorPath);
            result = true;
        }
        else
        {
            Debug.LogError($"ReadText: file not found at '{editorPath}'");
        }
        await UniTask.Yield();
#else
#if UNITY_WEBGL || TEST_BUILD
    Debug.Log($"[LocalResManager][Diag] ReadText via YooAsset location='{location}', state={YooAssetInitializer.Instance.State}");
#endif
        var textAsset = await LoadAssetWithRetryAsync<TextAsset>(location);
        string path = "Config/" + fileName;
        TextAsset textAsset = await Resources.LoadAsync<TextAsset>(path) as TextAsset; // 预热资源,避免后续首次加载时的额外延迟
        if (textAsset != null)
        {
            content = textAsset.text;
            result = true;
            Debug.Log($"ReadText '{location}' size:{content.Length}");
        }
#endif
        else
        {
            Debug.LogError("LoadResourceFailure " + path);
        }
        OnComplete?.Invoke(result, content);
    }
@@ -644,7 +734,7 @@
#endif
        }
        await ReadText("Assets/ResourcesOut/Config/InitialFunction.txt", (isOK, value) =>
        await ReadText("InitialFunction", (isOK, value) =>
        {
            if (isOK)
            {
Assets/Launch/Manager/YooAssetInitializer.cs
@@ -37,21 +37,25 @@
    // Launch 程序集无法引用 Main 的 YooAssetPackageConfig,在此重复定义
    // ====================================================================
    /// <summary>默认包名 — Prefab 包含启动必需资源</summary>
    public const string DEFAULT_PACKAGE_NAME = "Builtin";
    /// <summary>默认包名 — Builtin 包含启动必需资源</summary>
    public const string BUILTIN_PACKAGE_NAME = "Builtin";
    public const string CONFIG_PACKAGE_NAME = "Prefab";
    public const string DLL_PACKAGE_NAME = "Dll";
    public const string DEFAULT_PACKAGE_NAME = BUILTIN_PACKAGE_NAME;
    public const string CONFIG_PACKAGE_NAME = BUILTIN_PACKAGE_NAME;
    /// <summary>
    /// Launch 阶段需要初始化的包名(与 Collector 一致)。
    /// </summary>
    private static readonly string[] LAUNCH_PACKAGES = new[] { "Prefab", "UI", "UIEffect", "Battle", "Audio", "Builtin", "Video", "Dll" };
    private static readonly string[] LAUNCH_PACKAGES = new[] { BUILTIN_PACKAGE_NAME, DLL_PACKAGE_NAME };
    private EPlayMode _playMode;
    private IRemoteServices _remoteServices;
    private string _remoteCdnBaseUrl;
    private ResourcePackage _defaultPackage;
    private ResourcePackage _prefabPackage;
    private ResourcePackage _configPackage;
    private readonly Dictionary<string, ResourcePackage> _packages = new Dictionary<string, ResourcePackage>();
@@ -109,7 +113,11 @@
    /// </summary>
    public ResourcePackage DefaultPackage => _defaultPackage;
    public ResourcePackage PrefabPackage => _prefabPackage;
    public ResourcePackage BuiltinPackage => _defaultPackage;
    public ResourcePackage DllPackage => GetPackage(DLL_PACKAGE_NAME);
    public ResourcePackage PrefabPackage => _configPackage;
    /// <summary>
    /// 当前运行模式
@@ -205,10 +213,10 @@
                        YooAssets.SetDefaultPackage(package);
                    }
                    // CONFIG_PACKAGE_NAME 包作为配置包
                    // CONFIG_PACKAGE_NAME 包作为启动配置包
                    if (pkgName == CONFIG_PACKAGE_NAME)
                    {
                        _prefabPackage = package;
                        _configPackage = package;
                    }
                    Debug.Log($"[YooAssetInitializer] Package '{pkgName}' initialized.");
@@ -331,7 +339,7 @@
                    if (_playMode == EPlayMode.HostPlayMode)
                    {
                        Debug.Log($"[YooAssetInitializer] HostPlayMode fallback: loading buildin manifest for '{pkgName}'");
                        var fallbackVersionOp = package.RequestPackageVersionAsync(false);
                        var fallbackVersionOp = package.RequestPackageVersionAsync(true);
                        await fallbackVersionOp.ToUniTask();
                        if (fallbackVersionOp.Status == EOperationStatus.Succeed)
                        {
@@ -339,6 +347,14 @@
                            await fallbackManifestOp.ToUniTask();
                            if (fallbackManifestOp.Status == EOperationStatus.Succeed)
                            {
                                // Manifest 更新成功后清理旧版本缓存(非 Editor 模式下才有磁盘缓存)
#if !UNITY_EDITOR
                                var fallbackClearBundleOp = package.ClearCacheFilesAsync(EFileClearMode.ClearUnusedBundleFiles);
                                await fallbackClearBundleOp.ToUniTask();
                                var fallbackClearManifestOp = package.ClearCacheFilesAsync(EFileClearMode.ClearUnusedManifestFiles);
                                await fallbackClearManifestOp.ToUniTask();
#endif
                                Debug.Log($"[YooAssetInitializer] Package '{pkgName}' using buildin manifest v{fallbackVersionOp.PackageVersion}");
#if UNITY_WEBGL || TEST_BUILD
                                LogPackageState(pkgName, package, "fallback-manifest-success");
@@ -365,6 +381,14 @@
#endif
                    continue;
                }
                // Manifest 更新成功后清理旧版本缓存(非 Editor 模式下才有磁盘缓存)
#if !UNITY_EDITOR
                var clearBundleOp = package.ClearCacheFilesAsync(EFileClearMode.ClearUnusedBundleFiles);
                await clearBundleOp.ToUniTask();
                var clearManifestOp = package.ClearCacheFilesAsync(EFileClearMode.ClearUnusedManifestFiles);
                await clearManifestOp.ToUniTask();
#endif
                Debug.Log($"[YooAssetInitializer] Package '{pkgName}' manifest updated to {packageVersion}.");
#if UNITY_WEBGL || TEST_BUILD
@@ -402,7 +426,7 @@
        int assetTotalCount = -1;
        int bundleTotalCount = -1;
        string packageVersion = "manifest-null";
        string initialFunctionValid = "not-prefab";
        string initialFunctionValid = "not-config-package";
        try
        {
@@ -647,23 +671,26 @@
            case EPlayMode.WebPlayMode:
            {
                var webParams = new WebPlayModeParameters();
                var effectiveRemote = !string.IsNullOrEmpty(_remoteCdnBaseUrl)
                    ? new UrlRemoteServices(_remoteCdnBaseUrl, _remoteCdnBaseUrl, packageName)
                    : remoteServices;
#if UNITY_WEBGL && WEIXINMINIGAME && !UNITY_EDITOR
                // 微信小游戏:使用 WechatFileSystem 进行资源缓存
                string packageRoot = $"{WeChatWASM.WX.env.USER_DATA_PATH}/__GAME_FILE_CACHE";
                webParams.WebServerFileSystemParameters = WechatFileSystemCreater
                    .CreateFileSystemParameters(packageRoot, remoteServices);
                    .CreateFileSystemParameters(packageRoot, effectiveRemote);
#elif UNITY_WEBGL && DOUYINMINIGAME && !UNITY_EDITOR
                // 抖音小游戏:使用 TiktokFileSystem 进行资源缓存
                string packageRoot = TTSDK.TTFileSystem.USER_DATA_PATH + "/__GAME_FILE_CACHE";
                webParams.WebServerFileSystemParameters = TiktokFileSystemCreater
                    .CreateFileSystemParameters(packageRoot, remoteServices);
                    .CreateFileSystemParameters(packageRoot, effectiveRemote);
#else
                if (remoteServices != null)
                if (effectiveRemote != null)
                {
                    // 远程模式(LocalCDN/RemoteCDN):资源不在 StreamingAssets,
                    // 跳过 WebServerFileSystem,只用 WebRemoteFileSystem 从 HTTP 服务器加载
                    webParams.WebServerFileSystemParameters = FileSystemParameters
                        .CreateDefaultWebRemoteFileSystemParameters(remoteServices);
                        .CreateDefaultWebRemoteFileSystemParameters(effectiveRemote);
                }
                else
                {
@@ -685,6 +712,7 @@
    public void Release()
    {
        _defaultPackage = null;
        _configPackage = null;
        _packages.Clear();
        _remoteServices = null;
        _remoteCdnBaseUrl = null;