三国卡牌客户端基础资源仓库
yyl
2 天以前 0a3bfa23a80b0d31b08d09d3e48d30cb75964559
打包修改
13个文件已修改
10个文件已添加
2153 ■■■■ 已修改文件
Assets/AssetBundleCollectorSetting.asset 571 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/AssetBundleBrowser/AssetBundleBuildTab.cs 177 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/Tool/AssetsVersionCmpMaker.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/Tool/AssetsVersionMaker.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/Tool/ClientPackage.cs 72 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/Tool/ClientPackage_Standalone.cs 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/YooAsset/CollectConfigExcludeOPConfig.cs 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/YooAsset/CollectResBeforeUpdate.cs 57 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/YooAsset/CollectSpriteAtlas.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/YooAsset/CollectSpriteAtlas.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/YooAsset/PackFontAssetGroup.cs 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/YooAsset/PackFontAssetGroup.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/YooAsset/PackSpineAssetGroup.cs 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/YooAsset/PackSpineAssetGroup.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/YooAsset/PackTopDirectorySubDirectory.cs 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/YooAsset/PackTopDirectorySubDirectory.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/YooAsset/PackUIEffectAssetGroup.cs 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/YooAsset/PackUIEffectAssetGroup.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/YooAsset/YooAssetBuildTool.cs 393 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Launch/Launch.cs 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Launch/Manager/LocalResManager.cs 129 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Launch/Manager/YooAssetInitializer.cs 275 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Launch/UI/LaunchWins/LaunchExWin.cs 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/AssetBundleCollectorSetting.asset
@@ -16,8 +16,8 @@
  ShowEditorAlias: 0
  UniqueBundleName: 1
  Packages:
  - PackageName: UI
    PackageDesc: all uiprefabs
  - PackageName: Builtin
    PackageDesc: builtin resources
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
@@ -25,111 +25,7 @@
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: Prefab
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/UI
        CollectorGUID: 0a5282bb472d1144ab08a2d07b5a3801
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
      - CollectPath: Assets/ResourcesOut/UIComp
        CollectorGUID: cafb96babf67f0e47aa47d7bd7e851f9
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
      - CollectPath: Assets/ResourcesOut/Sprite
        CollectorGUID: d525c1ea56911ef46968923700fee47f
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
      - CollectPath: Assets/ResourcesOut/Texture
        CollectorGUID: cc4265f3ee528e64e8afa76bd8f7d3f7
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
    - GroupName: Font
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/Font
        CollectorGUID: 6c72fee81b7c8804dbc80d80a5770197
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
  - PackageName: Prefab
    PackageDesc: all prefab
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: Prefab
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/Shader
        CollectorGUID: 01ad80e2bb073fb46ad906e3572fbe50
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectAll
        AssetTags: shader
        UserData:
      - CollectPath: Assets/ResourcesOut/Materials
        CollectorGUID: 676c8b4532d6ca646804bf73ed43dd96
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
      - CollectPath: Assets/ResourcesOut/ScriptableObject
        CollectorGUID: 1482db32e9d88d444bc22e54075bf994
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
      - CollectPath: Assets/ResourcesOut/Scenes
        CollectorGUID: b3332ee13576c994186d5570af59b0b6
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
      - CollectPath: Assets/ResourcesOut/Config
        CollectorGUID: 6a0e1d814ea59174985b17c9f4ecaba3
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectConfigExcludeOPConfig
        AssetTags:
        UserData:
    - GroupName: BuiltinAfterUpdate
    - GroupName: BuiltIn
      GroupDesc: 
      AssetTags: 
      ActiveRuleName: EnableGroup
@@ -142,143 +38,6 @@
        FilterRuleName: CollectBuiltinAfterUpdate
        AssetTags: 
        UserData: 
  - PackageName: Dll
    PackageDesc: HybridCLR Generated
    EnableAddressable: 0
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 0
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: Default Group
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors: []
    - GroupName: HybridCLR Generated
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/HybridCLRDlls
        CollectorGUID:
        CollectorType: 0
        AddressRuleName: AddressByFileName
        PackRuleName: PackRawFile
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
  - PackageName: UIEffect
    PackageDesc:
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: Prefab
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/UIEffect
        CollectorGUID: 62eb3abc624381e4b8950f355812a8e9
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
  - PackageName: Battle
    PackageDesc: battle resources
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: Prefab
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/Hero
        CollectorGUID: 4ef1d5e33efbd83438f79bd2fd7c44ac
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
      - CollectPath: Assets/ResourcesOut/Battle
        CollectorGUID: f4b5330ed101c7b4c8cbfba3f0daff9f
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
  - PackageName: Audio
    PackageDesc: audio resources
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: Prefab
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/Audio
        CollectorGUID: 3d2c92bf721b286489d290a1a2c9763a
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
  - PackageName: Video
    PackageDesc: video resources
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: Prefab
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/Video
        CollectorGUID:
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
  - PackageName: Builtin
    PackageDesc: builtin resources
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: Prefab
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors: []
    - GroupName: ResBeforeUpdate
      GroupDesc: 
      AssetTags: 
@@ -324,6 +83,14 @@
        FilterRuleName: CollectAll
        AssetTags: 
        UserData: 
      - CollectPath: Assets/ResourcesOut/BuiltIn/Sprites/sprites.spriteatlasv2
        CollectorGUID: f8d4cf3f39e708b4796c455fbf07a55b
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackSeparately
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
      - CollectPath: Assets/ResourcesOut/Config/InitialFunction.txt
        CollectorGUID: ab2ea28be4c891a4e81ddf6317e8f7c0
        CollectorType: 0
@@ -348,3 +115,319 @@
        FilterRuleName: CollectAll
        AssetTags: 
        UserData: 
  - PackageName: Dll
    PackageDesc: HybridCLR Generated
    EnableAddressable: 0
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 0
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: HybridCLR Generated
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/HybridCLRDlls
        CollectorGUID: abc904664683ec645829ea85e356b657
        CollectorType: 0
        AddressRuleName: AddressByFileName
        PackRuleName: PackRawFile
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
  - PackageName: Audio
    PackageDesc: ResourcesOut/Audio
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: Audio
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/Audio
        CollectorGUID: 3d2c92bf721b286489d290a1a2c9763a
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackSeparately
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
  - PackageName: Battle
    PackageDesc: ResourcesOut/Battle
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: Battle
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/Battle
        CollectorGUID: f4b5330ed101c7b4c8cbfba3f0daff9f
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackSeparately
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
  - PackageName: Config
    PackageDesc: ResourcesOut/Config
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: Config
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/Config
        CollectorGUID: 6a0e1d814ea59174985b17c9f4ecaba3
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackSeparately
        FilterRuleName: CollectConfigExcludeOPConfig
        AssetTags:
        UserData:
  - PackageName: Hero
    PackageDesc: ResourcesOut/Hero
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: Hero
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/Hero
        CollectorGUID: 4ef1d5e33efbd83438f79bd2fd7c44ac
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
  - PackageName: Materials
    PackageDesc: ResourcesOut/Materials
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: Materials
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/Materials
        CollectorGUID: 676c8b4532d6ca646804bf73ed43dd96
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackSeparately
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
  - PackageName: Scenes
    PackageDesc: ResourcesOut/Scenes
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: Scenes
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/Scenes
        CollectorGUID: b3332ee13576c994186d5570af59b0b6
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackSeparately
        FilterRuleName: CollectScene
        AssetTags:
        UserData:
  - PackageName: ScriptableObject
    PackageDesc: ResourcesOut/ScriptableObject
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: ScriptableObject
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/ScriptableObject
        CollectorGUID: 1482db32e9d88d444bc22e54075bf994
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackSeparately
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
  - PackageName: Shader
    PackageDesc: ResourcesOut/Shader
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: Shader
      GroupDesc:
      AssetTags: shader
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/Shader
        CollectorGUID: 01ad80e2bb073fb46ad906e3572fbe50
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackShader
        FilterRuleName: CollectShader
        AssetTags: shader
        UserData:
  - PackageName: UI
    PackageDesc: ResourcesOut/UI + UIComp + Sprite + Texture + Font
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: UI
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/UI
        CollectorGUID: 0a5282bb472d1144ab08a2d07b5a3801
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackSeparately
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
    - GroupName: UIComp
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/UIComp
        CollectorGUID: cafb96babf67f0e47aa47d7bd7e851f9
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackSeparately
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
    - GroupName: Sprite
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/Sprite
        CollectorGUID: d525c1ea56911ef46968923700fee47f
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackSeparately
        FilterRuleName: CollectSpriteAtlas
        AssetTags:
        UserData:
    - GroupName: Texture
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/Texture
        CollectorGUID: cc4265f3ee528e64e8afa76bd8f7d3f7
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackSeparately
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
    - GroupName: Font
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/Font
        CollectorGUID: 6c72fee81b7c8804dbc80d80a5770197
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackFontAssetGroup
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
  - PackageName: UIEffect
    PackageDesc: ResourcesOut/UIEffect
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: UIEffect
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/UIEffect
        CollectorGUID: 62eb3abc624381e4b8950f355812a8e9
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackDirectory
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
  - PackageName: Video
    PackageDesc: ResourcesOut/Video
    EnableAddressable: 1
    SupportExtensionless: 1
    LocationToLower: 0
    IncludeAssetGUID: 1
    AutoCollectShaders: 1
    IgnoreRuleName: NormalIgnoreRule
    Groups:
    - GroupName: Video
      GroupDesc:
      AssetTags:
      ActiveRuleName: EnableGroup
      Collectors:
      - CollectPath: Assets/ResourcesOut/Video
        CollectorGUID: 81c62c4ce41908342a87b93cd29dd7ba
        CollectorType: 0
        AddressRuleName: AddressByRelativePath
        PackRuleName: PackVideoFile
        FilterRuleName: CollectAll
        AssetTags:
        UserData:
Assets/Editor/AssetBundleBrowser/AssetBundleBuildTab.cs
@@ -16,6 +16,7 @@
using HybridCLR.Editor;
using System.Text;
using Cysharp.Threading.Tasks;
using YooAsset.Editor;
namespace UnityEngine.AssetBundles
{
@@ -382,43 +383,7 @@
                EditorApplication.delayCall += ExecuteBuildAll;
            }
            EditorGUILayout.BeginHorizontal();
            if (GUILayout.Button("Prefab"))
            {
                EditorApplication.delayCall += ExcuteBuildConfig;
            }
            if (GUILayout.Button("UI+UIEffect"))
            {
                EditorApplication.delayCall += ExcuteBuildUI;
            }
            if (GUILayout.Button("Builtin"))
            {
                EditorApplication.delayCall += ExcuteBuildBuiltIn;
            }
            if (GUILayout.Button("Audio"))
            {
                EditorApplication.delayCall += ExcuteBuildAudio;
            }
            if (GUILayout.Button("Video"))
            {
                EditorApplication.delayCall += ExcuteBuildVideo;
            }
            if (GUILayout.Button("Battle"))
            {
                EditorApplication.delayCall += ExcuteBuildMobEffectShader;
            }
            if (GUILayout.Button("Dll(HybridCLR)"))
            {
                EditorApplication.delayCall += ExcuteBuildHybridclrUpdate;
            }
            EditorGUILayout.EndHorizontal();
            DrawYooAssetPackageButtons();
            EditorGUILayout.Space();
            if (GUILayout.Button("导出到 LocalCDN (D:\\LocalCDN)"))
@@ -492,8 +457,8 @@
            EditorGUILayout.Space();
            GUILayout.BeginHorizontal();
            ClientPackage.includeConfig = EditorGUILayout.Toggle("Include Config ", ClientPackage.includeConfig, GUILayout.Width(250));
            ClientPackage.includeUI = EditorGUILayout.Toggle("Include UI ", ClientPackage.includeUI, GUILayout.Width(250));
            ClientPackage.includeConfig = EditorGUILayout.Toggle("Include Config Package", ClientPackage.includeConfig, GUILayout.Width(250));
            ClientPackage.includeUI = EditorGUILayout.Toggle("Include UI Base Packages", ClientPackage.includeUI, GUILayout.Width(250));
            GUILayout.EndHorizontal();
            EditorGUILayout.Space();
@@ -617,6 +582,119 @@
            YooAssetBuildTool.CopyBuildOutputToLocalCDNFlat();
        }
        private static readonly string[] YooAssetPackageButtonOrder =
        {
            "Builtin",
            "Dll",
            "Config",
            "Shader",
            "Materials",
            "ScriptableObject",
            "Scenes",
            "Audio",
            "Battle",
            "Hero",
            "UI",
            "UIEffect",
            "Video",
        };
        private void DrawYooAssetPackageButtons()
        {
            var packageNames = GetYooAssetPackageNames();
            if (packageNames.Count == 0)
            {
                EditorGUILayout.HelpBox("AssetBundleCollectorSetting 中没有配置 YooAsset Package。", MessageType.Warning);
                return;
            }
            EditorGUILayout.LabelField("Build Single Package", EditorStyles.boldLabel);
            int columns = Mathf.Max(1, Mathf.FloorToInt((EditorGUIUtility.currentViewWidth - 30f) / 155f));
            for (int i = 0; i < packageNames.Count; i++)
            {
                if (i % columns == 0)
                    EditorGUILayout.BeginHorizontal();
                string packageName = packageNames[i];
                if (GUILayout.Button(GetYooAssetPackageButtonLabel(packageName)))
                {
                    string capturedPackageName = packageName;
                    EditorApplication.delayCall += () => ExecuteBuildYooAssetPackage(capturedPackageName);
                }
                if (i % columns == columns - 1 || i == packageNames.Count - 1)
                    EditorGUILayout.EndHorizontal();
            }
        }
        private List<string> GetYooAssetPackageNames()
        {
            var result = new List<string>();
            var setting = AssetBundleCollectorSettingData.Setting;
            if (setting == null || setting.Packages == null)
                return result;
            foreach (var packageName in YooAssetPackageButtonOrder)
            {
                if (HasYooAssetPackage(setting, packageName))
                    result.Add(packageName);
            }
            foreach (var package in setting.Packages)
            {
                if (package == null || string.IsNullOrEmpty(package.PackageName))
                    continue;
                if (!ContainsPackageName(result, package.PackageName))
                    result.Add(package.PackageName);
            }
            return result;
        }
        private static bool HasYooAssetPackage(AssetBundleCollectorSetting setting, string packageName)
        {
            foreach (var package in setting.Packages)
            {
                if (package != null && string.Equals(package.PackageName, packageName, StringComparison.OrdinalIgnoreCase))
                    return true;
            }
            return false;
        }
        private static bool ContainsPackageName(List<string> packageNames, string packageName)
        {
            foreach (var item in packageNames)
            {
                if (string.Equals(item, packageName, StringComparison.OrdinalIgnoreCase))
                    return true;
            }
            return false;
        }
        private static string GetYooAssetPackageButtonLabel(string packageName)
        {
            return string.Equals(packageName, "Dll", StringComparison.OrdinalIgnoreCase) ? "Dll(HybridCLR)" : packageName;
        }
        private void ExecuteBuildYooAssetPackage(string packageName)
        {
            if (string.Equals(packageName, "Builtin", StringComparison.OrdinalIgnoreCase))
            {
                ExcuteBuildBuiltIn();
            }
            else if (string.Equals(packageName, "Dll", StringComparison.OrdinalIgnoreCase))
            {
                ExcuteBuildHybridclrUpdate();
            }
            else
            {
                ExcuteBuildAsset(packageName);
            }
        }
        private void ExcuteBuildAsset(string yooPackageName)
        {
            string version = YooAssetBuildTool.GenerateVersion();
@@ -626,8 +704,11 @@
            if (ok)
            {
                YooAssetBuildTool.CopyStartupConfigsToStreamingAssets();
                Debug.Log($"[AssetBundleBuildTab] Package '{yooPackageName}' 打包成功!");
                bool copied = YooAssetBuildTool.CopySinglePackageBuildOutput(yooPackageName, m_UserData.m_OutputPath, version);
                if (copied)
                    Debug.Log($"[AssetBundleBuildTab] Package '{yooPackageName}' 打包并拷贝成功!");
                else
                    Debug.LogError($"[AssetBundleBuildTab] Package '{yooPackageName}' 打包成功,但拷贝到 StreamingAssets/外部资源目录失败!");
            }
            else
            {
@@ -662,7 +743,7 @@
        private void ExcuteBuildConfig()
        {
            ExcuteBuildAsset("Prefab");
            ExcuteBuildAsset("Config");
        }
        
        //发包时获取热更dll和裁剪AOT
@@ -700,8 +781,14 @@
                return;
            }
            if (!YooAssetBuildTool.CopySinglePackageBuildOutput("Dll", m_UserData.m_OutputPath, version))
            {
                Debug.LogError("[AssetBundleBuildTab] Dll Package 打包成功,但拷贝到 StreamingAssets/外部资源目录失败!");
                return;
            }
            AssetDatabase.Refresh();
            Debug.Log("[AssetBundleBuildTab] 热更新 DLL 增量打包完成。");
            Debug.Log("[AssetBundleBuildTab] 热更新 DLL 增量打包并拷贝完成。");
        }
@@ -870,13 +957,11 @@
        private void ExcuteBuildLevels()
        {
            // Scenes 在 YooAsset 的 Prefab Package 中
            ExcuteBuildAsset("Prefab");
            ExcuteBuildAsset("Scenes");
        }
        private void ExcuteBuildUI()
        {
            // UI 和 UIEffect 是两个独立 Package
            ExcuteBuildAsset("UI");
            ExcuteBuildAsset("UIEffect");
        }
Assets/Editor/Tool/AssetsVersionCmpMaker.cs
@@ -42,7 +42,7 @@
            {
                continue;
            }
            if (fileInfo.Name.EndsWith("OPConfig.txt", StringComparison.OrdinalIgnoreCase))
            if (AssetVersionUtility.IsOPConfigFileName(fileInfo.Name))
            {
                continue;
            }
Assets/Editor/Tool/AssetsVersionMaker.cs
@@ -33,7 +33,7 @@
            {
                continue;
            }
            if (fileInfo.Name.EndsWith("OPConfig.txt", StringComparison.OrdinalIgnoreCase))
            if (AssetVersionUtility.IsOPConfigFileName(fileInfo.Name))
            {
                continue;
            }
Assets/Editor/Tool/ClientPackage.cs
@@ -17,6 +17,7 @@
    public static readonly string versionsFilePath = Application.dataPath + "/Editor/VersionConfigs/Versions.txt";
    public static readonly string[] baseLevels = new string[] { "Assets/Resources/Scenes/Launch.unity" };
    private static readonly string[] LaunchRequiredYooPackages = { "Builtin", "Dll" };
    private static readonly string[] YooAssetNoCompressExtensions = { ".unity3d", ".bundle", ".bytes", ".hash", ".version", ".json", ".txt" };
    public static string auditOutTime = string.Empty;
@@ -177,9 +178,7 @@
        // ---- HalfAsset(中包):剔除部分 Package ----
        if (halfPackages.Count > 0)
        {
            // 按旧逻辑映射:hero→Battle, audio→Audio, video→Video, uieffect→UIEffect
            var removePackages = new List<string> { "Battle", "Audio", "Video", "UIEffect" };
            if (!includeUI) removePackages.Add("UI");
            var removePackages = CreateHalfAssetRemovePackages();
            foreach (var pkgName in removePackages)
            {
@@ -328,6 +327,7 @@
        // File.Copy(copySdkFile, File_google_services);
        copySdkFile = StringUtility.Concat(_sdkPath, "/Channel/Android/", versionConfig.sdkFileName, "/gradleTemplate.properties");
        File.Copy(copySdkFile, File_gradleTemplate);
        EnsureYooAssetNoCompressExtensions(File_gradleTemplate);
        copySdkFile = StringUtility.Concat(_sdkPath, "/Channel/Android/", versionConfig.sdkFileName, "/LauncherManifest.xml");
        File.Copy(copySdkFile, File_LauncherManifest);
        copySdkFile = StringUtility.Concat(_sdkPath, "/Channel/Android/", versionConfig.sdkFileName, "/launcherTemplate.gradle");
@@ -433,8 +433,7 @@
                Debug.Log("[ClientPackage] NullAsset Export: 已清空 StreamingAssets/yoo");
                break;
            case InstalledAsset.HalfAsset:
                var removePackages = new List<string> { "Battle", "Audio", "Video", "UIEffect" };
                if (!includeUI) removePackages.Add("UI");
                var removePackages = CreateHalfAssetRemovePackages();
                foreach (var pkgName in removePackages)
                {
                    string pkgDir = Path.Combine(yooRoot, pkgName);
@@ -514,11 +513,6 @@
            File.Copy(item.FullName, to, true);
        }
    }
    static bool IsOPConfigFile(FileInfo file)
    {
        return file.Name.EndsWith("OPConfig.txt", StringComparison.OrdinalIgnoreCase);
    }
    /// <summary>
@@ -650,7 +644,7 @@
            foreach (var item in fromFiles)
            {
                if (IsOPConfigFile(item))
                if (AssetVersionUtility.IsOPConfigFileName(item.Name))
                    continue;
                var extension = Path.GetExtension(item.FullName);
@@ -682,7 +676,7 @@
        FileExtersion.GetAllDirectoryFileInfos(_assetBundlePath, files);
        foreach (var file in files)
        {
            if (IsOPConfigFile(file))
            if (AssetVersionUtility.IsOPConfigFileName(file.Name))
                continue;
            var extension = Path.GetExtension(file.FullName);
@@ -785,6 +779,21 @@
    {
        Debug.Log("[ClientPackage] 恢复 YooAsset StreamingAssets 到完整状态...");
        YooAssetBuildTool.RestoreBuildOutputToStreamingAssets();
    }
    private static List<string> CreateHalfAssetRemovePackages()
    {
        var removePackages = new List<string> { "Battle", "Hero", "Audio", "Video", "UIEffect" };
        if (!includeUI)
        {
            removePackages.Add("UI");
            removePackages.Add("UIComp");
            removePackages.Add("Sprite");
            removePackages.Add("Texture");
            removePackages.Add("Font");
        }
        return removePackages;
    }
    //随包安装的资源不同平台不一定可以获取FileInfo,所以需要下载一个文件来获取资源的MD5信息
@@ -962,6 +971,45 @@
        }
    }
    static void EnsureYooAssetNoCompressExtensions(string gradleTemplatePath)
    {
        if (!File.Exists(gradleTemplatePath))
        {
            Debug.LogWarning($"[ClientPackage] gradleTemplate.properties not found: {gradleTemplatePath}");
            return;
        }
        string text = File.ReadAllText(gradleTemplatePath);
        var lineRegex = new Regex(@"(?m)^unityStreamingAssets\s*=\s*(.*)$");
        var match = lineRegex.Match(text);
        var extensions = new List<string>();
        var extensionSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        if (match.Success)
        {
            foreach (var value in match.Groups[1].Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
            {
                var extension = value.Trim();
                if (extension.Length > 0 && extensionSet.Add(extension))
                    extensions.Add(extension);
            }
        }
        foreach (var extension in YooAssetNoCompressExtensions)
        {
            if (extensionSet.Add(extension))
                extensions.Add(extension);
        }
        string newLine = "unityStreamingAssets=" + string.Join(",", extensions);
        text = match.Success
            ? lineRegex.Replace(text, newLine, 1)
            : text.TrimEnd() + Environment.NewLine + newLine + Environment.NewLine;
        File.WriteAllText(gradleTemplatePath, text, new UTF8Encoding(false));
        Debug.Log($"[ClientPackage] Ensured Android noCompress extensions: {newLine}");
    }
    /// <summary>
    /// 导出 Gradle 工程前清理 Assets/Plugins/Android(保留 libs 目录)
    /// </summary>
Assets/Editor/Tool/ClientPackage_Standalone.cs
@@ -24,11 +24,6 @@
        set { LocalSave.SetBool("obfuscatorEnabled", value); }
    }
    static bool IsOPConfigFile(FileInfo file)
    {
        return file.Name.EndsWith("OPConfig.txt", StringComparison.OrdinalIgnoreCase);
    }
    static bool IsBuiltinFile(FileInfo file)
    {
        if (file.FullName.IndexOf("builtin", StringComparison.OrdinalIgnoreCase) >= 0)
@@ -268,7 +263,7 @@
        foreach (var item in fromFiles)
        {
            if (IsOPConfigFile(item))
            if (AssetVersionUtility.IsOPConfigFileName(item.Name))
            {
                continue;
            }
@@ -305,7 +300,7 @@
        FileExtersion.GetAllDirectoryFileInfos(assetPath, fromFiles);
        foreach (var item in fromFiles)
        {
            if (IsOPConfigFile(item))
            if (AssetVersionUtility.IsOPConfigFileName(item.Name))
            {
                continue;
            }
Assets/Editor/YooAsset/CollectConfigExcludeOPConfig.cs
@@ -1,4 +1,3 @@
using System;
using System.IO;
using YooAsset.Editor;
@@ -7,6 +6,6 @@
{
    public bool IsCollectAsset(FilterRuleData data)
    {
        return !Path.GetFileName(data.AssetPath).EndsWith("OPConfig.txt", StringComparison.OrdinalIgnoreCase);
        return !AssetVersionUtility.IsOPConfigFileName(Path.GetFileName(data.AssetPath));
    }
}
Assets/Editor/YooAsset/CollectResBeforeUpdate.cs
@@ -7,9 +7,8 @@
public static class CollectResBeforeUpdate
{
    private const string BuiltinPackageName = "Builtin";
    private const string PrefabPackageName = "Prefab";
    private const string BuiltInGroupName = "BuiltIn";
    private const string GroupName = "ResBeforeUpdate";
    private const string AfterUpdateGroupName = "BuiltinAfterUpdate";
    private const string CollectBuiltinAfterUpdateRuleName = "CollectBuiltinAfterUpdate";
    private const string BuiltInRoot = "Assets/ResourcesOut/BuiltIn";
    private const string ConfigRoot = "Assets/ResourcesOut/Config";
@@ -23,6 +22,7 @@
        BuiltInRoot + "/Sprites/TY_TB_JH2.png",
        BuiltInRoot + "/Sprites/LoadingBottom.png",
        BuiltInRoot + "/Sprites/LoadingSlider.png",
        BuiltInRoot + "/Sprites/sprites.spriteatlasv2",
    };
    private static readonly string[] BuiltinConfigPaths =
@@ -58,6 +58,16 @@
            Debug.Log($"[CollectResBeforeUpdate] 创建 Package: {BuiltinPackageName}");
        }
        var builtInGroup = builtinPackage.Groups.Find(item => item.GroupName == BuiltInGroupName);
        if (builtInGroup == null)
        {
            builtInGroup = AssetBundleCollectorSettingData.CreateGroup(builtinPackage, BuiltInGroupName);
            changed = true;
            Debug.Log($"[CollectResBeforeUpdate] 创建 Group: {BuiltInGroupName}");
        }
        changed |= EnsureCollector(builtInGroup, BuiltInRoot, nameof(PackDirectory), CollectBuiltinAfterUpdateRuleName);
        var group = builtinPackage.Groups.Find(item => item.GroupName == GroupName);
        if (group == null)
        {
@@ -66,7 +76,6 @@
            Debug.Log($"[CollectResBeforeUpdate] 创建 Group: {GroupName}");
        }
        changed |= RemoveObsoleteBuiltinRootCollectors(builtinPackage);
        changed |= RemoveDuplicateBeforeUpdateCollectors(setting, builtinPackage, group);
        foreach (var assetPath in BeforeUpdateBuiltinAssetPaths)
        {
@@ -78,14 +87,13 @@
            changed |= EnsureCollector(group, configPath, nameof(PackSeparately), nameof(CollectAll));
        }
        changed |= EnsureAfterUpdateBuiltinCollector(setting);
        changed |= SyncResourcesInitialFunction();
        if (changed)
        {
            AssetBundleCollectorSettingData.SaveFile();
            AssetDatabase.Refresh();
            Debug.Log("[CollectResBeforeUpdate] 热更前资源收集配置已同步。Builtin 只保留 Launch 白名单,其余 BuiltIn 资源由 Prefab 包收集。");
            Debug.Log("[CollectResBeforeUpdate] 热更前资源收集配置已同步。Builtin 包包含 BuiltIn 整目录,并保留 Launch 白名单资源。");
        }
        else
        {
@@ -104,45 +112,6 @@
        }
        return false;
    }
    private static bool EnsureAfterUpdateBuiltinCollector(AssetBundleCollectorSetting setting)
    {
        var prefabPackage = setting.Packages.Find(package => package.PackageName == PrefabPackageName);
        if (prefabPackage == null)
        {
            prefabPackage = AssetBundleCollectorSettingData.CreatePackage(PrefabPackageName);
            Debug.Log($"[CollectResBeforeUpdate] 创建 Package: {PrefabPackageName}");
        }
        var group = prefabPackage.Groups.Find(item => item.GroupName == AfterUpdateGroupName);
        if (group == null)
        {
            group = AssetBundleCollectorSettingData.CreateGroup(prefabPackage, AfterUpdateGroupName);
            Debug.Log($"[CollectResBeforeUpdate] 创建 Group: {AfterUpdateGroupName}");
        }
        return EnsureCollector(group, BuiltInRoot, nameof(PackDirectory), CollectBuiltinAfterUpdateRuleName);
    }
    private static bool RemoveObsoleteBuiltinRootCollectors(AssetBundleCollectorPackage builtinPackage)
    {
        var changed = false;
        foreach (var group in builtinPackage.Groups)
        {
            for (var index = group.Collectors.Count - 1; index >= 0; index--)
            {
                var collector = group.Collectors[index];
                if (!IsSameAssetPath(collector.CollectPath, BuiltInRoot))
                    continue;
                AssetBundleCollectorSettingData.RemoveCollector(group, collector);
                changed = true;
                Debug.Log($"[CollectResBeforeUpdate] 从 Builtin Package 移除整目录 Collector: {collector.CollectPath}");
            }
        }
        return changed;
    }
    private static bool RemoveDuplicateBeforeUpdateCollectors(
Assets/Editor/YooAsset/CollectSpriteAtlas.cs
New file
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEditor;
using YooAsset.Editor;
[DisplayName("收集SpriteAtlas图集")]
public class CollectSpriteAtlas : IFilterRule
{
    public bool IsCollectAsset(FilterRuleData data)
    {
        var extension = Path.GetExtension(data.AssetPath);
        return string.Equals(extension, ".spriteatlasv2", System.StringComparison.OrdinalIgnoreCase)
               || string.Equals(extension, ".spriteatlas", System.StringComparison.OrdinalIgnoreCase);
    }
}
[InitializeOnLoad]
public static class YooAssetCustomFilterRuleRegistry
{
    static YooAssetCustomFilterRuleRegistry()
    {
        EditorApplication.delayCall -= Register;
        EditorApplication.delayCall += Register;
    }
    private static void Register()
    {
        RegisterFilterRule<CollectSpriteAtlas>();
    }
    private static void RegisterFilterRule<T>() where T : IFilterRule
    {
        var fieldInfo = typeof(AssetBundleCollectorSettingData).GetField(
            "_cacheFilterRuleTypes",
            BindingFlags.Static | BindingFlags.NonPublic);
        var cache = fieldInfo?.GetValue(null) as Dictionary<string, Type>;
        if (cache == null)
            return;
        cache[typeof(T).Name] = typeof(T);
    }
}
Assets/Editor/YooAsset/CollectSpriteAtlas.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0172787988c69a84db1d38139c18f351
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Assets/Editor/YooAsset/PackFontAssetGroup.cs
New file
@@ -0,0 +1,49 @@
using System;
using System.IO;
using YooAsset.Editor;
[DisplayName("资源包名: 字体资源组")]
public class PackFontAssetGroup : IPackRule
{
    PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data)
    {
        var assetPath = NormalizePath(data.AssetPath);
        var directory = NormalizePath(Path.GetDirectoryName(assetPath));
        var baseName = GetFontBaseName(assetPath);
        var bundleName = string.IsNullOrEmpty(baseName)
            ? EditorTools.RemoveExtension(assetPath)
            : $"{directory}/{baseName}";
        return new PackRuleResult(bundleName, DefaultPackRule.AssetBundleFileExtension);
    }
    private static string GetFontBaseName(string assetPath)
    {
        var extension = Path.GetExtension(assetPath);
        if (string.Equals(extension, ".ttf", StringComparison.OrdinalIgnoreCase))
            return Path.GetFileNameWithoutExtension(assetPath);
        var fileName = Path.GetFileNameWithoutExtension(assetPath);
        return StripTrailingAtlasPage(fileName);
    }
    private static string StripTrailingAtlasPage(string fileName)
    {
        var underscoreIndex = fileName.LastIndexOf('_');
        if (underscoreIndex < 0 || underscoreIndex == fileName.Length - 1)
            return fileName;
        for (var index = underscoreIndex + 1; index < fileName.Length; index++)
        {
            if (!char.IsDigit(fileName[index]))
                return fileName;
        }
        return fileName.Substring(0, underscoreIndex);
    }
    private static string NormalizePath(string path)
    {
        return string.IsNullOrEmpty(path) ? string.Empty : path.Replace('\\', '/');
    }
}
Assets/Editor/YooAsset/PackFontAssetGroup.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6a82435e7e7ab7b49927a640f82f875a
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Assets/Editor/YooAsset/PackSpineAssetGroup.cs
New file
@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.IO;
using YooAsset.Editor;
[DisplayName("资源包名: Spine资源组")]
public class PackSpineAssetGroup : IPackRule
{
    private const string SkeletonDataSuffix = "_SkeletonData";
    private static readonly Dictionary<string, List<string>> SkeletonBaseCache = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
    PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data)
    {
        var assetPath = NormalizePath(data.AssetPath);
        var directory = NormalizePath(Path.GetDirectoryName(assetPath));
        var spineBaseName = FindSpineBaseName(directory, assetPath);
        var bundleName = string.IsNullOrEmpty(spineBaseName)
            ? EditorTools.RemoveExtension(assetPath)
            : $"{directory}/{spineBaseName}";
        return new PackRuleResult(bundleName, DefaultPackRule.AssetBundleFileExtension);
    }
    private static string FindSpineBaseName(string directory, string assetPath)
    {
        if (string.IsNullOrEmpty(directory))
            return string.Empty;
        var fileName = Path.GetFileNameWithoutExtension(assetPath);
        var bestMatch = string.Empty;
        foreach (var baseName in GetSkeletonBaseNames(directory))
        {
            if (!IsSameSpineResource(fileName, baseName))
                continue;
            if (baseName.Length > bestMatch.Length)
                bestMatch = baseName;
        }
        return bestMatch;
    }
    private static bool IsSameSpineResource(string fileName, string baseName)
    {
        return string.Equals(fileName, baseName, StringComparison.OrdinalIgnoreCase)
               || fileName.StartsWith(baseName + "_", StringComparison.OrdinalIgnoreCase)
               || fileName.StartsWith(baseName + ".", StringComparison.OrdinalIgnoreCase);
    }
    private static List<string> GetSkeletonBaseNames(string directory)
    {
        if (SkeletonBaseCache.TryGetValue(directory, out var cached))
            return cached;
        var result = new List<string>();
        if (Directory.Exists(directory))
        {
            foreach (var filePath in Directory.GetFiles(directory, "*" + SkeletonDataSuffix + ".asset", SearchOption.TopDirectoryOnly))
            {
                var fileName = Path.GetFileNameWithoutExtension(filePath);
                if (fileName.EndsWith(SkeletonDataSuffix, StringComparison.OrdinalIgnoreCase))
                    result.Add(fileName.Substring(0, fileName.Length - SkeletonDataSuffix.Length));
            }
        }
        SkeletonBaseCache[directory] = result;
        return result;
    }
    private static string NormalizePath(string path)
    {
        return string.IsNullOrEmpty(path) ? string.Empty : path.Replace('\\', '/');
    }
}
Assets/Editor/YooAsset/PackSpineAssetGroup.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4145f25573e3ea84dbaeadb7ba012550
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Assets/Editor/YooAsset/PackTopDirectorySubDirectory.cs
New file
@@ -0,0 +1,48 @@
using System;
using System.IO;
using YooAsset.Editor;
[DisplayName("资源包名: 收集器下二级文件夹路径")]
public class PackTopDirectorySubDirectory : IPackRule
{
    PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data)
    {
        var assetPath = NormalizePath(data.AssetPath);
        var collectPath = NormalizePath(data.CollectPath).TrimEnd('/');
        var relativePath = GetRelativeAssetPath(assetPath, collectPath);
        var segments = relativePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
        if (segments.Length == 0 || Path.HasExtension(segments[0]))
            throw new Exception($"Not found top directory : {relativePath}");
        var bundleName = GetBundleName(collectPath, segments);
        return new PackRuleResult(bundleName, DefaultPackRule.AssetBundleFileExtension);
    }
    private static string GetBundleName(string collectPath, string[] segments)
    {
        if (segments.Length >= 2 && !Path.HasExtension(segments[1]))
            return $"{collectPath}/{segments[0]}/{segments[1]}";
        return $"{collectPath}/{segments[0]}";
    }
    private static string GetRelativeAssetPath(string assetPath, string collectPath)
    {
        if (string.IsNullOrEmpty(collectPath))
            return assetPath.TrimStart('/');
        if (assetPath.Equals(collectPath, StringComparison.OrdinalIgnoreCase))
            return string.Empty;
        if (assetPath.StartsWith(collectPath + "/", StringComparison.OrdinalIgnoreCase))
            return assetPath.Substring(collectPath.Length + 1);
        return assetPath;
    }
    private static string NormalizePath(string path)
    {
        return string.IsNullOrEmpty(path) ? string.Empty : path.Replace('\\', '/');
    }
}
Assets/Editor/YooAsset/PackTopDirectorySubDirectory.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a6553361f76f01840a08ad8ea9d67356
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Assets/Editor/YooAsset/PackUIEffectAssetGroup.cs
New file
@@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.IO;
using YooAsset.Editor;
[DisplayName("资源包名: UI特效资源组")]
public class PackUIEffectAssetGroup : IPackRule
{
    private const string SkeletonDataSuffix = "_SkeletonData";
    private const string UnityEffectRoot = "Assets/ResourcesOut/UIEffect/Unity";
    private static readonly Dictionary<string, List<string>> SkeletonBaseCache = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
    private static readonly Dictionary<string, string> SinglePrefabCache = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
    PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data)
    {
        var assetPath = NormalizePath(data.AssetPath);
        var directory = NormalizePath(Path.GetDirectoryName(assetPath));
        var bundleName = IsUnityEffectAsset(assetPath)
            ? GetUnityEffectBundleName(directory, assetPath)
            : GetSpineBundleName(directory, assetPath);
        return new PackRuleResult(bundleName, DefaultPackRule.AssetBundleFileExtension);
    }
    private static string GetSpineBundleName(string directory, string assetPath)
    {
        var spineBaseName = FindSpineBaseName(directory, assetPath);
        return string.IsNullOrEmpty(spineBaseName)
            ? EditorTools.RemoveExtension(assetPath)
            : $"{directory}/{spineBaseName}";
    }
    private static string GetUnityEffectBundleName(string directory, string assetPath)
    {
        var singlePrefabPath = FindSinglePrefabPath(directory);
        if (!string.IsNullOrEmpty(singlePrefabPath))
            return EditorTools.RemoveExtension(singlePrefabPath);
        return EditorTools.RemoveExtension(assetPath);
    }
    private static string FindSpineBaseName(string directory, string assetPath)
    {
        if (string.IsNullOrEmpty(directory))
            return string.Empty;
        var fileName = Path.GetFileNameWithoutExtension(assetPath);
        var bestMatch = string.Empty;
        foreach (var baseName in GetSkeletonBaseNames(directory))
        {
            if (!IsSameSpineResource(fileName, baseName))
                continue;
            if (baseName.Length > bestMatch.Length)
                bestMatch = baseName;
        }
        return bestMatch;
    }
    private static bool IsSameSpineResource(string fileName, string baseName)
    {
        return string.Equals(fileName, baseName, StringComparison.OrdinalIgnoreCase)
               || fileName.StartsWith(baseName + "_", StringComparison.OrdinalIgnoreCase)
               || fileName.StartsWith(baseName + ".", StringComparison.OrdinalIgnoreCase);
    }
    private static List<string> GetSkeletonBaseNames(string directory)
    {
        if (SkeletonBaseCache.TryGetValue(directory, out var cached))
            return cached;
        var result = new List<string>();
        if (Directory.Exists(directory))
        {
            foreach (var filePath in Directory.GetFiles(directory, "*" + SkeletonDataSuffix + ".asset", SearchOption.TopDirectoryOnly))
            {
                var fileName = Path.GetFileNameWithoutExtension(filePath);
                if (fileName.EndsWith(SkeletonDataSuffix, StringComparison.OrdinalIgnoreCase))
                    result.Add(fileName.Substring(0, fileName.Length - SkeletonDataSuffix.Length));
            }
        }
        SkeletonBaseCache[directory] = result;
        return result;
    }
    private static string FindSinglePrefabPath(string directory)
    {
        if (SinglePrefabCache.TryGetValue(directory, out var cached))
            return cached;
        var result = string.Empty;
        if (Directory.Exists(directory))
        {
            foreach (var prefabPath in Directory.GetFiles(directory, "*.prefab", SearchOption.TopDirectoryOnly))
            {
                if (!string.IsNullOrEmpty(result))
                {
                    result = string.Empty;
                    break;
                }
                result = NormalizePath(prefabPath);
            }
        }
        SinglePrefabCache[directory] = result;
        return result;
    }
    private static bool IsUnityEffectAsset(string assetPath)
    {
        return IsSameAssetPath(assetPath, UnityEffectRoot) || assetPath.StartsWith(UnityEffectRoot + "/", StringComparison.OrdinalIgnoreCase);
    }
    private static bool IsSameAssetPath(string lhs, string rhs)
    {
        return NormalizePath(lhs).TrimEnd('/').Equals(NormalizePath(rhs).TrimEnd('/'), StringComparison.OrdinalIgnoreCase);
    }
    private static string NormalizePath(string path)
    {
        return string.IsNullOrEmpty(path) ? string.Empty : path.Replace('\\', '/');
    }
}
Assets/Editor/YooAsset/PackUIEffectAssetGroup.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9f2c76b7cf0d4d44ba0c121e5a18f3d4
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Assets/Editor/YooAsset/YooAssetBuildTool.cs
@@ -14,10 +14,27 @@
public static class YooAssetBuildTool
{
    /// <summary>
    /// 所有需要打包的 Package 名称(与 AssetBundleCollectorSetting 一致)
    /// </summary>
    private static readonly string[] ALL_PACKAGES = { "Prefab", "UI", "UIEffect", "Battle", "Audio", "Video", "Builtin", "Dll" };
    private static string[] GetAllPackageNames()
    {
        var setting = AssetBundleCollectorSettingData.Setting;
        if (setting == null || setting.Packages == null)
        {
            return Array.Empty<string>();
        }
        var packageNames = new System.Collections.Generic.List<string>();
        foreach (var package in setting.Packages)
        {
            if (package == null || string.IsNullOrEmpty(package.PackageName))
            {
                continue;
            }
            packageNames.Add(package.PackageName);
        }
        return packageNames.ToArray();
    }
    /// <summary>
    /// HybridCLR .bytes 文件输出目录(相对 Assets)
@@ -371,11 +388,18 @@
    private static void BuildAllPackagesInternal(bool incremental)
    {
        string[] allPackages = GetAllPackageNames();
        if (allPackages.Length == 0)
        {
            EditorUtility.DisplayDialog("打包失败", "AssetBundleCollectorSetting 中没有配置任何 Package。", "确定");
            return;
        }
        string modeDesc = incremental ? "增量打包(跳过HybridCLR,复用版本号)" : "全量打包";
        if (!EditorUtility.DisplayDialog("确认打包",
            $"模式: {modeDesc}\n" +
            $"将使用 BuiltinBuildPipeline 打包以下 {ALL_PACKAGES.Length} 个 Package:\n" +
            $"{string.Join(", ", ALL_PACKAGES)}\n\n" +
            $"将使用 BuiltinBuildPipeline 打包以下 {allPackages.Length} 个 Package:\n" +
            $"{string.Join(", ", allPackages)}\n\n" +
            $"平台: {EditorUserBuildSettings.activeBuildTarget}\n" +
            $"输出: {AssetBundleBuilderHelper.GetDefaultBuildOutputRoot()}\n" +
            $"并自动拷贝到 StreamingAssets\n\n是否继续?",
@@ -420,11 +444,18 @@
            }
        }
        string[] allPackages = GetAllPackageNames();
        if (allPackages.Length == 0)
        {
            Debug.LogError("[YooAssetBuildTool] AssetBundleCollectorSetting 中没有配置任何 Package,无法打包。 ");
            return false;
        }
        // 增量模式复用上次版本号,避免 YooAsset 认为全新版本
        string version;
        if (incremental)
        {
            string lastVer = GetLastBuildVersion(ALL_PACKAGES[0]);
            string lastVer = GetLastBuildVersion(allPackages[0]);
            version = !string.IsNullOrEmpty(lastVer) ? lastVer : GenerateVersion();
            Debug.Log($"[YooAssetBuildTool] 增量模式,复用版本号: {version}");
        }
@@ -443,12 +474,12 @@
        ClearStreamingAssetsYooDirectory();
        for (int i = 0; i < ALL_PACKAGES.Length; i++)
        for (int i = 0; i < allPackages.Length; i++)
        {
            string pkgName = ALL_PACKAGES[i];
            string pkgName = allPackages[i];
            EditorUtility.DisplayProgressBar("YooAsset 打包中...",
                $"正在打包 {pkgName} ({i + 1}/{ALL_PACKAGES.Length})",
                (float)i / ALL_PACKAGES.Length);
                $"正在打包 {pkgName} ({i + 1}/{allPackages.Length})",
                (float)i / allPackages.Length);
            bool ok = BuildSinglePackage(pkgName, version);
            if (ok)
@@ -526,7 +557,7 @@
        System.IO.Directory.CreateDirectory(destFullPath);
        int copiedCount = 0;
        foreach (var packageName in ALL_PACKAGES)
        foreach (var packageName in GetAllPackageNames())
        {
            string sourcePackageDir = System.IO.Path.Combine(sourceFullPath, packageName);
            if (!System.IO.Directory.Exists(sourcePackageDir))
@@ -556,6 +587,125 @@
        return true;
    }
    public static bool CopySinglePackageBuildOutput(string packageName, string outputPath = null, string version = null)
    {
        if (string.IsNullOrEmpty(packageName))
        {
            Debug.LogError("[YooAssetBuildTool] Package 名为空,无法拷贝单包构建产物。");
            return false;
        }
        bool success = true;
        string streamingYooRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
        if (!CopyPackageBuildOutputToYooDirectory(packageName, streamingYooRoot, version, "StreamingAssets"))
        {
            success = false;
        }
        if (!CopyStartupConfigsToStreamingAssets())
        {
            success = false;
        }
        if (!string.IsNullOrWhiteSpace(outputPath))
        {
            string destYooRoot = ResolveYooOutputDirectory(outputPath);
            string streamingFullPath = NormalizeFullPath(streamingYooRoot);
            string destFullPath = NormalizeFullPath(destYooRoot);
            if (string.Equals(streamingFullPath, destFullPath, StringComparison.OrdinalIgnoreCase))
            {
                Debug.LogWarning($"[YooAssetBuildTool] 单包外部输出目录与 StreamingAssets 相同,跳过重复拷贝: {destFullPath}");
            }
            else
            {
                if (!CopyPackageBuildOutputToYooDirectory(packageName, destYooRoot, version, "外部资源目录"))
                {
                    success = false;
                }
                if (!CopyStartupConfigsToYooDirectory(destYooRoot))
                {
                    success = false;
                }
            }
        }
        return success;
    }
    private static bool CopyPackageBuildOutputToYooDirectory(string packageName, string targetYooRoot, string version, string targetName)
    {
        string sourceDir = GetPackageBuildOutputDirectory(packageName, version);
        if (string.IsNullOrEmpty(sourceDir))
        {
            return false;
        }
        string sourceFullPath = NormalizeFullPath(sourceDir);
        string targetFullPath = NormalizeFullPath(targetYooRoot);
        if (targetFullPath.StartsWith(sourceFullPath + "/", StringComparison.OrdinalIgnoreCase))
        {
            Debug.LogError($"[YooAssetBuildTool] {targetName} 目录不能位于构建产物目录内部: {targetFullPath}");
            return false;
        }
        DeleteObsoletePackageDirectories(targetFullPath);
        string destPackageDir = System.IO.Path.Combine(targetFullPath, packageName);
        if (System.IO.Directory.Exists(destPackageDir))
        {
            System.IO.Directory.Delete(destPackageDir, true);
        }
        int copiedCount = CopyDirectoryFiles(sourceFullPath, destPackageDir);
        Debug.Log($"[YooAssetBuildTool] 已拷贝 Package '{packageName}' 到{targetName}: {copiedCount} 个文件,from={sourceFullPath}, to={destPackageDir}");
        return true;
    }
    private static string GetPackageBuildOutputDirectory(string packageName, string version)
    {
        string buildRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
        string platform = EditorUserBuildSettings.activeBuildTarget.ToString();
        string packageRoot = System.IO.Path.Combine(buildRoot, platform, packageName);
        if (!System.IO.Directory.Exists(packageRoot))
        {
            Debug.LogError($"[YooAssetBuildTool] Package 构建输出目录不存在: {packageRoot}");
            return null;
        }
        if (!string.IsNullOrEmpty(version))
        {
            string versionDir = System.IO.Path.Combine(packageRoot, version);
            if (System.IO.Directory.Exists(versionDir))
            {
                return versionDir;
            }
            Debug.LogWarning($"[YooAssetBuildTool] 指定版本目录不存在,将使用最新版本目录: {versionDir}");
        }
        var versionDirs = new System.Collections.Generic.List<string>();
        foreach (var dir in System.IO.Directory.GetDirectories(packageRoot))
        {
            string dirName = System.IO.Path.GetFileName(dir);
            if (dirName == "OutputCache" || dirName == "Simulate")
            {
                continue;
            }
            versionDirs.Add(dir);
        }
        if (versionDirs.Count == 0)
        {
            Debug.LogError($"[YooAssetBuildTool] Package '{packageName}' 没有可用的版本目录: {packageRoot}");
            return null;
        }
        versionDirs.Sort(StringComparer.OrdinalIgnoreCase);
        return versionDirs[versionDirs.Count - 1];
    }
    public static void ClearStreamingAssetsYooDirectory()
    {
        string yooRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
@@ -567,12 +717,44 @@
        System.IO.Directory.CreateDirectory(yooRoot);
    }
    private static void DeleteObsoletePackageDirectories(string yooRoot)
    {
        if (!System.IO.Directory.Exists(yooRoot))
        {
            return;
        }
        var validPackages = new System.Collections.Generic.HashSet<string>(GetAllPackageNames(), StringComparer.OrdinalIgnoreCase);
        foreach (var dir in System.IO.Directory.GetDirectories(yooRoot))
        {
            var dirName = System.IO.Path.GetFileName(dir);
            if (validPackages.Contains(dirName) || IsReservedYooDirectory(dirName))
            {
                continue;
            }
            System.IO.Directory.Delete(dir, true);
            var metaPath = dir + ".meta";
            if (System.IO.File.Exists(metaPath))
            {
                System.IO.File.Delete(metaPath);
            }
            Debug.Log($"[YooAssetBuildTool] 已清理过期 Package 目录: {dirName}");
        }
    }
    private static bool IsReservedYooDirectory(string dirName)
    {
        return string.Equals(dirName, AssetVersionUtility.OPConfigDirectory, StringComparison.OrdinalIgnoreCase);
    }
    public static void DeleteStartupConfigsFromStreamingAssets()
    {
        string yooRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
        string initialFunctionPath = System.IO.Path.Combine(yooRoot, "InitialFunction.txt");
        string initialFunctionMetaPath = initialFunctionPath + ".meta";
        string configDir = System.IO.Path.Combine(yooRoot, "config");
        string configDir = System.IO.Path.Combine(yooRoot, AssetVersionUtility.OPConfigDirectory);
        string configMetaPath = configDir + ".meta";
        if (System.IO.File.Exists(initialFunctionPath))
@@ -596,7 +778,7 @@
        }
        AssetDatabase.Refresh();
        Debug.Log("[YooAssetBuildTool] 已清理 StreamingAssets/yoo 下的 InitialFunction.txt 和 config 目录");
        Debug.Log("[YooAssetBuildTool] 已清理 StreamingAssets/yoo 下的 InitialFunction.txt 和 OPConfig 目录");
    }
    private static string ResolveOutputDirectory(string outputPath)
@@ -671,7 +853,7 @@
    {
        string sourceConfigDir = System.IO.Path.Combine(Application.dataPath, "ResourcesOut", "Config");
        string yooDir = targetYooDir;
        string streamingConfigDir = System.IO.Path.Combine(yooDir, "config");
        string streamingConfigDir = System.IO.Path.Combine(yooDir, AssetVersionUtility.OPConfigDirectory);
        if (!System.IO.Directory.Exists(sourceConfigDir))
        {
@@ -824,8 +1006,10 @@
            return;
        }
        DeleteObsoletePackageDirectories(destRoot);
        int copiedCount = 0;
        foreach (string pkgName in ALL_PACKAGES)
        foreach (string pkgName in GetAllPackageNames())
        {
            string pkgBuildDir = System.IO.Path.Combine(srcRoot, pkgName);
            if (!System.IO.Directory.Exists(pkgBuildDir)) continue;
@@ -856,17 +1040,21 @@
    /// <summary>
    /// 扫描 StreamingAssets 中实际存在的 Package 子目录,写入 Resources/YooBuildinPackages.txt。
    /// 运行时通过 YooAssetInitializer.HasBuildinPackage() 读取,决定是否创建 BuildinFileSystem。
    /// 该文件仅用于打包诊断和旧包兼容;运行时 HostPlayMode 会探测 APK 内
    /// yoo/{PackageName}/BuildinCatalog.bytes,有索引才挂载 BuildinFileSystem。
    /// </summary>
    public static void WriteBuildinPackageList()
    {
        string yooRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
        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))
        {
            foreach (var dir in System.IO.Directory.GetDirectories(yooRoot))
            {
                existing.Add(System.IO.Path.GetFileName(dir));
                var dirName = System.IO.Path.GetFileName(dir);
                if (validPackages.Contains(dirName))
                    existing.Add(dirName);
            }
        }
        existing.Sort();
@@ -946,6 +1134,12 @@
        if (!ok)
        {
            EditorUtility.DisplayDialog("Dll 打包失败", "Dll Package 打包失败,已中止构建。\n请查看 Console 日志。", "确定");
            return;
        }
        if (!CopySinglePackageBuildOutput("Dll", null, version))
        {
            EditorUtility.DisplayDialog("Dll 拷贝失败", "Dll Package 打包成功,但拷贝到 StreamingAssets 失败,已中止构建。\n请查看 Console 日志。", "确定");
            return;
        }
@@ -1035,27 +1229,55 @@
    // 单个 Package 打包
    // ====================================================================
    [MenuItem("YooAsset工具/打包单个Package/Prefab", false, 300)]
    private static void BuildPrefab() => BuildSingleWithDialog("Prefab");
    [MenuItem("YooAsset工具/打包单个Package/UI", false, 301)]
    private static void BuildUI() => BuildSingleWithDialog("UI");
    [MenuItem("YooAsset工具/打包单个Package/UIEffect", false, 302)]
    private static void BuildUIEffect() => BuildSingleWithDialog("UIEffect");
    [MenuItem("YooAsset工具/打包单个Package/Battle", false, 303)]
    private static void BuildBattle() => BuildSingleWithDialog("Battle");
    [MenuItem("YooAsset工具/打包单个Package/Audio", false, 304)]
    private static void BuildAudio() => BuildSingleWithDialog("Audio");
    [MenuItem("YooAsset工具/打包单个Package/Video", false, 305)]
    private static void BuildVideo() => BuildSingleWithDialog("Video");
     [MenuItem("YooAsset工具/打包单个Package/Builtin", false, 306)]
    [MenuItem("YooAsset工具/打包单个Package/Builtin", false, 300)]
    private static void BuildBuiltin() => BuildSingleWithDialog("Builtin");
    [MenuItem("YooAsset工具/打包单个Package/Dll (HybridCLR)", false, 307)]
    [MenuItem("YooAsset工具/打包单个Package/Config", false, 301)]
    private static void BuildConfig() => BuildSingleWithDialog("Config");
    [MenuItem("YooAsset工具/打包单个Package/Shader", false, 302)]
    private static void BuildShader() => BuildSingleWithDialog("Shader");
    [MenuItem("YooAsset工具/打包单个Package/Materials", false, 303)]
    private static void BuildMaterials() => BuildSingleWithDialog("Materials");
    [MenuItem("YooAsset工具/打包单个Package/ScriptableObject", false, 304)]
    private static void BuildScriptableObject() => BuildSingleWithDialog("ScriptableObject");
    [MenuItem("YooAsset工具/打包单个Package/Scenes", false, 305)]
    private static void BuildScenes() => BuildSingleWithDialog("Scenes");
    [MenuItem("YooAsset工具/打包单个Package/UI", false, 306)]
    private static void BuildUI() => BuildSingleWithDialog("UI");
    [MenuItem("YooAsset工具/打包单个Package/UIComp", false, 307)]
    private static void BuildUIComp() => BuildSingleWithDialog("UIComp");
    [MenuItem("YooAsset工具/打包单个Package/UIEffect", false, 308)]
    private static void BuildUIEffect() => BuildSingleWithDialog("UIEffect");
    [MenuItem("YooAsset工具/打包单个Package/Battle", false, 309)]
    private static void BuildBattle() => BuildSingleWithDialog("Battle");
    [MenuItem("YooAsset工具/打包单个Package/Hero", false, 310)]
    private static void BuildHero() => BuildSingleWithDialog("Hero");
    [MenuItem("YooAsset工具/打包单个Package/Sprite", false, 311)]
    private static void BuildSprite() => BuildSingleWithDialog("Sprite");
    [MenuItem("YooAsset工具/打包单个Package/Texture", false, 312)]
    private static void BuildTexture() => BuildSingleWithDialog("Texture");
    [MenuItem("YooAsset工具/打包单个Package/Font", false, 313)]
    private static void BuildFont() => BuildSingleWithDialog("Font");
    [MenuItem("YooAsset工具/打包单个Package/Audio", false, 314)]
    private static void BuildAudio() => BuildSingleWithDialog("Audio");
    [MenuItem("YooAsset工具/打包单个Package/Video", false, 315)]
    private static void BuildVideo() => BuildSingleWithDialog("Video");
    [MenuItem("YooAsset工具/打包单个Package/Dll (HybridCLR)", false, 316)]
    private static void BuildDll()
    {
        string version = GenerateVersion();
@@ -1081,9 +1303,16 @@
        EditorUtility.ClearProgressBar();
        if (ok)
            EditorUtility.DisplayDialog("打包成功", "Package 'Dll' 打包完成!", "确定");
        {
            if (CopySinglePackageBuildOutput("Dll", null, version))
                EditorUtility.DisplayDialog("打包成功", "Package 'Dll' 打包完成!", "确定");
            else
                EditorUtility.DisplayDialog("拷贝失败", "Package 'Dll' 打包成功,但拷贝到 StreamingAssets 失败,请查看 Console 日志。", "确定");
        }
        else
        {
            EditorUtility.DisplayDialog("打包失败", "Package 'Dll' 打包失败,请查看 Console 日志。", "确定");
        }
        AssetDatabase.Refresh();
    }
@@ -1104,7 +1333,10 @@
        if (ok)
        {
            EditorUtility.DisplayDialog("打包成功", $"Package '{packageName}' 打包完成!", "确定");
            if (CopySinglePackageBuildOutput(packageName, null, version))
                EditorUtility.DisplayDialog("打包成功", $"Package '{packageName}' 打包完成!", "确定");
            else
                EditorUtility.DisplayDialog("拷贝失败", $"Package '{packageName}' 打包成功,但拷贝到 StreamingAssets 失败,请查看 Console 日志。", "确定");
        }
        else
        {
@@ -1441,4 +1673,87 @@
        EditorUtility.DisplayDialog("YooAsset 状态", msg, "确定");
    }
    [MenuItem("YooAsset工具/同步资源PackRule配置", false, 203)]
    public static void SyncCollectorPackRulesMenu()
    {
        var changed = SyncCollectorPackRules();
        EditorUtility.DisplayDialog(
            "YooAsset PackRule",
            changed ? "PackRule 配置已同步。" : "PackRule 配置已经是最新。",
            "确定");
    }
    private static readonly CollectorPackRule[] CollectorPackRules =
    {
        new CollectorPackRule("UI", "Assets/ResourcesOut/Font", "PackFontAssetGroup"),
        new CollectorPackRule("UI", "Assets/ResourcesOut/Sprite", "PackSeparately"),
        new CollectorPackRule("Hero", "Assets/ResourcesOut/Hero", "PackTopDirectorySubDirectory"),
        new CollectorPackRule("UIEffect", "Assets/ResourcesOut/UIEffect", "PackTopDirectorySubDirectory"),
    };
    private static bool SyncCollectorPackRules()
    {
        var setting = AssetBundleCollectorSettingData.Setting;
        var changed = false;
        foreach (var rule in CollectorPackRules)
        {
            var package = setting.Packages.Find(item => string.Equals(item.PackageName, rule.PackageName, StringComparison.OrdinalIgnoreCase));
            if (package == null)
            {
                Debug.LogWarning($"[YooAssetBuildTool] 找不到 Package: {rule.PackageName}");
                continue;
            }
            foreach (var group in package.Groups)
            {
                foreach (var collector in group.Collectors)
                {
                    if (!IsSameAssetPath(collector.CollectPath, rule.CollectPath))
                        continue;
                    if (collector.PackRuleName == rule.PackRuleName)
                        continue;
                    collector.PackRuleName = rule.PackRuleName;
                    AssetBundleCollectorSettingData.ModifyCollector(group, collector);
                    changed = true;
                    Debug.Log($"[YooAssetBuildTool] 同步 PackRule: {rule.PackageName} -> {rule.PackRuleName}");
                }
            }
        }
        if (changed)
        {
            AssetBundleCollectorSettingData.SaveFile();
            AssetDatabase.Refresh();
        }
        return changed;
    }
    private static bool IsSameAssetPath(string lhs, string rhs)
    {
        return NormalizeAssetPath(lhs).Equals(NormalizeAssetPath(rhs), StringComparison.OrdinalIgnoreCase);
    }
    private static string NormalizeAssetPath(string assetPath)
    {
        return string.IsNullOrEmpty(assetPath) ? string.Empty : assetPath.Replace('\\', '/').TrimEnd('/');
    }
    private readonly struct CollectorPackRule
    {
        public readonly string PackageName;
        public readonly string CollectPath;
        public readonly string PackRuleName;
        public CollectorPackRule(string packageName, string collectPath, string packRuleName)
        {
            PackageName = packageName;
            CollectPath = collectPath;
            PackRuleName = packRuleName;
        }
    }
}
Assets/Launch/Launch.cs
@@ -79,7 +79,6 @@
#else
        Debug.unityLogger.logEnabled = true;
#endif
    }
    // 启动入口
@@ -412,15 +411,46 @@
            AOTMetaAssemblyFiles.Add(dllName);
            var location = $"Assets/HybridCLRDlls/{dllName}";
            bool needRemote = false;
            int downloadCount = 0;
            long downloadBytes = 0;
            float downloadStartTime = 0f;
            try
            {
                if (YooAssetInitializer.Instance.PlayMode == EPlayMode.HostPlayMode && dllPackage.PackageValid && dllPackage.IsNeedDownloadFromRemote(location))
                {
                    var downloader = dllPackage.CreateBundleDownloader(location, false, 1, 0);
                    downloadCount = downloader != null ? downloader.TotalDownloadCount : 0;
                    downloadBytes = downloader != null ? downloader.TotalDownloadBytes : 0;
                    needRemote = downloadCount > 0 || downloadBytes > 0;
                    downloadStartTime = Time.realtimeSinceStartup;
                    Debug.Log($"[YooAssetDownload] Start on-demand source=Launch.ReadDllBytes, package={dllPackage.PackageName}, location='{location}', files={downloadCount}, size={FormatBytes(downloadBytes)}");
                }
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"[YooAssetDownload] Check DLL download failed. location='{location}', error={ex.Message}");
            }
            var handle = dllPackage.LoadAssetAsync<TextAsset>(location);
            await handle.ToUniTask();
            if (handle.Status != EOperationStatus.Succeed)
            {
                if (needRemote)
                {
                    float cost = Time.realtimeSinceStartup - downloadStartTime;
                    Debug.LogError($"[YooAssetDownload] Failed on-demand source=Launch.ReadDllBytes, package={dllPackage.PackageName}, location='{location}', files={downloadCount}, size={FormatBytes(downloadBytes)}, cost={cost:F2}s, error={handle.LastError}");
                }
                Debug.LogError($"[Launch] 加载 DLL 失败: {dllName}: {handle.LastError}");
            }
            else
            {
                if (needRemote)
                {
                    float cost = Time.realtimeSinceStartup - downloadStartTime;
                    Debug.Log($"[YooAssetDownload] Finish on-demand source=Launch.ReadDllBytes, package={dllPackage.PackageName}, location='{location}', files={downloadCount}, size={FormatBytes(downloadBytes)}, cost={cost:F2}s");
                }
                s_assetDatas[dllName] = ((TextAsset)handle.AssetObject).bytes;
                Debug.Log($"[Launch] Loaded DLL: {dllName} size:{s_assetDatas[dllName].Length}");
            }
@@ -431,6 +461,17 @@
        callback?.Invoke();
    }
    private static string FormatBytes(long bytes)
    {
        if (bytes < 1024L)
            return $"{bytes} B";
        if (bytes < 1024L * 1024L)
            return $"{bytes / 1024f:F2} KB";
        if (bytes < 1024L * 1024L * 1024L)
            return $"{bytes / 1024f / 1024f:F2} MB";
        return $"{bytes / 1024f / 1024f / 1024f:F2} GB";
    }
#if UNITY_EDITOR
    /// <summary>
    /// Editor 专用:递归扫描本地目录,建立文件名→URI 索引,模拟 CDN Remote File System。
Assets/Launch/Manager/LocalResManager.cs
@@ -139,11 +139,22 @@
    private const int YOO_MAX_RETRY = 3;
    private const int YOO_BASE_DELAY_MS = 500;
    // package 为 null 时默认使用启动配置包(当前为 Builtin)
    private struct DownloadLogContext
    {
        public bool Enabled;
        public string Source;
        public string PackageName;
        public string Location;
        public int Count;
        public long Bytes;
        public float StartTime;
    }
    // package 为 null 时默认使用启动 Builtin 包
    // BuiltIn 路径下的资源(Sprites/Prefabs)应传入 DefaultPackage(= Builtin 包)
    private async UniTask<T> LoadAssetWithRetryAsync<T>(string location, ResourcePackage package = null) where T : UnityEngine.Object
    {
        package ??= YooAssetInitializer.Instance.PrefabPackage;
        package ??= YooAssetInitializer.Instance.DefaultPackage;
        Exception lastEx = null;
        for (int attempt = 0; attempt <= YOO_MAX_RETRY; attempt++)
        {
@@ -158,10 +169,15 @@
                if (!package.PackageValid)
                    throw new Exception($"YooAsset package '{package.PackageName}' has no active manifest. State={YooAssetInitializer.Instance.State}, location='{location}'");
                var downloadLog = BeginOnDemandDownloadLog(package, location, $"Launch.LoadAssetAsync<{typeof(T).Name}>");
                var handle = package.LoadAssetAsync<T>(location);
                await handle.ToUniTask();
                if (handle.Status != EOperationStatus.Succeed)
                {
                    EndOnDemandDownloadLog(downloadLog, false, handle.LastError);
                    throw new Exception($"YooAsset load failed for '{location}': {handle.LastError}");
                }
                EndOnDemandDownloadLog(downloadLog, true);
                return handle.GetAssetObject<T>();
            }
            catch (Exception ex)
@@ -177,6 +193,70 @@
        }
        Debug.LogError($"[LocalResManager] Load '{location}' failed after {YOO_MAX_RETRY + 1} attempts: {lastEx?.Message}");
        return null;
    }
    private DownloadLogContext BeginOnDemandDownloadLog(ResourcePackage package, string location, string source)
    {
        var context = new DownloadLogContext
        {
            Enabled = false,
            Source = source,
            PackageName = package != null ? package.PackageName : "NULL",
            Location = location,
            StartTime = Time.realtimeSinceStartup,
        };
        try
        {
            if (YooAssetInitializer.Instance.PlayMode != EPlayMode.HostPlayMode || package == null || !package.PackageValid)
                return context;
            if (!package.IsNeedDownloadFromRemote(location))
                return context;
            var downloader = package.CreateBundleDownloader(location, false, 1, 0);
            context.Count = downloader != null ? downloader.TotalDownloadCount : 0;
            context.Bytes = downloader != null ? downloader.TotalDownloadBytes : 0;
            context.Enabled = context.Count > 0 || context.Bytes > 0;
            if (context.Enabled)
            {
                Debug.Log($"[YooAssetDownload] Start on-demand source={source}, package={context.PackageName}, location='{location}', files={context.Count}, size={FormatBytes(context.Bytes)}");
            }
        }
        catch (Exception ex)
        {
            Debug.LogWarning($"[YooAssetDownload] Check on-demand download failed. source={source}, package={context.PackageName}, location='{location}', error={ex.Message}");
        }
        return context;
    }
    private void EndOnDemandDownloadLog(DownloadLogContext context, bool succeed, string error = null)
    {
        if (!context.Enabled)
            return;
        float cost = Time.realtimeSinceStartup - context.StartTime;
        if (succeed)
        {
            Debug.Log($"[YooAssetDownload] Finish on-demand source={context.Source}, package={context.PackageName}, location='{context.Location}', files={context.Count}, size={FormatBytes(context.Bytes)}, cost={cost:F2}s");
        }
        else
        {
            Debug.LogError($"[YooAssetDownload] Failed on-demand source={context.Source}, package={context.PackageName}, location='{context.Location}', files={context.Count}, size={FormatBytes(context.Bytes)}, cost={cost:F2}s, error={error}");
        }
    }
    private static string FormatBytes(long bytes)
    {
        if (bytes < 1024L)
            return $"{bytes} B";
        if (bytes < 1024L * 1024L)
            return $"{bytes / 1024f:F2} KB";
        if (bytes < 1024L * 1024L * 1024L)
            return $"{bytes / 1024f / 1024f:F2} MB";
        return $"{bytes / 1024f / 1024f / 1024f:F2} GB";
    }
#if UNITY_WEBGL || TEST_BUILD
@@ -390,11 +470,11 @@
#if UNITY_EDITOR
        await TryLoadInitialFunctionFromResourcesAsync();
#else
        bool configReady = await TryLoadInitialFunctionFromCdnAsync(initFuncUrl);
        if (!configReady)
        {
            await InitializeYooAssetAndEnterReadBytesAsync();
        }
    bool configReady = await TryLoadInitialFunctionFromCdnAsync(initFuncUrl);
    if (!configReady)
    {
        await InitializeYooAssetAndEnterReadBytesAsync();
    }
#endif
        await InitializeYooAssetAndEnterReadBytesAsync();
    }
@@ -554,9 +634,16 @@
        }
        await UniTask.Yield();
#else
        var spritePath = $"Assets/ResourcesOut/BuiltIn/Sprites/{name}.png";
        // BuiltIn/Sprites 在 Builtin 包(DefaultPackage)
        sprite = await LoadAssetWithRetryAsync<Sprite>(spritePath, YooAssetInitializer.Instance.DefaultPackage);
        if (excludePngs.Contains(StringUtility.Concat(name, ".png")))
        {
            var spritePath = $"Assets/ResourcesOut/BuiltIn/Sprites/{name}.png";
            sprite = await LoadAssetWithRetryAsync<Sprite>(spritePath, YooAssetInitializer.Instance.DefaultPackage);
        }
        else
        {
            var atlas = await LoadAssetWithRetryAsync<SpriteAtlas>("Assets/ResourcesOut/BuiltIn/Sprites/sprites.spriteatlasv2", YooAssetInitializer.Instance.DefaultPackage);
            sprite = atlas != null ? atlas.GetSprite(name) : null;
        }
#endif
        if (sprite == null)
        {
@@ -583,11 +670,23 @@
            sprite = spriteAtlas.GetSprite(name);
        }
#else
        // BuiltIn/Sprites 在 Builtin 包(DefaultPackage)
        var handle = YooAssetInitializer.Instance.DefaultPackage.LoadAssetSync<Sprite>(
            $"Assets/ResourcesOut/BuiltIn/Sprites/{name}.png");
        if (handle.Status == EOperationStatus.Succeed)
            sprite = handle.GetAssetObject<Sprite>();
        if (excludePngs.Contains(StringUtility.Concat(name, ".png")))
        {
            var handle = YooAssetInitializer.Instance.DefaultPackage.LoadAssetSync<Sprite>(
                $"Assets/ResourcesOut/BuiltIn/Sprites/{name}.png");
            if (handle.Status == EOperationStatus.Succeed)
                sprite = handle.GetAssetObject<Sprite>();
        }
        else
        {
            var handle = YooAssetInitializer.Instance.DefaultPackage.LoadAssetSync<SpriteAtlas>(
                "Assets/ResourcesOut/BuiltIn/Sprites/sprites.spriteatlasv2");
            if (handle.Status == EOperationStatus.Succeed)
            {
                var spriteAtlas = handle.GetAssetObject<SpriteAtlas>();
                sprite = spriteAtlas != null ? spriteAtlas.GetSprite(name) : null;
            }
        }
#endif
        if (sprite == null)
        {
Assets/Launch/Manager/YooAssetInitializer.cs
@@ -44,7 +44,9 @@
    public const string DEFAULT_PACKAGE_NAME = BUILTIN_PACKAGE_NAME;
    public const string CONFIG_PACKAGE_NAME = BUILTIN_PACKAGE_NAME;
    // Launch 阶段读取的启动配置文件被 CollectResBeforeUpdate 额外收进 Builtin 包。
    // Main 阶段的 ResourcesOut/Config 由独立 Config 包负责。
    public const string LAUNCH_CONFIG_PACKAGE_NAME = BUILTIN_PACKAGE_NAME;
    /// <summary>
    /// Launch 阶段需要初始化的包名(与 Collector 一致)。
@@ -58,45 +60,6 @@
    private ResourcePackage _configPackage;
    private readonly Dictionary<string, ResourcePackage> _packages = new Dictionary<string, ResourcePackage>();
    /// <summary>
    /// 随包安装的 Package 列表(从 Resources/YooBuildinPackages.txt 读取)。
    /// 打包时由 YooAssetBuildTool.WriteBuildinPackageList() 写入。
    /// null 表示未找到标记文件(Editor / 旧包),此时默认所有包都有 Buildin。
    /// </summary>
    private static HashSet<string> _buildinPackages;
    private static bool _buildinPackagesLoaded;
    /// <summary>
    /// 检查指定包是否存在于 StreamingAssets(随包安装)。
    /// Editor 或未找到标记文件时返回 true(向后兼容)。
    /// </summary>
    public static bool HasBuildinPackage(string packageName)
    {
        if (!_buildinPackagesLoaded)
        {
            _buildinPackagesLoaded = true;
            var textAsset = Resources.Load<TextAsset>("YooBuildinPackages");
            if (textAsset != null)
            {
                _buildinPackages = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                foreach (var line in textAsset.text.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries))
                {
                    _buildinPackages.Add(line.Trim());
                }
                Resources.UnloadAsset(textAsset);
                Debug.Log($"[YooAssetInitializer] Buildin packages: [{string.Join(", ", _buildinPackages)}]");
            }
            else
            {
                Debug.Log("[YooAssetInitializer] YooBuildinPackages.txt not found, assuming all packages have buildin.");
            }
        }
        // 未找到标记文件 → 默认都有(兼容旧包和 Editor)
        if (_buildinPackages == null) return true;
        return _buildinPackages.Contains(packageName);
    }
    /// <summary>
    /// 当前初始化状态
@@ -117,6 +80,9 @@
    public ResourcePackage DllPackage => GetPackage(DLL_PACKAGE_NAME);
    public ResourcePackage LaunchConfigPackage => _configPackage;
    [Obsolete("Use LaunchConfigPackage or BuiltinPackage instead.")]
    public ResourcePackage PrefabPackage => _configPackage;
    /// <summary>
@@ -175,10 +141,12 @@
                try
                {
                    // 先验证初始化参数(如 SimulateBuild),避免 CreatePackage 后抛异常留下僵尸包裹
                    bool enableBuildinFileSystem;
                    InitializeParameters initParams;
                    try
                    {
                        initParams = CreateInitParameters(playMode, remoteServices, pkgName);
                        enableBuildinFileSystem = await ShouldEnableBuildinFileSystemAsync(playMode, pkgName, nameof(YooAssetInitializer));
                        initParams = CreateInitParameters(playMode, remoteServices, pkgName, enableBuildinFileSystem);
                    }
                    catch (Exception paramEx)
                    {
@@ -187,20 +155,18 @@
                    }
                    var package = YooAssets.CreatePackage(pkgName);
                    var initOp = package.InitializeAsync(initParams);
                    await initOp.ToUniTask();
                    if (initOp.Status != EOperationStatus.Succeed)
                    try
                    {
                        Debug.LogError($"[YooAssetInitializer] Package '{pkgName}' init failed: {initOp.Error}");
                        try
                        {
                            var destroyOp = package.DestroyAsync();
                            await destroyOp.ToUniTask();
                            YooAssets.RemovePackage(pkgName);
                        }
                        catch { /* ignore cleanup errors */ }
                        continue;
                        await InitializePackageOperationAsync(pkgName, package, initParams);
                    }
                    catch (Exception initEx) when (playMode == EPlayMode.HostPlayMode && enableBuildinFileSystem)
                    {
                        Debug.LogWarning($"[YooAssetInitializer] Package '{pkgName}' init with BuildinFileSystem failed, retry cache-only: {initEx.Message}");
                        await RemovePackageSafelyAsync(pkgName, package);
                        package = YooAssets.CreatePackage(pkgName);
                        initParams = CreateInitParameters(playMode, remoteServices, pkgName, false);
                        await InitializePackageOperationAsync(pkgName, package, initParams);
                    }
                    _packages[pkgName] = package;
@@ -213,8 +179,8 @@
                        YooAssets.SetDefaultPackage(package);
                    }
                    // CONFIG_PACKAGE_NAME 包作为启动配置包
                    if (pkgName == CONFIG_PACKAGE_NAME)
                    // LAUNCH_CONFIG_PACKAGE_NAME 包作为启动配置包
                    if (pkgName == LAUNCH_CONFIG_PACKAGE_NAME)
                    {
                        _configPackage = package;
                    }
@@ -226,22 +192,7 @@
                    // 如果 CreatePackage 成功但后续失败,清理僵尸包裹
                    var zombiePkg = YooAssets.TryGetPackage(pkgName);
                    if (zombiePkg != null)
                    {
                        try
                        {
                            if (zombiePkg.InitializeStatus == EOperationStatus.None)
                            {
                                // 未初始化可以直接移除
                                YooAssets.RemovePackage(pkgName);
                            }
                            else
                            {
                                // 已初始化需先销毁再移除(但这里是 catch 块,无法 await)
                                // 配合 YooAssetService 的自愈机制处理
                            }
                        }
                        catch { /* ignore cleanup errors */ }
                    }
                        await RemovePackageSafelyAsync(pkgName, zombiePkg);
                    Debug.LogError($"[YooAssetInitializer] Package '{pkgName}' init exception (skipped): {ex.Message}");
                }
            }
@@ -306,7 +257,7 @@
#endif
                // 1. 请求版本号(对关键包最多重试3次,间隔1s)
                int versionRetryMax = string.Equals(pkgName, CONFIG_PACKAGE_NAME, StringComparison.OrdinalIgnoreCase) ? 3 : 0;
                int versionRetryMax = string.Equals(pkgName, LAUNCH_CONFIG_PACKAGE_NAME, StringComparison.OrdinalIgnoreCase) ? 3 : 0;
                EOperationStatus versionStatus = EOperationStatus.Failed;
                string versionOpError = string.Empty;
                string packageVersion = string.Empty;
@@ -437,12 +388,12 @@
                bundleTotalCount = details.BundleTotalCount;
                packageVersion = details.PackageVersion ?? "null";
                if (string.Equals(pkgName, CONFIG_PACKAGE_NAME, StringComparison.OrdinalIgnoreCase))
                if (string.Equals(pkgName, LAUNCH_CONFIG_PACKAGE_NAME, StringComparison.OrdinalIgnoreCase))
                {
                    initialFunctionValid = package.CheckLocationValid("Assets/ResourcesOut/Config/InitialFunction.txt").ToString();
                }
            }
            else if (string.Equals(pkgName, CONFIG_PACKAGE_NAME, StringComparison.OrdinalIgnoreCase))
            else if (string.Equals(pkgName, LAUNCH_CONFIG_PACKAGE_NAME, StringComparison.OrdinalIgnoreCase))
            {
                initialFunctionValid = "manifest-null";
            }
@@ -517,13 +468,17 @@
            return;
        }
        Debug.Log($"[YooAssetInitializer] Start downloading {downloader.TotalDownloadCount} files, total {downloader.TotalDownloadBytes} bytes");
        BindDownloadLogCallbacks(downloader, "Launch.DownloadAsync");
        float startTime = Time.realtimeSinceStartup;
        float lastLogTime = 0f;
        int lastLogPercent = -1;
        downloader.BeginDownload();
        while (!downloader.IsDone)
        {
            progress?.Report(downloader.Progress);
            LogDownloaderProgress(downloader, "Launch.DownloadAsync", startTime, ref lastLogTime, ref lastLogPercent);
            await UniTask.Yield();
        }
@@ -593,6 +548,12 @@
            long pkgBytes = downloader.TotalDownloadBytes;
            Debug.Log($"[YooAssetInitializer] Downloading package '{pkgName}': {downloader.TotalDownloadCount} files, {pkgBytes / 1024f / 1024f:F2} MB");
            string source = $"Launch.DownloadAllPending package='{pkgName}'";
            BindDownloadLogCallbacks(downloader, source);
            float startTime = Time.realtimeSinceStartup;
            float lastLogTime = 0f;
            int lastLogPercent = -1;
            downloader.BeginDownload();
            while (!downloader.IsDone)
@@ -602,6 +563,7 @@
                    ? (downloadedBytes + (long)(pkgBytes * currentPkgProgress)) / (float)totalBytes
                    : 0f;
                progress?.Report(overallProgress);
                LogDownloaderProgress(downloader, source, startTime, ref lastLogTime, ref lastLogPercent, overallProgress);
                await UniTask.Yield();
            }
@@ -622,11 +584,167 @@
        Debug.Log($"[YooAssetInitializer] All pending downloads finished. {downloaders.Count} packages processed.");
    }
    private static void BindDownloadLogCallbacks(ResourceDownloaderOperation downloader, string source)
    {
        if (downloader == null)
            return;
        float startTime = Time.realtimeSinceStartup;
        Debug.Log($"[YooAssetDownload] Start downloader source={source}, files={downloader.TotalDownloadCount}, size={FormatBytes(downloader.TotalDownloadBytes)}");
        downloader.DownloadFileBeginCallback = data =>
        {
            Debug.Log($"[YooAssetDownload] File begin source={source}, package={data.PackageName}, file='{data.FileName}', size={FormatBytes(data.FileSize)}");
        };
        downloader.DownloadErrorCallback = data =>
        {
            float cost = Time.realtimeSinceStartup - startTime;
            Debug.LogError($"[YooAssetDownload] File error source={source}, package={data.PackageName}, file='{data.FileName}', cost={cost:F2}s, error={data.ErrorInfo}");
        };
        downloader.DownloadFinishCallback = data =>
        {
            float cost = Time.realtimeSinceStartup - startTime;
            string result = data.Succeed ? "Finish" : "Failed";
            Debug.Log($"[YooAssetDownload] {result} downloader source={source}, package={data.PackageName}, files={downloader.CurrentDownloadCount}/{downloader.TotalDownloadCount}, size={FormatBytes(downloader.CurrentDownloadBytes)}/{FormatBytes(downloader.TotalDownloadBytes)}, cost={cost:F2}s");
        };
    }
    private static void LogDownloaderProgress(ResourceDownloaderOperation downloader, string source, float startTime,
        ref float lastLogTime, ref int lastLogPercent, float overallProgress = -1f)
    {
        if (downloader == null)
            return;
        float now = Time.realtimeSinceStartup;
        int percent = Mathf.Clamp(Mathf.FloorToInt(downloader.Progress * 100f), 0, 100);
        if (percent < 100 && percent < lastLogPercent + 5 && now - lastLogTime < 1f)
            return;
        lastLogTime = now;
        lastLogPercent = percent;
        string overall = overallProgress >= 0f ? $", overall={Mathf.Clamp01(overallProgress) * 100f:F1}%" : string.Empty;
        Debug.Log($"[YooAssetDownload] Progress source={source}, progress={percent}%, files={downloader.CurrentDownloadCount}/{downloader.TotalDownloadCount}, size={FormatBytes(downloader.CurrentDownloadBytes)}/{FormatBytes(downloader.TotalDownloadBytes)}, cost={now - startTime:F2}s{overall}");
    }
    private static string FormatBytes(long bytes)
    {
        if (bytes < 1024L)
            return $"{bytes} B";
        if (bytes < 1024L * 1024L)
            return $"{bytes / 1024f:F2} KB";
        if (bytes < 1024L * 1024L * 1024L)
            return $"{bytes / 1024f / 1024f:F2} MB";
        return $"{bytes / 1024f / 1024f / 1024f:F2} GB";
    }
    // ====================================================================
    // 辅助方法
    // ====================================================================
    private InitializeParameters CreateInitParameters(EPlayMode playMode, IRemoteServices remoteServices, string packageName)
    public static async UniTask<bool> HasBuildinCatalogAsync(string packageName)
    {
        if (string.IsNullOrEmpty(packageName))
            return false;
#if UNITY_ANDROID && !UNITY_EDITOR
    return AndroidAssetExists($"yoo/{packageName}/BuildinCatalog.bytes");
#else
        string catalogUrl = $"{Application.streamingAssetsPath}/yoo/{packageName}/BuildinCatalog.bytes";
        catalogUrl = catalogUrl.Replace('\\', '/');
#if !UNITY_WEBGL
        if (!catalogUrl.StartsWith("jar:", StringComparison.OrdinalIgnoreCase) &&
            !catalogUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
            !catalogUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase) &&
            !catalogUrl.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
            return System.IO.File.Exists(catalogUrl);
#endif
        try
        {
            using var request = UnityEngine.Networking.UnityWebRequest.Get(catalogUrl);
            request.timeout = 3;
            await request.SendWebRequest().ToUniTask();
            return request.result == UnityEngine.Networking.UnityWebRequest.Result.Success;
        }
        catch (Exception ex)
        {
            Debug.LogWarning($"[YooAssetInitializer] Probe buildin catalog exception. package='{packageName}', url='{catalogUrl}', error={ex.Message}");
            return false;
        }
#endif
    }
    public static async UniTask<bool> ShouldEnableBuildinFileSystemAsync(EPlayMode playMode, string packageName, string logPrefix)
    {
        bool enableBuildinFileSystem = true;
        if (playMode == EPlayMode.HostPlayMode)
        {
            enableBuildinFileSystem = await HasBuildinCatalogAsync(packageName);
            if (enableBuildinFileSystem)
                Debug.Log($"[{logPrefix}] Package '{packageName}' has buildin catalog, enable BuildinFileSystem.");
            else
                Debug.Log($"[{logPrefix}] Package '{packageName}' has no buildin catalog, skip BuildinFileSystem.");
        }
        return enableBuildinFileSystem;
    }
#if UNITY_ANDROID && !UNITY_EDITOR
    private static bool AndroidAssetExists(string assetPath)
    {
        try
        {
            using var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
            using var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
            using var assets = activity.Call<AndroidJavaObject>("getAssets");
            using var stream = assets.Call<AndroidJavaObject>("open", assetPath);
            stream.Call("close");
            return true;
        }
        catch
        {
            return false;
        }
    }
#endif
    private async UniTask RemovePackageSafelyAsync(string packageName, ResourcePackage package)
    {
        try
        {
            if (package != null && package.InitializeStatus != EOperationStatus.None)
            {
                var destroyOp = package.DestroyAsync();
                await destroyOp.ToUniTask();
            }
            YooAssets.RemovePackage(packageName);
        }
        catch
        {
            // ignore cleanup errors
        }
    }
    private async UniTask InitializePackageOperationAsync(string packageName, ResourcePackage package, InitializeParameters initParams)
    {
        var initOp = package.InitializeAsync(initParams);
        await initOp.ToUniTask();
        if (initOp.Status != EOperationStatus.Succeed)
            throw new InvalidOperationException(initOp.Error);
    }
    private async UniTask<InitializeParameters> CreateInitParametersAsync(EPlayMode playMode, IRemoteServices remoteServices, string packageName)
    {
        bool enableBuildinFileSystem = await ShouldEnableBuildinFileSystemAsync(playMode, packageName, nameof(YooAssetInitializer));
        return CreateInitParameters(playMode, remoteServices, packageName, enableBuildinFileSystem);
    }
    private InitializeParameters CreateInitParameters(EPlayMode playMode, IRemoteServices remoteServices, string packageName, bool enableBuildinFileSystem)
    {
        switch (playMode)
        {
@@ -645,7 +763,6 @@
            }
            case EPlayMode.HostPlayMode:
            {
                bool hasBuildin = HasBuildinPackage(packageName);
                // 有 remoteCdnBaseUrl 时为每个包单独创建带子路径的 RemoteServices
                // 例:{cdnBase}/android/{packageName}/{fileName}
                var effectiveRemote = !string.IsNullOrEmpty(_remoteCdnBaseUrl)
@@ -653,7 +770,7 @@
                    : remoteServices;
                return new HostPlayModeParameters
                {
                    BuildinFileSystemParameters = hasBuildin
                    BuildinFileSystemParameters = enableBuildinFileSystem
                        ? FileSystemParameters.CreateDefaultBuildinFileSystemParameters()
                        : null,
                    CacheFileSystemParameters = FileSystemParameters
Assets/Launch/UI/LaunchWins/LaunchExWin.cs
@@ -34,7 +34,9 @@
    void Awake()
    {
        SetProgressBarImagesVisible(false);
        InitSpritesAsync().Forget();
        UpdateProgress();
    }
    /// <summary>
@@ -59,6 +61,27 @@
        imageCircle.sprite = sprites.Item2;
        imagebg3.sprite = sprites.Item3;
        imageloding.sprite = sprites.Item4;
        if (sprites.Item3 != null && sprites.Item4 != null)
        {
            SetProgressBarImagesVisible(true);
        }
        else
        {
            Debug.LogError($"[LaunchExWin] Progress bar sprite load failed. LoadingBottom={sprites.Item3 != null}, LoadingSlider={sprites.Item4 != null}");
        }
    }
    private void SetProgressBarImagesVisible(bool visible)
    {
        if (imagebg3 != null)
        {
            imagebg3.enabled = visible;
        }
        if (imageloding != null)
        {
            imageloding.enabled = visible;
        }
    }
    private void OnEnable()