三国卡牌客户端基础资源仓库
yyl
4 天以前 da38bd1ed3e888b9d33e191f28d18f4505de6df0
多语言资源进包的工具/开关 流程
5个文件已修改
315 ■■■■ 已修改文件
Assets/Editor/AssetBundleBrowser/AssetBundleBuildTab.cs 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/Tool/ClientPackage.cs 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/YooAsset/YooAssetBuildTool.cs 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Launch/Manager/YooAssetInitializer.cs 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Launch/VersionConfigEx.cs 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/AssetBundleBrowser/AssetBundleBuildTab.cs
@@ -469,6 +469,25 @@
            EditorGUILayout.Space();
            GUILayout.BeginHorizontal();
            // 当前渠道(publishers)对应 Versions.txt 中的 m_EnableLanguagePackage 列,切换 Publishers 时会随 SetVersionConfig 自动刷新。
            if (VersionConfig.config != null)
            {
                bool newEnableLanguagePackage = EditorGUILayout.Toggle("Enable Language Package", VersionConfig.config.enableLanguagePackage, GUILayout.Width(250));
                if (newEnableLanguagePackage != VersionConfig.config.enableLanguagePackage)
                {
                    VersionConfig.config.enableLanguagePackage = newEnableLanguagePackage;
                    EditorUtility.SetDirty(VersionConfig.config);
                }
            }
            // 本次打包使用的语言子目录标识(如 zh/en),仅在上方开关开启时生效;为空则沿用无语言目录结构。
            // 打包时会实时从磁盘重新加载 VersionConfig.asset 判断开关,不依赖本面板的缓存值。
            YooAssetBuildTool.BuildLanguage = EditorGUILayout.TextField("Build Language", YooAssetBuildTool.BuildLanguage, GUILayout.Width(250));
            GUILayout.EndHorizontal();
            EditorGUILayout.Space();
            GUILayout.BeginHorizontal();
            //ClientPackage.obfuscatorEnabled = EditorGUILayout.Toggle("Obfuscator Enable ", ClientPackage.obfuscatorEnabled, GUILayout.Width(250));
#if UNITY_STANDALONE
Assets/Editor/Tool/ClientPackage.cs
@@ -173,7 +173,7 @@
            // ---- NullAsset(小包):不随包携带 StreamingAssets 资源,全部从 CDN 按需下载 ----
            if (smallPackages.Count > 0)
            {
                YooAssetBuildTool.ClearStreamingAssetsYooDirectory();
                YooAssetBuildTool.ClearAllStreamingAssetsYoo();
                Debug.Log("[ClientPackage] NullAsset: 已清空 StreamingAssets/yoo");
                // 写入随包 Package 列表,NullAsset 为空列表
@@ -198,13 +198,17 @@
            {
                var removePackages = CreateHalfAssetRemovePackages();
                foreach (var pkgName in removePackages)
                // 多语言开启时需对每个语言子目录分别剔除。
                foreach (var container in YooAssetBuildTool.GetPackageContainerDirectories(yooRoot))
                {
                    string pkgDir = Path.Combine(yooRoot, pkgName);
                    if (Directory.Exists(pkgDir))
                    foreach (var pkgName in removePackages)
                    {
                        Directory.Delete(pkgDir, true);
                        Debug.Log($"[ClientPackage] HalfAsset: 已剔除 Package '{pkgName}'");
                        string pkgDir = Path.Combine(container, pkgName);
                        if (Directory.Exists(pkgDir))
                        {
                            Directory.Delete(pkgDir, true);
                            Debug.Log($"[ClientPackage] HalfAsset: 已剔除 Package '{Path.GetFileName(container)}/{pkgName}'");
                        }
                    }
                }
@@ -453,18 +457,21 @@
        switch (versionConfig.assetAccess)
        {
            case InstalledAsset.NullAsset:
                YooAssetBuildTool.ClearStreamingAssetsYooDirectory();
                YooAssetBuildTool.ClearAllStreamingAssetsYoo();
                Debug.Log("[ClientPackage] NullAsset Export: 已清空 StreamingAssets/yoo");
                break;
            case InstalledAsset.HalfAsset:
                var removePackages = CreateHalfAssetRemovePackages();
                foreach (var pkgName in removePackages)
                foreach (var container in YooAssetBuildTool.GetPackageContainerDirectories(yooRoot))
                {
                    string pkgDir = Path.Combine(yooRoot, pkgName);
                    if (Directory.Exists(pkgDir))
                    foreach (var pkgName in removePackages)
                    {
                        Directory.Delete(pkgDir, true);
                        Debug.Log($"[ClientPackage] HalfAsset Export: 已剔除 Package '{pkgName}'");
                        string pkgDir = Path.Combine(container, pkgName);
                        if (Directory.Exists(pkgDir))
                        {
                            Directory.Delete(pkgDir, true);
                            Debug.Log($"[ClientPackage] HalfAsset Export: 已剔除 Package '{Path.GetFileName(container)}/{pkgName}'");
                        }
                    }
                }
                break;
@@ -851,11 +858,16 @@
    private static bool ValidateLaunchRequiredPackages(string yooRoot)
    {
        var missing = new List<string>();
        for (int i = 0; i < LaunchRequiredYooPackages.Length; i++)
        // 多语言开启时,yooRoot 下是语言子目录,每个语言目录都必须含 Builtin/Dll。
        var containers = YooAssetBuildTool.GetPackageContainerDirectories(yooRoot);
        foreach (var container in containers)
        {
            string packageDir = Path.Combine(yooRoot, LaunchRequiredYooPackages[i]);
            if (!Directory.Exists(packageDir))
                missing.Add(LaunchRequiredYooPackages[i]);
            for (int i = 0; i < LaunchRequiredYooPackages.Length; i++)
            {
                string packageDir = Path.Combine(container, LaunchRequiredYooPackages[i]);
                if (!Directory.Exists(packageDir))
                    missing.Add($"{Path.GetFileName(container)}/{LaunchRequiredYooPackages[i]}");
            }
        }
        if (missing.Count > 0)
Assets/Editor/YooAsset/YooAssetBuildTool.cs
@@ -46,6 +46,116 @@
    private const string LaunchBackgroundFileName = "LoginBackground.jpg";
    private const string LaunchBackgroundVersionFileName = "version.txt";
    // ====================================================================
    // 多语言随包资源:构建时开关与目录拼接
    // ====================================================================
    private const string BUILD_LANGUAGE_PREF = "YooAsset_BuildLanguage";
    /// <summary>
    /// 当前构建使用的语言标识(与运行时 LocalResManager.Id/Language.Id 约定一致,如 "zh""en")。
    /// 仅影响构建输出目录,不影响实际打包的资源内容来源(若需按语言切换 Collector 源资源,需在调用本工具前另行处理)。
    /// </summary>
    public static string BuildLanguage
    {
        get { return EditorPrefs.GetString(BUILD_LANGUAGE_PREF, string.Empty); }
        set { EditorPrefs.SetString(BUILD_LANGUAGE_PREF, value); }
    }
    /// <summary>
    /// 实时从磁盘重新加载 VersionConfig.asset确定本次构建是否启用多语言随包目录。
    /// 不依赖 VersionConfig.config 静态缓存实例(可能在 PreBuild 重建资产后过期),确保每次构建都拿到磁盘上的最新开关值。
    /// 开关关闭或未配置语言时返回 null,保持原有无语言目录结构。
    /// </summary>
    private static string ResolveActiveLanguage()
    {
        if (!IsLanguagePackageBuildEnabled())
        {
            return null;
        }
        string language = BuildLanguage;
        return string.IsNullOrEmpty(language) ? null : language;
    }
    /// <summary>
    /// 实时从磁盘 VersionConfig.asset 判断是否启用多语言随包目录(与面板勾选同一个 asset)。
    /// APK/Export 消费侧根据此开关决定 StreamingAssets/yoo 下是平铺结构还是语言子目录结构。
    /// </summary>
    public static bool IsLanguagePackageBuildEnabled()
    {
        var versionConfig = AssetDatabase.LoadAssetAtPath<VersionConfig>("Assets/Resources/VersionConfig.asset");
        return versionConfig != null && versionConfig.enableLanguagePackage;
    }
    /// <summary>
    /// 返回 yooRoot 下“包所在的容器目录”列表,用于兼容平铺与多语言两种结构。
    /// 依据**实际目录结构**判断(不靠语言开关,避免开关开启但资源仍是平铺时误判):
    /// - yoo 根下直接含已知包目录(如 Builtin)→ 平铺结构,容器就是 yoo 根本身,返回 [yooRoot]。
    /// - 否则视 yoo 根下的子目录为语言目录,返回其中直接含包的子目录(yoo/zh, yoo/en...)。
    /// - 都识别不到包则兜底返回 [yooRoot]。
    /// APK/Export 流程中所有“遍历包目录”的操作(校验、裁剪删包)应基于此列表。
    /// </summary>
    public static System.Collections.Generic.List<string> GetPackageContainerDirectories(string yooRoot)
    {
        var result = new System.Collections.Generic.List<string>();
        // 开关关闭:永远走平铺结构,行为与改动前完全一致。
        if (!IsLanguagePackageBuildEnabled())
        {
            result.Add(yooRoot);
            return result;
        }
        if (!System.IO.Directory.Exists(yooRoot))
        {
            result.Add(yooRoot);
            return result;
        }
        // 平铺结构:包直接在 yoo 根下。
        if (DirectoryContainsAnyPackage(yooRoot))
        {
            result.Add(yooRoot);
            return result;
        }
        // 语言结构:包在 yoo/{lang}/ 下。
        foreach (var sub in System.IO.Directory.GetDirectories(yooRoot))
        {
            if (DirectoryContainsAnyPackage(sub))
                result.Add(sub);
        }
        if (result.Count == 0)
            result.Add(yooRoot);
        return result;
    }
    /// <summary>
    /// 目录下是否直接包含任一已知 Package 子目录(用于区分平铺/语言目录)。
    /// </summary>
    private static bool DirectoryContainsAnyPackage(string dir)
    {
        var validPackages = new System.Collections.Generic.HashSet<string>(GetAllPackageNames(), StringComparer.OrdinalIgnoreCase);
        foreach (var d in System.IO.Directory.GetDirectories(dir))
        {
            if (validPackages.Contains(System.IO.Path.GetFileName(d)))
                return true;
        }
        return false;
    }
    /// <summary>
    /// 在根目录后拼接语言子目录。language 为空时原样返回,与运行时 GetLanguagePackageRoot 约定保持一致。
    /// </summary>
    private static string ApplyLanguageSubfolder(string root, string language)
    {
        return string.IsNullOrEmpty(language) ? root : System.IO.Path.Combine(root, language);
    }
    /// <summary>
    /// AOT 元数据 DLL 列表 —— 所有平台通用部分
    /// (与 AOTGenericReferences.PatchedAOTAssemblyList 保持一致)
@@ -608,15 +718,24 @@
        System.IO.Directory.CreateDirectory(destFullPath);
        int copiedCount = 0;
        foreach (var packageName in GetAllPackageNames())
        // 语言模式下包在 yoo/{lang}/{pkg},需按容器目录遍历并保留语言子目录结构;平铺模式容器即 yoo 根。
        foreach (var container in GetPackageContainerDirectories(sourceFullPath))
        {
            string sourcePackageDir = System.IO.Path.Combine(sourceFullPath, packageName);
            if (!System.IO.Directory.Exists(sourcePackageDir))
            bool isRoot = NormalizeFullPath(container) == sourceFullPath;
            string langSub = isRoot ? null : System.IO.Path.GetFileName(container);
            foreach (var packageName in GetAllPackageNames())
            {
                continue;
            }
                string sourcePackageDir = System.IO.Path.Combine(container, packageName);
                if (!System.IO.Directory.Exists(sourcePackageDir))
                {
                    continue;
                }
            copiedCount += CopyDirectoryFiles(sourcePackageDir, System.IO.Path.Combine(destFullPath, packageName));
                string destPackageDir = string.IsNullOrEmpty(langSub)
                    ? System.IO.Path.Combine(destFullPath, packageName)
                    : System.IO.Path.Combine(destFullPath, langSub, packageName);
                copiedCount += CopyDirectoryFiles(sourcePackageDir, destPackageDir);
            }
        }
        bool configCopied = false;
@@ -653,7 +772,8 @@
        }
        bool success = true;
        string streamingYooRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
        string language = ResolveActiveLanguage();
        string streamingYooRoot = ApplyLanguageSubfolder(AssetBundleBuilderHelper.GetStreamingAssetsRoot(), language);
        if (!CopyPackageBuildOutputToYooDirectory(packageName, streamingYooRoot, version, "StreamingAssets"))
        {
            success = false;
@@ -666,7 +786,7 @@
        if (!string.IsNullOrWhiteSpace(outputPath))
        {
            string destYooRoot = ResolveYooOutputDirectory(outputPath);
            string destYooRoot = ApplyLanguageSubfolder(ResolveYooOutputDirectory(outputPath), language);
            string streamingFullPath = NormalizeFullPath(streamingYooRoot);
            string destFullPath = NormalizeFullPath(destYooRoot);
            if (string.Equals(streamingFullPath, destFullPath, StringComparison.OrdinalIgnoreCase))
@@ -764,6 +884,50 @@
    }
    public static void ClearStreamingAssetsYooDirectory()
    {
        string rootYoo = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
        string language = ResolveActiveLanguage();
        if (string.IsNullOrEmpty(language))
        {
            // 平铺模式:清整个 yoo 根。
            if (System.IO.Directory.Exists(rootYoo))
                System.IO.Directory.Delete(rootYoo, true);
            System.IO.Directory.CreateDirectory(rootYoo);
            return;
        }
        // 语言模式:清当前语言目录(重新 build),并清理 yoo 根下残留的平铺包目录(从平铺模式切换过来的旧产物),
        // 保留其他语言目录(不破坏“随包两份语言”)。
        string langYoo = System.IO.Path.Combine(rootYoo, language);
        if (System.IO.Directory.Exists(langYoo))
            System.IO.Directory.Delete(langYoo, true);
        System.IO.Directory.CreateDirectory(langYoo);
        if (System.IO.Directory.Exists(rootYoo))
        {
            var validPackages = new System.Collections.Generic.HashSet<string>(GetAllPackageNames(), StringComparer.OrdinalIgnoreCase);
            foreach (var dir in System.IO.Directory.GetDirectories(rootYoo))
            {
                string name = System.IO.Path.GetFileName(dir);
                if (!validPackages.Contains(name))
                    continue;
                System.IO.Directory.Delete(dir, true);
                string metaPath = dir + ".meta";
                if (System.IO.File.Exists(metaPath))
                    System.IO.File.Delete(metaPath);
                Debug.Log($"[YooAssetBuildTool] 已清理旧平铺包目录(残留): {name}");
            }
        }
    }
    /// <summary>
    /// 清空整个 StreamingAssets/yoo 根目录(含所有语言子目录)。
    /// 用于 NullAsset(小包)打包:不随包携带任何 yoo 资源,全部从 CDN 按需下载。
    /// 与 ClearStreamingAssetsYooDirectory(仅清当前语言子目录,build 前用)区分。
    /// </summary>
    public static void ClearAllStreamingAssetsYoo()
    {
        string yooRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
        if (System.IO.Directory.Exists(yooRoot))
@@ -1154,7 +1318,7 @@
        string buildRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
        string platform = EditorUserBuildSettings.activeBuildTarget.ToString();
        string srcRoot = $"{buildRoot}/{platform}";
        string destRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
        string destRoot = ApplyLanguageSubfolder(AssetBundleBuilderHelper.GetStreamingAssetsRoot(), ResolveActiveLanguage());
        if (!System.IO.Directory.Exists(srcRoot))
        {
@@ -1202,11 +1366,14 @@
    public static void WriteBuildinPackageList()
    {
        string yooRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
        // 多语言开启时,包在语言子目录下(yoo/{lang}/{pkg}),从第一个容器目录扫包名即可(各语言包列表一致)。
        var containers = GetPackageContainerDirectories(yooRoot);
        string scanDir = containers.Count > 0 ? containers[0] : yooRoot;
        var existing = new System.Collections.Generic.List<string>();
        var validPackages = new System.Collections.Generic.HashSet<string>(GetAllPackageNames(), StringComparer.OrdinalIgnoreCase);
        if (System.IO.Directory.Exists(yooRoot))
        if (System.IO.Directory.Exists(scanDir))
        {
            foreach (var dir in System.IO.Directory.GetDirectories(yooRoot))
            foreach (var dir in System.IO.Directory.GetDirectories(scanDir))
            {
                var dirName = System.IO.Path.GetFileName(dir);
                if (validPackages.Contains(dirName))
@@ -1768,11 +1935,15 @@
    {
        try
        {
            Debug.Log($"[YooAssetBuildTool] 开始打包 Package: {packageName}, 版本: {version}");
            string language = ResolveActiveLanguage();
            Debug.Log($"[YooAssetBuildTool] 开始打包 Package: {packageName}, 版本: {version}" +
                (string.IsNullOrEmpty(language) ? string.Empty : $", 语言: {language}"));
            var buildParameters = new BuiltinBuildParameters();
            buildParameters.BuildOutputRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
            buildParameters.BuildinFileRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
            // 多语言随包资源:开关开启且配置了语言时,YooAsset 内部拷贝直接写入 yoo/{language}/{packageName},
            // 与运行时 YooAssetService/YooAssetInitializer 的 GetLanguagePackageRoot 探测路径保持一致。
            buildParameters.BuildinFileRoot = ApplyLanguageSubfolder(AssetBundleBuilderHelper.GetStreamingAssetsRoot(), language);
            buildParameters.BuildPipeline = EBuildPipeline.BuiltinBuildPipeline.ToString();
            buildParameters.BuildBundleType = (int)EBuildBundleType.AssetBundle;
            buildParameters.BuildTarget = EditorUserBuildSettings.activeBuildTarget;
Assets/Launch/Manager/YooAssetInitializer.cs
@@ -660,15 +660,29 @@
    // 辅助方法
    // ====================================================================
    /// <summary>
    /// 多语言随包资源开关。读取 VersionConfigEx.config.enableLanguagePackage(由 VersionConfig 经 VersionConfigEx.txt 同步而来)。
    /// 关闭或配置尚未加载时一律视为关闭,保持原有无语言目录结构,不影响未接入多语言的渠道。
    /// </summary>
    private static bool IsLanguagePackageEnabled()
    {
        return VersionConfigEx.config != null && VersionConfigEx.config.enableLanguagePackage;
    }
    public static async UniTask<bool> HasBuildinCatalogAsync(string packageName)
    {
        if (string.IsNullOrEmpty(packageName))
            return false;
        // 多语言随包资源:开关开启且 LocalResManager.Id 非空时,随包目录带语言前缀(与 CDN 端 fixPath 约定一致,
        // 例:yoo/zh/UI/BuildinCatalog.bytes)。否则退化为原有无语言目录结构。
        string languageId = IsLanguagePackageEnabled() ? LocalResManager.Id : null;
        string langSegment = string.IsNullOrEmpty(languageId) ? packageName : $"{languageId}/{packageName}";
#if UNITY_ANDROID && !UNITY_EDITOR
    return AndroidAssetExists($"yoo/{packageName}/BuildinCatalog.bytes");
    return AndroidAssetExists($"yoo/{langSegment}/BuildinCatalog.bytes");
#else
        string catalogUrl = $"{Application.streamingAssetsPath}/yoo/{packageName}/BuildinCatalog.bytes";
        string catalogUrl = $"{Application.streamingAssetsPath}/yoo/{langSegment}/BuildinCatalog.bytes";
        catalogUrl = catalogUrl.Replace('\\', '/');
#if !UNITY_WEBGL
@@ -762,12 +776,25 @@
        return CreateInitParameters(playMode, remoteServices, packageName, enableBuildinFileSystem);
    }
    /// <summary>
    /// 多语言随包资源根目录。language 为空时返回 null(沿用 YooAsset 默认目录,不区分语言)。
    /// 约定目录结构:{basePath}/yoo/{language}/{packageName},需与实际打包/CDN 上传目录结构保持一致。
    /// 注:packageRoot 覆盖行为依赖 YooAsset 内部实现,接入多语言后请通过 DiagDumpPackageStatus
    /// 确认对应包的 assets/bundles 数量非 0,验证根目录解析正确。
    /// </summary>
    private static string GetLanguagePackageRoot(string basePath, string packageName, string language)
    {
        return string.IsNullOrEmpty(language) ? null : $"{basePath}/yoo/{language}/{packageName}";
    }
    private InitializeParameters CreateInitParameters(EPlayMode playMode, IRemoteServices remoteServices, string packageName, bool enableBuildinFileSystem)
    {
        switch (playMode)
        {
            case EPlayMode.EditorSimulateMode:
            {
                // TODO: 编辑器模拟模式暂未接入多语言(SimulateBuild 按固定的 Bundles/{Platform}/{PackageName}/Simulate 约定读取,
                // 需要在构建工具侧支持语言子目录后才能在编辑器内模拟切换语言)。
#if UNITY_EDITOR
                var simulateResult = EditorSimulateModeHelper.SimulateBuild(packageName);
                return new EditorSimulateModeParameters
@@ -786,21 +813,28 @@
                var effectiveRemote = !string.IsNullOrEmpty(_remoteCdnBaseUrl)
                    ? new UrlRemoteServices(_remoteCdnBaseUrl, _remoteCdnBaseUrl, packageName)
                    : remoteServices;
                // 多语言随包资源:开关开启时,随包/本地缓存目录带语言前缀,
                // 需与实际打包输出的目录结构一致(例:StreamingAssets/yoo/zh/UI/...)。
                string languageId = IsLanguagePackageEnabled() ? LocalResManager.Id : null;
                string buildinRoot = GetLanguagePackageRoot(Application.streamingAssetsPath, packageName, languageId);
                string cacheRoot = GetLanguagePackageRoot(Application.persistentDataPath, packageName, languageId);
                return new HostPlayModeParameters
                {
                    BuildinFileSystemParameters = enableBuildinFileSystem
                        ? FileSystemParameters.CreateDefaultBuildinFileSystemParameters()
                        ? FileSystemParameters.CreateDefaultBuildinFileSystemParameters(packageRoot: buildinRoot)
                        : null,
                    CacheFileSystemParameters = FileSystemParameters
                        .CreateDefaultCacheFileSystemParameters(effectiveRemote)
                        .CreateDefaultCacheFileSystemParameters(effectiveRemote, packageRoot: cacheRoot)
                };
            }
            case EPlayMode.OfflinePlayMode:
            {
                string languageId = IsLanguagePackageEnabled() ? LocalResManager.Id : null;
                string buildinRoot = GetLanguagePackageRoot(Application.streamingAssetsPath, packageName, languageId);
                return new OfflinePlayModeParameters
                {
                    BuildinFileSystemParameters = FileSystemParameters
                        .CreateDefaultBuildinFileSystemParameters()
                        .CreateDefaultBuildinFileSystemParameters(packageRoot: buildinRoot)
                };
            }
            case EPlayMode.WebPlayMode:
Assets/Launch/VersionConfigEx.cs
@@ -159,6 +159,17 @@
        set { m_CdnUrl = value; }
    }
    /// <summary>
    /// 是否启用多语言随包资源目录(YooAsset Buildin/Cache 根目录按语言分子目录)。
    /// 与 Main 层 VersionConfig.enableLanguagePackage 字段名一致,通过 VersionConfigEx.txt 的 JSON 序列化互通。
    /// </summary>
    public bool m_EnableLanguagePackage;
    public bool enableLanguagePackage
    {
        get { return m_EnableLanguagePackage; }
        set { m_EnableLanguagePackage = value; }
    }
    public static VersionConfigEx config = null;
#if UNITY_WEBGL